Skip to main content

volga_open_api/
config.rs

1//! Types and utils for OpenAPI configuration.
2
3pub(super) const DEFAULT_SPEC_PATH: &str = "/openapi.json";
4const DEFAULT_SPEC_NAME: &str = "v1";
5const DEFAULT_UI_PATH: &str = "/openapi";
6
7/// OpenAPI runtime configuration.
8#[derive(Clone, Debug, serde::Deserialize)]
9#[serde(default)]
10pub struct OpenApiConfig {
11    /// Returns `true` if the OpenAPI have been exposed.
12    ///
13    /// This is a runtime flag set by `App::use_open_api` and is not configurable from a file.
14    #[serde(skip)]
15    pub exposed: bool,
16
17    pub(super) title: String,
18    pub(super) version: String,
19    pub(super) description: Option<String>,
20    pub(super) specs: Vec<OpenApiSpec>,
21    pub(super) ui_enabled: bool,
22    pub(super) ui_path: String,
23}
24
25/// OpenAPI spec
26#[derive(Clone, Debug, serde::Deserialize)]
27pub struct OpenApiSpec {
28    /// Spec name. Used to distinguish between multiple OpenAPI specs.
29    pub name: String,
30
31    /// Path to OpenAPI spec JSON.
32    pub spec_path: String,
33}
34
35impl Default for OpenApiConfig {
36    fn default() -> Self {
37        Self {
38            exposed: false,
39            title: "Volga API".to_string(),
40            version: "0.1.0".to_string(),
41            description: None,
42            specs: vec![OpenApiSpec::default()],
43            ui_enabled: false,
44            ui_path: DEFAULT_UI_PATH.to_string(),
45        }
46    }
47}
48
49impl Default for OpenApiSpec {
50    fn default() -> Self {
51        Self {
52            name: DEFAULT_SPEC_NAME.to_string(),
53            spec_path: DEFAULT_SPEC_PATH.to_string(),
54        }
55    }
56}
57
58impl<T: Into<String>> From<T> for OpenApiSpec {
59    #[inline]
60    fn from(value: T) -> Self {
61        Self::new(value)
62    }
63}
64
65impl OpenApiSpec {
66    /// Creates a new OpenAPI spec with the given name.
67    #[inline]
68    pub fn new(name: impl Into<String>) -> Self {
69        let name = name.into();
70        Self {
71            spec_path: format!("/{name}{DEFAULT_SPEC_PATH}"),
72            name,
73        }
74    }
75
76    /// Sets OpenAPI spec path.
77    pub fn with_path(mut self, path: impl Into<String>) -> Self {
78        self.spec_path = path.into();
79        self
80    }
81}
82
83impl OpenApiConfig {
84    /// Creates a new [`OpenApiConfig`] with defaults.
85    pub fn new() -> Self {
86        Self::default()
87    }
88
89    /// Sets the OpenAPI document title.
90    pub fn with_title(mut self, title: impl Into<String>) -> Self {
91        self.title = title.into();
92        self
93    }
94
95    /// Sets the OpenAPI document version.
96    pub fn with_version(mut self, version: impl Into<String>) -> Self {
97        self.version = version.into();
98        self
99    }
100
101    /// Sets the OpenAPI document description.
102    pub fn with_description(mut self, description: impl Into<String>) -> Self {
103        self.description = Some(description.into());
104        self
105    }
106
107    /// Sets the OpenAPI spec.
108    ///
109    /// Default: `/openapi.json`
110    pub fn with_spec(mut self, spec: OpenApiSpec) -> Self {
111        self.specs = vec![spec];
112        self
113    }
114
115    /// Sets the OpenAPI specs.
116    ///
117    /// Default: `/openapi.json`
118    pub fn with_specs<I, S>(mut self, specs: I) -> Self
119    where
120        I: IntoIterator<Item = S>,
121        S: Into<OpenApiSpec>,
122    {
123        let specs: Vec<_> = specs.into_iter().map(Into::into).collect();
124        self.specs = if specs.is_empty() {
125            vec![OpenApiSpec::default()]
126        } else {
127            specs
128        };
129        self
130    }
131
132    /// Enables or disables the OpenAPI UI.
133    ///
134    /// Default: `false`
135    pub fn with_ui(mut self) -> Self {
136        self.ui_enabled = true;
137        self
138    }
139
140    /// Sets the path where the OpenAPI UI is served.
141    ///
142    /// Default: `/openapi`
143    pub fn with_ui_path(mut self, path: impl Into<String>) -> Self {
144        self.ui_path = path.into();
145        self
146    }
147
148    /// Returns the title.
149    pub fn title(&self) -> &str {
150        &self.title
151    }
152
153    /// Returns a list of registered OpenAPI specs.
154    pub fn specs(&self) -> &[OpenApiSpec] {
155        &self.specs
156    }
157
158    /// Returns `true` if UI is enabled.
159    pub fn ui_enabled(&self) -> bool {
160        self.ui_enabled
161    }
162
163    /// Returns UI URL path.
164    pub fn ui_path(&self) -> &str {
165        &self.ui_path
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::{DEFAULT_SPEC_PATH, OpenApiConfig, OpenApiSpec};
172
173    #[test]
174    fn default_config_contains_single_default_spec_and_ui_disabled() {
175        let config = OpenApiConfig::default();
176
177        assert_eq!(config.title(), "Volga API");
178        assert_eq!(config.specs().len(), 1);
179        assert_eq!(config.specs()[0].name, "v1");
180        assert_eq!(config.specs()[0].spec_path, DEFAULT_SPEC_PATH);
181        assert!(!config.ui_enabled());
182        assert_eq!(config.ui_path(), "/openapi");
183    }
184
185    #[test]
186    fn spec_new_builds_path_from_spec_name() {
187        let spec = OpenApiSpec::new("admin");
188        assert_eq!(spec.name, "admin");
189        assert_eq!(spec.spec_path, "/admin/openapi.json");
190    }
191
192    #[test]
193    fn with_specs_accepts_mixed_inputs() {
194        let config = OpenApiConfig::new().with_specs([
195            OpenApiSpec::new("v1").with_path("/docs/v1.json"),
196            OpenApiSpec::from("v2"),
197        ]);
198
199        assert_eq!(config.specs().len(), 2);
200        assert_eq!(config.specs()[0].spec_path, "/docs/v1.json");
201        assert_eq!(config.specs()[1].name, "v2");
202        assert_eq!(config.specs()[1].spec_path, "/v2/openapi.json");
203    }
204
205    #[test]
206    fn with_spec_replaces_existing_specs() {
207        let config = OpenApiConfig::new().with_spec(OpenApiSpec::new("admin"));
208
209        assert_eq!(config.specs().len(), 1);
210        assert_eq!(config.specs()[0].name, "admin");
211        assert_eq!(config.specs()[0].spec_path, "/admin/openapi.json");
212    }
213
214    #[test]
215    fn with_title_version_and_description_override_defaults() {
216        let config = OpenApiConfig::new()
217            .with_title("Custom API")
218            .with_version("2.5.1")
219            .with_description("custom description");
220
221        assert_eq!(config.title(), "Custom API");
222        assert_eq!(config.version, "2.5.1");
223        assert_eq!(config.description.as_deref(), Some("custom description"));
224    }
225
226    #[test]
227    fn with_specs_restores_default_when_given_empty_list() {
228        let config = OpenApiConfig::new().with_specs(Vec::<OpenApiSpec>::new());
229
230        assert_eq!(config.specs().len(), 1);
231        assert_eq!(config.specs()[0].name, "v1");
232        assert_eq!(config.specs()[0].spec_path, DEFAULT_SPEC_PATH);
233    }
234}