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