1use crate::{
2 error::{OpenApiError, OpenApiResult},
3 specification::Schema,
4};
5use serde_json::Value;
6use std::collections::HashMap;
7
8pub struct SchemaGenerator {
10 schemas: HashMap<String, Schema>,
12 config: SchemaConfig,
14}
15
16#[derive(Debug, Clone)]
18pub struct SchemaConfig {
19 pub nullable_optional: bool,
21 pub include_examples: bool,
23 pub custom_mappings: HashMap<String, Schema>,
25}
26
27#[derive(Debug, Clone)]
29pub struct TypeSchema {
30 pub name: String,
32 pub schema: Schema,
34 pub dependencies: Vec<String>,
36}
37
38impl SchemaGenerator {
39 pub fn new(config: SchemaConfig) -> Self {
41 Self {
42 schemas: HashMap::new(),
43 config,
44 }
45 }
46
47 pub fn generate_schema(&mut self, type_name: &str) -> OpenApiResult<Schema> {
49 if let Some(schema) = self.schemas.get(type_name) {
51 return Ok(schema.clone());
52 }
53
54 if let Some(schema) = self.config.custom_mappings.get(type_name) {
56 self.schemas.insert(type_name.to_string(), schema.clone());
57 return Ok(schema.clone());
58 }
59
60 let schema = self.generate_schema_for_type(type_name)?;
62 self.schemas.insert(type_name.to_string(), schema.clone());
63 Ok(schema)
64 }
65
66 fn generate_schema_for_type(&self, type_name: &str) -> OpenApiResult<Schema> {
68 match type_name {
69 "String" | "str" | "&str" => Ok(Schema {
71 schema_type: Some("string".to_string()),
72 ..Default::default()
73 }),
74
75 "i8" | "i16" | "i32" => Ok(Schema {
77 schema_type: Some("integer".to_string()),
78 format: Some("int32".to_string()),
79 ..Default::default()
80 }),
81 "i64" => Ok(Schema {
82 schema_type: Some("integer".to_string()),
83 format: Some("int64".to_string()),
84 ..Default::default()
85 }),
86 "u8" | "u16" | "u32" => Ok(Schema {
87 schema_type: Some("integer".to_string()),
88 format: Some("int32".to_string()),
89 minimum: Some(0.0),
90 ..Default::default()
91 }),
92 "u64" => Ok(Schema {
93 schema_type: Some("integer".to_string()),
94 format: Some("int64".to_string()),
95 minimum: Some(0.0),
96 ..Default::default()
97 }),
98 "f32" => Ok(Schema {
99 schema_type: Some("number".to_string()),
100 format: Some("float".to_string()),
101 ..Default::default()
102 }),
103 "f64" => Ok(Schema {
104 schema_type: Some("number".to_string()),
105 format: Some("double".to_string()),
106 ..Default::default()
107 }),
108
109 "bool" => Ok(Schema {
111 schema_type: Some("boolean".to_string()),
112 ..Default::default()
113 }),
114
115 "Uuid" => Ok(Schema {
117 schema_type: Some("string".to_string()),
118 format: Some("uuid".to_string()),
119 ..Default::default()
120 }),
121
122 "DateTime" | "DateTime<Utc>" => Ok(Schema {
124 schema_type: Some("string".to_string()),
125 format: Some("date-time".to_string()),
126 ..Default::default()
127 }),
128 "NaiveDate" => Ok(Schema {
129 schema_type: Some("string".to_string()),
130 format: Some("date".to_string()),
131 ..Default::default()
132 }),
133
134 type_name if type_name.starts_with("Option<") => {
136 let inner_type = self.extract_generic_type(type_name, "Option")?;
137 let mut schema = self.generate_schema_for_type(&inner_type)?;
138 if self.config.nullable_optional {
139 schema.nullable = Some(true);
140 }
141 Ok(schema)
142 }
143 type_name if type_name.starts_with("Vec<") => {
144 let inner_type = self.extract_generic_type(type_name, "Vec")?;
145 let items_schema = self.generate_schema_for_type(&inner_type)?;
146 Ok(Schema {
147 schema_type: Some("array".to_string()),
148 items: Some(Box::new(items_schema)),
149 ..Default::default()
150 })
151 }
152 type_name if type_name.starts_with("HashMap<") => {
153 let value_type = self.extract_hashmap_value_type(type_name)?;
155 let value_schema = self.generate_schema_for_type(&value_type)?;
156 Ok(Schema {
157 schema_type: Some("object".to_string()),
158 additional_properties: Some(Box::new(value_schema)),
159 ..Default::default()
160 })
161 }
162
163 _ => Ok(Schema {
165 reference: Some(format!("#/components/schemas/{}", type_name)),
166 ..Default::default()
167 }),
168 }
169 }
170
171 fn extract_generic_type(&self, type_name: &str, wrapper: &str) -> OpenApiResult<String> {
173 let start = wrapper.len() + 1; let end = type_name.len() - 1; if start >= end {
177 return Err(OpenApiError::schema_error(format!(
178 "Invalid generic type: {}",
179 type_name
180 )));
181 }
182
183 Ok(type_name[start..end].to_string())
184 }
185
186 fn extract_hashmap_value_type(&self, type_name: &str) -> OpenApiResult<String> {
188 let inner = type_name
190 .strip_prefix("HashMap<")
191 .and_then(|s| s.strip_suffix(">"))
192 .ok_or_else(|| {
193 OpenApiError::schema_error(format!("Invalid HashMap type: {}", type_name))
194 })?;
195
196 let parts: Vec<&str> = inner.split(',').collect();
197 if parts.len() != 2 {
198 return Err(OpenApiError::schema_error(format!(
199 "Invalid HashMap type: {}",
200 type_name
201 )));
202 }
203
204 Ok(parts[1].trim().to_string())
205 }
206
207 pub fn generate_struct_schema(
209 &mut self,
210 struct_name: &str,
211 fields: &[(String, String, Option<String>)], ) -> OpenApiResult<Schema> {
213 let mut properties = HashMap::new();
214 let mut required = Vec::new();
215 let mut dependencies = Vec::new();
216
217 for (field_name, field_type, description) in fields {
218 let mut field_schema = self.generate_schema(field_type)?;
219
220 if let Some(desc) = description {
221 field_schema.description = Some(desc.clone());
222 }
223
224 if !field_type.starts_with("Option<") {
226 required.push(field_name.clone());
227 }
228
229 properties.insert(field_name.clone(), field_schema);
230
231 if !self.is_primitive_type(field_type) {
233 dependencies.push(field_type.clone());
234 }
235 }
236
237 let schema = Schema {
238 schema_type: Some("object".to_string()),
239 properties,
240 required,
241 ..Default::default()
242 };
243
244 self.schemas.insert(struct_name.to_string(), schema.clone());
245 Ok(schema)
246 }
247
248 pub fn generate_enum_schema(
250 &mut self,
251 enum_name: &str,
252 variants: &[String],
253 ) -> OpenApiResult<Schema> {
254 let enum_values: Vec<Value> = variants.iter().map(|v| Value::String(v.clone())).collect();
255
256 let schema = Schema {
257 schema_type: Some("string".to_string()),
258 enum_values,
259 ..Default::default()
260 };
261
262 self.schemas.insert(enum_name.to_string(), schema.clone());
263 Ok(schema)
264 }
265
266 fn is_primitive_type(&self, type_name: &str) -> bool {
268 matches!(
269 type_name,
270 "String"
271 | "str"
272 | "&str"
273 | "i8"
274 | "i16"
275 | "i32"
276 | "i64"
277 | "u8"
278 | "u16"
279 | "u32"
280 | "u64"
281 | "f32"
282 | "f64"
283 | "bool"
284 | "Uuid"
285 | "DateTime"
286 | "DateTime<Utc>"
287 | "NaiveDate"
288 ) || type_name.starts_with("Option<")
289 || type_name.starts_with("Vec<")
290 || type_name.starts_with("HashMap<")
291 }
292
293 pub fn get_schemas(&self) -> &HashMap<String, Schema> {
295 &self.schemas
296 }
297
298 pub fn clear_cache(&mut self) {
300 self.schemas.clear();
301 }
302}
303
304impl Default for SchemaConfig {
305 fn default() -> Self {
306 Self {
307 nullable_optional: true,
308 include_examples: true,
309 custom_mappings: HashMap::new(),
310 }
311 }
312}
313
314impl SchemaConfig {
315 pub fn new() -> Self {
317 Self::default()
318 }
319
320 pub fn with_nullable_optional(mut self, nullable: bool) -> Self {
322 self.nullable_optional = nullable;
323 self
324 }
325
326 pub fn with_examples(mut self, include: bool) -> Self {
328 self.include_examples = include;
329 self
330 }
331
332 pub fn with_custom_mapping(mut self, type_name: &str, schema: Schema) -> Self {
334 self.custom_mappings.insert(type_name.to_string(), schema);
335 self
336 }
337}
338
339#[cfg(test)]
340mod tests {
341 use super::*;
342
343 #[test]
344 fn test_primitive_schema_generation() {
345 let mut generator = SchemaGenerator::new(SchemaConfig::default());
346
347 let string_schema = generator.generate_schema("String").unwrap();
348 assert_eq!(string_schema.schema_type, Some("string".to_string()));
349
350 let int_schema = generator.generate_schema("i32").unwrap();
351 assert_eq!(int_schema.schema_type, Some("integer".to_string()));
352 assert_eq!(int_schema.format, Some("int32".to_string()));
353
354 let bool_schema = generator.generate_schema("bool").unwrap();
355 assert_eq!(bool_schema.schema_type, Some("boolean".to_string()));
356 }
357
358 #[test]
359 fn test_optional_schema_generation() {
360 let mut generator = SchemaGenerator::new(SchemaConfig::default());
361
362 let optional_string_schema = generator.generate_schema("Option<String>").unwrap();
363 assert_eq!(
364 optional_string_schema.schema_type,
365 Some("string".to_string())
366 );
367 assert_eq!(optional_string_schema.nullable, Some(true));
368 }
369
370 #[test]
371 fn test_array_schema_generation() {
372 let mut generator = SchemaGenerator::new(SchemaConfig::default());
373
374 let array_schema = generator.generate_schema("Vec<String>").unwrap();
375 assert_eq!(array_schema.schema_type, Some("array".to_string()));
376 assert!(array_schema.items.is_some());
377
378 let items = array_schema.items.unwrap();
379 assert_eq!(items.schema_type, Some("string".to_string()));
380 }
381
382 #[test]
383 fn test_struct_schema_generation() {
384 let mut generator = SchemaGenerator::new(SchemaConfig::default());
385
386 let fields = vec![
387 ("id".to_string(), "i32".to_string(), None),
388 (
389 "name".to_string(),
390 "String".to_string(),
391 Some("User name".to_string()),
392 ),
393 ("email".to_string(), "Option<String>".to_string(), None),
394 ];
395
396 let schema = generator.generate_struct_schema("User", &fields).unwrap();
397 assert_eq!(schema.schema_type, Some("object".to_string()));
398 assert_eq!(schema.properties.len(), 3);
399 assert_eq!(schema.required.len(), 2); assert!(schema.properties.contains_key("id"));
401 assert!(schema.properties.contains_key("name"));
402 assert!(schema.properties.contains_key("email"));
403 }
404
405 #[test]
406 fn test_tuple_schema_representation() {
407 let tuple_schema = crate::specification::Schema {
412 schema_type: Some("array".to_string()),
413 title: Some("TestTuple".to_string()),
414 description: Some("A tuple with 2 fields in fixed order: (String, i32). Note: OpenAPI 3.0 cannot precisely represent tuple types - this is a generic array representation.".to_string()),
415 items: Some(Box::new(crate::specification::Schema {
416 description: Some("Tuple element (type varies by position)".to_string()),
417 ..Default::default()
418 })),
419 ..Default::default()
420 };
421
422 assert_eq!(tuple_schema.schema_type, Some("array".to_string()));
424 assert!(tuple_schema.description.is_some());
425 assert!(tuple_schema
426 .description
427 .as_ref()
428 .unwrap()
429 .contains("fixed order"));
430 assert!(tuple_schema
431 .description
432 .as_ref()
433 .unwrap()
434 .contains("OpenAPI 3.0 cannot precisely represent"));
435
436 assert!(tuple_schema.items.is_some());
438 let items = tuple_schema.items.as_ref().unwrap();
439 assert!(items.one_of.is_empty()); }
441}