Skip to main content

reinhardt_conf/settings/
openapi.rs

1//! OpenAPI documentation endpoint configuration
2//!
3//! Provides composable OpenAPI configuration as a `SettingsFragment`.
4//!
5//! # TOML Configuration
6//!
7//! ```toml
8//! [openapi]
9//! enabled = true
10//! title = "My API"
11//! version = "2.0.0"
12//! swagger_path = "/api/swagger"
13//! redoc_path = "/api/redoc"
14//! json_path = "/api/openapi.json"
15//! description = "API documentation"
16//! ```
17
18use reinhardt_macros::settings;
19
20/// OpenAPI documentation endpoint configuration fragment.
21///
22/// Controls how OpenAPI documentation endpoints are mounted by the
23/// `runserver` command. Maps to the `[openapi]` TOML section.
24///
25/// # Default Paths
26///
27/// - Swagger UI: `/api/docs`
28/// - Redoc UI: `/api/redoc`
29/// - OpenAPI JSON: `/api/openapi.json`
30///
31/// # Example
32///
33/// ```rust
34/// use reinhardt_conf::settings::openapi::OpenApiSettings;
35/// use reinhardt_conf::settings::fragment::SettingsFragment;
36///
37/// let settings = OpenApiSettings::default();
38/// assert!(settings.enabled);
39/// assert_eq!(OpenApiSettings::section(), "openapi");
40/// ```
41#[settings(fragment = true, section = "openapi")]
42#[non_exhaustive]
43pub struct OpenApiSettings {
44	/// Enable OpenAPI endpoints (default: true)
45	///
46	/// Set to `false` to disable automatic endpoint mounting.
47	/// This is equivalent to using the `--no-docs` command-line flag.
48	#[serde(default = "default_true")]
49	pub enabled: bool,
50
51	/// OpenAPI JSON endpoint path (default: "/api/openapi.json")
52	///
53	/// The path where the OpenAPI 3.0 JSON schema will be served.
54	#[serde(default = "default_json_path")]
55	pub json_path: String,
56
57	/// Swagger UI endpoint path (default: "/api/docs")
58	///
59	/// The path where the interactive Swagger UI will be served.
60	#[serde(default = "default_swagger_path")]
61	pub swagger_path: String,
62
63	/// Redoc UI endpoint path (default: "/api/redoc")
64	///
65	/// The path where the alternative Redoc UI will be served.
66	#[serde(default = "default_redoc_path")]
67	pub redoc_path: String,
68
69	/// API title (default: "API Documentation")
70	///
71	/// The title displayed in the OpenAPI schema and documentation UIs.
72	#[serde(default = "default_title")]
73	pub title: String,
74
75	/// API version (default: "1.0.0")
76	///
77	/// The version displayed in the OpenAPI schema.
78	#[serde(default = "default_version")]
79	pub version: String,
80
81	/// API description (optional)
82	///
83	/// An optional description displayed in the OpenAPI schema and documentation UIs.
84	#[serde(default)]
85	pub description: Option<String>,
86}
87
88impl Default for OpenApiSettings {
89	fn default() -> Self {
90		Self {
91			enabled: true,
92			json_path: "/api/openapi.json".to_string(),
93			swagger_path: "/api/docs".to_string(),
94			redoc_path: "/api/redoc".to_string(),
95			title: "API Documentation".to_string(),
96			version: "1.0.0".to_string(),
97			description: None,
98		}
99	}
100}
101
102// Default value functions for serde
103fn default_true() -> bool {
104	true
105}
106
107fn default_json_path() -> String {
108	"/api/openapi.json".to_string()
109}
110
111fn default_swagger_path() -> String {
112	"/api/docs".to_string()
113}
114
115fn default_redoc_path() -> String {
116	"/api/redoc".to_string()
117}
118
119fn default_title() -> String {
120	"API Documentation".to_string()
121}
122
123fn default_version() -> String {
124	"1.0.0".to_string()
125}
126
127#[cfg(test)]
128mod tests {
129	use super::*;
130	use crate::settings::fragment::SettingsFragment;
131	use rstest::rstest;
132
133	#[rstest]
134	fn test_openapi_section_name() {
135		// Arrange / Act
136		let section = OpenApiSettings::section();
137
138		// Assert
139		assert_eq!(section, "openapi");
140	}
141
142	#[rstest]
143	fn test_openapi_default_values() {
144		// Arrange / Act
145		let settings = OpenApiSettings::default();
146
147		// Assert
148		assert!(settings.enabled);
149		assert_eq!(settings.json_path, "/api/openapi.json");
150		assert_eq!(settings.swagger_path, "/api/docs");
151		assert_eq!(settings.redoc_path, "/api/redoc");
152		assert_eq!(settings.title, "API Documentation");
153		assert_eq!(settings.version, "1.0.0");
154		assert_eq!(settings.description, None);
155	}
156
157	#[rstest]
158	fn test_openapi_serde_roundtrip() {
159		// Arrange
160		let original = OpenApiSettings {
161			title: "My API".to_string(),
162			version: "2.0.0".to_string(),
163			description: Some("Custom API".to_string()),
164			..Default::default()
165		};
166
167		// Act
168		let json = serde_json::to_string(&original).unwrap();
169		let deserialized: OpenApiSettings = serde_json::from_str(&json).unwrap();
170
171		// Assert
172		assert_eq!(deserialized.title, "My API");
173		assert_eq!(deserialized.version, "2.0.0");
174		assert_eq!(deserialized.description, Some("Custom API".to_string()));
175		assert!(deserialized.enabled);
176	}
177
178	#[rstest]
179	fn test_openapi_serde_missing_fields_use_defaults() {
180		// Arrange
181		let json = r#"{"title":"Test API"}"#;
182
183		// Act
184		let settings: OpenApiSettings = serde_json::from_str(json).unwrap();
185
186		// Assert
187		assert_eq!(settings.title, "Test API");
188		assert_eq!(settings.version, "1.0.0");
189		assert!(settings.enabled);
190		assert_eq!(settings.json_path, "/api/openapi.json");
191		assert_eq!(settings.swagger_path, "/api/docs");
192		assert_eq!(settings.redoc_path, "/api/redoc");
193		assert_eq!(settings.description, None);
194	}
195
196	#[rstest]
197	fn test_openapi_toml_deserialization() {
198		// Arrange
199		let toml_str = r#"
200enabled = true
201title = "My REST API"
202version = "3.0.0"
203swagger_path = "/docs/swagger"
204redoc_path = "/docs/redoc"
205json_path = "/docs/openapi.json"
206description = "Full API documentation"
207"#;
208
209		// Act
210		let settings: OpenApiSettings = toml::from_str(toml_str).expect("failed to parse TOML");
211
212		// Assert
213		assert!(settings.enabled);
214		assert_eq!(settings.title, "My REST API");
215		assert_eq!(settings.version, "3.0.0");
216		assert_eq!(settings.swagger_path, "/docs/swagger");
217		assert_eq!(settings.redoc_path, "/docs/redoc");
218		assert_eq!(settings.json_path, "/docs/openapi.json");
219		assert_eq!(
220			settings.description,
221			Some("Full API documentation".to_string())
222		);
223	}
224
225	#[rstest]
226	fn test_openapi_toml_partial_uses_defaults() {
227		// Arrange
228		let toml_str = r#"
229title = "Partial API"
230"#;
231
232		// Act
233		let settings: OpenApiSettings = toml::from_str(toml_str).expect("failed to parse TOML");
234
235		// Assert
236		assert!(settings.enabled);
237		assert_eq!(settings.title, "Partial API");
238		assert_eq!(settings.version, "1.0.0");
239		assert_eq!(settings.swagger_path, "/api/docs");
240	}
241
242	#[rstest]
243	fn test_openapi_disabled() {
244		// Arrange
245		let json = r#"{"enabled":false}"#;
246
247		// Act
248		let settings: OpenApiSettings = serde_json::from_str(json).unwrap();
249
250		// Assert
251		assert!(!settings.enabled);
252		assert_eq!(settings.title, "API Documentation");
253	}
254
255	#[rstest]
256	fn test_has_openapi_settings_trait() {
257		// Arrange
258		struct Wrapper {
259			openapi: OpenApiSettings,
260		}
261
262		impl HasOpenApiSettings for Wrapper {
263			fn openapi(&self) -> &OpenApiSettings {
264				&self.openapi
265			}
266		}
267
268		let wrapper = Wrapper {
269			openapi: OpenApiSettings {
270				title: "Trait Test".to_string(),
271				..Default::default()
272			},
273		};
274
275		// Act
276		let settings = wrapper.openapi();
277
278		// Assert
279		assert_eq!(settings.title, "Trait Test");
280	}
281}