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