1use 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
14pub struct CADSImporter;
16
17impl CADSImporter {
18 pub fn new() -> Self {
20 Self
21 }
22
23 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 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 let json_value: JsonValue =
66 serde_json::to_value(yaml).context("Failed to convert YAML to JSON")?;
67
68 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}