Skip to main content

data_modelling_core/import/
cads.rs

1//! CADS (Compute Asset Description Specification) importer
2//!
3//! Parses CADS v1.0 YAML files and converts them to CADSAsset models.
4
5use super::ImportError;
6use crate::models::Tag;
7use crate::models::cads::*;
8use anyhow::{Context, Result};
9use serde_json::Value as JsonValue;
10use serde_yaml::Value as YamlValue;
11use std::collections::HashMap;
12use std::str::FromStr;
13
14/// CADS importer for parsing CADS v1.0 YAML files
15pub struct CADSImporter;
16
17impl CADSImporter {
18    /// Create a new CADS importer instance
19    pub fn new() -> Self {
20        Self
21    }
22
23    /// Import CADS YAML content and create CADSAsset
24    ///
25    /// # Arguments
26    ///
27    /// * `yaml_content` - CADS YAML content as a string
28    ///
29    /// # Returns
30    ///
31    /// A `CADSAsset` parsed from the YAML content
32    ///
33    /// # Example
34    ///
35    /// ```rust
36    /// use data_modelling_core::import::cads::CADSImporter;
37    ///
38    /// let importer = CADSImporter::new();
39    /// let yaml = r#"
40    /// apiVersion: v1.0
41    /// kind: AIModel
42    /// id: 550e8400-e29b-41d4-a716-446655440000
43    /// name: sentiment-analysis-model
44    /// version: 1.0.0
45    /// status: production
46    /// "#;
47    /// let asset = importer.import(yaml).unwrap();
48    /// assert_eq!(asset.name, "sentiment-analysis-model");
49    /// ```
50    pub fn import(&self, yaml_content: &str) -> Result<CADSAsset, ImportError> {
51        let yaml_value: YamlValue = serde_yaml::from_str(yaml_content)
52            .map_err(|e| ImportError::ParseError(format!("Failed to parse YAML: {}", e)))?;
53
54        self.parse_cads_asset(&yaml_value)
55            .map_err(|e| ImportError::ParseError(e.to_string()))
56    }
57
58    /// Parse CADS asset from YAML value
59    fn parse_cads_asset(&self, yaml: &YamlValue) -> Result<CADSAsset> {
60        let _obj = yaml
61            .as_mapping()
62            .ok_or_else(|| anyhow::anyhow!("CADS YAML must be a mapping"))?;
63
64        // Convert YAML to JSON for easier parsing
65        let json_value: JsonValue =
66            serde_json::to_value(yaml).context("Failed to convert YAML to JSON")?;
67
68        // Parse required fields
69        let api_version = json_value
70            .get("apiVersion")
71            .and_then(|v| v.as_str())
72            .ok_or_else(|| anyhow::anyhow!("Missing required field: apiVersion"))?
73            .to_string();
74
75        let kind_str = json_value
76            .get("kind")
77            .and_then(|v| v.as_str())
78            .ok_or_else(|| anyhow::anyhow!("Missing required field: kind"))?;
79
80        let kind = match kind_str {
81            "AIModel" => CADSKind::AIModel,
82            "MLPipeline" => CADSKind::MLPipeline,
83            "Application" => CADSKind::Application,
84            "ETLPipeline" => CADSKind::ETLPipeline,
85            "SourceSystem" => CADSKind::SourceSystem,
86            "DestinationSystem" => CADSKind::DestinationSystem,
87            _ => return Err(anyhow::anyhow!("Invalid kind: {}", kind_str)),
88        };
89
90        let id = json_value
91            .get("id")
92            .and_then(|v| v.as_str())
93            .ok_or_else(|| anyhow::anyhow!("Missing required field: id"))?
94            .to_string();
95
96        let name = json_value
97            .get("name")
98            .and_then(|v| v.as_str())
99            .ok_or_else(|| anyhow::anyhow!("Missing required field: name"))?
100            .to_string();
101
102        let version = json_value
103            .get("version")
104            .and_then(|v| v.as_str())
105            .ok_or_else(|| anyhow::anyhow!("Missing required field: version"))?
106            .to_string();
107
108        let status_str = json_value
109            .get("status")
110            .and_then(|v| v.as_str())
111            .ok_or_else(|| anyhow::anyhow!("Missing required field: status"))?;
112
113        let status = match status_str {
114            "draft" => CADSStatus::Draft,
115            "validated" => CADSStatus::Validated,
116            "production" => CADSStatus::Production,
117            "deprecated" => CADSStatus::Deprecated,
118            _ => return Err(anyhow::anyhow!("Invalid status: {}", status_str)),
119        };
120
121        // Parse optional fields
122        let domain = json_value
123            .get("domain")
124            .and_then(|v| v.as_str())
125            .map(|s| s.to_string());
126
127        let tags = self.parse_tags(&json_value)?;
128        let description = self.parse_description(&json_value)?;
129        let runtime = self.parse_runtime(&json_value)?;
130        let sla = self.parse_sla(&json_value)?;
131        let pricing = self.parse_pricing(&json_value)?;
132        let team = self.parse_team(&json_value)?;
133        let risk = self.parse_risk(&json_value)?;
134        let compliance = self.parse_compliance(&json_value)?;
135        let validation_profiles = self.parse_validation_profiles(&json_value)?;
136        let bpmn_models = self.parse_bpmn_models(&json_value)?;
137        let dmn_models = self.parse_dmn_models(&json_value)?;
138        let openapi_specs = self.parse_openapi_specs(&json_value)?;
139        let custom_properties = self.parse_custom_properties(&json_value)?;
140
141        // Parse domain_id if present
142        let domain_id = json_value
143            .get("domainId")
144            .or_else(|| json_value.get("domain_id"))
145            .and_then(|v| v.as_str())
146            .and_then(|s| uuid::Uuid::parse_str(s).ok());
147
148        Ok(CADSAsset {
149            api_version,
150            kind,
151            id,
152            name,
153            version,
154            status,
155            domain,
156            domain_id,
157            tags,
158            description,
159            runtime,
160            sla,
161            pricing,
162            team,
163            risk,
164            compliance,
165            validation_profiles,
166            bpmn_models,
167            dmn_models,
168            openapi_specs,
169            custom_properties,
170            created_at: Some(chrono::Utc::now()),
171            updated_at: Some(chrono::Utc::now()),
172        })
173    }
174
175    /// Parse tags array
176    fn parse_tags(&self, json: &JsonValue) -> Result<Vec<Tag>> {
177        let mut tags = Vec::new();
178        if let Some(tags_arr) = json.get("tags").and_then(|v| v.as_array()) {
179            for item in tags_arr {
180                if let Some(s) = item.as_str() {
181                    if let Ok(tag) = Tag::from_str(s) {
182                        tags.push(tag);
183                    } else {
184                        tags.push(Tag::Simple(s.to_string()));
185                    }
186                }
187            }
188        }
189        Ok(tags)
190    }
191
192    /// Parse description object
193    fn parse_description(&self, json: &JsonValue) -> Result<Option<CADSDescription>> {
194        if let Some(desc_obj) = json.get("description").and_then(|v| v.as_object()) {
195            let purpose = desc_obj
196                .get("purpose")
197                .and_then(|v| v.as_str())
198                .map(|s| s.to_string());
199            let usage = desc_obj
200                .get("usage")
201                .and_then(|v| v.as_str())
202                .map(|s| s.to_string());
203            let limitations = desc_obj
204                .get("limitations")
205                .and_then(|v| v.as_str())
206                .map(|s| s.to_string());
207
208            let external_links =
209                if let Some(links_arr) = desc_obj.get("externalLinks").and_then(|v| v.as_array()) {
210                    let mut links = Vec::new();
211                    for link_item in links_arr {
212                        if let Some(link_obj) = link_item.as_object()
213                            && let Some(url) = link_obj.get("url").and_then(|v| v.as_str())
214                        {
215                            links.push(CADSExternalLink {
216                                url: url.to_string(),
217                                description: link_obj
218                                    .get("description")
219                                    .and_then(|v| v.as_str())
220                                    .map(|s| s.to_string()),
221                            });
222                        }
223                    }
224                    if !links.is_empty() { Some(links) } else { None }
225                } else {
226                    None
227                };
228
229            Ok(Some(CADSDescription {
230                purpose,
231                usage,
232                limitations,
233                external_links,
234            }))
235        } else {
236            Ok(None)
237        }
238    }
239
240    /// Parse runtime object
241    fn parse_runtime(&self, json: &JsonValue) -> Result<Option<CADSRuntime>> {
242        if let Some(runtime_obj) = json.get("runtime").and_then(|v| v.as_object()) {
243            let environment = runtime_obj
244                .get("environment")
245                .and_then(|v| v.as_str())
246                .map(|s| s.to_string());
247
248            let endpoints = if let Some(endpoints_arr) =
249                runtime_obj.get("endpoints").and_then(|v| v.as_array())
250            {
251                let mut eps = Vec::new();
252                for ep in endpoints_arr {
253                    if let Some(s) = ep.as_str() {
254                        eps.push(s.to_string());
255                    }
256                }
257                if !eps.is_empty() { Some(eps) } else { None }
258            } else {
259                None
260            };
261
262            let container = runtime_obj
263                .get("container")
264                .and_then(|v| v.as_object())
265                .map(|container_obj| CADSRuntimeContainer {
266                    image: container_obj
267                        .get("image")
268                        .and_then(|v| v.as_str())
269                        .map(|s| s.to_string()),
270                });
271
272            let resources = runtime_obj
273                .get("resources")
274                .and_then(|v| v.as_object())
275                .map(|resources_obj| CADSRuntimeResources {
276                    cpu: resources_obj
277                        .get("cpu")
278                        .and_then(|v| v.as_str())
279                        .map(|s| s.to_string()),
280                    memory: resources_obj
281                        .get("memory")
282                        .and_then(|v| v.as_str())
283                        .map(|s| s.to_string()),
284                    gpu: resources_obj
285                        .get("gpu")
286                        .and_then(|v| v.as_str())
287                        .map(|s| s.to_string()),
288                });
289
290            Ok(Some(CADSRuntime {
291                environment,
292                endpoints,
293                container,
294                resources,
295            }))
296        } else {
297            Ok(None)
298        }
299    }
300
301    /// Parse SLA object
302    fn parse_sla(&self, json: &JsonValue) -> Result<Option<CADSSLA>> {
303        if let Some(sla_obj) = json.get("sla").and_then(|v| v.as_object()) {
304            let properties =
305                if let Some(props_arr) = sla_obj.get("properties").and_then(|v| v.as_array()) {
306                    let mut props = Vec::new();
307                    for prop_item in props_arr {
308                        if let Some(prop_obj) = prop_item.as_object()
309                            && let (Some(element), Some(value), Some(unit)) = (
310                                prop_obj.get("element").and_then(|v| v.as_str()),
311                                prop_obj.get("value"),
312                                prop_obj.get("unit").and_then(|v| v.as_str()),
313                            )
314                        {
315                            props.push(CADSSLAProperty {
316                                element: element.to_string(),
317                                value: value.clone(),
318                                unit: unit.to_string(),
319                                driver: prop_obj
320                                    .get("driver")
321                                    .and_then(|v| v.as_str())
322                                    .map(|s| s.to_string()),
323                            });
324                        }
325                    }
326                    if !props.is_empty() { Some(props) } else { None }
327                } else {
328                    None
329                };
330
331            Ok(Some(CADSSLA { properties }))
332        } else {
333            Ok(None)
334        }
335    }
336
337    /// Parse pricing object
338    fn parse_pricing(&self, json: &JsonValue) -> Result<Option<CADSPricing>> {
339        if let Some(pricing_obj) = json.get("pricing").and_then(|v| v.as_object()) {
340            let model = pricing_obj
341                .get("model")
342                .and_then(|v| v.as_str())
343                .and_then(|s| match s {
344                    "per_request" => Some(CADSPricingModel::PerRequest),
345                    "per_hour" => Some(CADSPricingModel::PerHour),
346                    "per_batch" => Some(CADSPricingModel::PerBatch),
347                    "subscription" => Some(CADSPricingModel::Subscription),
348                    "internal" => Some(CADSPricingModel::Internal),
349                    _ => None,
350                });
351
352            Ok(Some(CADSPricing {
353                model,
354                currency: pricing_obj
355                    .get("currency")
356                    .and_then(|v| v.as_str())
357                    .map(|s| s.to_string()),
358                unit_cost: pricing_obj.get("unitCost").and_then(|v| v.as_f64()),
359                billing_unit: pricing_obj
360                    .get("billingUnit")
361                    .and_then(|v| v.as_str())
362                    .map(|s| s.to_string()),
363                notes: pricing_obj
364                    .get("notes")
365                    .and_then(|v| v.as_str())
366                    .map(|s| s.to_string()),
367            }))
368        } else {
369            Ok(None)
370        }
371    }
372
373    /// Parse team array
374    fn parse_team(&self, json: &JsonValue) -> Result<Option<Vec<CADSTeamMember>>> {
375        if let Some(team_arr) = json.get("team").and_then(|v| v.as_array()) {
376            let mut team = Vec::new();
377            for member_item in team_arr {
378                if let Some(member_obj) = member_item.as_object()
379                    && let (Some(role), Some(name)) = (
380                        member_obj.get("role").and_then(|v| v.as_str()),
381                        member_obj.get("name").and_then(|v| v.as_str()),
382                    )
383                {
384                    team.push(CADSTeamMember {
385                        role: role.to_string(),
386                        name: name.to_string(),
387                        contact: member_obj
388                            .get("contact")
389                            .and_then(|v| v.as_str())
390                            .map(|s| s.to_string()),
391                    });
392                }
393            }
394            if !team.is_empty() {
395                Ok(Some(team))
396            } else {
397                Ok(None)
398            }
399        } else {
400            Ok(None)
401        }
402    }
403
404    /// Parse risk object
405    fn parse_risk(&self, json: &JsonValue) -> Result<Option<CADSRisk>> {
406        if let Some(risk_obj) = json.get("risk").and_then(|v| v.as_object()) {
407            let classification = risk_obj
408                .get("classification")
409                .and_then(|v| v.as_str())
410                .and_then(|s| match s {
411                    "minimal" => Some(CADSRiskClassification::Minimal),
412                    "low" => Some(CADSRiskClassification::Low),
413                    "medium" => Some(CADSRiskClassification::Medium),
414                    "high" => Some(CADSRiskClassification::High),
415                    _ => None,
416                });
417
418            let impact_areas =
419                if let Some(areas_arr) = risk_obj.get("impactAreas").and_then(|v| v.as_array()) {
420                    let mut areas = Vec::new();
421                    for area_item in areas_arr {
422                        if let Some(s) = area_item.as_str()
423                            && let Ok(area) =
424                                serde_json::from_str::<CADSImpactArea>(&format!("\"{}\"", s))
425                        {
426                            areas.push(area);
427                        }
428                    }
429                    if !areas.is_empty() { Some(areas) } else { None }
430                } else {
431                    None
432                };
433
434            let assessment =
435                risk_obj
436                    .get("assessment")
437                    .and_then(|v| v.as_object())
438                    .map(|assess_obj| CADSRiskAssessment {
439                        methodology: assess_obj
440                            .get("methodology")
441                            .and_then(|v| v.as_str())
442                            .map(|s| s.to_string()),
443                        date: assess_obj
444                            .get("date")
445                            .and_then(|v| v.as_str())
446                            .map(|s| s.to_string()),
447                        assessor: assess_obj
448                            .get("assessor")
449                            .and_then(|v| v.as_str())
450                            .map(|s| s.to_string()),
451                    });
452
453            let mitigations =
454                if let Some(mit_arr) = risk_obj.get("mitigations").and_then(|v| v.as_array()) {
455                    let mut mitigations = Vec::new();
456                    for mit_item in mit_arr {
457                        if let Some(mit_obj) = mit_item.as_object()
458                            && let (Some(description), Some(status_str)) = (
459                                mit_obj.get("description").and_then(|v| v.as_str()),
460                                mit_obj.get("status").and_then(|v| v.as_str()),
461                            )
462                        {
463                            let status = match status_str {
464                                "planned" => CADSMitigationStatus::Planned,
465                                "implemented" => CADSMitigationStatus::Implemented,
466                                "verified" => CADSMitigationStatus::Verified,
467                                _ => continue,
468                            };
469                            mitigations.push(CADSRiskMitigation {
470                                description: description.to_string(),
471                                status,
472                            });
473                        }
474                    }
475                    if !mitigations.is_empty() {
476                        Some(mitigations)
477                    } else {
478                        None
479                    }
480                } else {
481                    None
482                };
483
484            Ok(Some(CADSRisk {
485                classification,
486                impact_areas,
487                intended_use: risk_obj
488                    .get("intendedUse")
489                    .and_then(|v| v.as_str())
490                    .map(|s| s.to_string()),
491                out_of_scope_use: risk_obj
492                    .get("outOfScopeUse")
493                    .and_then(|v| v.as_str())
494                    .map(|s| s.to_string()),
495                assessment,
496                mitigations,
497            }))
498        } else {
499            Ok(None)
500        }
501    }
502
503    /// Parse compliance object
504    fn parse_compliance(&self, json: &JsonValue) -> Result<Option<CADSCompliance>> {
505        if let Some(comp_obj) = json.get("compliance").and_then(|v| v.as_object()) {
506            let frameworks = if let Some(frameworks_arr) =
507                comp_obj.get("frameworks").and_then(|v| v.as_array())
508            {
509                let mut frameworks = Vec::new();
510                for fw_item in frameworks_arr {
511                    if let Some(fw_obj) = fw_item.as_object()
512                        && let (Some(name), Some(status_str)) = (
513                            fw_obj.get("name").and_then(|v| v.as_str()),
514                            fw_obj.get("status").and_then(|v| v.as_str()),
515                        )
516                    {
517                        let status = match status_str {
518                            "not_applicable" => CADSComplianceStatus::NotApplicable,
519                            "assessed" => CADSComplianceStatus::Assessed,
520                            "compliant" => CADSComplianceStatus::Compliant,
521                            "non_compliant" => CADSComplianceStatus::NonCompliant,
522                            _ => continue,
523                        };
524                        frameworks.push(CADSComplianceFramework {
525                            name: name.to_string(),
526                            category: fw_obj
527                                .get("category")
528                                .and_then(|v| v.as_str())
529                                .map(|s| s.to_string()),
530                            status,
531                        });
532                    }
533                }
534                if !frameworks.is_empty() {
535                    Some(frameworks)
536                } else {
537                    None
538                }
539            } else {
540                None
541            };
542
543            let controls =
544                if let Some(controls_arr) = comp_obj.get("controls").and_then(|v| v.as_array()) {
545                    let mut controls = Vec::new();
546                    for ctrl_item in controls_arr {
547                        if let Some(ctrl_obj) = ctrl_item.as_object()
548                            && let (Some(id), Some(description)) = (
549                                ctrl_obj.get("id").and_then(|v| v.as_str()),
550                                ctrl_obj.get("description").and_then(|v| v.as_str()),
551                            )
552                        {
553                            controls.push(CADSComplianceControl {
554                                id: id.to_string(),
555                                description: description.to_string(),
556                                evidence: ctrl_obj
557                                    .get("evidence")
558                                    .and_then(|v| v.as_str())
559                                    .map(|s| s.to_string()),
560                            });
561                        }
562                    }
563                    if !controls.is_empty() {
564                        Some(controls)
565                    } else {
566                        None
567                    }
568                } else {
569                    None
570                };
571
572            Ok(Some(CADSCompliance {
573                frameworks,
574                controls,
575            }))
576        } else {
577            Ok(None)
578        }
579    }
580
581    /// Parse validation profiles array
582    fn parse_validation_profiles(
583        &self,
584        json: &JsonValue,
585    ) -> Result<Option<Vec<CADSValidationProfile>>> {
586        if let Some(profiles_arr) = json.get("validationProfiles").and_then(|v| v.as_array()) {
587            let mut profiles = Vec::new();
588            for profile_item in profiles_arr {
589                if let Some(profile_obj) = profile_item.as_object()
590                    && let (Some(name), Some(checks_arr)) = (
591                        profile_obj.get("name").and_then(|v| v.as_str()),
592                        profile_obj.get("requiredChecks").and_then(|v| v.as_array()),
593                    )
594                {
595                    let mut required_checks = Vec::new();
596                    for check_item in checks_arr {
597                        if let Some(s) = check_item.as_str() {
598                            required_checks.push(s.to_string());
599                        }
600                    }
601
602                    let applies_to = profile_obj
603                        .get("appliesTo")
604                        .and_then(|v| v.as_object())
605                        .map(|applies_obj| CADSValidationProfileAppliesTo {
606                            kind: applies_obj
607                                .get("kind")
608                                .and_then(|v| v.as_str())
609                                .map(|s| s.to_string()),
610                            risk_classification: applies_obj
611                                .get("riskClassification")
612                                .and_then(|v| v.as_str())
613                                .map(|s| s.to_string()),
614                        });
615
616                    profiles.push(CADSValidationProfile {
617                        name: name.to_string(),
618                        applies_to,
619                        required_checks,
620                    });
621                }
622            }
623            if !profiles.is_empty() {
624                Ok(Some(profiles))
625            } else {
626                Ok(None)
627            }
628        } else {
629            Ok(None)
630        }
631    }
632
633    /// Parse BPMN models array
634    fn parse_bpmn_models(&self, json: &JsonValue) -> Result<Option<Vec<CADSBPMNModel>>> {
635        if let Some(models_arr) = json.get("bpmnModels").and_then(|v| v.as_array()) {
636            let mut models = Vec::new();
637            for model_item in models_arr {
638                if let Some(model_obj) = model_item.as_object()
639                    && let (Some(name), Some(reference), Some(format_str)) = (
640                        model_obj.get("name").and_then(|v| v.as_str()),
641                        model_obj.get("reference").and_then(|v| v.as_str()),
642                        model_obj.get("format").and_then(|v| v.as_str()),
643                    )
644                {
645                    let format = match format_str {
646                        "bpmn20-xml" => CADSBPMNFormat::Bpmn20Xml,
647                        "json" => CADSBPMNFormat::Json,
648                        _ => continue,
649                    };
650
651                    models.push(CADSBPMNModel {
652                        name: name.to_string(),
653                        reference: reference.to_string(),
654                        format,
655                        description: model_obj
656                            .get("description")
657                            .and_then(|v| v.as_str())
658                            .map(|s| s.to_string()),
659                    });
660                }
661            }
662            if !models.is_empty() {
663                Ok(Some(models))
664            } else {
665                Ok(None)
666            }
667        } else {
668            Ok(None)
669        }
670    }
671
672    /// Parse DMN models array
673    fn parse_dmn_models(&self, json: &JsonValue) -> Result<Option<Vec<CADSDMNModel>>> {
674        if let Some(models_arr) = json.get("dmnModels").and_then(|v| v.as_array()) {
675            let mut models = Vec::new();
676            for model_item in models_arr {
677                if let Some(model_obj) = model_item.as_object()
678                    && let (Some(name), Some(reference), Some(format_str)) = (
679                        model_obj.get("name").and_then(|v| v.as_str()),
680                        model_obj.get("reference").and_then(|v| v.as_str()),
681                        model_obj.get("format").and_then(|v| v.as_str()),
682                    )
683                {
684                    let format = match format_str {
685                        "dmn13-xml" => CADSDMNFormat::Dmn13Xml,
686                        _ => continue,
687                    };
688
689                    models.push(CADSDMNModel {
690                        name: name.to_string(),
691                        reference: reference.to_string(),
692                        format,
693                        description: model_obj
694                            .get("description")
695                            .and_then(|v| v.as_str())
696                            .map(|s| s.to_string()),
697                    });
698                }
699            }
700            if !models.is_empty() {
701                Ok(Some(models))
702            } else {
703                Ok(None)
704            }
705        } else {
706            Ok(None)
707        }
708    }
709
710    /// Parse OpenAPI specs array
711    fn parse_openapi_specs(&self, json: &JsonValue) -> Result<Option<Vec<CADSOpenAPISpec>>> {
712        if let Some(specs_arr) = json.get("openapiSpecs").and_then(|v| v.as_array()) {
713            let mut specs = Vec::new();
714            for spec_item in specs_arr {
715                if let Some(spec_obj) = spec_item.as_object()
716                    && let (Some(name), Some(reference), Some(format_str)) = (
717                        spec_obj.get("name").and_then(|v| v.as_str()),
718                        spec_obj.get("reference").and_then(|v| v.as_str()),
719                        spec_obj.get("format").and_then(|v| v.as_str()),
720                    )
721                {
722                    let format = match format_str {
723                        "openapi-3.0" => CADSOpenAPIFormat::Openapi30,
724                        "openapi-3.1" => CADSOpenAPIFormat::Openapi31,
725                        "swagger-2.0" => CADSOpenAPIFormat::Swagger20,
726                        _ => continue,
727                    };
728
729                    specs.push(CADSOpenAPISpec {
730                        name: name.to_string(),
731                        reference: reference.to_string(),
732                        format,
733                        description: spec_obj
734                            .get("description")
735                            .and_then(|v| v.as_str())
736                            .map(|s| s.to_string()),
737                    });
738                }
739            }
740            if !specs.is_empty() {
741                Ok(Some(specs))
742            } else {
743                Ok(None)
744            }
745        } else {
746            Ok(None)
747        }
748    }
749
750    /// Parse custom properties object
751    fn parse_custom_properties(
752        &self,
753        json: &JsonValue,
754    ) -> Result<Option<HashMap<String, serde_json::Value>>> {
755        if let Some(custom_obj) = json.get("customProperties").and_then(|v| v.as_object()) {
756            let mut props = HashMap::new();
757            for (key, value) in custom_obj {
758                props.insert(key.clone(), value.clone());
759            }
760            if !props.is_empty() {
761                Ok(Some(props))
762            } else {
763                Ok(None)
764            }
765        } else {
766            Ok(None)
767        }
768    }
769}
770
771impl Default for CADSImporter {
772    fn default() -> Self {
773        Self::new()
774    }
775}