1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
#![warn(missing_docs)]
#![warn(rustdoc::broken_intra_doc_links)]
#![cfg_attr(doc_cfg, feature(doc_cfg))]
//! This crate works as a bridge between [utoipa](https://docs.rs/utoipa/latest/utoipa/) and [RapiDoc](https://rapidocweb.com/) OpenAPI visualizer.
//!
//! Utoipa-rapidoc provides simple mechanism to transform OpenAPI spec resource to a servable HTML
//! file which can be served via [predefined framework integration][Self#examples] or used
//! [standalone][Self#using-standalone] and served manually.
//!
//! You may find fullsize examples from utoipa's Github [repository][examples].
//!
//! # Crate Features
//!
//! * **actix-web** Allows serving [`RapiDoc`] via _**`actix-web`**_.
//! * **rocket** Allows serving [`RapiDoc`] via _**`rocket`**_.
//! * **axum** Allows serving [`RapiDoc`] via _**`axum`**_.
//!
//! # Install
//!
//! Use RapiDoc only without any boiler plate implementation.
//! ```toml
//! [dependencies]
//! utoipa-rapidoc = "4"
//! ```
//!
//! Enable actix-web integration with RapiDoc.
//! ```toml
//! [dependencies]
//! utoipa-rapidoc = { version = "4", features = ["actix-web"] }
//! ```
//!
//! # Using standalone
//!
//! Utoipa-rapidoc can be used standalone as simply as creating a new [`RapiDoc`] instance and then
//! serving it by what ever means available as `text/html` from http handler in your favourite web
//! framework.
//!
//! [`RapiDoc::to_html`] method can be used to convert the [`RapiDoc`] instance to a servable html
//! file.
//! ```
//! # use utoipa_rapidoc::RapiDoc;
//! # use utoipa::OpenApi;
//! # use serde_json::json;
//! # #[derive(OpenApi)]
//! # #[openapi()]
//! # struct ApiDoc;
//! #
//! let rapidoc = RapiDoc::new("/api-docs/openapi.json");
//!
//! // Then somewhere in your application that handles http operation.
//! // Make sure you return correct content type `text/html`.
//! let rapidoc_handler = move || {
//!     rapidoc.to_html()
//! };
//! ```
//!
//! # Customization
//!
//! Utoipa-rapidoc can be customized and configured only via [`RapiDoc::custom_html`] method. This
//! method empowers users to use a custom HTML template to modify the looks of the RapiDoc UI.
//!
//! * [All allowed RapiDoc configuration options][rapidoc_api]
//! * [Default HTML template][rapidoc_quickstart]
//!
//! The template should contain _**`$specUrl`**_ variable which will be replaced with user defined
//! OpenAPI spec url provided with [`RapiDoc::new`] function when creating a new [`RapiDoc`]
//! instance. Variable will be replaced during [`RapiDoc::to_html`] function execution.
//!
//! _**Overriding the HTML template with a custom one.**_
//! ```rust
//! # use utoipa_rapidoc::RapiDoc;
//! # use utoipa::OpenApi;
//! # use serde_json::json;
//! # #[derive(OpenApi)]
//! # #[openapi()]
//! # struct ApiDoc;
//! #
//! let html = "...";
//! RapiDoc::new("/api-docs/openapi.json").custom_html(html);
//! ```
//!
//! # Examples
//!
//! _**Serve [`RapiDoc`] via `actix-web` framework.**_
//! ```no_run
//! use actix_web::App;
//! use utoipa_rapidoc::RapiDoc;
//!
//! # use utoipa::OpenApi;
//! # use std::net::Ipv4Addr;
//! # #[derive(OpenApi)]
//! # #[openapi()]
//! # struct ApiDoc;
//! App::new().service(RapiDoc::with_openapi("/rapidoc", ApiDoc::openapi()));
//! ```
//!
//! _**Serve [`RapiDoc`] via `rocket` framework.**_
//! ```no_run
//! # use rocket;
//! use utoipa_rapidoc::RapiDoc;
//!
//! # use utoipa::OpenApi;
//! # #[derive(OpenApi)]
//! # #[openapi()]
//! # struct ApiDoc;
//! rocket::build()
//!     .mount(
//!         "/",
//!         RapiDoc::with_openapi("/rapidoc", ApiDoc::openapi()),
//!     );
//! ```
//!
//! _**Serve [`RapiDoc`] via `axum` framework.**_
//!  ```no_run
//!  use axum::Router;
//!  use utoipa_rapidoc::RapiDoc;
//!  # use utoipa::OpenApi;
//! # #[derive(OpenApi)]
//! # #[openapi()]
//! # struct ApiDoc;
//! #
//! # fn inner<S>()
//! # where
//! #     S: Clone + Send + Sync + 'static,
//! # {
//!
//!  let app = Router::<S>::new()
//!      .merge(RapiDoc::with_openapi("/rapidoc", ApiDoc::openapi()));
//! # }
//! ```
//!
//! [rapidoc_api]: <https://rapidocweb.com/api.html>
//! [examples]: <https://github.com/juhaku/utoipa/tree/master/examples>
//! [rapidoc_quickstart]: <https://rapidocweb.com/quickstart.html>

