1pub(super) const DEFAULT_SPEC_PATH: &str = "/openapi.json";
4const DEFAULT_SPEC_NAME: &str = "v1";
5const DEFAULT_UI_PATH: &str = "/openapi";
6
7#[derive(Clone, Debug, serde::Deserialize)]
9#[serde(default)]
10pub struct OpenApiConfig {
11 #[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#[derive(Clone, Debug, serde::Deserialize)]
27pub struct OpenApiSpec {
28 pub name: String,
30
31 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 #[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 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 pub fn new() -> Self {
86 Self::default()
87 }
88
89 pub fn with_title(mut self, title: impl Into<String>) -> Self {
91 self.title = title.into();
92 self
93 }
94
95 pub fn with_version(mut self, version: impl Into<String>) -> Self {
97 self.version = version.into();
98 self
99 }
100
101 pub fn with_description(mut self, description: impl Into<String>) -> Self {
103 self.description = Some(description.into());
104 self
105 }
106
107 pub fn with_spec(mut self, spec: OpenApiSpec) -> Self {
111 self.specs = vec![spec];
112 self
113 }
114
115 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 pub fn with_ui(mut self) -> Self {
136 self.ui_enabled = true;
137 self
138 }
139
140 pub fn with_ui_path(mut self, path: impl Into<String>) -> Self {
144 self.ui_path = path.into();
145 self
146 }
147
148 pub fn title(&self) -> &str {
150 &self.title
151 }
152
153 pub fn specs(&self) -> &[OpenApiSpec] {
155 &self.specs
156 }
157
158 pub fn ui_enabled(&self) -> bool {
160 self.ui_enabled
161 }
162
163 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}