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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}