1use std::path::Path;
42
43use serde::Deserialize;
44
45#[derive(Debug, Deserialize)]
50#[serde(default)]
51pub struct ProjectConfig {
52 pub error_schema_ref: String,
54
55 pub unimplemented_methods: Vec<String>,
57
58 pub public_methods: Vec<String>,
60
61 pub deprecated_methods: Vec<String>,
63
64 pub plain_text_endpoints: Vec<PlainTextEndpoint>,
66
67 pub metrics_path: Option<String>,
69
70 pub readiness_path: Option<String>,
72
73 pub servers: Vec<ServerEntry>,
75
76 pub info: InfoOverrides,
78
79 pub write_only_fields: Vec<String>,
81
82 pub read_only_fields: Vec<String>,
84
85 pub transforms: TransformConfig,
87}
88
89#[derive(Debug, Clone, Deserialize)]
91pub struct PlainTextEndpoint {
92 pub path: String,
94 pub example: Option<String>,
96}
97
98#[derive(Debug, Clone, Deserialize)]
100pub struct ServerEntry {
101 pub url: String,
103 #[serde(default, skip_serializing_if = "Option::is_none")]
105 pub description: Option<String>,
106}
107
108#[derive(Debug, Clone, Default, Deserialize)]
110#[serde(default)]
111pub struct InfoOverrides {
112 pub contact: Option<ContactInfo>,
114 pub license: Option<LicenseInfo>,
116 pub external_docs: Option<ExternalDocsInfo>,
118 pub terms_of_service: Option<String>,
120}
121
122#[derive(Debug, Clone, Deserialize)]
124pub struct ContactInfo {
125 pub name: Option<String>,
127 pub email: Option<String>,
129 pub url: Option<String>,
131}
132
133#[derive(Debug, Clone, Deserialize)]
135pub struct LicenseInfo {
136 pub name: String,
138 pub url: Option<String>,
140}
141
142#[derive(Debug, Clone, Deserialize)]
144pub struct ExternalDocsInfo {
145 pub url: String,
147 pub description: Option<String>,
149}
150
151#[derive(Debug, Clone, Copy, Deserialize)]
156#[serde(default)]
157#[allow(clippy::struct_excessive_bools)]
158pub struct TransformConfig {
159 pub upgrade_to_3_1: bool,
164
165 pub annotate_sse: bool,
170
171 pub inject_validation: bool,
176
177 pub add_security: bool,
182
183 pub inline_request_bodies: bool,
188
189 pub flatten_uuid_refs: bool,
194
195 pub normalize_line_endings: bool,
200
201 pub inject_servers: bool,
206
207 pub rewrite_create_responses: bool,
212
213 pub annotate_field_access: bool,
219}
220
221impl Default for ProjectConfig {
222 fn default() -> Self {
223 Self {
224 error_schema_ref: crate::DEFAULT_ERROR_SCHEMA_REF.to_string(),
225 unimplemented_methods: Vec::new(),
226 public_methods: Vec::new(),
227 deprecated_methods: Vec::new(),
228 plain_text_endpoints: Vec::new(),
229 metrics_path: None,
230 readiness_path: None,
231 servers: Vec::new(),
232 info: InfoOverrides::default(),
233 write_only_fields: Vec::new(),
234 read_only_fields: Vec::new(),
235 transforms: TransformConfig::default(),
236 }
237 }
238}
239
240impl Default for TransformConfig {
241 fn default() -> Self {
242 Self {
243 upgrade_to_3_1: true,
244 annotate_sse: true,
245 inject_validation: true,
246 add_security: true,
247 inline_request_bodies: true,
248 flatten_uuid_refs: true,
249 normalize_line_endings: true,
250 inject_servers: true,
251 rewrite_create_responses: true,
252 annotate_field_access: true,
253 }
254 }
255}
256
257impl ProjectConfig {
258 pub fn load(path: &Path) -> crate::error::Result<Self> {
264 let content = std::fs::read_to_string(path)?;
265 let config: Self = serde_yaml_ng::from_str(&content)?;
266 Ok(config)
267 }
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273
274 #[test]
275 fn deserialize_defaults() {
276 let config: ProjectConfig = serde_yaml_ng::from_str("{}").unwrap();
277 assert!(config.unimplemented_methods.is_empty());
278 assert!(config.public_methods.is_empty());
279 assert!(config.deprecated_methods.is_empty());
280 assert!(config.plain_text_endpoints.is_empty());
281 assert!(config.metrics_path.is_none());
282 assert!(config.readiness_path.is_none());
283 assert!(config.servers.is_empty());
284 assert!(config.info.contact.is_none());
285 assert!(config.info.license.is_none());
286 assert!(config.write_only_fields.is_empty());
287 assert!(config.read_only_fields.is_empty());
288 assert!(config.transforms.upgrade_to_3_1);
289 assert!(config.transforms.annotate_sse);
290 assert!(config.transforms.inject_servers);
291 assert!(config.transforms.rewrite_create_responses);
292 assert!(config.transforms.annotate_field_access);
293 }
294
295 #[test]
296 fn deserialize_full() {
297 let yaml = r##"
298error_schema_ref: "#/components/schemas/MyError"
299unimplemented_methods:
300 - SetupMfa
301 - DisableMfa
302public_methods:
303 - Authenticate
304deprecated_methods:
305 - OldEndpoint
306plain_text_endpoints:
307 - path: /health/live
308 example: "OK"
309 - path: /metrics
310metrics_path: /metrics
311readiness_path: /health/ready
312servers:
313 - url: https://api.example.com
314 description: Production
315 - url: http://localhost:8080
316 description: Local dev
317info:
318 contact:
319 name: API Team
320 email: api@example.com
321 license:
322 name: MIT
323 url: https://opensource.org/licenses/MIT
324 external_docs:
325 url: https://docs.example.com
326 description: Full documentation
327 terms_of_service: https://example.com/tos
328write_only_fields:
329 - apiKey
330read_only_fields:
331 - lastSyncAt
332transforms:
333 add_security: false
334 inject_servers: false
335"##;
336 let config: ProjectConfig = serde_yaml_ng::from_str(yaml).unwrap();
337 assert_eq!(config.error_schema_ref, "#/components/schemas/MyError");
338 assert_eq!(config.unimplemented_methods, vec!["SetupMfa", "DisableMfa"]);
339 assert_eq!(config.public_methods, vec!["Authenticate"]);
340 assert_eq!(config.deprecated_methods, vec!["OldEndpoint"]);
341 assert_eq!(config.plain_text_endpoints.len(), 2);
342 assert_eq!(config.plain_text_endpoints[0].path, "/health/live");
343 assert_eq!(
344 config.plain_text_endpoints[0].example.as_deref(),
345 Some("OK")
346 );
347 assert!(config.plain_text_endpoints[1].example.is_none());
348 assert_eq!(config.metrics_path.as_deref(), Some("/metrics"));
349 assert_eq!(config.readiness_path.as_deref(), Some("/health/ready"));
350 assert_eq!(config.servers.len(), 2);
351 assert_eq!(config.servers[0].url, "https://api.example.com");
352 assert_eq!(config.servers[0].description.as_deref(), Some("Production"));
353 assert!(config.info.contact.is_some());
354 assert_eq!(
355 config.info.contact.as_ref().unwrap().name.as_deref(),
356 Some("API Team")
357 );
358 assert!(config.info.license.is_some());
359 assert_eq!(config.info.license.as_ref().unwrap().name, "MIT");
360 assert!(config.info.external_docs.is_some());
361 assert_eq!(
362 config.info.terms_of_service.as_deref(),
363 Some("https://example.com/tos")
364 );
365 assert_eq!(config.write_only_fields, vec!["apiKey"]);
366 assert_eq!(config.read_only_fields, vec!["lastSyncAt"]);
367 assert!(!config.transforms.add_security);
368 assert!(!config.transforms.inject_servers);
369 assert!(config.transforms.upgrade_to_3_1);
371 assert!(config.transforms.inline_request_bodies);
372 assert!(config.transforms.rewrite_create_responses);
373 assert!(config.transforms.annotate_field_access);
374 }
375
376 #[test]
377 fn load_from_file() {
378 let dir = std::env::temp_dir().join("tonic-rest-openapi-test");
379 std::fs::create_dir_all(&dir).unwrap();
380 let path = dir.join("test-config.yaml");
381 std::fs::write(
382 &path,
383 "public_methods:\n - Login\nmetrics_path: /metrics\n",
384 )
385 .unwrap();
386
387 let config = ProjectConfig::load(&path).unwrap();
388 assert_eq!(config.public_methods, vec!["Login"]);
389 assert_eq!(config.metrics_path.as_deref(), Some("/metrics"));
390 assert!(config.transforms.upgrade_to_3_1);
392
393 std::fs::remove_dir_all(&dir).ok();
394 }
395
396 #[test]
397 fn load_nonexistent_file_returns_error() {
398 let result = ProjectConfig::load(Path::new("/nonexistent/config.yaml"));
399 assert!(result.is_err());
400 }
401
402 #[test]
403 fn load_invalid_yaml_returns_error() {
404 let dir = std::env::temp_dir().join("tonic-rest-openapi-test-invalid");
405 std::fs::create_dir_all(&dir).unwrap();
406 let path = dir.join("bad.yaml");
407 std::fs::write(&path, "public_methods: [[[invalid").unwrap();
408
409 let result = ProjectConfig::load(&path);
410 assert!(result.is_err());
411
412 std::fs::remove_dir_all(&dir).ok();
413 }
414}