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