Repository: poem-web/poem (language: rust)

Issue:
[poem-openapi] Expose generated json
## Description of the feature

Hi, first of all good work with poem and poem-openapi, I'm enjoying a lot using them and I'll probably migrate some projects to it.

Anyway, I'd like to be able to get (and maybe serve) the generated `.json` file that contains the open api specification; it would also be nice to be able to deactivate the ui feature entirely.

This is because I prefer redoc and I'd like to inspect/edit the final open api document.

## Code example (if possible)

```rust
#[tokio::main]
async fn main() -> Result<(), std::io::Error> {
    let api_service = OpenApiService::new(Api)
        .title("Hello World")
        .server("http://localhost:3000/api");

    // Get open api spec
    let openapi_spec = api_service.get_spec();

    let listener = TcpListener::bind("127.0.0.1:3000");

    poem::Server::new(listener)
        .await?
        .run(
            Route::new()
                .nest("/api", api_service)
                .nest("/openapi.json", serve_openapi_spec(openapi_spec))
        )
        .await
}

```


Discussion / hints:
Released in `v1.0.2` 😁
> Released in `v1.0.2` grin

Wonderful! What do you think of hiding `swagger-ui` behind a feature flag? I could open a PR for that
> > Released in `v1.0.2` grin
> 
> Wonderful! What do you think of hiding `swagger-ui` behind a feature flag? I could open a PR for that

Yes, welcome to PR.😁

Fix patch:
diff --git a/poem-openapi/Cargo.toml b/poem-openapi/Cargo.toml
index a8fb4c850e..8b46710c1e 100644
--- a/poem-openapi/Cargo.toml
+++ b/poem-openapi/Cargo.toml
@@ -16,7 +16,9 @@ categories = [
 ]
 
 [features]
-default = []
+default = ["swagger-ui"]
+
+swagger-ui = ["askama"]
 
 [dependencies]
 poem-openapi-derive = { path = "../poem-openapi-derive", version = "1.0.0" }
@@ -27,13 +29,13 @@ serde_json = "1.0.68"
 base64 = "0.13.0"
 serde = { version = "1.0.130", features = ["derive"] }
 thiserror = "1.0.29"
-askama = "0.10.5"
 derive_more = "0.99.16"
 num-traits = "0.2.14"
 regex = "1.5.4"
 typed-headers = "0.2.0"
 
 # Feature optional dependencies
+askama = { version = "0.10.5", optional = true }
 chrono = { version = "0.4.19", optional = true }
 
 [dev-dependencies]
diff --git a/poem-openapi/README.md b/poem-openapi/README.md
index ef61425004..b7365cf962 100644
--- a/poem-openapi/README.md
+++ b/poem-openapi/README.md
@@ -46,11 +46,12 @@ important business implementations.
 
 ## Crate features
 
-To avoid compiling unused dependencies, Poem gates certain features, all of which are disabled by default:
+To avoid compiling unused dependencies, Poem gates certain features, some of which are disabled by default:
 
-|Feature           |Description                     |
-|------------------|--------------------------------|
-|chrono            | Integrate with the [`chrono` crate](https://crates.io/crates/chrono).          |
+| Feature    | Description                                                  | Default enabled    |
+| ---------- | ------------------------------------------------------------ | ------------------ |
+| chrono     | Integrate with the [`chrono` crate](https://crates.io/crates/chrono). | :x:                |
+| swagger-ui | Add swagger UI support                                       | :heavy_check_mark: |
 
 ## Safety
 
diff --git a/poem-openapi/src/lib.rs b/poem-openapi/src/lib.rs
index 8064d7a7a4..bf121a2a84 100644
--- a/poem-openapi/src/lib.rs
+++ b/poem-openapi/src/lib.rs
@@ -22,12 +22,12 @@
 //!
 //! ## Crate features
 //!
-//! To avoid compiling unused dependencies, Poem gates certain features, all of
-//! which are disabled by default:
-//!
-//! |Feature           |Description                     |
-//! |------------------|--------------------------------|
-//! |chrono            | Integrate with the [`chrono` crate](https://crates.io/crates/chrono).          |
+//! To avoid compiling unused dependencies, Poem gates certain features, some of which are disabled by default:
+//! 
+//! | Feature    | Description                                                  | Default enabled    |
+//! | ---------- | ------------------------------------------------------------ | ------------------ |
+//! | chrono     | Integrate with the [`chrono` crate](https://crates.io/crates/chrono). | :x:                |
+//! | swagger-ui | Add swagger UI support                                       | :heavy_check_mark: |
 //!
 //! ## Example
 //!
@@ -98,6 +98,7 @@ pub mod payload;
 pub mod registry;
 pub mod types;
 #[doc(hidden)]
+#[cfg(feature = "swagger-ui")]
 pub mod ui;
 #[doc(hidden)]
 pub mod validation;
diff --git a/poem-openapi/src/openapi.rs b/poem-openapi/src/openapi.rs
index 4f29008f7e..94bdcca2a9 100644
--- a/poem-openapi/src/openapi.rs
+++ b/poem-openapi/src/openapi.rs
@@ -1,12 +1,14 @@
 use poem::{
-    endpoint::BoxEndpoint, web::cookie::CookieKey, Endpoint, EndpointExt, IntoEndpoint, Response,
-    Route,
+    endpoint::{make_sync, BoxEndpoint},
+    web::{cookie::CookieKey, Json},
+    Endpoint, EndpointExt, IntoEndpoint, Response, Route,
 };
 
+#[cfg(feature = "swagger-ui")]
+use crate::ui::create_ui_endpoint;
 use crate::{
     param::InternalCookieKey,
     registry::{Document, MetaInfo, MetaServer, Registry},
-    ui::create_ui_endpoint,
     OpenApi,
 };
 
@@ -90,6 +92,7 @@ impl<T> OpenApiService<T> {
 
     /// Create the Swagger UI endpoint.
     #[must_use]
+    #[cfg(feature = "swagger-ui")]
     pub fn swagger_ui(&self, absolute_uri: impl AsRef<str>) -> impl Endpoint
     where
         T: OpenApi,
@@ -97,8 +100,24 @@ impl<T> OpenApiService<T> {
         create_ui_endpoint(absolute_uri.as_ref(), &self.spec())
     }
 
+    /// Create an endpoint to serve the open api specification.
+    pub fn serve_spec(&self, absolute_uri: impl AsRef<str>) -> impl Endpoint
+    where
+        T: OpenApi,
+    {
+        let spec = self.spec();
+        poem::Route::new().at(
+            absolute_uri,
+            make_sync(move |_| {
+                Response::builder()
+                    .content_type("application/json")
+                    .body(spec.clone())
+            }),
+        )
+    }
+
     /// Returns the OAS specification file.
-    fn spec(&self) -> String
+    pub fn spec(&self) -> String
     where
         T: OpenApi,
     {


Test patch: