ddex_builder/schema/
types.rs

1//! Type generation for TypeScript and Python from JSON Schema
2
3use super::*;
4use indexmap::IndexSet;
5
6impl SchemaGenerator {
7    /// Convert JSON Schema to TypeScript interface definition
8    pub(crate) fn schema_to_typescript(&self, name: &str, schema: &JsonSchema) -> Result<String, BuildError> {
9        let mut output = String::new();
10        let mut imports = IndexSet::new();
11        
12        // Add JSDoc comment if description exists
13        if let Some(ref description) = schema.description {
14            output.push_str(&format!("/**\n * {}\n */\n", description));
15        }
16        
17        match schema.schema_type.as_deref() {
18            Some("object") => {
19                output.push_str(&format!("export interface {} {{\n", name));
20                
21                if let Some(ref properties) = schema.properties {
22                    let required = schema.required.as_ref().map(|r| r.iter().collect::<IndexSet<_>>()).unwrap_or_default();
23                    
24                    for (prop_name, prop_schema) in properties {
25                        let optional = if required.contains(prop_name) { "" } else { "?" };
26                        let ts_type = self.schema_to_typescript_type(prop_schema, &mut imports)?;
27                        
28                        // Add property documentation
29                        if let Some(ref description) = prop_schema.description {
30                            output.push_str(&format!("  /** {} */\n", description));
31                        }
32                        
33                        output.push_str(&format!("  {}{}: {};\n", prop_name, optional, ts_type));
34                    }
35                }
36                
37                output.push_str("}\n");
38            },
39            Some("array") => {
40                if let Some(ref items) = schema.items {
41                    let item_type = self.schema_to_typescript_type(items, &mut imports)?;
42                    output.push_str(&format!("export type {} = {}[];\n", name, item_type));
43                } else {
44                    output.push_str(&format!("export type {} = any[];\n", name));
45                }
46            },
47            _ => {
48                if let Some(ref enum_values) = schema.enum_values {
49                    let enum_variants: Vec<String> = enum_values.iter()
50                        .map(|v| match v {
51                            JsonValue::String(s) => format!("\"{}\"", s),
52                            JsonValue::Number(n) => n.to_string(),
53                            JsonValue::Bool(b) => b.to_string(),
54                            _ => "unknown".to_string(),
55                        })
56                        .collect();
57                    
58                    output.push_str(&format!("export type {} = {};\n", name, enum_variants.join(" | ")));
59                } else {
60                    let ts_type = self.schema_to_typescript_type(schema, &mut imports)?;
61                    output.push_str(&format!("export type {} = {};\n", name, ts_type));
62                }
63            }
64        }
65        
66        // Add imports at the beginning if needed
67        if !imports.is_empty() {
68            let import_statements: Vec<String> = imports.into_iter()
69                .map(|import| format!("import {{ {} }} from './types';", import))
70                .collect();
71            output = format!("{}\n\n{}", import_statements.join("\n"), output);
72        }
73        
74        Ok(output)
75    }
76    
77    /// Convert JSON Schema to TypeScript type string
78    fn schema_to_typescript_type(&self, schema: &JsonSchema, imports: &mut IndexSet<String>) -> Result<String, BuildError> {
79        // Handle references
80        if let Some(ref reference) = schema.reference {
81            if reference.starts_with("#/$defs/") {
82                let type_name = &reference[8..];
83                imports.insert(type_name.to_string());
84                return Ok(type_name.to_string());
85            }
86        }
87        
88        // Handle type unions (anyOf, oneOf)
89        if let Some(ref any_of) = schema.any_of {
90            let union_types: Result<Vec<String>, BuildError> = any_of.iter()
91                .map(|s| self.schema_to_typescript_type(s, imports))
92                .collect();
93            return Ok(format!("({})", union_types?.join(" | ")));
94        }
95        
96        if let Some(ref one_of) = schema.one_of {
97            let union_types: Result<Vec<String>, BuildError> = one_of.iter()
98                .map(|s| self.schema_to_typescript_type(s, imports))
99                .collect();
100            return Ok(format!("({})", union_types?.join(" | ")));
101        }
102        
103        // Handle enum values
104        if let Some(ref enum_values) = schema.enum_values {
105            let variants: Vec<String> = enum_values.iter()
106                .map(|v| match v {
107                    JsonValue::String(s) => format!("\"{}\"", s),
108                    JsonValue::Number(n) => n.to_string(),
109                    JsonValue::Bool(b) => b.to_string(),
110                    _ => "unknown".to_string(),
111                })
112                .collect();
113            return Ok(variants.join(" | "));
114        }
115        
116        // Handle basic types
117        match schema.schema_type.as_deref() {
118            Some("string") => {
119                // Check for specific string formats
120                match schema.format.as_deref() {
121                    Some("date") => Ok("string /* date: YYYY-MM-DD */".to_string()),
122                    Some("date-time") => Ok("string /* date-time: ISO 8601 */".to_string()),
123                    Some("uri") => Ok("string /* URI */".to_string()),
124                    _ => {
125                        // Add pattern information if available
126                        if let Some(ref pattern) = schema.pattern {
127                            Ok(format!("string /* pattern: {} */", pattern))
128                        } else {
129                            Ok("string".to_string())
130                        }
131                    }
132                }
133            },
134            Some("number") => Ok("number".to_string()),
135            Some("integer") => Ok("number".to_string()),
136            Some("boolean") => Ok("boolean".to_string()),
137            Some("null") => Ok("null".to_string()),
138            Some("array") => {
139                if let Some(ref items) = schema.items {
140                    let item_type = self.schema_to_typescript_type(items, imports)?;
141                    Ok(format!("{}[]", item_type))
142                } else {
143                    Ok("any[]".to_string())
144                }
145            },
146            Some("object") => {
147                if let Some(ref properties) = schema.properties {
148                    let mut object_type = String::from("{\n");
149                    let required = schema.required.as_ref().map(|r| r.iter().collect::<IndexSet<_>>()).unwrap_or_default();
150                    
151                    for (prop_name, prop_schema) in properties {
152                        let optional = if required.contains(prop_name) { "" } else { "?" };
153                        let prop_type = self.schema_to_typescript_type(prop_schema, imports)?;
154                        object_type.push_str(&format!("    {}{}: {};\n", prop_name, optional, prop_type));
155                    }
156                    
157                    object_type.push_str("  }");
158                    Ok(object_type)
159                } else {
160                    Ok("Record<string, any>".to_string())
161                }
162            },
163            _ => Ok("any".to_string()),
164        }
165    }
166    
167    /// Convert JSON Schema to Python TypedDict definition
168    pub(crate) fn schema_to_python(&self, name: &str, schema: &JsonSchema) -> Result<String, BuildError> {
169        let mut output = String::new();
170        let mut imports = IndexSet::new();
171        
172        // Add docstring if description exists
173        if let Some(ref description) = schema.description {
174            output.push_str(&format!("\"\"\"{}.\"\"\"\n", description));
175        }
176        
177        match schema.schema_type.as_deref() {
178            Some("object") => {
179                // Determine if we need total=False for optional fields
180                let required = schema.required.as_ref().map(|r| r.iter().collect::<IndexSet<_>>()).unwrap_or_default();
181                let has_optional = schema.properties.as_ref()
182                    .map(|props| props.keys().any(|k| !required.contains(k)))
183                    .unwrap_or(false);
184                
185                let total_param = if has_optional { ", total=False" } else { "" };
186                output.push_str(&format!("class {}(TypedDict{}):\n", name, total_param));
187                
188                if let Some(ref properties) = schema.properties {
189                    for (prop_name, prop_schema) in properties {
190                        let python_type = self.schema_to_python_type(prop_schema, &mut imports)?;
191                        let field_type = if required.contains(prop_name) || !has_optional {
192                            python_type
193                        } else {
194                            format!("Optional[{}]", python_type)
195                        };
196                        
197                        // Add field documentation
198                        if let Some(ref description) = prop_schema.description {
199                            output.push_str(&format!("    # {}\n", description));
200                        }
201                        
202                        output.push_str(&format!("    {}: {}\n", prop_name, field_type));
203                    }
204                    
205                    if properties.is_empty() {
206                        output.push_str("    pass\n");
207                    }
208                } else {
209                    output.push_str("    pass\n");
210                }
211            },
212            Some("array") => {
213                if let Some(ref items) = schema.items {
214                    let item_type = self.schema_to_python_type(items, &mut imports)?;
215                    output.push_str(&format!("{} = List[{}]\n", name, item_type));
216                } else {
217                    output.push_str(&format!("{} = List[Any]\n", name));
218                }
219            },
220            _ => {
221                if let Some(ref enum_values) = schema.enum_values {
222                    // Create a Literal type for enum values
223                    let enum_variants: Vec<String> = enum_values.iter()
224                        .map(|v| match v {
225                            JsonValue::String(s) => format!("\"{}\"", s),
226                            JsonValue::Number(n) => n.to_string(),
227                            JsonValue::Bool(b) => b.to_string(),
228                            _ => "None".to_string(),
229                        })
230                        .collect();
231                    
232                    output.push_str(&format!("{} = Literal[{}]\n", name, enum_variants.join(", ")));
233                } else {
234                    let python_type = self.schema_to_python_type(schema, &mut imports)?;
235                    output.push_str(&format!("{} = {}\n", name, python_type));
236                }
237            }
238        }
239        
240        Ok(output)
241    }
242    
243    /// Convert JSON Schema to Python type string
244    fn schema_to_python_type(&self, schema: &JsonSchema, imports: &mut IndexSet<String>) -> Result<String, BuildError> {
245        // Handle references
246        if let Some(ref reference) = schema.reference {
247            if reference.starts_with("#/$defs/") {
248                let type_name = &reference[8..];
249                return Ok(type_name.to_string());
250            }
251        }
252        
253        // Handle type unions
254        if let Some(ref any_of) = schema.any_of {
255            let union_types: Result<Vec<String>, BuildError> = any_of.iter()
256                .map(|s| self.schema_to_python_type(s, imports))
257                .collect();
258            return Ok(format!("Union[{}]", union_types?.join(", ")));
259        }
260        
261        if let Some(ref one_of) = schema.one_of {
262            let union_types: Result<Vec<String>, BuildError> = one_of.iter()
263                .map(|s| self.schema_to_python_type(s, imports))
264                .collect();
265            return Ok(format!("Union[{}]", union_types?.join(", ")));
266        }
267        
268        // Handle enum values
269        if let Some(ref enum_values) = schema.enum_values {
270            let variants: Vec<String> = enum_values.iter()
271                .map(|v| match v {
272                    JsonValue::String(s) => format!("\"{}\"", s),
273                    JsonValue::Number(n) => n.to_string(),
274                    JsonValue::Bool(b) => b.to_string(),
275                    _ => "None".to_string(),
276                })
277                .collect();
278            return Ok(format!("Literal[{}]", variants.join(", ")));
279        }
280        
281        // Handle basic types
282        match schema.schema_type.as_deref() {
283            Some("string") => {
284                match schema.format.as_deref() {
285                    Some("date") => Ok("str  # date: YYYY-MM-DD".to_string()),
286                    Some("date-time") => Ok("datetime  # ISO 8601 datetime".to_string()),
287                    Some("uri") => Ok("str  # URI".to_string()),
288                    _ => Ok("str".to_string()),
289                }
290            },
291            Some("number") => Ok("float".to_string()),
292            Some("integer") => Ok("int".to_string()),
293            Some("boolean") => Ok("bool".to_string()),
294            Some("null") => Ok("None".to_string()),
295            Some("array") => {
296                if let Some(ref items) = schema.items {
297                    let item_type = self.schema_to_python_type(items, imports)?;
298                    Ok(format!("List[{}]", item_type))
299                } else {
300                    Ok("List[Any]".to_string())
301                }
302            },
303            Some("object") => {
304                if schema.properties.is_some() {
305                    // For inline objects, use Dict with type hints if possible
306                    Ok("Dict[str, Any]".to_string())
307                } else {
308                    Ok("Dict[str, Any]".to_string())
309                }
310            },
311            _ => Ok("Any".to_string()),
312        }
313    }
314    
315    /// Generate OpenAPI specification from schema
316    pub fn generate_openapi_spec(&self, schema: &JsonSchema) -> Result<String, BuildError> {
317        let mut openapi = serde_json::json!({
318            "openapi": "3.0.3",
319            "info": {
320                "title": format!("DDEX Builder API - ERN {}", self.version_string()),
321                "description": format!("REST API for DDEX Builder with {} profile", self.profile_string()),
322                "version": "1.0.0"
323            },
324            "servers": [{
325                "url": "https://api.ddex-builder.example.com",
326                "description": "DDEX Builder API Server"
327            }],
328            "paths": {
329                "/build": {
330                    "post": {
331                        "summary": "Build DDEX message",
332                        "description": "Create a DDEX XML message from structured data",
333                        "requestBody": {
334                            "required": true,
335                            "content": {
336                                "application/json": {
337                                    "schema": {
338                                        "$ref": "#/components/schemas/BuildRequest"
339                                    }
340                                }
341                            }
342                        },
343                        "responses": {
344                            "200": {
345                                "description": "Successfully generated DDEX XML",
346                                "content": {
347                                    "application/xml": {
348                                        "schema": {
349                                            "type": "string"
350                                        }
351                                    }
352                                }
353                            },
354                            "400": {
355                                "description": "Invalid request data",
356                                "content": {
357                                    "application/json": {
358                                        "schema": {
359                                            "$ref": "#/components/schemas/Error"
360                                        }
361                                    }
362                                }
363                            }
364                        }
365                    }
366                },
367                "/validate": {
368                    "post": {
369                        "summary": "Validate DDEX data",
370                        "description": "Validate structured data against DDEX schema",
371                        "requestBody": {
372                            "required": true,
373                            "content": {
374                                "application/json": {
375                                    "schema": {
376                                        "$ref": "#/components/schemas/BuildRequest"
377                                    }
378                                }
379                            }
380                        },
381                        "responses": {
382                            "200": {
383                                "description": "Validation result",
384                                "content": {
385                                    "application/json": {
386                                        "schema": {
387                                            "$ref": "#/components/schemas/ValidationResult"
388                                        }
389                                    }
390                                }
391                            }
392                        }
393                    }
394                },
395                "/schema": {
396                    "get": {
397                        "summary": "Get JSON Schema",
398                        "description": "Retrieve the current JSON Schema for validation",
399                        "parameters": [{
400                            "name": "version",
401                            "in": "query",
402                            "description": "DDEX version",
403                            "schema": {
404                                "type": "string",
405                                "enum": ["4.1", "4.2", "4.3"]
406                            }
407                        }, {
408                            "name": "profile",
409                            "in": "query", 
410                            "description": "Message profile",
411                            "schema": {
412                                "type": "string",
413                                "enum": ["AudioAlbum", "AudioSingle", "VideoAlbum", "VideoSingle", "Mixed"]
414                            }
415                        }],
416                        "responses": {
417                            "200": {
418                                "description": "JSON Schema",
419                                "content": {
420                                    "application/json": {
421                                        "schema": {
422                                            "type": "object"
423                                        }
424                                    }
425                                }
426                            }
427                        }
428                    }
429                }
430            },
431            "components": {
432                "schemas": {}
433            }
434        });
435        
436        // Add schema definitions to components
437        if let Some(ref definitions) = schema.definitions {
438            let mut components_schemas = serde_json::Map::new();
439            
440            for (name, def_schema) in definitions {
441                // Convert our JsonSchema to OpenAPI schema format
442                let openapi_schema = self.convert_to_openapi_schema(def_schema)?;
443                components_schemas.insert(name.clone(), openapi_schema);
444            }
445            
446            // Add standard error schema
447            components_schemas.insert("Error".to_string(), json!({
448                "type": "object",
449                "required": ["code", "message"],
450                "properties": {
451                    "code": {
452                        "type": "string",
453                        "description": "Error code"
454                    },
455                    "message": {
456                        "type": "string", 
457                        "description": "Error message"
458                    },
459                    "field": {
460                        "type": "string",
461                        "description": "Field that caused the error"
462                    }
463                }
464            }));
465            
466            // Add validation result schema
467            components_schemas.insert("ValidationResult".to_string(), json!({
468                "type": "object",
469                "required": ["valid", "errors", "warnings"],
470                "properties": {
471                    "valid": {
472                        "type": "boolean",
473                        "description": "Whether validation passed"
474                    },
475                    "errors": {
476                        "type": "array",
477                        "items": {"$ref": "#/components/schemas/Error"},
478                        "description": "Validation errors"
479                    },
480                    "warnings": {
481                        "type": "array",
482                        "items": {"$ref": "#/components/schemas/Error"},
483                        "description": "Validation warnings"
484                    }
485                }
486            }));
487            
488            openapi["components"]["schemas"] = JsonValue::Object(components_schemas);
489        }
490        
491        serde_json::to_string_pretty(&openapi)
492            .map_err(|e| BuildError::InvalidFormat {
493                field: "openapi".to_string(),
494                message: format!("Failed to serialize OpenAPI spec: {}", e),
495            })
496    }
497    
498    /// Convert our JsonSchema format to OpenAPI 3.0 schema format
499    fn convert_to_openapi_schema(&self, schema: &JsonSchema) -> Result<JsonValue, BuildError> {
500        let mut openapi_schema = serde_json::Map::new();
501        
502        if let Some(ref title) = schema.title {
503            openapi_schema.insert("title".to_string(), JsonValue::String(title.clone()));
504        }
505        
506        if let Some(ref description) = schema.description {
507            openapi_schema.insert("description".to_string(), JsonValue::String(description.clone()));
508        }
509        
510        if let Some(ref schema_type) = schema.schema_type {
511            openapi_schema.insert("type".to_string(), JsonValue::String(schema_type.clone()));
512        }
513        
514        if let Some(ref properties) = schema.properties {
515            let mut openapi_properties = serde_json::Map::new();
516            for (name, prop_schema) in properties {
517                openapi_properties.insert(name.clone(), self.convert_to_openapi_schema(prop_schema)?);
518            }
519            openapi_schema.insert("properties".to_string(), JsonValue::Object(openapi_properties));
520        }
521        
522        if let Some(ref required) = schema.required {
523            let required_array: Vec<JsonValue> = required.iter()
524                .map(|r| JsonValue::String(r.clone()))
525                .collect();
526            openapi_schema.insert("required".to_string(), JsonValue::Array(required_array));
527        }
528        
529        if let Some(additional_properties) = schema.additional_properties {
530            openapi_schema.insert("additionalProperties".to_string(), JsonValue::Bool(additional_properties));
531        }
532        
533        if let Some(ref items) = schema.items {
534            openapi_schema.insert("items".to_string(), self.convert_to_openapi_schema(items)?);
535        }
536        
537        if let Some(ref enum_values) = schema.enum_values {
538            openapi_schema.insert("enum".to_string(), JsonValue::Array(enum_values.clone()));
539        }
540        
541        if let Some(ref pattern) = schema.pattern {
542            openapi_schema.insert("pattern".to_string(), JsonValue::String(pattern.clone()));
543        }
544        
545        if let Some(ref format) = schema.format {
546            openapi_schema.insert("format".to_string(), JsonValue::String(format.clone()));
547        }
548        
549        if let Some(min_length) = schema.min_length {
550            openapi_schema.insert("minLength".to_string(), JsonValue::Number(min_length.into()));
551        }
552        
553        if let Some(max_length) = schema.max_length {
554            openapi_schema.insert("maxLength".to_string(), JsonValue::Number(max_length.into()));
555        }
556        
557        if let Some(ref examples) = schema.examples {
558            openapi_schema.insert("examples".to_string(), JsonValue::Array(examples.clone()));
559        }
560        
561        if let Some(ref reference) = schema.reference {
562            openapi_schema.insert("$ref".to_string(), JsonValue::String(reference.clone()));
563        }
564        
565        Ok(JsonValue::Object(openapi_schema))
566    }
567}