1use crate::error::BuildError;
139use crate::presets::{DdexVersion, MessageProfile, PartnerPreset};
140use indexmap::IndexMap;
141use serde::{Deserialize, Serialize};
142use serde_json::{json, Value as JsonValue};
143
144mod generators;
145mod types;
146mod validation;
147
148pub use validation::{
150    SchemaValidator, ValidationConfig as SchemaValidationConfig,
151    ValidationResult as SchemaValidationResult,
152};
153
154#[derive(Debug, Clone)]
156pub struct SchemaGenerator {
157    version: DdexVersion,
159    profile: MessageProfile,
161    #[allow(dead_code)]
163    preset: Option<PartnerPreset>,
164    config: SchemaConfig,
166}
167
168#[derive(Debug, Clone)]
170pub struct SchemaConfig {
171    pub draft_version: SchemaDraft,
173    pub include_examples: bool,
175    pub include_descriptions: bool,
177    pub strict_validation: bool,
179    pub include_deprecated: bool,
181    pub version_conditionals: bool,
183}
184
185#[derive(Debug, Clone, Copy)]
187pub enum SchemaDraft {
188    Draft202012,
190    Draft07,
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct JsonSchema {
197    #[serde(rename = "$schema")]
199    pub schema: String,
200    #[serde(rename = "$id", skip_serializing_if = "Option::is_none")]
202    pub id: Option<String>,
203    #[serde(skip_serializing_if = "Option::is_none")]
205    pub title: Option<String>,
206    #[serde(skip_serializing_if = "Option::is_none")]
208    pub description: Option<String>,
209    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
211    pub schema_type: Option<String>,
212    #[serde(skip_serializing_if = "Option::is_none")]
214    pub properties: Option<IndexMap<String, JsonSchema>>,
215    #[serde(skip_serializing_if = "Option::is_none")]
217    pub required: Option<Vec<String>>,
218    #[serde(
220        rename = "additionalProperties",
221        skip_serializing_if = "Option::is_none"
222    )]
223    pub additional_properties: Option<bool>,
224    #[serde(skip_serializing_if = "Option::is_none")]
226    pub items: Option<Box<JsonSchema>>,
227    #[serde(rename = "enum", skip_serializing_if = "Option::is_none")]
229    pub enum_values: Option<Vec<JsonValue>>,
230    #[serde(skip_serializing_if = "Option::is_none")]
232    pub pattern: Option<String>,
233    #[serde(skip_serializing_if = "Option::is_none")]
235    pub format: Option<String>,
236    #[serde(rename = "minLength", skip_serializing_if = "Option::is_none")]
238    pub min_length: Option<usize>,
239    #[serde(rename = "maxLength", skip_serializing_if = "Option::is_none")]
241    pub max_length: Option<usize>,
242    #[serde(skip_serializing_if = "Option::is_none")]
244    pub examples: Option<Vec<JsonValue>>,
245    #[serde(rename = "$defs", skip_serializing_if = "Option::is_none")]
247    pub definitions: Option<IndexMap<String, JsonSchema>>,
248    #[serde(rename = "$ref", skip_serializing_if = "Option::is_none")]
250    pub reference: Option<String>,
251    #[serde(rename = "allOf", skip_serializing_if = "Option::is_none")]
253    pub all_of: Option<Vec<JsonSchema>>,
254    #[serde(rename = "anyOf", skip_serializing_if = "Option::is_none")]
256    pub any_of: Option<Vec<JsonSchema>>,
257    #[serde(rename = "oneOf", skip_serializing_if = "Option::is_none")]
259    pub one_of: Option<Vec<JsonSchema>>,
260    #[serde(rename = "if", skip_serializing_if = "Option::is_none")]
262    pub if_schema: Option<Box<JsonSchema>>,
263    #[serde(rename = "then", skip_serializing_if = "Option::is_none")]
265    pub then_schema: Option<Box<JsonSchema>>,
266    #[serde(rename = "else", skip_serializing_if = "Option::is_none")]
268    pub else_schema: Option<Box<JsonSchema>>,
269    #[serde(flatten)]
271    pub annotations: IndexMap<String, JsonValue>,
272}
273
274#[derive(Debug, Clone)]
276pub struct SchemaGenerationResult {
277    pub schema: JsonSchema,
279    pub metadata: SchemaMetadata,
281    pub warnings: Vec<SchemaWarning>,
283}
284
285#[derive(Debug, Clone)]
287pub struct SchemaMetadata {
288    pub ddex_version: DdexVersion,
290    pub profile: MessageProfile,
292    pub draft_version: SchemaDraft,
294    pub generated_at: chrono::DateTime<chrono::Utc>,
296    pub property_count: usize,
298    pub required_count: usize,
300    pub complexity_score: f64,
302}
303
304#[derive(Debug, Clone)]
306pub struct SchemaWarning {
307    pub code: String,
309    pub message: String,
311    pub field_path: Option<String>,
313    pub suggestion: Option<String>,
315}
316
317impl Default for SchemaConfig {
318    fn default() -> Self {
319        Self {
320            draft_version: SchemaDraft::Draft202012,
321            include_examples: true,
322            include_descriptions: true,
323            strict_validation: true,
324            include_deprecated: false,
325            version_conditionals: true,
326        }
327    }
328}
329
330impl SchemaGenerator {
331    pub fn new(version: DdexVersion, profile: MessageProfile) -> Self {
333        Self {
334            version,
335            profile,
336            preset: None,
337            config: SchemaConfig::default(),
338        }
339    }
340
341    pub fn with_preset(
343        version: DdexVersion,
344        profile: MessageProfile,
345        preset: PartnerPreset,
346    ) -> Self {
347        Self {
348            version,
349            profile,
350            preset: Some(preset),
351            config: SchemaConfig::default(),
352        }
353    }
354
355    pub fn with_config(mut self, config: SchemaConfig) -> Self {
357        self.config = config;
358        self
359    }
360
361    pub fn generate_build_request_schema(&self) -> Result<SchemaGenerationResult, BuildError> {
363        let mut warnings = Vec::new();
364
365        let schema = self.build_request_schema(&mut warnings)?;
366
367        let metadata = SchemaMetadata {
368            ddex_version: self.version,
369            profile: self.profile,
370            draft_version: self.config.draft_version,
371            generated_at: chrono::Utc::now(),
372            property_count: self.count_properties(&schema),
373            required_count: schema.required.as_ref().map(|r| r.len()).unwrap_or(0),
374            complexity_score: self.calculate_complexity(&schema),
375        };
376
377        Ok(SchemaGenerationResult {
378            schema,
379            metadata,
380            warnings,
381        })
382    }
383
384    pub fn generate_flat_release_schema(&self) -> Result<SchemaGenerationResult, BuildError> {
386        let mut warnings = Vec::new();
387
388        let schema = self.flat_release_schema(&mut warnings)?;
389
390        let metadata = SchemaMetadata {
391            ddex_version: self.version,
392            profile: self.profile,
393            draft_version: self.config.draft_version,
394            generated_at: chrono::Utc::now(),
395            property_count: self.count_properties(&schema),
396            required_count: schema.required.as_ref().map(|r| r.len()).unwrap_or(0),
397            complexity_score: self.calculate_complexity(&schema),
398        };
399
400        Ok(SchemaGenerationResult {
401            schema,
402            metadata,
403            warnings,
404        })
405    }
406
407    pub fn generate_complete_schema(&self) -> Result<SchemaGenerationResult, BuildError> {
409        let mut warnings = Vec::new();
410        let mut definitions = IndexMap::new();
411
412        definitions.insert(
414            "BuildRequest".to_string(),
415            self.build_request_schema(&mut warnings)?,
416        );
417        definitions.insert(
418            "ReleaseRequest".to_string(),
419            self.release_request_schema(&mut warnings)?,
420        );
421        definitions.insert(
422            "TrackRequest".to_string(),
423            self.track_request_schema(&mut warnings)?,
424        );
425        definitions.insert(
426            "DealRequest".to_string(),
427            self.deal_request_schema(&mut warnings)?,
428        );
429        definitions.insert(
430            "MessageHeader".to_string(),
431            self.message_header_schema(&mut warnings)?,
432        );
433
434        definitions.extend(self.common_type_definitions(&mut warnings)?);
436
437        let schema = JsonSchema {
438            schema: self.schema_draft_url(),
439            id: Some("https://ddex.net/schema/ern/builder".to_string()),
440            title: Some(format!(
441                "DDEX Builder Schema - ERN {} {}",
442                self.version_string(),
443                self.profile_string()
444            )),
445            description: Some(format!(
446                "Complete JSON Schema for DDEX Builder structures targeting ERN {} with {} profile",
447                self.version_string(),
448                self.profile_string()
449            )),
450            schema_type: Some("object".to_string()),
451            definitions: Some(definitions),
452            additional_properties: Some(false),
453            ..Default::default()
454        };
455
456        let metadata = SchemaMetadata {
457            ddex_version: self.version,
458            profile: self.profile,
459            draft_version: self.config.draft_version,
460            generated_at: chrono::Utc::now(),
461            property_count: self.count_properties(&schema),
462            required_count: 0, complexity_score: self.calculate_complexity(&schema),
464        };
465
466        Ok(SchemaGenerationResult {
467            schema,
468            metadata,
469            warnings,
470        })
471    }
472
473    pub fn generate_typescript_types(&self, schema: &JsonSchema) -> Result<String, BuildError> {
475        let mut typescript = String::new();
476
477        typescript.push_str(&format!(
478            "// Generated TypeScript types for DDEX Builder - ERN {} {}\n",
479            self.version_string(),
480            self.profile_string()
481        ));
482        typescript.push_str("// Generated at: ");
483        typescript.push_str(&chrono::Utc::now().to_rfc3339());
484        typescript.push_str("\n\n");
485
486        if let Some(ref definitions) = schema.definitions {
487            for (name, def_schema) in definitions {
488                typescript.push_str(&self.schema_to_typescript(name, def_schema)?);
489                typescript.push_str("\n\n");
490            }
491        }
492
493        Ok(typescript)
494    }
495
496    pub fn generate_python_types(&self, schema: &JsonSchema) -> Result<String, BuildError> {
498        let mut python = String::new();
499
500        python.push_str(&format!(
501            "# Generated Python TypedDict types for DDEX Builder - ERN {} {}\n",
502            self.version_string(),
503            self.profile_string()
504        ));
505        python.push_str("# Generated at: ");
506        python.push_str(&chrono::Utc::now().to_rfc3339());
507        python.push_str("\n\n");
508        python.push_str("from typing import TypedDict, Optional, List, Union, Literal\nfrom datetime import datetime\n\n");
509
510        if let Some(ref definitions) = schema.definitions {
511            for (name, def_schema) in definitions {
512                python.push_str(&self.schema_to_python(name, def_schema)?);
513                python.push_str("\n\n");
514            }
515        }
516
517        Ok(python)
518    }
519
520    fn schema_draft_url(&self) -> String {
523        match self.config.draft_version {
524            SchemaDraft::Draft202012 => "https://json-schema.org/draft/2020-12/schema".to_string(),
525            SchemaDraft::Draft07 => "https://json-schema.org/draft-07/schema".to_string(),
526        }
527    }
528
529    fn version_string(&self) -> &str {
530        match self.version {
531            DdexVersion::Ern43 => "4.3",
532            DdexVersion::Ern42 => "4.2",
533            DdexVersion::Ern41 => "4.1",
534            DdexVersion::Ern382 => "3.8.2",
535        }
536    }
537
538    fn profile_string(&self) -> &str {
539        match self.profile {
540            MessageProfile::AudioAlbum => "AudioAlbum",
541            MessageProfile::AudioSingle => "AudioSingle",
542            MessageProfile::VideoAlbum => "VideoAlbum",
543            MessageProfile::VideoSingle => "VideoSingle",
544            MessageProfile::Mixed => "Mixed",
545        }
546    }
547
548    fn count_properties(&self, schema: &JsonSchema) -> usize {
549        let mut count = 0;
550
551        if let Some(ref properties) = schema.properties {
552            count += properties.len();
553            for (_, prop_schema) in properties {
554                count += self.count_properties(prop_schema);
555            }
556        }
557
558        if let Some(ref definitions) = schema.definitions {
559            for (_, def_schema) in definitions {
560                count += self.count_properties(def_schema);
561            }
562        }
563
564        count
565    }
566
567    fn calculate_complexity(&self, schema: &JsonSchema) -> f64 {
568        let mut complexity = 0.0;
569
570        if let Some(ref properties) = schema.properties {
572            complexity += properties.len() as f64;
573
574            for (_, prop_schema) in properties {
575                complexity += self.calculate_complexity(prop_schema) * 0.5;
576            }
577        }
578
579        if schema.all_of.is_some() {
581            complexity += 2.0;
582        }
583        if schema.any_of.is_some() {
584            complexity += 3.0;
585        }
586        if schema.one_of.is_some() {
587            complexity += 4.0;
588        }
589        if schema.if_schema.is_some() {
590            complexity += 5.0;
591        }
592        if schema.pattern.is_some() {
593            complexity += 1.0;
594        }
595        if schema.enum_values.is_some() {
596            complexity += 0.5;
597        }
598
599        complexity
600    }
601}
602
603impl Default for JsonSchema {
604    fn default() -> Self {
605        Self {
606            schema: String::new(),
607            id: None,
608            title: None,
609            description: None,
610            schema_type: None,
611            properties: None,
612            required: None,
613            additional_properties: None,
614            items: None,
615            enum_values: None,
616            pattern: None,
617            format: None,
618            min_length: None,
619            max_length: None,
620            examples: None,
621            definitions: None,
622            reference: None,
623            all_of: None,
624            any_of: None,
625            one_of: None,
626            if_schema: None,
627            then_schema: None,
628            else_schema: None,
629            annotations: IndexMap::new(),
630        }
631    }
632}
633
634impl std::fmt::Display for SchemaDraft {
635    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
636        match self {
637            SchemaDraft::Draft202012 => write!(f, "2020-12"),
638            SchemaDraft::Draft07 => write!(f, "draft-07"),
639        }
640    }
641}
642
643#[derive(Debug, Clone)]
645pub struct SchemaCommand {
646    pub version: String,
648    pub profile: String,
650    pub output: Option<String>,
652    pub typescript: bool,
654    pub python: bool,
656    pub examples: bool,
658    pub strict: bool,
660}
661
662impl SchemaCommand {
663    pub fn execute(&self) -> Result<(), BuildError> {
665        let version = self.parse_version()?;
666        let profile = self.parse_profile()?;
667
668        let config = SchemaConfig {
669            include_examples: self.examples,
670            strict_validation: self.strict,
671            ..Default::default()
672        };
673
674        let generator = SchemaGenerator::new(version, profile).with_config(config);
675        let result = generator.generate_complete_schema()?;
676
677        let schema_json = serde_json::to_string_pretty(&result.schema).map_err(|e| {
679            BuildError::InvalidFormat {
680                field: "schema".to_string(),
681                message: format!("Failed to serialize schema: {}", e),
682            }
683        })?;
684
685        if let Some(ref output_path) = self.output {
686            std::fs::write(output_path, &schema_json).map_err(|e| BuildError::InvalidFormat {
687                field: "output".to_string(),
688                message: format!("Failed to write schema: {}", e),
689            })?;
690            } else {
692            println!("{}", schema_json);
693        }
694
695        if self.typescript {
697            let ts_types = generator.generate_typescript_types(&result.schema)?;
698            let ts_path = self
699                .output
700                .as_ref()
701                .map(|p| p.replace(".json", ".d.ts"))
702                .unwrap_or_else(|| "ddex-types.d.ts".to_string());
703
704            std::fs::write(&ts_path, ts_types).map_err(|e| BuildError::InvalidFormat {
705                field: "typescript".to_string(),
706                message: format!("Failed to write TypeScript types: {}", e),
707            })?;
708            }
710
711        if self.python {
713            let py_types = generator.generate_python_types(&result.schema)?;
714            let py_path = self
715                .output
716                .as_ref()
717                .map(|p| p.replace(".json", ".py"))
718                .unwrap_or_else(|| "ddex_types.py".to_string());
719
720            std::fs::write(&py_path, py_types).map_err(|e| BuildError::InvalidFormat {
721                field: "python".to_string(),
722                message: format!("Failed to write Python types: {}", e),
723            })?;
724            }
726
727        Ok(())
732    }
733
734    fn parse_version(&self) -> Result<DdexVersion, BuildError> {
735        match self.version.as_str() {
736            "4.3" | "43" => Ok(DdexVersion::Ern43),
737            "4.2" | "42" => Ok(DdexVersion::Ern42),
738            "4.1" | "41" => Ok(DdexVersion::Ern41),
739            "3.8.2" | "382" => Ok(DdexVersion::Ern382),
740            _ => Err(BuildError::InvalidFormat {
741                field: "version".to_string(),
742                message: format!("Unsupported DDEX version: {}", self.version),
743            }),
744        }
745    }
746
747    fn parse_profile(&self) -> Result<MessageProfile, BuildError> {
748        match self.profile.to_lowercase().as_str() {
749            "audioalbum" | "audio-album" => Ok(MessageProfile::AudioAlbum),
750            "audiosingle" | "audio-single" => Ok(MessageProfile::AudioSingle),
751            "videoalbum" | "video-album" => Ok(MessageProfile::VideoAlbum),
752            "videosingle" | "video-single" => Ok(MessageProfile::VideoSingle),
753            "mixed" => Ok(MessageProfile::Mixed),
754            _ => Err(BuildError::InvalidFormat {
755                field: "profile".to_string(),
756                message: format!("Unsupported message profile: {}", self.profile),
757            }),
758        }
759    }
760}
761
762#[cfg(test)]
763mod tests {
764    use super::*;
765
766    #[test]
767    fn test_schema_generator_creation() {
768        let generator = SchemaGenerator::new(DdexVersion::Ern43, MessageProfile::AudioAlbum);
769        assert_eq!(generator.version, DdexVersion::Ern43);
770        assert_eq!(generator.profile, MessageProfile::AudioAlbum);
771        assert!(generator.preset.is_none());
772    }
773
774    #[test]
775    fn test_schema_config_defaults() {
776        let config = SchemaConfig::default();
777
778        assert!(matches!(config.draft_version, SchemaDraft::Draft202012));
779        assert!(config.include_examples);
780        assert!(config.include_descriptions);
781        assert!(config.strict_validation);
782        assert!(!config.include_deprecated);
783        assert!(config.version_conditionals);
784    }
785
786    #[test]
787    fn test_build_request_schema_generation() {
788        let generator = SchemaGenerator::new(DdexVersion::Ern43, MessageProfile::AudioAlbum);
789        let result = generator.generate_build_request_schema().unwrap();
790
791        assert!(result.schema.title.is_some());
792        assert!(result.schema.schema_type == Some("object".to_string()));
793        assert!(result.schema.properties.is_some());
794        assert!(result.schema.required.is_some());
795
796        let properties = result.schema.properties.unwrap();
797        assert!(properties.contains_key("header"));
798        assert!(properties.contains_key("releases"));
799
800        let required = result.schema.required.unwrap();
801        assert!(required.contains(&"header".to_string()));
802        assert!(required.contains(&"releases".to_string()));
803
804        assert!(result.metadata.property_count > 0);
806        assert!(result.metadata.complexity_score > 0.0);
807    }
808
809    #[test]
810    fn test_complete_schema_generation() {
811        let generator = SchemaGenerator::new(DdexVersion::Ern43, MessageProfile::AudioAlbum);
812        let result = generator.generate_complete_schema().unwrap();
813
814        assert!(result.schema.definitions.is_some());
815
816        let definitions = result.schema.definitions.unwrap();
817        assert!(definitions.contains_key("BuildRequest"));
818        assert!(definitions.contains_key("ReleaseRequest"));
819        assert!(definitions.contains_key("TrackRequest"));
820        assert!(definitions.contains_key("DealRequest"));
821        assert!(definitions.contains_key("MessageHeader"));
822        assert!(definitions.contains_key("LocalizedString"));
823        assert!(definitions.contains_key("Party"));
824        assert!(definitions.contains_key("DealTerms"));
825
826        assert_eq!(
828            result.schema.schema,
829            "https://json-schema.org/draft/2020-12/schema"
830        );
831        assert!(result.schema.id.is_some());
832        assert!(result.schema.title.is_some());
833        assert!(result.schema.description.is_some());
834    }
835
836    #[test]
837    fn test_version_strings() {
838        let generator_43 = SchemaGenerator::new(DdexVersion::Ern43, MessageProfile::AudioAlbum);
839        let generator_42 = SchemaGenerator::new(DdexVersion::Ern42, MessageProfile::AudioAlbum);
840        let generator_41 = SchemaGenerator::new(DdexVersion::Ern41, MessageProfile::AudioAlbum);
841
842        assert_eq!(generator_43.version_string(), "4.3");
843        assert_eq!(generator_42.version_string(), "4.2");
844        assert_eq!(generator_41.version_string(), "4.1");
845    }
846
847    #[test]
848    fn test_profile_strings() {
849        let audio_album = SchemaGenerator::new(DdexVersion::Ern43, MessageProfile::AudioAlbum);
850        let audio_single = SchemaGenerator::new(DdexVersion::Ern43, MessageProfile::AudioSingle);
851        let video_album = SchemaGenerator::new(DdexVersion::Ern43, MessageProfile::VideoAlbum);
852        let video_single = SchemaGenerator::new(DdexVersion::Ern43, MessageProfile::VideoSingle);
853        let mixed = SchemaGenerator::new(DdexVersion::Ern43, MessageProfile::Mixed);
854
855        assert_eq!(audio_album.profile_string(), "AudioAlbum");
856        assert_eq!(audio_single.profile_string(), "AudioSingle");
857        assert_eq!(video_album.profile_string(), "VideoAlbum");
858        assert_eq!(video_single.profile_string(), "VideoSingle");
859        assert_eq!(mixed.profile_string(), "Mixed");
860    }
861
862    #[test]
863    fn test_schema_command_parsing() {
864        let command = SchemaCommand {
865            version: "4.3".to_string(),
866            profile: "AudioAlbum".to_string(),
867            output: Some("schema.json".to_string()),
868            typescript: true,
869            python: true,
870            examples: true,
871            strict: true,
872        };
873
874        let parsed_version = command.parse_version().unwrap();
875        let parsed_profile = command.parse_profile().unwrap();
876
877        assert!(matches!(parsed_version, DdexVersion::Ern43));
878        assert!(matches!(parsed_profile, MessageProfile::AudioAlbum));
879    }
880}