utoipa_redoc/
lib.rs

1#![warn(missing_docs)]
2#![warn(rustdoc::broken_intra_doc_links)]
3#![cfg_attr(doc_cfg, feature(doc_cfg))]
4//! This crate works as a bridge between [utoipa](https://docs.rs/utoipa/latest/utoipa/) and [Redoc](https://redocly.com/) OpenAPI visualizer.
5//!
6//! Utoipa-redoc provides simple mechanism to transform OpenAPI spec resource to a servable HTML
7//! file which can be served via [predefined framework integration][Self#examples] or used
8//! [standalone][Self#using-standalone] and served manually.
9//!
10//! You may find fullsize examples from utoipa's Github [repository][examples].
11//!
12//! # Crate Features
13//!
14//! * **actix-web** Allows serving [`Redoc`] via _**`actix-web`**_.
15//! * **rocket** Allows serving [`Redoc`] via _**`rocket`**_.
16//! * **axum** Allows serving [`Redoc`] via _**`axum`**_.
17//!
18//! # Install
19//!
20//! Use Redoc only without any boiler plate implementation.
21//! ```toml
22//! [dependencies]
23//! utoipa-redoc = "6"
24//! ```
25//!
26//! Enable actix-web integration with Redoc.
27//! ```toml
28//! [dependencies]
29//! utoipa-redoc = { version = "6", features = ["actix-web"] }
30//! ```
31//!
32//! # Using standalone
33//!
34//! Utoipa-redoc can be used standalone as simply as creating a new [`Redoc`] instance and then
35//! serving it by what ever means available as `text/html` from http handler in your favourite web
36//! framework.
37//!
38//! [`Redoc::to_html`] method can be used to convert the [`Redoc`] instance to a servable html
39//! file.
40//! ```
41//! # use utoipa_redoc::Redoc;
42//! # use utoipa::OpenApi;
43//! # use serde_json::json;
44//! # #[derive(OpenApi)]
45//! # #[openapi()]
46//! # struct ApiDoc;
47//! #
48//! let redoc = Redoc::new(ApiDoc::openapi());
49//!
50//! // Then somewhere in your application that handles http operation.
51//! // Make sure you return correct content type `text/html`.
52//! let redoc_handler = move || {
53//!     redoc.to_html()
54//! };
55//! ```
56//!
57//! # Customization
58//!
59//! Utoipa-redoc enables full customization support for [Redoc][redoc] according to what can be
60//! customized by modifying the HTML template and [configuration options][Self#configuration].
61//!
62//! The default [HTML template][redoc_html_quickstart] can be fully overridden to ones liking with
63//! [`Redoc::custom_html`] method. The HTML template **must** contain **`$spec`** and **`$config`**
64//! variables which are replaced during [`Redoc::to_html`] execution.
65//!
66//! * **`$spec`** Will be the [`Spec`] that will be rendered via [Redoc][redoc].
67//! * **`$config`** Will be the current [`Config`]. By default this is [`EmptyConfig`].
68//!
69//! _**Overriding the HTML template with a custom one.**_
70//! ```rust
71//! # use utoipa_redoc::Redoc;
72//! # use utoipa::OpenApi;
73//! # use serde_json::json;
74//! # #[derive(OpenApi)]
75//! # #[openapi()]
76//! # struct ApiDoc;
77//! #
78//! let html = "...";
79//! Redoc::new(ApiDoc::openapi()).custom_html(html);
80//! ```
81//!
82//! # Configuration
83//!
84//! Redoc can be configured with JSON either inlined with the [`Redoc`] declaration or loaded from
85//! user defined file with [`FileConfig`].
86//!
87//! * [All supported Redoc configuration options][redoc_config].
88//!
89//! _**Inlining the configuration.**_
90//! ```rust
91//! # use utoipa_redoc::Redoc;
92//! # use utoipa::OpenApi;
93//! # use serde_json::json;
94//! # #[derive(OpenApi)]
95//! # #[openapi()]
96//! # struct ApiDoc;
97//! #
98//! Redoc::with_config(ApiDoc::openapi(), || json!({ "disableSearch": true }));
99//! ```
100//!
101//! _**Using [`FileConfig`].**_
102//! ```no_run
103//! # use utoipa_redoc::{Redoc, FileConfig};
104//! # use utoipa::OpenApi;
105//! # use serde_json::json;
106//! # #[derive(OpenApi)]
107//! # #[openapi()]
108//! # struct ApiDoc;
109//! #
110//! Redoc::with_config(ApiDoc::openapi(), FileConfig);
111//! ```
112//!
113//! Read more details in [`Config`].
114//!
115//! # Examples
116//!
117//! _**Serve [`Redoc`] via `actix-web` framework.**_
118//! ```no_run
119//! use actix_web::App;
120//! use utoipa_redoc::{Redoc, Servable};
121//!
122//! # use utoipa::OpenApi;
123//! # use std::net::Ipv4Addr;
124//! # #[derive(OpenApi)]
125//! # #[openapi()]
126//! # struct ApiDoc;
127//! App::new().service(Redoc::with_url("/redoc", ApiDoc::openapi()));
128//! ```
129//!
130//! _**Serve [`Redoc`] via `rocket` framework.**_
131//! ```no_run
132//! # use rocket;
133//! use utoipa_redoc::{Redoc, Servable};
134//!
135//! # use utoipa::OpenApi;
136//! # #[derive(OpenApi)]
137//! # #[openapi()]
138//! # struct ApiDoc;
139//! rocket::build()
140//!     .mount(
141//!         "/",
142//!         Redoc::with_url("/redoc", ApiDoc::openapi()),
143//!     );
144//! ```
145//!
146//! _**Serve [`Redoc`] via `axum` framework.**_
147//!  ```no_run
148//!  use axum::Router;
149//!  use utoipa_redoc::{Redoc, Servable};
150//!  # use utoipa::OpenApi;
151//! # #[derive(OpenApi)]
152//! # #[openapi()]
153//! # struct ApiDoc;
154//! #
155//! # fn inner<S>()
156//! # where
157//! #     S: Clone + Send + Sync + 'static,
158//! # {
159//!
160//!  let app = Router::<S>::new()
161//!      .merge(Redoc::with_url("/redoc", ApiDoc::openapi()));
162//! # }
163//! ```
164//!
165//! _**Use [`Redoc`] to serve OpenAPI spec from url.**_
166//! ```
167//! # use utoipa_redoc::Redoc;
168//! Redoc::new(
169//!   "https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml");
170//! ```
171//!
172//! _**Use [`Redoc`] to serve custom OpenAPI spec using serde's `json!()` macro.**_
173//! ```rust
174//! # use utoipa_redoc::Redoc;
175//! # use serde_json::json;
176//! Redoc::new(json!({"openapi": "3.1.0"}));
177//! ```
178//!
179//! [redoc]: <https://redocly.com/>
180//! [redoc_html_quickstart]: <https://redocly.com/docs/redoc/quickstart/>
181//! [redoc_config]: <https://redocly.com/docs/api-reference-docs/configuration/functionality/#configuration-options-for-api-docs>
182//! [examples]: <https://github.com/juhaku/utoipa/tree/master/examples>
183
184use std::fs::OpenOptions;
185use std::{borrow::Cow, env};
186
187use serde::Serialize;
188use serde_json::{json, Value};
189use utoipa::openapi::OpenApi;
190
191mod actix;
192mod axum;
193mod rocket;
194
195const DEFAULT_HTML: &str = include_str!("../res/redoc.html");
196
197/// Trait makes [`Redoc`] to accept an _`URL`_ the [Redoc][redoc] will be served via predefined web
198/// server.
199///
200/// This is used **only** with **`actix-web`**, **`rocket`** or **`axum`** since they have implicit
201/// implementation for serving the [`Redoc`] via the _`URL`_.
202///
203/// [redoc]: <https://redocly.com/>
204#[cfg(any(feature = "actix-web", feature = "rocket", feature = "axum"))]
205#[cfg_attr(
206    doc_cfg,
207    doc(cfg(any(feature = "actix-web", feature = "rocket", feature = "axum")))
208)]
209pub trait Servable<S>
210where
211    S: Spec,
212{
213    /// Construct a new [`Servable`] instance of _`openapi`_ with given _`url`_.
214    ///
215    /// * **url** Must point to location where the [`Servable`] is served.
216    /// * **openapi** Is [`Spec`] that is served via this [`Servable`] from the _**url**_.
217    fn with_url<U: Into<Cow<'static, str>>>(url: U, openapi: S) -> Self;
218
219    /// Construct a new [`Servable`] instance of _`openapi`_ with given _`url`_ and _`config`_.
220    ///
221    /// * **url** Must point to location where the [`Servable`] is served.
222    /// * **openapi** Is [`Spec`] that is served via this [`Servable`] from the _**url**_.
223    /// * **config** Is custom [`Config`] that is used to configure the [`Servable`].
224    fn with_url_and_config<U: Into<Cow<'static, str>>, C: Config>(
225        url: U,
226        openapi: S,
227        config: C,
228    ) -> Self;
229}
230
231#[cfg(any(feature = "actix-web", feature = "rocket", feature = "axum"))]
232impl<S: Spec> Servable<S> for Redoc<S> {
233    fn with_url<U: Into<Cow<'static, str>>>(url: U, openapi: S) -> Self {
234        Self::with_url_and_config(url, openapi, EmptyConfig)
235    }
236
237    fn with_url_and_config<U: Into<Cow<'static, str>>, C: Config>(
238        url: U,
239        openapi: S,
240        config: C,
241    ) -> Self {
242        Self {
243            url: url.into(),
244            html: Cow::Borrowed(DEFAULT_HTML),
245            openapi,
246            config: config.load(),
247        }
248    }
249}
250
251/// Is standalone instance of [Redoc UI][redoc].
252///
253/// This can be used together with predefined web framework integration or standalone with
254/// framework of your choice. [`Redoc::to_html`] method will convert this [`Redoc`] instance to
255/// servable HTML file.
256///
257/// [redoc]: <https://redocly.com/>
258#[non_exhaustive]
259#[derive(Clone)]
260pub struct Redoc<S: Spec> {
261    #[allow(unused)]
262    url: Cow<'static, str>,
263    html: Cow<'static, str>,
264    openapi: S,
265    config: Value,
266}
267
268impl<S: Spec> Redoc<S> {
269    /// Constructs a new [`Redoc`] instance for given _`openapi`_ [`Spec`].
270    ///
271    /// This will create [`Redoc`] with [`EmptyConfig`].
272    ///
273    /// # Examples
274    ///
275    /// _**Create new [`Redoc`] instance with [`EmptyConfig`].**_
276    /// ```
277    /// # use utoipa_redoc::Redoc;
278    /// # use serde_json::json;
279    /// Redoc::new(json!({"openapi": "3.1.0"}));
280    /// ```
281    pub fn new(openapi: S) -> Self {
282        Self::with_config(openapi, EmptyConfig)
283    }
284
285    /// Constructs a new [`Redoc`] instance for given _`openapi`_ [`Spec`] and _`config`_ [`Config`] of choice.
286    ///
287    /// # Examples
288    ///
289    /// _**Create new [`Redoc`] instance with [`FileConfig`].**_
290    /// ```no_run
291    /// # use utoipa_redoc::{Redoc, FileConfig};
292    /// # use serde_json::json;
293    /// Redoc::with_config(json!({"openapi": "3.1.0"}), FileConfig);
294    /// ```
295    pub fn with_config<C: Config>(openapi: S, config: C) -> Self {
296        Self {
297            html: Cow::Borrowed(DEFAULT_HTML),
298            url: Cow::Borrowed(""),
299            openapi,
300            config: config.load(),
301        }
302    }
303
304    /// Override the [default HTML template][redoc_html_quickstart] with new one. See
305    /// [customization] for more details.
306    ///
307    /// [redoc_html_quickstart]: <https://redocly.com/docs/redoc/quickstart/>
308    /// [customization]: index.html#customization
309    pub fn custom_html<H: Into<Cow<'static, str>>>(mut self, html: H) -> Self {
310        self.html = html.into();
311
312        self
313    }
314
315    /// Converts this [`Redoc`] instance to servable HTML file.
316    ///
317    /// This will replace _**`$config`**_ variable placeholder with [`Config`] of this instance and
318    /// _**`$spec`**_ with [`Spec`] provided to this instance serializing it to JSON from the HTML
319    /// template used with the [`Redoc`]. If HTML template is not overridden with
320    /// [`Redoc::custom_html`] then the [default HTML template][redoc_html_quickstart] will be used.
321    ///
322    /// See more details in [customization][customization].
323    ///
324    /// [redoc_html_quickstart]: <https://redocly.com/docs/redoc/quickstart/>
325    /// [customization]: index.html#customization
326    pub fn to_html(&self) -> String {
327        self.html
328            .replace("$config", &self.config.to_string())
329            .replace(
330                "$spec",
331                &serde_json::to_string(&self.openapi).expect(
332                    "Invalid OpenAPI spec, expected OpenApi, String, &str or serde_json::Value",
333                ),
334            )
335    }
336}
337
338/// Trait defines OpenAPI spec resource types supported by [`Redoc`].
339///
340/// By default this trait is implemented for [`utoipa::openapi::OpenApi`], [`String`], [`&str`] and
341/// [`serde_json::Value`].
342///
343/// * **OpenApi** implementation allows using utoipa's OpenApi struct as a OpenAPI spec resource
344///   for the [`Redoc`].
345/// * **String** and **&str** implementations allows defining HTTP URL for [`Redoc`] to load the
346///   OpenAPI spec from.
347/// * **Value** implementation enables the use of arbitrary JSON values with serde's `json!()`
348///   macro as a OpenAPI spec for the [`Redoc`].
349///
350/// # Examples
351///
352/// _**Use [`Redoc`] to serve utoipa's OpenApi.**_
353/// ```no_run
354/// # use utoipa_redoc::Redoc;
355/// # use utoipa::openapi::OpenApiBuilder;
356/// #
357/// Redoc::new(OpenApiBuilder::new().build());
358/// ```
359///
360/// _**Use [`Redoc`] to serve OpenAPI spec from url.**_
361/// ```
362/// # use utoipa_redoc::Redoc;
363/// Redoc::new(
364///   "https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml");
365/// ```
366///
367/// _**Use [`Redoc`] to serve custom OpenAPI spec using serde's `json!()` macro.**_
368/// ```rust
369/// # use utoipa_redoc::Redoc;
370/// # use serde_json::json;
371/// Redoc::new(json!({"openapi": "3.1.0"}));
372/// ```
373pub trait Spec: Serialize {}
374
375impl Spec for OpenApi {}
376
377impl Spec for String {}
378
379impl Spec for &str {}
380
381impl Spec for Value {}
382
383/// Trait defines configuration options for [`Redoc`].
384///
385/// There are 3 configuration methods [`EmptyConfig`], [`FileConfig`] and [`FnOnce`] closure
386/// config. The [`Config`] must be able to load and serialize valid JSON.
387///
388/// * **EmptyConfig** is the default config and serializes to empty JSON object _`{}`_.
389/// * **FileConfig** Allows [`Redoc`] to be configured via user defined file which serializes to
390///   JSON.
391/// * **FnOnce** closure config allows inlining JSON serializable config directly to [`Redoc`]
392///   declaration.
393///
394/// Configuration format and allowed options can be found from Redocly's own API documentation.
395///
396/// * [All supported Redoc configuration options][redoc_config].
397///
398/// **Note!** There is no validity check for configuration options and all options provided are
399/// serialized as is to the [Redoc][redoc]. It is users own responsibility to check for possible
400/// misspelled configuration options against the valid configuration options.
401///
402/// # Examples
403///
404/// _**Using [`FnOnce`] closure config.**_
405/// ```rust
406/// # use utoipa_redoc::Redoc;
407/// # use utoipa::OpenApi;
408/// # use serde_json::json;
409/// # #[derive(OpenApi)]
410/// # #[openapi()]
411/// # struct ApiDoc;
412/// #
413/// Redoc::with_config(ApiDoc::openapi(), || json!({ "disableSearch": true }));
414/// ```
415///
416/// _**Using [`FileConfig`].**_
417/// ```no_run
418/// # use utoipa_redoc::{Redoc, FileConfig};
419/// # use utoipa::OpenApi;
420/// # use serde_json::json;
421/// # #[derive(OpenApi)]
422/// # #[openapi()]
423/// # struct ApiDoc;
424/// #
425/// Redoc::with_config(ApiDoc::openapi(), FileConfig);
426/// ```
427///
428/// [redoc]: <https://redocly.com/>
429/// [redoc_config]: <https://redocly.com/docs/api-reference-docs/configuration/functionality/#configuration-options-for-api-docs>
430pub trait Config {
431    /// Implementor must implement the logic which loads the configuration of choice and converts it
432    /// to serde's [`serde_json::Value`].
433    fn load(self) -> Value;
434}
435
436impl<S: Serialize, F: FnOnce() -> S> Config for F {
437    fn load(self) -> Value {
438        json!(self())
439    }
440}
441
442/// Makes [`Redoc`] load it's configuration from a user defined file.
443///
444/// The config file must be defined via _**`UTOIPA_REDOC_CONFIG_FILE`**_ env variable for your
445/// application. It can either be defined in runtime before the [`Redoc`] declaration or before
446/// application startup or at compile time via `build.rs` file.
447///
448/// The file must be located relative to your application runtime directory.
449///
450/// The file must be loadable via [`Config`] and it must return a JSON object representing the
451/// [Redoc configuration][redoc_config].
452///
453/// # Examples
454///
455/// _**Using a `build.rs` file to define the config file.**_
456/// ```rust
457/// # fn main() {
458/// println!("cargo:rustc-env=UTOIPA_REDOC_CONFIG_FILE=redoc.config.json");
459/// # }
460/// ```
461///
462/// _**Defining config file at application startup.**_
463/// ```bash
464/// UTOIPA_REDOC_CONFIG_FILE=redoc.config.json cargo run
465/// ```
466///
467/// [redoc_config]: <https://redocly.com/docs/api-reference-docs/configuration/functionality/#configuration-options-for-api-docs>
468pub struct FileConfig;
469
470impl Config for FileConfig {
471    fn load(self) -> Value {
472        let path = env::var("UTOIPA_REDOC_CONFIG_FILE")
473            .expect("Missing `UTOIPA_REDOC_CONFIG_FILE` env variable, cannot load file config.");
474
475        let file = OpenOptions::new()
476            .read(true)
477            .open(&path)
478            .unwrap_or_else(|_| panic!("File `{path}` is not readable or does not exist."));
479        serde_json::from_reader(file).expect("Config file cannot be parsed to JSON")
480    }
481}
482
483/// Is the default configuration and serializes to empty JSON object _`{}`_.
484pub struct EmptyConfig;
485
486impl Config for EmptyConfig {
487    fn load(self) -> Value {
488        json!({})
489    }
490}