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