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)]
9pub struct OpenApiConfig {
10 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#[derive(Clone, Debug)]
23pub struct OpenApiSpec {
24 pub name: String,
26
27 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 #[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 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 pub fn new() -> Self {
82 Self::default()
83 }
84
85 pub fn with_title(mut self, title: impl Into<String>) -> Self {
87 self.title = title.into();
88 self
89 }
90
91 pub fn with_version(mut self, version: impl Into<String>) -> Self {
93 self.version = version.into();
94 self
95 }
96
97 pub fn with_description(mut self, description: impl Into<String>) -> Self {
99 self.description = Some(description.into());
100 self
101 }
102
103 pub fn with_spec(mut self, spec: OpenApiSpec) -> Self {
107 self.specs = vec![spec];
108 self
109 }
110
111 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 pub fn with_ui(mut self) -> Self {
132 self.ui_enabled = true;
133 self
134 }
135
136 pub fn with_ui_path(mut self, path: impl Into<String>) -> Self {
140 self.ui_path = path.into();
141 self
142 }
143
144 pub fn title(&self) -> &str {
146 &self.title
147 }
148
149 pub fn specs(&self) -> &[OpenApiSpec] {
151 &self.specs
152 }
153
154 pub fn ui_enabled(&self) -> bool {
156 self.ui_enabled
157 }
158
159 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}