use std::borrow::Cow;

const DEFAULT_HTML: &str = include_str!("../res/rapidoc.html");

/// Is [RapiDoc][rapidoc] UI.
///
/// This is an entry point for serving [RapiDoc][rapidoc] via predefined framework integration or
/// in standalone fashion by calling [`RapiDoc::to_html`] within custom HTTP handler handles
/// serving the [RapiDoc][rapidoc] UI. See more at [running standalone][standalone]
///
/// [rapidoc]: <https://rapidocweb.com>
/// [standalone]: index.html#using-standalone
#[non_exhaustive]
pub struct RapiDoc {
    #[allow(unused)]
    path: Cow<'static, str>,
    spec_url: Cow<'static, str>,
    html: Cow<'static, str>,
    #[cfg(any(feature = "actix-web", feature = "rocket", feature = "axum"))]
    openapi: Option<utoipa::openapi::OpenApi>,
}

impl RapiDoc {
    /// Construct a new [`RapiDoc`] that points to given `spec_url`. Spec url must be valid URL and
    /// available for RapiDoc to consume.
    ///
    /// # Examples
    ///
    /// _**Create new [`RapiDoc`].**_
    ///
    /// ```
    /// # use utoipa_rapidoc::RapiDoc;
    /// RapiDoc::new("https://petstore3.swagger.io/api/v3/openapi.json");
    /// ```
    pub fn new<U: Into<Cow<'static, str>>>(spec_url: U) -> Self {
        Self {
            path: Cow::Borrowed(""),
            spec_url: spec_url.into(),
            html: Cow::Borrowed(DEFAULT_HTML),
            #[cfg(any(feature = "actix-web", feature = "rocket", feature = "axum"))]
            openapi: None,
        }
    }

    /// Construct a new [`RapiDoc`] with given `spec_url` and `openapi`. The spec url must point to
    /// the location where the `openapi` will be served.
    ///
    /// [`RapiDoc`] is only able to create endpoint that serves the `openapi` JSON for predefined
    /// frameworks. _**For other frameworks such endpoint must be created manually.**_
    ///
    /// # Examples
    ///
    /// _**Create new [`RapiDoc`].**_
    ///
    /// ```
    /// # use utoipa_rapidoc::RapiDoc;
    /// # use utoipa::OpenApi;
    /// # #[derive(OpenApi)]
    /// # #[openapi()]
    /// # struct ApiDoc;
    /// RapiDoc::with_openapi(
    ///     "/api-docs/openapi.json",
    ///     ApiDoc::openapi()
    /// );
    /// ```
    #[cfg(any(feature = "actix-web", feature = "rocket", feature = "axum"))]
    #[cfg_attr(
        doc_cfg,
        doc(cfg(any(feature = "actix-web", feature = "rocket", feature = "axum")))
    )]
    pub fn with_openapi<U: Into<Cow<'static, str>>>(
        spec_url: U,
        openapi: utoipa::openapi::OpenApi,
    ) -> Self {
        Self {
            path: Cow::Borrowed(""),
            spec_url: spec_url.into(),
            html: Cow::Borrowed(DEFAULT_HTML),
            openapi: Some(openapi),
        }
    }

    /// Override the [default HTML template][rapidoc_quickstart] with new one. See
    /// [customization] for more details.
    ///
    /// [rapidoc_quickstart]: <https://rapidocweb.com/quickstart.html>
    /// [customization]: index.html#customization
    pub fn custom_html<H: Into<Cow<'static, str>>>(mut self, html: H) -> Self {
        self.html = html.into();

        self
    }

    /// Add `path` the [`RapiDoc`] will be served from.
    ///
    /// # Examples
    ///
    /// _**Make [`RapiDoc`] servable from `/rapidoc` path.**_
    /// ```
    /// # use utoipa_rapidoc::RapiDoc;
    ///
    /// RapiDoc::new("https://petstore3.swagger.io/api/v3/openapi.json")
    ///     .path("/rapidoc");
    /// ```
    #[cfg(any(feature = "actix-web", feature = "rocket", feature = "axum"))]
    pub fn path<U: Into<Cow<'static, str>>>(mut self, path: U) -> Self {
        self.path = path.into();

        self
    }

    /// Converts this [`RapiDoc`] instance to servable HTML file.
    ///
    /// This will replace _**`$specUrl`**_ variable placeholder with the spec
    /// url provided to the [`RapiDoc`] instance. If HTML template is not overridden with
    /// [`RapiDoc::custom_html`] then the [default HTML template][rapidoc_quickstart]
    /// will be used.
    ///
    /// See more details in [customization][customization].
    ///
    /// [rapidoc_quickstart]: <https://rapidocweb.com/quickstart.html>
    /// [customization]: index.html#customization
    pub fn to_html(&self) -> String {
        self.html.replace("$specUrl", self.spec_url.as_ref())
    }
}

