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