1use super::*;
4use indexmap::IndexSet;
5
6impl SchemaGenerator {
7 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 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 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 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 fn schema_to_typescript_type(&self, schema: &JsonSchema, imports: &mut IndexSet<String>) -> Result<String, BuildError> {
79 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 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 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 match schema.schema_type.as_deref() {
118 Some("string") => {
119 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 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 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 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 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 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 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 fn schema_to_python_type(&self, schema: &JsonSchema, imports: &mut IndexSet<String>) -> Result<String, BuildError> {
245 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 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 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 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 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 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 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 let openapi_schema = self.convert_to_openapi_schema(def_schema)?;
443 components_schemas.insert(name.clone(), openapi_schema);
444 }
445
446 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 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 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}