mod actix {
    #![cfg(feature = "actix-web")]

    use actix_web::dev::HttpServiceFactory;
    use actix_web::guard::Get;
    use actix_web::web::Data;
    use actix_web::{HttpResponse, Resource, Responder};

    use crate::RapiDoc;

    impl HttpServiceFactory for RapiDoc {
        fn register(self, config: &mut actix_web::dev::AppService) {
            let html = self.to_html();

            async fn serve_rapidoc(rapidoc: Data<String>) -> impl Responder {
                HttpResponse::Ok()
                    .content_type("text/html")
                    .body(rapidoc.to_string())
            }

            Resource::new(self.path.as_ref())
                .guard(Get())
                .app_data(Data::new(html))
                .to(serve_rapidoc)
                .register(config);

            if let Some(openapi) = self.openapi {
                async fn serve_openapi(openapi: Data<String>) -> impl Responder {
                    HttpResponse::Ok()
                        .content_type("application/json")
                        .body(openapi.into_inner().to_string())
                }

                Resource::new(self.spec_url.as_ref())
                    .guard(Get())
                    .app_data(Data::new(
                        openapi.to_json().expect("Should serialize to JSON"),
                    ))
                    .to(serve_openapi)
                    .register(config);
            }
        }
    }
}

mod axum {
    #![cfg(feature = "axum")]

    use axum::response::Html;
    use axum::{routing, Json, Router};

    use crate::RapiDoc;

    impl<R> From<RapiDoc> for Router<R>
    where
        R: Clone + Send + Sync + 'static,
    {
        fn from(value: RapiDoc) -> Self {
            let html = value.to_html();
            let openapi = value.openapi;

            let mut router = Router::<R>::new().route(
                value.path.as_ref(),
                routing::get(move || async { Html(html) }),
            );

            if let Some(openapi) = openapi {
                router = router.route(
                    value.spec_url.as_ref(),
                    routing::get(move || async { Json(openapi) }),
                );
            }

            router
        }
    }
}

mod rocket {
    #![cfg(feature = "rocket")]

    use rocket::http::Method;
    use rocket::response::content::RawHtml;
    use rocket::route::{Handler, Outcome};
    use rocket::serde::json::Json;
    use rocket::{Data, Request, Route};

    use crate::RapiDoc;

    impl From<RapiDoc> for Vec<Route> {
        fn from(value: RapiDoc) -> Self {
            let mut routes = vec![Route::new(
                Method::Get,
                value.path.as_ref(),
                RapiDocHandler(value.to_html()),
            )];

            if let Some(openapi) = value.openapi {
                routes.push(Route::new(
                    Method::Get,
                    value.spec_url.as_ref(),
                    OpenApiHandler(openapi.to_json().expect("Should serialize to JSON")),
                ));
            }

            routes
        }
    }

    #[derive(Clone)]
    struct RapiDocHandler(String);

    #[rocket::async_trait]
    impl Handler for RapiDocHandler {
        async fn handle<'r>(&self, request: &'r Request<'_>, _: Data<'r>) -> Outcome<'r> {
            Outcome::from(request, RawHtml(self.0.clone()))
        }
    }

    #[derive(Clone)]
    struct OpenApiHandler(String);

    #[rocket::async_trait]
    impl Handler for OpenApiHandler {
        async fn handle<'r>(&self, request: &'r Request<'_>, _: Data<'r>) -> Outcome<'r> {
            Outcome::from(request, Json(self.0.clone()))
        }
    }
}