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        Ok(CADSAsset {
142            api_version,
143            kind,
144            id,
145            name,
146            version,
147            status,
148            domain,
149            tags,
150            description,
151            runtime,
152            sla,
153            pricing,
154            team,
155            risk,
156            compliance,
157            validation_profiles,
158            bpmn_models,
159            dmn_models,
160            openapi_specs,
161            custom_properties,
162            created_at: Some(chrono::Utc::now()),
163            updated_at: Some(chrono::Utc::now()),
164        })
165    }
166
167    /// Parse tags array
168    fn parse_tags(&self, json: &JsonValue) -> Result<Vec<Tag>> {
169        let mut tags = Vec::new();
170        if let Some(tags_arr) = json.get("tags").and_then(|v| v.as_array()) {
171            for item in tags_arr {
172                if let Some(s) = item.as_str() {
173                    if let Ok(tag) = Tag::from_str(s) {
174                        tags.push(tag);
175                    } else {
176                        tags.push(Tag::Simple(s.to_string()));
177                    }
178                }
179            }
180        }
181        Ok(tags)
182    }
183
184    /// Parse description object
185    fn parse_description(&self, json: &JsonValue) -> Result<Option<CADSDescription>> {
186        if let Some(desc_obj) = json.get("description").and_then(|v| v.as_object()) {
187            let purpose = desc_obj
188                .get("purpose")
189                .and_then(|v| v.as_str())
190                .map(|s| s.to_string());
191            let usage = desc_obj
192                .get("usage")
193                .and_then(|v| v.as_str())
194                .map(|s| s.to_string());
195            let limitations = desc_obj
196                .get("limitations")
197                .and_then(|v| v.as_str())
198                .map(|s| s.to_string());
199
200            let external_links =
201                if let Some(links_arr) = desc_obj.get("externalLinks").and_then(|v| v.as_array()) {
202                    let mut links = Vec::new();
203                    for link_item in links_arr {
204                        if let Some(link_obj) = link_item.as_object()
205                            && let Some(url) = link_obj.get("url").and_then(|v| v.as_str())
206                        {
207                            links.push(CADSExternalLink {
208                                url: url.to_string(),
209                                description: link_obj
210                                    .get("description")
211                                    .and_then(|v| v.as_str())
212                                    .map(|s| s.to_string()),
213                            });
214                        }
215                    }
216                    if !links.is_empty() { Some(links) } else { None }
217                } else {
218                    None
219                };
220
221            Ok(Some(CADSDescription {
222                purpose,
223                usage,
224                limitations,
225                external_links,
226            }))
227        } else {
228            Ok(None)
229        }
230    }
231
232    /// Parse runtime object
233    fn parse_runtime(&self, json: &JsonValue) -> Result<Option<CADSRuntime>> {
234        if let Some(runtime_obj) = json.get("runtime").and_then(|v| v.as_object()) {
235            let environment = runtime_obj
236                .get("environment")
237                .and_then(|v| v.as_str())
238                .map(|s| s.to_string());
239
240            let endpoints = if let Some(endpoints_arr) =
241                runtime_obj.get("endpoints").and_then(|v| v.as_array())
242            {
243                let mut eps = Vec::new();
244                for ep in endpoints_arr {
245                    if let Some(s) = ep.as_str() {
246                        eps.push(s.to_string());
247                    }
248                }
249                if !eps.is_empty() { Some(eps) } else { None }
250            } else {
251                None
252            };
253
254            let container = runtime_obj
255                .get("container")
256                .and_then(|v| v.as_object())
257                .map(|container_obj| CADSRuntimeContainer {
258                    image: container_obj
259                        .get("image")
260                        .and_then(|v| v.as_str())
261                        .map(|s| s.to_string()),
262                });
263
264            let resources = runtime_obj
265                .get("resources")
266                .and_then(|v| v.as_object())
267                .map(|resources_obj| CADSRuntimeResources {
268                    cpu: resources_obj
269                        .get("cpu")
270                        .and_then(|v| v.as_str())
271                        .map(|s| s.to_string()),
272                    memory: resources_obj
273                        .get("memory")
274                        .and_then(|v| v.as_str())
275                        .map(|s| s.to_string()),
276                    gpu: resources_obj
277                        .get("gpu")
278                        .and_then(|v| v.as_str())
279                        .map(|s| s.to_string()),
280                });
281
282            Ok(Some(CADSRuntime {
283                environment,
284                endpoints,
285                container,
286                resources,
287            }))
288        } else {
289            Ok(None)
290        }
291    }
292
293    /// Parse SLA object
294    fn parse_sla(&self, json: &JsonValue) -> Result<Option<CADSSLA>> {
295        if let Some(sla_obj) = json.get("sla").and_then(|v| v.as_object()) {
296            let properties =
297                if let Some(props_arr) = sla_obj.get("properties").and_then(|v| v.as_array()) {
298                    let mut props = Vec::new();
299                    for prop_item in props_arr {
300                        if let Some(prop_obj) = prop_item.as_object()
301                            && let (Some(element), Some(value), Some(unit)) = (
302                                prop_obj.get("element").and_then(|v| v.as_str()),
303                                prop_obj.get("value"),
304                                prop_obj.get("unit").and_then(|v| v.as_str()),
305                            )
306                        {
307                            props.push(CADSSLAProperty {
308                                element: element.to_string(),
309                                value: value.clone(),
310                                unit: unit.to_string(),
311                                driver: prop_obj
312                                    .get("driver")
313                                    .and_then(|v| v.as_str())
314                                    .map(|s| s.to_string()),
315                            });
316                        }
317                    }
318                    if !props.is_empty() { Some(props) } else { None }
319                } else {
320                    None
321                };
322
323            Ok(Some(CADSSLA { properties }))
324        } else {
325            Ok(None)
326        }
327    }
328
329    /// Parse pricing object
330    fn parse_pricing(&self, json: &JsonValue) -> Result<Option<CADSPricing>> {
331        if let Some(pricing_obj) = json.get("pricing").and_then(|v| v.as_object()) {
332            let model = pricing_obj
333                .get("model")
334                .and_then(|v| v.as_str())
335                .and_then(|s| match s {
336                    "per_request" => Some(CADSPricingModel::PerRequest),
337                    "per_hour" => Some(CADSPricingModel::PerHour),
338                    "per_batch" => Some(CADSPricingModel::PerBatch),
339                    "subscription" => Some(CADSPricingModel::Subscription),
340                    "internal" => Some(CADSPricingModel::Internal),
341                    _ => None,
342                });
343
344            Ok(Some(CADSPricing {
345                model,
346                currency: pricing_obj
347                    .get("currency")
348                    .and_then(|v| v.as_str())
349                    .map(|s| s.to_string()),
350                unit_cost: pricing_obj.get("unitCost").and_then(|v| v.as_f64()),
351                billing_unit: pricing_obj
352                    .get("billingUnit")
353                    .and_then(|v| v.as_str())
354                    .map(|s| s.to_string()),
355                notes: pricing_obj
356                    .get("notes")
357                    .and_then(|v| v.as_str())
358                    .map(|s| s.to_string()),
359            }))
360        } else {
361            Ok(None)
362        }
363    }
364
365    /// Parse team array
366    fn parse_team(&self, json: &JsonValue) -> Result<Option<Vec<CADSTeamMember>>> {
367        if let Some(team_arr) = json.get("team").and_then(|v| v.as_array()) {
368            let mut team = Vec::new();
369            for member_item in team_arr {
370                if let Some(member_obj) = member_item.as_object()
371                    && let (Some(role), Some(name)) = (
372                        member_obj.get("role").and_then(|v| v.as_str()),
373                        member_obj.get("name").and_then(|v| v.as_str()),
374                    )
375                {
376                    team.push(CADSTeamMember {
377                        role: role.to_string(),
378                        name: name.to_string(),
379                        contact: member_obj
380                            .get("contact")
381                            .and_then(|v| v.as_str())
382                            .map(|s| s.to_string()),
383                    });
384                }
385            }
386            if !team.is_empty() {
387                Ok(Some(team))
388            } else {
389                Ok(None)
390            }
391        } else {
392            Ok(None)
393        }
394    }
395
396    /// Parse risk object
397    fn parse_risk(&self, json: &JsonValue) -> Result<Option<CADSRisk>> {
398        if let Some(risk_obj) = json.get("risk").and_then(|v| v.as_object()) {
399            let classification = risk_obj
400                .get("classification")
401                .and_then(|v| v.as_str())
402                .and_then(|s| match s {
403                    "minimal" => Some(CADSRiskClassification::Minimal),
404                    "low" => Some(CADSRiskClassification::Low),
405                    "medium" => Some(CADSRiskClassification::Medium),
406                    "high" => Some(CADSRiskClassification::High),
407                    _ => None,
408                });
409
410            let impact_areas =
411                if let Some(areas_arr) = risk_obj.get("impactAreas").and_then(|v| v.as_array()) {
412                    let mut areas = Vec::new();
413                    for area_item in areas_arr {
414                        if let Some(s) = area_item.as_str()
415                            && let Ok(area) =
416                                serde_json::from_str::<CADSImpactArea>(&format!("\"{}\"", s))
417                        {
418                            areas.push(area);
419                        }
420                    }
421                    if !areas.is_empty() { Some(areas) } else { None }
422                } else {
423                    None
424                };
425
426            let assessment =
427                risk_obj
428                    .get("assessment")
429                    .and_then(|v| v.as_object())
430                    .map(|assess_obj| CADSRiskAssessment {
431                        methodology: assess_obj
432                            .get("methodology")
433                            .and_then(|v| v.as_str())
434                            .map(|s| s.to_string()),
435                        date: assess_obj
436                            .get("date")
437                            .and_then(|v| v.as_str())
438                            .map(|s| s.to_string()),
439                        assessor: assess_obj
440                            .get("assessor")
441                            .and_then(|v| v.as_str())
442                            .map(|s| s.to_string()),
443                    });
444
445            let mitigations =
446                if let Some(mit_arr) = risk_obj.get("mitigations").and_then(|v| v.as_array()) {
447                    let mut mitigations = Vec::new();
448                    for mit_item in mit_arr {
449                        if let Some(mit_obj) = mit_item.as_object()
450                            && let (Some(description), Some(status_str)) = (
451                                mit_obj.get("description").and_then(|v| v.as_str()),
452                                mit_obj.get("status").and_then(|v| v.as_str()),
453                            )
454                        {
455                            let status = match status_str {
456                                "planned" => CADSMitigationStatus::Planned,
457                                "implemented" => CADSMitigationStatus::Implemented,
458                                "verified" => CADSMitigationStatus::Verified,
459                                _ => continue,
460                            };
461                            mitigations.push(CADSRiskMitigation {
462                                description: description.to_string(),
463                                status,
464                            });
465                        }
466                    }
467                    if !mitigations.is_empty() {
468                        Some(mitigations)
469                    } else {
470                        None
471                    }
472                } else {
473                    None
474                };
475
476            Ok(Some(CADSRisk {
477                classification,
478                impact_areas,
479                intended_use: risk_obj
480                    .get("intendedUse")
481                    .and_then(|v| v.as_str())
482                    .map(|s| s.to_string()),
483                out_of_scope_use: risk_obj
484                    .get("outOfScopeUse")
485                    .and_then(|v| v.as_str())
486                    .map(|s| s.to_string()),
487                assessment,
488                mitigations,
489            }))
490        } else {
491            Ok(None)
492        }
493    }
494
495    /// Parse compliance object
496    fn parse_compliance(&self, json: &JsonValue) -> Result<Option<CADSCompliance>> {
497        if let Some(comp_obj) = json.get("compliance").and_then(|v| v.as_object()) {
498            let frameworks = if let Some(frameworks_arr) =
499                comp_obj.get("frameworks").and_then(|v| v.as_array())
500            {
501                let mut frameworks = Vec::new();
502                for fw_item in frameworks_arr {
503                    if let Some(fw_obj) = fw_item.as_object()
504                        && let (Some(name), Some(status_str)) = (
505                            fw_obj.get("name").and_then(|v| v.as_str()),
506                            fw_obj.get("status").and_then(|v| v.as_str()),
507                        )
508                    {
509                        let status = match status_str {
510                            "not_applicable" => CADSComplianceStatus::NotApplicable,
511                            "assessed" => CADSComplianceStatus::Assessed,
512                            "compliant" => CADSComplianceStatus::Compliant,
513                            "non_compliant" => CADSComplianceStatus::NonCompliant,
514                            _ => continue,
515                        };
516                        frameworks.push(CADSComplianceFramework {
517                            name: name.to_string(),
518                            category: fw_obj
519                                .get("category")
520                                .and_then(|v| v.as_str())
521                                .map(|s| s.to_string()),
522                            status,
523                        });
524                    }
525                }
526                if !frameworks.is_empty() {
527                    Some(frameworks)
528                } else {
529                    None
530                }
531            } else {
532                None
533            };
534
535            let controls =
536                if let Some(controls_arr) = comp_obj.get("controls").and_then(|v| v.as_array()) {
537                    let mut controls = Vec::new();
538                    for ctrl_item in controls_arr {
539                        if let Some(ctrl_obj) = ctrl_item.as_object()
540                            && let (Some(id), Some(description)) = (
541                                ctrl_obj.get("id").and_then(|v| v.as_str()),
542                                ctrl_obj.get("description").and_then(|v| v.as_str()),
543                            )
544                        {
545                            controls.push(CADSComplianceControl {
546                                id: id.to_string(),
547                                description: description.to_string(),
548                                evidence: ctrl_obj
549                                    .get("evidence")
550                                    .and_then(|v| v.as_str())
551                                    .map(|s| s.to_string()),
552                            });
553                        }
554                    }
555                    if !controls.is_empty() {
556                        Some(controls)
557                    } else {
558                        None
559                    }
560                } else {
561                    None
562                };
563
564            Ok(Some(CADSCompliance {
565                frameworks,
566                controls,
567            }))
568        } else {
569            Ok(None)
570        }
571    }
572
573    /// Parse validation profiles array
574    fn parse_validation_profiles(
575        &self,
576        json: &JsonValue,
577    ) -> Result<Option<Vec<CADSValidationProfile>>> {
578        if let Some(profiles_arr) = json.get("validationProfiles").and_then(|v| v.as_array()) {
579            let mut profiles = Vec::new();
580            for profile_item in profiles_arr {
581                if let Some(profile_obj) = profile_item.as_object()
582                    && let (Some(name), Some(checks_arr)) = (
583                        profile_obj.get("name").and_then(|v| v.as_str()),
584                        profile_obj.get("requiredChecks").and_then(|v| v.as_array()),
585                    )
586                {
587                    let mut required_checks = Vec::new();
588                    for check_item in checks_arr {
589                        if let Some(s) = check_item.as_str() {
590                            required_checks.push(s.to_string());
591                        }
592                    }
593
594                    let applies_to = profile_obj
595                        .get("appliesTo")
596                        .and_then(|v| v.as_object())
597                        .map(|applies_obj| CADSValidationProfileAppliesTo {
598                            kind: applies_obj
599                                .get("kind")
600                                .and_then(|v| v.as_str())
601                                .map(|s| s.to_string()),
602                            risk_classification: applies_obj
603                                .get("riskClassification")
604                                .and_then(|v| v.as_str())
605                                .map(|s| s.to_string()),
606                        });
607
608                    profiles.push(CADSValidationProfile {
609                        name: name.to_string(),
610                        applies_to,
611                        required_checks,
612                    });
613                }
614            }
615            if !profiles.is_empty() {
616                Ok(Some(profiles))
617            } else {
618                Ok(None)
619            }
620        } else {
621            Ok(None)
622        }
623    }
624
625    /// Parse BPMN models array
626    fn parse_bpmn_models(&self, json: &JsonValue) -> Result<Option<Vec<CADSBPMNModel>>> {
627        if let Some(models_arr) = json.get("bpmnModels").and_then(|v| v.as_array()) {
628            let mut models = Vec::new();
629            for model_item in models_arr {
630                if let Some(model_obj) = model_item.as_object()
631                    && let (Some(name), Some(reference), Some(format_str)) = (
632                        model_obj.get("name").and_then(|v| v.as_str()),
633                        model_obj.get("reference").and_then(|v| v.as_str()),
634                        model_obj.get("format").and_then(|v| v.as_str()),
635                    )
636                {
637                    let format = match format_str {
638                        "bpmn20-xml" => CADSBPMNFormat::Bpmn20Xml,
639                        "json" => CADSBPMNFormat::Json,
640                        _ => continue,
641                    };
642
643                    models.push(CADSBPMNModel {
644                        name: name.to_string(),
645                        reference: reference.to_string(),
646                        format,
647                        description: model_obj
648                            .get("description")
649                            .and_then(|v| v.as_str())
650                            .map(|s| s.to_string()),
651                    });
652                }
653            }
654            if !models.is_empty() {
655                Ok(Some(models))
656            } else {
657                Ok(None)
658            }
659        } else {
660            Ok(None)
661        }
662    }
663
664    /// Parse DMN models array
665    fn parse_dmn_models(&self, json: &JsonValue) -> Result<Option<Vec<CADSDMNModel>>> {
666        if let Some(models_arr) = json.get("dmnModels").and_then(|v| v.as_array()) {
667            let mut models = Vec::new();
668            for model_item in models_arr {
669                if let Some(model_obj) = model_item.as_object()
670                    && let (Some(name), Some(reference), Some(format_str)) = (
671                        model_obj.get("name").and_then(|v| v.as_str()),
672                        model_obj.get("reference").and_then(|v| v.as_str()),
673                        model_obj.get("format").and_then(|v| v.as_str()),
674                    )
675                {
676                    let format = match format_str {
677                        "dmn13-xml" => CADSDMNFormat::Dmn13Xml,
678                        _ => continue,
679                    };
680
681                    models.push(CADSDMNModel {
682                        name: name.to_string(),
683                        reference: reference.to_string(),
684                        format,
685                        description: model_obj
686                            .get("description")
687                            .and_then(|v| v.as_str())
688                            .map(|s| s.to_string()),
689                    });
690                }
691            }
692            if !models.is_empty() {
693                Ok(Some(models))
694            } else {
695                Ok(None)
696            }
697        } else {
698            Ok(None)
699        }
700    }
701
702    /// Parse OpenAPI specs array
703    fn parse_openapi_specs(&self, json: &JsonValue) -> Result<Option<Vec<CADSOpenAPISpec>>> {
704        if let Some(specs_arr) = json.get("openapiSpecs").and_then(|v| v.as_array()) {
705            let mut specs = Vec::new();
706            for spec_item in specs_arr {
707                if let Some(spec_obj) = spec_item.as_object()
708                    && let (Some(name), Some(reference), Some(format_str)) = (
709                        spec_obj.get("name").and_then(|v| v.as_str()),
710                        spec_obj.get("reference").and_then(|v| v.as_str()),
711                        spec_obj.get("format").and_then(|v| v.as_str()),
712                    )
713                {
714                    let format = match format_str {
715                        "openapi-3.0" => CADSOpenAPIFormat::Openapi30,
716                        "openapi-3.1" => CADSOpenAPIFormat::Openapi31,
717                        "swagger-2.0" => CADSOpenAPIFormat::Swagger20,
718                        _ => continue,
719                    };
720
721                    specs.push(CADSOpenAPISpec {
722                        name: name.to_string(),
723                        reference: reference.to_string(),
724                        format,
725                        description: spec_obj
726                            .get("description")
727                            .and_then(|v| v.as_str())
728                            .map(|s| s.to_string()),
729                    });
730                }
731            }
732            if !specs.is_empty() {
733                Ok(Some(specs))
734            } else {
735                Ok(None)
736            }
737        } else {
738            Ok(None)
739        }
740    }
741
742    /// Parse custom properties object
743    fn parse_custom_properties(
744        &self,
745        json: &JsonValue,
746    ) -> Result<Option<HashMap<String, serde_json::Value>>> {
747        if let Some(custom_obj) = json.get("customProperties").and_then(|v| v.as_object()) {
748            let mut props = HashMap::new();
749            for (key, value) in custom_obj {
750                props.insert(key.clone(), value.clone());
751            }
752            if !props.is_empty() {
753                Ok(Some(props))
754            } else {
755                Ok(None)
756            }
757        } else {
758            Ok(None)
759        }
760    }
761}
762
763impl Default for CADSImporter {
764    fn default() -> Self {
765        Self::new()
766    }
767}