Skip to main content

openapi_ui/
ui.rs

1//! High-level API for generating documentation HTML.
2
3use crate::error::{Result, UIError};
4use crate::openapi::OpenAPISpec;
5use crate::template::{base_template, template, template_with_custom_theme};
6
7pub use crate::theme::ThemeMode;
8
9/// Configuration for the documentation UI generator.
10#[derive(Debug, Clone)]
11pub struct UIConfig {
12    pub spec: OpenAPISpec,
13    pub theme: String,
14    pub base_url: Option<String>,
15    pub favicon: String,
16}
17
18impl Default for UIConfig {
19    fn default() -> Self {
20        Self {
21            spec: OpenAPISpec {
22                openapi: "3.0.0".to_string(),
23                info: crate::openapi::Info {
24                    title: "API Documentation".to_string(),
25                    version: "1.0.0".to_string(),
26                    description: None,
27                    terms_of_service: None,
28                    contact: None,
29                    license: None,
30                    x_logo: None,
31                },
32                servers: vec![],
33                paths: std::collections::HashMap::new(),
34                components: None,
35                security: None,
36                tags: None,
37                external_docs: None,
38            },
39            theme: "system".to_string(),
40            base_url: None,
41            favicon:
42                "https://www.openapis.org/wp-content/uploads/sites/31/2019/06/favicon-140x140.png"
43                    .to_string(),
44        }
45    }
46}
47
48/// Generates HTML from a [`UIConfig`].
49pub fn generate_ui_with_config(config: UIConfig) -> String {
50    template(&config.spec, &config.theme, &config.favicon)
51}
52
53/// Generates HTML from a parsed [`OpenAPISpec`].
54pub fn generate_ui(spec: &OpenAPISpec) -> String {
55    let config = UIConfig {
56        spec: spec.clone(),
57        ..Default::default()
58    };
59    template(spec, &config.theme, &config.favicon)
60}
61
62/// Generates a demo page using built-in sample data.
63pub fn generate_base_ui() -> String {
64    base_template()
65}
66
67/// Builder for constructing documentation HTML with a fluent API.
68pub struct UIBuilder {
69    config: UIConfig,
70}
71
72impl UIBuilder {
73    /// Creates a new builder from an [`OpenAPISpec`].
74    pub fn new(spec: OpenAPISpec) -> Self {
75        Self {
76            config: UIConfig {
77                spec,
78                ..Default::default()
79            },
80        }
81    }
82
83    /// Sets the theme mode (`"light"`, `"dark"`, or `"system"`).
84    pub fn theme(mut self, theme: &str) -> Self {
85        self.config.theme = theme.to_string();
86        self
87    }
88
89    /// Sets the base URL for API requests.
90    pub fn base_url(mut self, url: &str) -> Self {
91        self.config.base_url = Some(url.to_string());
92        self
93    }
94
95    /// Sets a custom favicon URL.
96    pub fn favicon(mut self, url: &str) -> Self {
97        self.config.favicon = url.to_string();
98        self
99    }
100
101    /// Builds and returns the final HTML string.
102    pub fn build(self) -> String {
103        generate_ui_with_config(self.config)
104    }
105}
106
107/// Generates documentation HTML from an OpenAPI JSON string.
108///
109/// This is the primary entry point for the library. Pass an OpenAPI JSON string,
110/// a [`ThemeMode`], optional custom CSS, and an optional favicon URL.
111///
112/// Returns the generated HTML as a `String`, or a [`UIError`] on failure.
113pub fn generate_docs(
114    json: &str,
115    mode: ThemeMode,
116    custom_css: Option<&str>,
117    favicon: Option<&str>,
118) -> Result<String> {
119    if json.trim().is_empty() {
120        return Ok(generate_base_ui());
121    }
122    let spec: OpenAPISpec = serde_json::from_str(json).map_err(UIError::JsonError)?;
123    let fav = favicon.unwrap_or(
124        "https://www.openapis.org/wp-content/uploads/sites/31/2019/06/favicon-140x140.png",
125    );
126    Ok(template_with_custom_theme(
127        &spec,
128        mode.as_str(),
129        custom_css,
130        fav,
131    ))
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137    use crate::openapi::Info;
138    use std::collections::HashMap;
139
140    #[test]
141    fn test_generate_ui() {
142        let spec = OpenAPISpec {
143            openapi: "3.0.0".to_string(),
144            info: Info {
145                title: "Test API".to_string(),
146                version: "1.0.0".to_string(),
147                description: Some("A test API".to_string()),
148                terms_of_service: None,
149                contact: None,
150                license: None,
151                x_logo: None,
152            },
153            servers: vec![],
154            paths: HashMap::new(),
155            components: None,
156            security: None,
157            tags: None,
158            external_docs: None,
159        };
160
161        let html = generate_ui(&spec);
162        assert!(html.contains("Test API"));
163        assert!(html.contains("<!doctype html>"));
164    }
165
166    #[test]
167    fn test_generate_docs() {
168        let json = r#"{
169            "openapi": "3.0.0",
170            "info": {
171                "title": "JSON API",
172                "version": "2.0.0"
173            },
174            "paths": {}
175        }"#;
176
177        let result = generate_docs(json, ThemeMode::System, None, None);
178        assert!(result.is_ok());
179        let html = result.unwrap();
180        assert!(html.contains("JSON API"));
181    }
182
183    #[test]
184    fn test_generate_docs_with_theme_mode() {
185        let json = r#"{
186            "openapi": "3.0.0",
187            "info": {
188                "title": "Theme Mode API",
189                "version": "1.0.0"
190            },
191            "paths": {}
192        }"#;
193
194        // Test light mode
195        let html = generate_docs(json, ThemeMode::Light, None, None).unwrap();
196        assert!(html.contains("Theme Mode API"));
197        assert!(html.contains(":root"));
198
199        // Test dark mode
200        let html = generate_docs(json, ThemeMode::Dark, None, None).unwrap();
201        assert!(html.contains("Theme Mode API"));
202        assert!(html.contains("[data-theme=\"dark\"]"));
203    }
204
205    #[test]
206    fn test_ui_builder() {
207        let spec = OpenAPISpec {
208            openapi: "3.0.0".to_string(),
209            info: Info {
210                title: "Builder API".to_string(),
211                version: "1.0.0".to_string(),
212                description: None,
213                terms_of_service: None,
214                contact: None,
215                license: None,
216                x_logo: None,
217            },
218            servers: vec![],
219            paths: HashMap::new(),
220            components: None,
221            security: None,
222            tags: None,
223            external_docs: None,
224        };
225
226        let html = UIBuilder::new(spec)
227            .theme("light")
228            .base_url("https://api.example.com")
229            .build();
230
231        assert!(html.contains("Builder API"));
232    }
233
234    #[test]
235    fn test_generate_base_ui() {
236        let html = generate_base_ui();
237        assert!(html.contains("<!doctype html>"));
238        assert!(html.contains("INJECTED_SPEC"));
239    }
240}