data_modelling_sdk/export/
cads.rs

1//! CADS (Compute Asset Description Specification) exporter
2//!
3//! Exports CADSAsset models to CADS v1.0 YAML format.
4
5use crate::export::ExportError;
6use crate::models::cads::*;
7use serde_yaml;
8
9/// CADS exporter for generating CADS v1.0 YAML from CADSAsset models
10pub struct CADSExporter;
11
12impl CADSExporter {
13    /// Export a CADS asset to CADS v1.0 YAML format (instance method for WASM compatibility)
14    ///
15    /// # Arguments
16    ///
17    /// * `asset` - The CADS asset to export
18    ///
19    /// # Returns
20    ///
21    /// A Result containing the YAML string in CADS v1.0 format, or an ExportError
22    pub fn export(&self, asset: &CADSAsset) -> Result<String, ExportError> {
23        let yaml = Self::export_asset(asset);
24
25        // Validate exported YAML against CADS schema (if feature enabled)
26        #[cfg(feature = "schema-validation")]
27        {
28            #[cfg(feature = "cli")]
29            {
30                use crate::cli::validation::validate_cads_internal;
31                validate_cads_internal(&yaml).map_err(ExportError::ValidationError)?;
32            }
33            #[cfg(not(feature = "cli"))]
34            {
35                // Inline validation when CLI feature is not enabled
36                use jsonschema::Validator;
37                use serde_json::Value;
38
39                let schema_content = include_str!("../../schemas/cads.schema.json");
40                let schema: Value = serde_json::from_str(schema_content).map_err(|e| {
41                    ExportError::ValidationError(format!("Failed to load CADS schema: {}", e))
42                })?;
43
44                let validator = Validator::new(&schema).map_err(|e| {
45                    ExportError::ValidationError(format!("Failed to compile CADS schema: {}", e))
46                })?;
47
48                let data: Value = serde_yaml::from_str(&yaml).map_err(|e| {
49                    ExportError::ValidationError(format!("Failed to parse YAML: {}", e))
50                })?;
51
52                if let Err(error) = validator.validate(&data) {
53                    return Err(ExportError::ValidationError(format!(
54                        "CADS validation failed: {}",
55                        error
56                    )));
57                }
58            }
59        }
60
61        Ok(yaml)
62    }
63
64    /// Export a CADS asset to CADS v1.0 YAML format
65    ///
66    /// # Arguments
67    ///
68    /// * `asset` - The CADS asset to export
69    ///
70    /// # Returns
71    ///
72    /// A YAML string in CADS v1.0 format
73    ///
74    /// # Example
75    ///
76    /// ```rust
77    /// use data_modelling_sdk::export::cads::CADSExporter;
78    /// use data_modelling_sdk::models::cads::*;
79    ///
80    /// let asset = CADSAsset {
81    ///     api_version: "v1.0".to_string(),
82    ///     kind: CADSKind::AIModel,
83    ///     id: "550e8400-e29b-41d4-a716-446655440000".to_string(),
84    ///     name: "sentiment-analysis-model".to_string(),
85    ///     version: "1.0.0".to_string(),
86    ///     status: CADSStatus::Production,
87    ///     domain: None,
88    ///     tags: vec![],
89    ///     description: None,
90    ///     runtime: None,
91    ///     sla: None,
92    ///     pricing: None,
93    ///     team: None,
94    ///     risk: None,
95    ///     compliance: None,
96    ///     validation_profiles: None,
97    ///     bpmn_models: None,
98    ///     dmn_models: None,
99    ///     openapi_specs: None,
100    ///     custom_properties: None,
101    ///     created_at: None,
102    ///     updated_at: None,
103    /// };
104    ///
105    /// let yaml = CADSExporter::export_asset(&asset);
106    /// assert!(yaml.contains("apiVersion: v1.0"));
107    /// assert!(yaml.contains("kind: AIModel"));
108    /// ```
109    pub fn export_asset(asset: &CADSAsset) -> String {
110        let mut yaml = serde_yaml::Mapping::new();
111
112        // Required fields
113        yaml.insert(
114            serde_yaml::Value::String("apiVersion".to_string()),
115            serde_yaml::Value::String(asset.api_version.clone()),
116        );
117
118        let kind_str = match asset.kind {
119            CADSKind::AIModel => "AIModel",
120            CADSKind::MLPipeline => "MLPipeline",
121            CADSKind::Application => "Application",
122            CADSKind::ETLPipeline => "ETLPipeline",
123            CADSKind::SourceSystem => "SourceSystem",
124            CADSKind::DestinationSystem => "DestinationSystem",
125        };
126        yaml.insert(
127            serde_yaml::Value::String("kind".to_string()),
128            serde_yaml::Value::String(kind_str.to_string()),
129        );
130
131        yaml.insert(
132            serde_yaml::Value::String("id".to_string()),
133            serde_yaml::Value::String(asset.id.clone()),
134        );
135
136        yaml.insert(
137            serde_yaml::Value::String("name".to_string()),
138            serde_yaml::Value::String(asset.name.clone()),
139        );
140
141        yaml.insert(
142            serde_yaml::Value::String("version".to_string()),
143            serde_yaml::Value::String(asset.version.clone()),
144        );
145
146        let status_str = match asset.status {
147            CADSStatus::Draft => "draft",
148            CADSStatus::Validated => "validated",
149            CADSStatus::Production => "production",
150            CADSStatus::Deprecated => "deprecated",
151        };
152        yaml.insert(
153            serde_yaml::Value::String("status".to_string()),
154            serde_yaml::Value::String(status_str.to_string()),
155        );
156
157        // Optional fields
158        if let Some(domain) = &asset.domain {
159            yaml.insert(
160                serde_yaml::Value::String("domain".to_string()),
161                serde_yaml::Value::String(domain.clone()),
162            );
163        }
164
165        if !asset.tags.is_empty() {
166            let tags_yaml: Vec<serde_yaml::Value> = asset
167                .tags
168                .iter()
169                .map(|t| serde_yaml::Value::String(t.to_string()))
170                .collect();
171            yaml.insert(
172                serde_yaml::Value::String("tags".to_string()),
173                serde_yaml::Value::Sequence(tags_yaml),
174            );
175        }
176
177        if let Some(description) = &asset.description {
178            let mut desc_map = serde_yaml::Mapping::new();
179            if let Some(purpose) = &description.purpose {
180                desc_map.insert(
181                    serde_yaml::Value::String("purpose".to_string()),
182                    serde_yaml::Value::String(purpose.clone()),
183                );
184            }
185            if let Some(usage) = &description.usage {
186                desc_map.insert(
187                    serde_yaml::Value::String("usage".to_string()),
188                    serde_yaml::Value::String(usage.clone()),
189                );
190            }
191            if let Some(limitations) = &description.limitations {
192                desc_map.insert(
193                    serde_yaml::Value::String("limitations".to_string()),
194                    serde_yaml::Value::String(limitations.clone()),
195                );
196            }
197            if let Some(external_links) = &description.external_links {
198                let links_yaml: Vec<serde_yaml::Value> = external_links
199                    .iter()
200                    .map(|link| {
201                        let mut link_map = serde_yaml::Mapping::new();
202                        link_map.insert(
203                            serde_yaml::Value::String("url".to_string()),
204                            serde_yaml::Value::String(link.url.clone()),
205                        );
206                        if let Some(desc) = &link.description {
207                            link_map.insert(
208                                serde_yaml::Value::String("description".to_string()),
209                                serde_yaml::Value::String(desc.clone()),
210                            );
211                        }
212                        serde_yaml::Value::Mapping(link_map)
213                    })
214                    .collect();
215                desc_map.insert(
216                    serde_yaml::Value::String("externalLinks".to_string()),
217                    serde_yaml::Value::Sequence(links_yaml),
218                );
219            }
220            if !desc_map.is_empty() {
221                yaml.insert(
222                    serde_yaml::Value::String("description".to_string()),
223                    serde_yaml::Value::Mapping(desc_map),
224                );
225            }
226        }
227
228        if let Some(runtime) = &asset.runtime {
229            let mut runtime_map = serde_yaml::Mapping::new();
230            if let Some(environment) = &runtime.environment {
231                runtime_map.insert(
232                    serde_yaml::Value::String("environment".to_string()),
233                    serde_yaml::Value::String(environment.clone()),
234                );
235            }
236            if let Some(endpoints) = &runtime.endpoints {
237                let endpoints_yaml: Vec<serde_yaml::Value> = endpoints
238                    .iter()
239                    .map(|e| serde_yaml::Value::String(e.clone()))
240                    .collect();
241                runtime_map.insert(
242                    serde_yaml::Value::String("endpoints".to_string()),
243                    serde_yaml::Value::Sequence(endpoints_yaml),
244                );
245            }
246            if let Some(container) = &runtime.container {
247                let mut container_map = serde_yaml::Mapping::new();
248                if let Some(image) = &container.image {
249                    container_map.insert(
250                        serde_yaml::Value::String("image".to_string()),
251                        serde_yaml::Value::String(image.clone()),
252                    );
253                }
254                if !container_map.is_empty() {
255                    runtime_map.insert(
256                        serde_yaml::Value::String("container".to_string()),
257                        serde_yaml::Value::Mapping(container_map),
258                    );
259                }
260            }
261            if let Some(resources) = &runtime.resources {
262                let mut resources_map = serde_yaml::Mapping::new();
263                if let Some(cpu) = &resources.cpu {
264                    resources_map.insert(
265                        serde_yaml::Value::String("cpu".to_string()),
266                        serde_yaml::Value::String(cpu.clone()),
267                    );
268                }
269                if let Some(memory) = &resources.memory {
270                    resources_map.insert(
271                        serde_yaml::Value::String("memory".to_string()),
272                        serde_yaml::Value::String(memory.clone()),
273                    );
274                }
275                if let Some(gpu) = &resources.gpu {
276                    resources_map.insert(
277                        serde_yaml::Value::String("gpu".to_string()),
278                        serde_yaml::Value::String(gpu.clone()),
279                    );
280                }
281                if !resources_map.is_empty() {
282                    runtime_map.insert(
283                        serde_yaml::Value::String("resources".to_string()),
284                        serde_yaml::Value::Mapping(resources_map),
285                    );
286                }
287            }
288            if !runtime_map.is_empty() {
289                yaml.insert(
290                    serde_yaml::Value::String("runtime".to_string()),
291                    serde_yaml::Value::Mapping(runtime_map),
292                );
293            }
294        }
295
296        if let Some(sla) = &asset.sla
297            && let Some(properties) = &sla.properties
298        {
299            let mut sla_map = serde_yaml::Mapping::new();
300            let props_yaml: Vec<serde_yaml::Value> = properties
301                .iter()
302                .map(|prop| {
303                    let mut prop_map = serde_yaml::Mapping::new();
304                    prop_map.insert(
305                        serde_yaml::Value::String("element".to_string()),
306                        serde_yaml::Value::String(prop.element.clone()),
307                    );
308                    prop_map.insert(
309                        serde_yaml::Value::String("value".to_string()),
310                        Self::json_to_yaml_value(&prop.value),
311                    );
312                    prop_map.insert(
313                        serde_yaml::Value::String("unit".to_string()),
314                        serde_yaml::Value::String(prop.unit.clone()),
315                    );
316                    if let Some(driver) = &prop.driver {
317                        prop_map.insert(
318                            serde_yaml::Value::String("driver".to_string()),
319                            serde_yaml::Value::String(driver.clone()),
320                        );
321                    }
322                    serde_yaml::Value::Mapping(prop_map)
323                })
324                .collect();
325            sla_map.insert(
326                serde_yaml::Value::String("properties".to_string()),
327                serde_yaml::Value::Sequence(props_yaml),
328            );
329            yaml.insert(
330                serde_yaml::Value::String("sla".to_string()),
331                serde_yaml::Value::Mapping(sla_map),
332            );
333        }
334
335        if let Some(pricing) = &asset.pricing {
336            let mut pricing_map = serde_yaml::Mapping::new();
337            if let Some(model) = &pricing.model {
338                let model_str = match model {
339                    CADSPricingModel::PerRequest => "per_request",
340                    CADSPricingModel::PerHour => "per_hour",
341                    CADSPricingModel::PerBatch => "per_batch",
342                    CADSPricingModel::Subscription => "subscription",
343                    CADSPricingModel::Internal => "internal",
344                };
345                pricing_map.insert(
346                    serde_yaml::Value::String("model".to_string()),
347                    serde_yaml::Value::String(model_str.to_string()),
348                );
349            }
350            if let Some(currency) = &pricing.currency {
351                pricing_map.insert(
352                    serde_yaml::Value::String("currency".to_string()),
353                    serde_yaml::Value::String(currency.clone()),
354                );
355            }
356            if let Some(unit_cost) = pricing.unit_cost {
357                pricing_map.insert(
358                    serde_yaml::Value::String("unitCost".to_string()),
359                    serde_yaml::Value::Number(serde_yaml::Number::from(unit_cost)),
360                );
361            }
362            if let Some(billing_unit) = &pricing.billing_unit {
363                pricing_map.insert(
364                    serde_yaml::Value::String("billingUnit".to_string()),
365                    serde_yaml::Value::String(billing_unit.clone()),
366                );
367            }
368            if let Some(notes) = &pricing.notes {
369                pricing_map.insert(
370                    serde_yaml::Value::String("notes".to_string()),
371                    serde_yaml::Value::String(notes.clone()),
372                );
373            }
374            if !pricing_map.is_empty() {
375                yaml.insert(
376                    serde_yaml::Value::String("pricing".to_string()),
377                    serde_yaml::Value::Mapping(pricing_map),
378                );
379            }
380        }
381
382        if let Some(team) = &asset.team {
383            let team_yaml: Vec<serde_yaml::Value> = team
384                .iter()
385                .map(|member| {
386                    let mut member_map = serde_yaml::Mapping::new();
387                    member_map.insert(
388                        serde_yaml::Value::String("role".to_string()),
389                        serde_yaml::Value::String(member.role.clone()),
390                    );
391                    member_map.insert(
392                        serde_yaml::Value::String("name".to_string()),
393                        serde_yaml::Value::String(member.name.clone()),
394                    );
395                    if let Some(contact) = &member.contact {
396                        member_map.insert(
397                            serde_yaml::Value::String("contact".to_string()),
398                            serde_yaml::Value::String(contact.clone()),
399                        );
400                    }
401                    serde_yaml::Value::Mapping(member_map)
402                })
403                .collect();
404            yaml.insert(
405                serde_yaml::Value::String("team".to_string()),
406                serde_yaml::Value::Sequence(team_yaml),
407            );
408        }
409
410        if let Some(risk) = &asset.risk {
411            let mut risk_map = serde_yaml::Mapping::new();
412            if let Some(classification) = &risk.classification {
413                let class_str = match classification {
414                    CADSRiskClassification::Minimal => "minimal",
415                    CADSRiskClassification::Low => "low",
416                    CADSRiskClassification::Medium => "medium",
417                    CADSRiskClassification::High => "high",
418                };
419                risk_map.insert(
420                    serde_yaml::Value::String("classification".to_string()),
421                    serde_yaml::Value::String(class_str.to_string()),
422                );
423            }
424            if let Some(impact_areas) = &risk.impact_areas {
425                let areas_yaml: Vec<serde_yaml::Value> = impact_areas
426                    .iter()
427                    .map(|area| {
428                        let area_str = match area {
429                            CADSImpactArea::Fairness => "fairness",
430                            CADSImpactArea::Privacy => "privacy",
431                            CADSImpactArea::Safety => "safety",
432                            CADSImpactArea::Security => "security",
433                            CADSImpactArea::Financial => "financial",
434                            CADSImpactArea::Operational => "operational",
435                            CADSImpactArea::Reputational => "reputational",
436                        };
437                        serde_yaml::Value::String(area_str.to_string())
438                    })
439                    .collect();
440                risk_map.insert(
441                    serde_yaml::Value::String("impactAreas".to_string()),
442                    serde_yaml::Value::Sequence(areas_yaml),
443                );
444            }
445            if let Some(intended_use) = &risk.intended_use {
446                risk_map.insert(
447                    serde_yaml::Value::String("intendedUse".to_string()),
448                    serde_yaml::Value::String(intended_use.clone()),
449                );
450            }
451            if let Some(out_of_scope_use) = &risk.out_of_scope_use {
452                risk_map.insert(
453                    serde_yaml::Value::String("outOfScopeUse".to_string()),
454                    serde_yaml::Value::String(out_of_scope_use.clone()),
455                );
456            }
457            if let Some(assessment) = &risk.assessment {
458                let mut assess_map = serde_yaml::Mapping::new();
459                if let Some(methodology) = &assessment.methodology {
460                    assess_map.insert(
461                        serde_yaml::Value::String("methodology".to_string()),
462                        serde_yaml::Value::String(methodology.clone()),
463                    );
464                }
465                if let Some(date) = &assessment.date {
466                    assess_map.insert(
467                        serde_yaml::Value::String("date".to_string()),
468                        serde_yaml::Value::String(date.clone()),
469                    );
470                }
471                if let Some(assessor) = &assessment.assessor {
472                    assess_map.insert(
473                        serde_yaml::Value::String("assessor".to_string()),
474                        serde_yaml::Value::String(assessor.clone()),
475                    );
476                }
477                if !assess_map.is_empty() {
478                    risk_map.insert(
479                        serde_yaml::Value::String("assessment".to_string()),
480                        serde_yaml::Value::Mapping(assess_map),
481                    );
482                }
483            }
484            if let Some(mitigations) = &risk.mitigations {
485                let mitigations_yaml: Vec<serde_yaml::Value> = mitigations
486                    .iter()
487                    .map(|mit| {
488                        let mut mit_map = serde_yaml::Mapping::new();
489                        mit_map.insert(
490                            serde_yaml::Value::String("description".to_string()),
491                            serde_yaml::Value::String(mit.description.clone()),
492                        );
493                        let status_str = match mit.status {
494                            CADSMitigationStatus::Planned => "planned",
495                            CADSMitigationStatus::Implemented => "implemented",
496                            CADSMitigationStatus::Verified => "verified",
497                        };
498                        mit_map.insert(
499                            serde_yaml::Value::String("status".to_string()),
500                            serde_yaml::Value::String(status_str.to_string()),
501                        );
502                        serde_yaml::Value::Mapping(mit_map)
503                    })
504                    .collect();
505                risk_map.insert(
506                    serde_yaml::Value::String("mitigations".to_string()),
507                    serde_yaml::Value::Sequence(mitigations_yaml),
508                );
509            }
510            if !risk_map.is_empty() {
511                yaml.insert(
512                    serde_yaml::Value::String("risk".to_string()),
513                    serde_yaml::Value::Mapping(risk_map),
514                );
515            }
516        }
517
518        if let Some(compliance) = &asset.compliance {
519            let mut comp_map = serde_yaml::Mapping::new();
520            if let Some(frameworks) = &compliance.frameworks {
521                let frameworks_yaml: Vec<serde_yaml::Value> = frameworks
522                    .iter()
523                    .map(|fw| {
524                        let mut fw_map = serde_yaml::Mapping::new();
525                        fw_map.insert(
526                            serde_yaml::Value::String("name".to_string()),
527                            serde_yaml::Value::String(fw.name.clone()),
528                        );
529                        if let Some(category) = &fw.category {
530                            fw_map.insert(
531                                serde_yaml::Value::String("category".to_string()),
532                                serde_yaml::Value::String(category.clone()),
533                            );
534                        }
535                        let status_str = match fw.status {
536                            CADSComplianceStatus::NotApplicable => "not_applicable",
537                            CADSComplianceStatus::Assessed => "assessed",
538                            CADSComplianceStatus::Compliant => "compliant",
539                            CADSComplianceStatus::NonCompliant => "non_compliant",
540                        };
541                        fw_map.insert(
542                            serde_yaml::Value::String("status".to_string()),
543                            serde_yaml::Value::String(status_str.to_string()),
544                        );
545                        serde_yaml::Value::Mapping(fw_map)
546                    })
547                    .collect();
548                comp_map.insert(
549                    serde_yaml::Value::String("frameworks".to_string()),
550                    serde_yaml::Value::Sequence(frameworks_yaml),
551                );
552            }
553            if let Some(controls) = &compliance.controls {
554                let controls_yaml: Vec<serde_yaml::Value> = controls
555                    .iter()
556                    .map(|ctrl| {
557                        let mut ctrl_map = serde_yaml::Mapping::new();
558                        ctrl_map.insert(
559                            serde_yaml::Value::String("id".to_string()),
560                            serde_yaml::Value::String(ctrl.id.clone()),
561                        );
562                        ctrl_map.insert(
563                            serde_yaml::Value::String("description".to_string()),
564                            serde_yaml::Value::String(ctrl.description.clone()),
565                        );
566                        if let Some(evidence) = &ctrl.evidence {
567                            ctrl_map.insert(
568                                serde_yaml::Value::String("evidence".to_string()),
569                                serde_yaml::Value::String(evidence.clone()),
570                            );
571                        }
572                        serde_yaml::Value::Mapping(ctrl_map)
573                    })
574                    .collect();
575                comp_map.insert(
576                    serde_yaml::Value::String("controls".to_string()),
577                    serde_yaml::Value::Sequence(controls_yaml),
578                );
579            }
580            if !comp_map.is_empty() {
581                yaml.insert(
582                    serde_yaml::Value::String("compliance".to_string()),
583                    serde_yaml::Value::Mapping(comp_map),
584                );
585            }
586        }
587
588        if let Some(validation_profiles) = &asset.validation_profiles {
589            let profiles_yaml: Vec<serde_yaml::Value> = validation_profiles
590                .iter()
591                .map(|profile| {
592                    let mut profile_map = serde_yaml::Mapping::new();
593                    profile_map.insert(
594                        serde_yaml::Value::String("name".to_string()),
595                        serde_yaml::Value::String(profile.name.clone()),
596                    );
597                    if let Some(applies_to) = &profile.applies_to {
598                        let mut applies_map = serde_yaml::Mapping::new();
599                        if let Some(kind) = &applies_to.kind {
600                            applies_map.insert(
601                                serde_yaml::Value::String("kind".to_string()),
602                                serde_yaml::Value::String(kind.clone()),
603                            );
604                        }
605                        if let Some(risk_classification) = &applies_to.risk_classification {
606                            applies_map.insert(
607                                serde_yaml::Value::String("riskClassification".to_string()),
608                                serde_yaml::Value::String(risk_classification.clone()),
609                            );
610                        }
611                        if !applies_map.is_empty() {
612                            profile_map.insert(
613                                serde_yaml::Value::String("appliesTo".to_string()),
614                                serde_yaml::Value::Mapping(applies_map),
615                            );
616                        }
617                    }
618                    let checks_yaml: Vec<serde_yaml::Value> = profile
619                        .required_checks
620                        .iter()
621                        .map(|c| serde_yaml::Value::String(c.clone()))
622                        .collect();
623                    profile_map.insert(
624                        serde_yaml::Value::String("requiredChecks".to_string()),
625                        serde_yaml::Value::Sequence(checks_yaml),
626                    );
627                    serde_yaml::Value::Mapping(profile_map)
628                })
629                .collect();
630            yaml.insert(
631                serde_yaml::Value::String("validationProfiles".to_string()),
632                serde_yaml::Value::Sequence(profiles_yaml),
633            );
634        }
635
636        if let Some(bpmn_models) = &asset.bpmn_models {
637            let models_yaml: Vec<serde_yaml::Value> = bpmn_models
638                .iter()
639                .map(|model| {
640                    let mut model_map = serde_yaml::Mapping::new();
641                    model_map.insert(
642                        serde_yaml::Value::String("name".to_string()),
643                        serde_yaml::Value::String(model.name.clone()),
644                    );
645                    model_map.insert(
646                        serde_yaml::Value::String("reference".to_string()),
647                        serde_yaml::Value::String(model.reference.clone()),
648                    );
649                    let format_str = match model.format {
650                        CADSBPMNFormat::Bpmn20Xml => "bpmn20-xml",
651                        CADSBPMNFormat::Json => "json",
652                    };
653                    model_map.insert(
654                        serde_yaml::Value::String("format".to_string()),
655                        serde_yaml::Value::String(format_str.to_string()),
656                    );
657                    if let Some(description) = &model.description {
658                        model_map.insert(
659                            serde_yaml::Value::String("description".to_string()),
660                            serde_yaml::Value::String(description.clone()),
661                        );
662                    }
663                    serde_yaml::Value::Mapping(model_map)
664                })
665                .collect();
666            yaml.insert(
667                serde_yaml::Value::String("bpmnModels".to_string()),
668                serde_yaml::Value::Sequence(models_yaml),
669            );
670        }
671
672        if let Some(dmn_models) = &asset.dmn_models {
673            let models_yaml: Vec<serde_yaml::Value> = dmn_models
674                .iter()
675                .map(|model| {
676                    let mut model_map = serde_yaml::Mapping::new();
677                    model_map.insert(
678                        serde_yaml::Value::String("name".to_string()),
679                        serde_yaml::Value::String(model.name.clone()),
680                    );
681                    model_map.insert(
682                        serde_yaml::Value::String("reference".to_string()),
683                        serde_yaml::Value::String(model.reference.clone()),
684                    );
685                    let format_str = match model.format {
686                        CADSDMNFormat::Dmn13Xml => "dmn13-xml",
687                    };
688                    model_map.insert(
689                        serde_yaml::Value::String("format".to_string()),
690                        serde_yaml::Value::String(format_str.to_string()),
691                    );
692                    if let Some(description) = &model.description {
693                        model_map.insert(
694                            serde_yaml::Value::String("description".to_string()),
695                            serde_yaml::Value::String(description.clone()),
696                        );
697                    }
698                    serde_yaml::Value::Mapping(model_map)
699                })
700                .collect();
701            yaml.insert(
702                serde_yaml::Value::String("dmnModels".to_string()),
703                serde_yaml::Value::Sequence(models_yaml),
704            );
705        }
706
707        if let Some(openapi_specs) = &asset.openapi_specs {
708            let specs_yaml: Vec<serde_yaml::Value> = openapi_specs
709                .iter()
710                .map(|spec| {
711                    let mut spec_map = serde_yaml::Mapping::new();
712                    spec_map.insert(
713                        serde_yaml::Value::String("name".to_string()),
714                        serde_yaml::Value::String(spec.name.clone()),
715                    );
716                    spec_map.insert(
717                        serde_yaml::Value::String("reference".to_string()),
718                        serde_yaml::Value::String(spec.reference.clone()),
719                    );
720                    let format_str = match spec.format {
721                        CADSOpenAPIFormat::Openapi311Yaml => "openapi-311-yaml",
722                        CADSOpenAPIFormat::Openapi311Json => "openapi-311-json",
723                    };
724                    spec_map.insert(
725                        serde_yaml::Value::String("format".to_string()),
726                        serde_yaml::Value::String(format_str.to_string()),
727                    );
728                    if let Some(description) = &spec.description {
729                        spec_map.insert(
730                            serde_yaml::Value::String("description".to_string()),
731                            serde_yaml::Value::String(description.clone()),
732                        );
733                    }
734                    serde_yaml::Value::Mapping(spec_map)
735                })
736                .collect();
737            yaml.insert(
738                serde_yaml::Value::String("openapiSpecs".to_string()),
739                serde_yaml::Value::Sequence(specs_yaml),
740            );
741        }
742
743        if let Some(custom_properties) = &asset.custom_properties {
744            let mut custom_map = serde_yaml::Mapping::new();
745            for (key, value) in custom_properties {
746                custom_map.insert(
747                    serde_yaml::Value::String(key.clone()),
748                    Self::json_to_yaml_value(value),
749                );
750            }
751            if !custom_map.is_empty() {
752                yaml.insert(
753                    serde_yaml::Value::String("customProperties".to_string()),
754                    serde_yaml::Value::Mapping(custom_map),
755                );
756            }
757        }
758
759        // Serialize to YAML string
760        serde_yaml::to_string(&serde_yaml::Value::Mapping(yaml))
761            .unwrap_or_else(|_| String::from(""))
762    }
763
764    /// Helper to convert serde_json::Value to serde_yaml::Value
765    fn json_to_yaml_value(json: &serde_json::Value) -> serde_yaml::Value {
766        match json {
767            serde_json::Value::Null => serde_yaml::Value::Null,
768            serde_json::Value::Bool(b) => serde_yaml::Value::Bool(*b),
769            serde_json::Value::Number(n) => {
770                if let Some(i) = n.as_i64() {
771                    serde_yaml::Value::Number(serde_yaml::Number::from(i))
772                } else if let Some(f) = n.as_f64() {
773                    serde_yaml::Value::Number(serde_yaml::Number::from(f))
774                } else {
775                    serde_yaml::Value::String(n.to_string())
776                }
777            }
778            serde_json::Value::String(s) => serde_yaml::Value::String(s.clone()),
779            serde_json::Value::Array(arr) => {
780                let yaml_arr: Vec<serde_yaml::Value> =
781                    arr.iter().map(Self::json_to_yaml_value).collect();
782                serde_yaml::Value::Sequence(yaml_arr)
783            }
784            serde_json::Value::Object(obj) => {
785                let mut yaml_map = serde_yaml::Mapping::new();
786                for (k, v) in obj {
787                    yaml_map.insert(
788                        serde_yaml::Value::String(k.clone()),
789                        Self::json_to_yaml_value(v),
790                    );
791                }
792                serde_yaml::Value::Mapping(yaml_map)
793            }
794        }
795    }
796}