|
1 | 1 | //! Metadata collection and storage for routes and schemas |
2 | 2 |
|
3 | | -use std::collections::BTreeMap; |
| 3 | +use std::collections::{BTreeMap, HashMap}; |
4 | 4 |
|
5 | 5 | use serde::{Deserialize, Serialize}; |
6 | 6 |
|
@@ -102,6 +102,29 @@ impl CollectedMetadata { |
102 | 102 | structs: Vec::new(), |
103 | 103 | } |
104 | 104 | } |
| 105 | + |
| 106 | + /// Check for duplicate schema names among `include_in_openapi` structs. |
| 107 | + /// Returns `Err` with a descriptive message if duplicates are found. |
| 108 | + pub fn check_duplicate_schema_names(&self) -> Result<(), String> { |
| 109 | + let mut seen: HashMap<&str, usize> = HashMap::new(); |
| 110 | + for (i, s) in self.structs.iter().enumerate() { |
| 111 | + if !s.include_in_openapi { |
| 112 | + continue; |
| 113 | + } |
| 114 | + if let Some(&prev_idx) = seen.get(s.name.as_str()) { |
| 115 | + // Only report if definitions actually differ (identical re-registration is OK) |
| 116 | + if self.structs[prev_idx].definition != s.definition { |
| 117 | + return Err(format!( |
| 118 | + "Duplicate OpenAPI schema name '{}'. Two different structs produce the same schema name, which would corrupt the OpenAPI spec. Rename one of them or use #[schema(name = \"...\")].", |
| 119 | + s.name |
| 120 | + )); |
| 121 | + } |
| 122 | + } else { |
| 123 | + seen.insert(&s.name, i); |
| 124 | + } |
| 125 | + } |
| 126 | + Ok(()) |
| 127 | + } |
105 | 128 | } |
106 | 129 |
|
107 | 130 | #[cfg(test)] |
@@ -167,4 +190,63 @@ mod tests { |
167 | 190 | assert!(meta.routes.is_empty()); |
168 | 191 | assert!(meta.structs.is_empty()); |
169 | 192 | } |
| 193 | + |
| 194 | + #[test] |
| 195 | + fn test_check_duplicate_schema_names_no_duplicates() { |
| 196 | + let mut meta = CollectedMetadata::new(); |
| 197 | + meta.structs |
| 198 | + .push(StructMetadata::new("User".into(), "struct User {}".into())); |
| 199 | + meta.structs |
| 200 | + .push(StructMetadata::new("Post".into(), "struct Post {}".into())); |
| 201 | + assert!(meta.check_duplicate_schema_names().is_ok()); |
| 202 | + } |
| 203 | + |
| 204 | + #[test] |
| 205 | + fn test_check_duplicate_schema_names_different_definitions() { |
| 206 | + let mut meta = CollectedMetadata::new(); |
| 207 | + meta.structs.push(StructMetadata::new( |
| 208 | + "User".into(), |
| 209 | + "struct User { id: i32 }".into(), |
| 210 | + )); |
| 211 | + meta.structs.push(StructMetadata::new( |
| 212 | + "User".into(), |
| 213 | + "struct User { name: String }".into(), |
| 214 | + )); |
| 215 | + let err = meta.check_duplicate_schema_names().unwrap_err(); |
| 216 | + assert!( |
| 217 | + err.contains("Duplicate OpenAPI schema name 'User'"), |
| 218 | + "got: {err}" |
| 219 | + ); |
| 220 | + } |
| 221 | + |
| 222 | + #[test] |
| 223 | + fn test_check_duplicate_schema_names_identical_definition_ok() { |
| 224 | + let mut meta = CollectedMetadata::new(); |
| 225 | + let def = "struct User { id: i32 }".to_string(); |
| 226 | + meta.structs |
| 227 | + .push(StructMetadata::new("User".into(), def.clone())); |
| 228 | + meta.structs.push(StructMetadata::new("User".into(), def)); |
| 229 | + assert!(meta.check_duplicate_schema_names().is_ok()); |
| 230 | + } |
| 231 | + |
| 232 | + #[test] |
| 233 | + fn test_check_duplicate_schema_names_ignores_models() { |
| 234 | + let mut meta = CollectedMetadata::new(); |
| 235 | + meta.structs.push(StructMetadata::new_model( |
| 236 | + "Model".into(), |
| 237 | + "struct Model { id: i32 }".into(), |
| 238 | + )); |
| 239 | + meta.structs.push(StructMetadata::new_model( |
| 240 | + "Model".into(), |
| 241 | + "struct Model { name: String }".into(), |
| 242 | + )); |
| 243 | + // Models (include_in_openapi=false) are not checked |
| 244 | + assert!(meta.check_duplicate_schema_names().is_ok()); |
| 245 | + } |
| 246 | + |
| 247 | + #[test] |
| 248 | + fn test_check_duplicate_schema_names_empty() { |
| 249 | + let meta = CollectedMetadata::new(); |
| 250 | + assert!(meta.check_duplicate_schema_names().is_ok()); |
| 251 | + } |
170 | 252 | } |
0 commit comments