1use crate::compiled::{CompiledSchema, PropSchema};
11use data_source::types::{
12 SchemaMetadataCatalog, SchemaMetadataEntry, SchemaPropertyConstraints, primary_identifier_override,
13};
14use std::collections::{HashMap, HashSet};
15use std::sync::LazyLock;
16
17pub(crate) static GETATT_RETURN_TYPE_OVERRIDES: LazyLock<HashMap<String, HashMap<String, String>>> =
23 LazyLock::new(|| {
24 let artifact: serde_json::Value =
25 serde_json::from_slice(&data_source::embedded::GETATT_RETURN_TYPE_OVERRIDES_BYTES)
26 .expect("Embedded getatt_return_type_overrides must be valid JSON");
27 let overrides = artifact
28 .get("getatt_return_type_overrides")
29 .expect("Embedded getatt_return_type_overrides must contain getatt_return_type_overrides");
30 let overrides: HashMap<String, HashMap<String, String>> = serde_json::from_value(overrides.clone())
31 .expect("Embedded getatt_return_type_overrides must contain a valid override map");
32 assert!(!overrides.is_empty(), "Embedded getatt_return_type_overrides must not be empty");
33 overrides
34 });
35
36fn apply_getatt_return_type_overrides(type_name: &str, attr_types: &mut HashMap<String, String>) {
39 if let Some(corrections) = GETATT_RETURN_TYPE_OVERRIDES.get(type_name) {
40 for (attribute, return_type) in corrections {
41 if attr_types.contains_key(attribute) {
42 attr_types.insert(attribute.clone(), return_type.clone());
43 }
44 }
45 }
46}
47
48#[doc(hidden)]
54#[derive(Debug, Clone, Default)]
55pub struct OverlayCatalog {
56 pub type_names: Vec<String>,
58 pub getatt_attributes: HashMap<String, Vec<String>>,
60 pub getatt_attribute_types: HashMap<String, HashMap<String, String>>,
64 pub primary_identifiers: HashMap<String, Vec<String>>,
68 pub ref_returns: HashMap<String, String>,
72 pub schema_metadata: SchemaMetadataCatalog,
76}
77
78impl OverlayCatalog {
79 pub fn is_empty(&self) -> bool {
81 self.type_names.is_empty()
82 }
83
84 pub fn from_store(store: &crate::store::CompiledSchemaStore, overlaid_type_names: &[String]) -> Self {
87 if overlaid_type_names.is_empty() {
88 return Self::default();
89 }
90
91 let mut type_names: Vec<String> = overlaid_type_names.to_vec();
92 type_names.sort();
93 type_names.dedup();
94
95 let mut getatt_attributes: HashMap<String, Vec<String>> = HashMap::new();
96 let mut getatt_attribute_types: HashMap<String, HashMap<String, String>> = HashMap::new();
97 let mut primary_identifiers: HashMap<String, Vec<String>> = HashMap::new();
98 let mut ref_returns: HashMap<String, String> = HashMap::new();
99 let mut schema_metadata: HashMap<String, SchemaMetadataEntry> = HashMap::new();
100
101 for type_name in &type_names {
102 let Some(schema) = store.get(type_name) else {
103 continue;
104 };
105
106 let attrs = derive_getatt_attributes(schema);
108 if !attrs.is_empty() {
109 getatt_attributes.insert(type_name.clone(), attrs.clone());
110 }
111
112 let attr_types = derive_getatt_attribute_types(schema);
113 if !attr_types.is_empty() {
114 getatt_attribute_types.insert(type_name.clone(), attr_types);
115 }
116
117 let read_only_set: HashSet<&str> = schema.read_only_properties.iter().map(|s| s.as_str()).collect();
120 if let Some(pids) = derive_primary_identifiers(schema, &read_only_set) {
121 primary_identifiers.insert(type_name.clone(), pids);
122 }
123
124 if let Some(ref_type) = derive_ref_return_type(schema, &read_only_set) {
128 ref_returns.insert(type_name.clone(), ref_type);
129 }
130
131 let entry = derive_schema_metadata(schema);
133 schema_metadata.insert(type_name.clone(), entry);
134 }
135
136 OverlayCatalog {
137 type_names,
138 getatt_attributes,
139 getatt_attribute_types,
140 primary_identifiers,
141 ref_returns,
142 schema_metadata,
143 }
144 }
145}
146
147fn derive_getatt_attributes(schema: &CompiledSchema) -> Vec<String> {
154 let mut attrs: Vec<String> = schema.read_only_properties.clone();
155 attrs.sort();
156 attrs.dedup();
157 attrs
158}
159
160pub(crate) fn derive_getatt_attribute_types(schema: &CompiledSchema) -> HashMap<String, String> {
168 let mut types = HashMap::new();
169
170 for (name, prop) in &schema.properties {
171 let resolved = prop.resolve(&schema.definitions);
172 if let Some(pt) = resolved.prop_type.as_ref().and_then(|p| p.primary()) {
173 types.insert(name.clone(), pt.to_string());
174 }
175 }
176
177 for attr in &schema.read_only_properties {
178 if attr.contains('.')
179 && let Some(prop_type) = resolve_property_type(schema, attr)
180 {
181 types.insert(attr.clone(), prop_type);
182 }
183 }
184
185 apply_getatt_return_type_overrides(&schema.type_name, &mut types);
186 types
187}
188
189fn derive_primary_identifiers(schema: &CompiledSchema, read_only_set: &HashSet<&str>) -> Option<Vec<String>> {
195 if let Some(overridden) = primary_identifier_override(&schema.type_name) {
196 return Some(overridden);
197 }
198 if schema.primary_identifier.is_empty() {
199 return None;
200 }
201
202 if schema.primary_identifier.iter().any(|p| read_only_set.contains(p.as_str())) {
204 return None;
205 }
206
207 let props: Vec<String> = schema
209 .primary_identifier
210 .iter()
211 .filter_map(|p| {
212 if p.contains('.') {
214 return None;
215 }
216 Some(p.clone())
217 })
218 .collect();
219
220 if props.is_empty() || props.len() != schema.primary_identifier.len() {
222 return None;
223 }
224
225 Some(props)
226}
227
228pub(crate) fn derive_ref_return_type(schema: &CompiledSchema, read_only_set: &HashSet<&str>) -> Option<String> {
234 if schema.primary_identifier.is_empty() {
235 return None;
236 }
237
238 if schema.primary_identifier.len() > 1 {
240 return Some("string".to_string());
241 }
242
243 let id_prop = &schema.primary_identifier[0];
245 if read_only_set.contains(id_prop.as_str()) {
246 return Some("string".to_string());
247 }
248
249 match resolve_property_type(schema, id_prop) {
251 Some(t) => Some(t),
252 None => Some("string".to_string()),
253 }
254}
255
256pub(crate) fn resolve_property_type(schema: &CompiledSchema, path: &str) -> Option<String> {
260 if path.is_empty() {
261 return None;
262 }
263 let parts: Vec<&str> = path.split('.').collect();
264 resolve_nested_property_type(schema, &parts, &schema.properties, &mut HashSet::new())
265}
266
267fn resolve_nested_property_type(
270 schema: &CompiledSchema,
271 remaining_parts: &[&str],
272 properties: &HashMap<String, PropSchema>,
273 visited_refs: &mut HashSet<String>,
274) -> Option<String> {
275 if remaining_parts.is_empty() {
276 return None;
277 }
278 let prop = properties.get(remaining_parts[0])?;
279
280 if let Some(ref ref_name) = prop.ref_name {
282 if visited_refs.contains(ref_name) {
283 return None;
284 }
285 visited_refs.insert(ref_name.clone());
286 }
287
288 let resolved = prop.resolve(&schema.definitions);
289 if remaining_parts.len() == 1 {
290 return resolved.prop_type.as_ref().and_then(|pt| pt.primary()).map(|s| s.to_string());
291 }
292 resolve_nested_property_type(schema, &remaining_parts[1..], &resolved.properties, visited_refs)
293}
294
295fn derive_schema_metadata(schema: &CompiledSchema) -> SchemaMetadataEntry {
300 let mut properties: Vec<String> = schema.properties.keys().cloned().collect();
301 properties.sort();
302
303 let mut property_types: HashMap<String, String> = HashMap::new();
304 let mut property_enums: HashMap<String, Vec<serde_json::Value>> = HashMap::new();
305 let mut property_constraints: HashMap<String, SchemaPropertyConstraints> = HashMap::new();
306
307 for (name, prop) in &schema.properties {
308 let resolved = prop.resolve(&schema.definitions);
309 if let Some(pt) = resolved.prop_type.as_ref().and_then(|p| p.primary()) {
310 property_types.insert(name.clone(), pt.to_string());
311 }
312 if !resolved.enum_values.is_empty() {
315 property_enums.insert(name.clone(), resolved.enum_values.clone());
316 } else if !resolved.enum_case_insensitive.is_empty() {
317 property_enums.insert(name.clone(), resolved.enum_case_insensitive.clone());
318 }
319 if let Some(constraint) = build_property_constraint(&resolved) {
320 property_constraints.insert(name.clone(), constraint);
321 }
322 }
323
324 SchemaMetadataEntry {
325 properties,
326 required: schema.required.clone(),
327 property_types,
328 property_enums,
329 property_constraints,
330 dependent_required: schema.dependent_required.clone(),
331 dependent_excluded: schema.dependent_excluded.clone(),
332 required_or: schema.required_or.clone(),
333 required_xor: schema.required_xor.clone(),
334 ..Default::default()
335 }
336}
337
338fn f64_to_number(value: f64) -> Option<serde_json::Number> {
346 if value.fract() == 0.0 && value >= i64::MIN as f64 && value <= i64::MAX as f64 {
347 Some(serde_json::Number::from(value as i64))
348 } else {
349 serde_json::Number::from_f64(value)
350 }
351}
352
353fn build_property_constraint(prop: &PropSchema) -> Option<SchemaPropertyConstraints> {
357 let constraints = SchemaPropertyConstraints {
358 pattern: prop.pattern.clone(),
359 minimum: prop.minimum.and_then(f64_to_number),
360 maximum: prop.maximum.and_then(f64_to_number),
361 min_length: prop.min_length,
362 max_length: prop.max_length,
363 min_items: prop.min_items,
364 max_items: prop.max_items,
365 format: prop.format.clone(),
366 unique_items: (prop.unique_items == Some(true)).then_some(true),
367 additional: Default::default(),
368 };
369 let SchemaPropertyConstraints {
370 pattern,
371 minimum,
372 maximum,
373 min_length,
374 max_length,
375 min_items,
376 max_items,
377 format,
378 unique_items,
379 additional: _,
380 } = &constraints;
381 let any = pattern.is_some()
382 || minimum.is_some()
383 || maximum.is_some()
384 || min_length.is_some()
385 || max_length.is_some()
386 || min_items.is_some()
387 || max_items.is_some()
388 || format.is_some()
389 || unique_items.is_some();
390 any.then_some(constraints)
391}
392
393#[cfg(test)]
394mod tests {
395 use super::*;
396 use crate::compiled::PropType;
397 use crate::store::CompiledSchemaStore;
398 use serde_json::json;
399
400 #[test]
401 fn resolve_property_type_returns_none_for_empty_path() {
402 let schema = CompiledSchema {
403 properties: HashMap::from([(
404 String::new(),
405 PropSchema { prop_type: Some(PropType::Single("string".into())), ..Default::default() },
406 )]),
407 ..Default::default()
408 };
409
410 assert_eq!(
411 resolve_property_type(&schema, ""),
412 None,
413 "an empty path must not resolve an empty-string property name"
414 );
415 }
416
417 #[test]
418 fn empty_overlay_produces_empty_catalog() {
419 let catalog = OverlayCatalog::from_store(&CompiledSchemaStore::new(), &[]);
420 assert!(catalog.is_empty());
421 assert!(catalog.type_names.is_empty());
422 }
423
424 #[test]
425 fn catalog_for_existing_type_extracts_getatt_and_primary_id() {
426 let store = CompiledSchemaStore::new();
427 let catalog = OverlayCatalog::from_store(&store, &["AWS::S3::Bucket".to_string()]);
428 assert_eq!(catalog.type_names, vec!["AWS::S3::Bucket".to_string()]);
429 assert!(
430 catalog.getatt_attributes.contains_key("AWS::S3::Bucket"),
431 "S3 Bucket must have GetAtt attributes derived from read_only_properties"
432 );
433 let attr_types = catalog.getatt_attribute_types.get("AWS::S3::Bucket").expect("must have attr types");
435 assert!(
436 attr_types.len() > catalog.getatt_attributes.get("AWS::S3::Bucket").unwrap().len(),
437 "attribute types must include more entries than just readOnly attributes"
438 );
439 }
440
441 #[test]
442 fn catalog_type_names_are_sorted_and_deduped() {
443 let store = CompiledSchemaStore::new();
444 let catalog = OverlayCatalog::from_store(
445 &store,
446 &["AWS::Lambda::Function".to_string(), "AWS::S3::Bucket".to_string(), "AWS::Lambda::Function".to_string()],
447 );
448 assert_eq!(catalog.type_names, vec!["AWS::Lambda::Function".to_string(), "AWS::S3::Bucket".to_string()]);
449 }
450
451 #[test]
452 fn catalog_schema_metadata_includes_properties_and_types() {
453 let store = CompiledSchemaStore::new();
454 let catalog = OverlayCatalog::from_store(&store, &["AWS::S3::Bucket".to_string()]);
455 let entry = catalog.schema_metadata.get("AWS::S3::Bucket").expect("S3 metadata must exist");
456 assert!(!entry.properties.is_empty(), "S3 Bucket must have properties");
457 assert!(!entry.property_types.is_empty(), "S3 Bucket must have property types");
458 }
459
460 #[test]
461 fn catalog_for_unknown_type_is_skipped() {
462 let store = CompiledSchemaStore::new();
463 let catalog = OverlayCatalog::from_store(&store, &["AWS::Fake::NotExist".to_string()]);
464 assert_eq!(catalog.type_names, vec!["AWS::Fake::NotExist".to_string()]);
465 assert!(!catalog.getatt_attributes.contains_key("AWS::Fake::NotExist"));
466 assert!(!catalog.schema_metadata.contains_key("AWS::Fake::NotExist"));
467 }
468
469 #[test]
470 fn catalog_derives_overlay_only_type_metadata() {
471 let mut store = CompiledSchemaStore::new();
472 store
473 .apply_overlay(
474 "AWS::Test::OverlayOnly",
475 &json!({
476 "properties": {
477 "Name": { "type": "string", "minLength": 1, "maxLength": 64 },
478 "Count": { "type": "integer", "enum": [1, 2, 3] },
479 "Arn": { "type": "string" }
480 },
481 "required": ["Name"],
482 "readOnlyProperties": ["/properties/Arn"],
483 "primaryIdentifier": ["/properties/Name"],
484 "additionalProperties": false
485 }),
486 )
487 .expect("overlay applies");
488
489 let catalog = OverlayCatalog::from_store(&store, &["AWS::Test::OverlayOnly".to_string()]);
490
491 let attrs = catalog.getatt_attributes.get("AWS::Test::OverlayOnly").expect("attrs");
493 assert_eq!(attrs, &vec!["Arn".to_string()]);
494
495 let attr_types = catalog.getatt_attribute_types.get("AWS::Test::OverlayOnly").expect("attr types");
497 assert_eq!(attr_types.get("Arn"), Some(&"string".to_string()));
498 assert_eq!(attr_types.get("Name"), Some(&"string".to_string()));
499 assert_eq!(attr_types.get("Count"), Some(&"integer".to_string()));
500
501 let pids = catalog.primary_identifiers.get("AWS::Test::OverlayOnly").expect("pids");
503 assert_eq!(pids, &vec!["Name".to_string()]);
504
505 assert_eq!(catalog.ref_returns.get("AWS::Test::OverlayOnly"), Some(&"string".to_string()));
507
508 let meta = catalog.schema_metadata.get("AWS::Test::OverlayOnly").expect("schema metadata");
510 assert!(meta.properties.contains(&"Name".to_string()));
511 assert!(meta.properties.contains(&"Count".to_string()));
512 assert_eq!(meta.required, vec!["Name".to_string()]);
513 assert_eq!(meta.property_types.get("Name"), Some(&"string".to_string()));
514 assert_eq!(meta.property_types.get("Count"), Some(&"integer".to_string()));
515 assert_eq!(meta.property_enums.get("Count"), Some(&vec![json!(1), json!(2), json!(3)]));
516
517 let name_constraints = meta.property_constraints.get("Name").expect("Name constraints");
519 assert_eq!(name_constraints.min_length, Some(1));
520 assert_eq!(name_constraints.max_length, Some(64));
521 }
522
523 #[test]
524 fn catalog_readonly_primary_excludes_entry() {
525 let mut store = CompiledSchemaStore::new();
526 store
527 .apply_overlay(
528 "AWS::Test::ReadOnlyPrimary",
529 &json!({
530 "properties": {
531 "Id": { "type": "string" },
532 "Name": { "type": "string" }
533 },
534 "readOnlyProperties": ["/properties/Id"],
535 "primaryIdentifier": ["/properties/Id"]
536 }),
537 )
538 .expect("overlay applies");
539
540 let catalog = OverlayCatalog::from_store(&store, &["AWS::Test::ReadOnlyPrimary".to_string()]);
541
542 assert!(
544 !catalog.primary_identifiers.contains_key("AWS::Test::ReadOnlyPrimary"),
545 "readOnly primary identifier must exclude the whole entry"
546 );
547 assert_eq!(
550 catalog.ref_returns.get("AWS::Test::ReadOnlyPrimary"),
551 Some(&"string".to_string()),
552 "readOnly primary identifier produces 'string' ref type"
553 );
554 }
555
556 #[test]
557 fn catalog_no_primary_no_ref_returns_entry() {
558 let mut store = CompiledSchemaStore::new();
559 store
560 .apply_overlay(
561 "AWS::Test::NoPrimary",
562 &json!({
563 "properties": {
564 "Name": { "type": "string" }
565 }
566 }),
567 )
568 .expect("overlay applies");
569
570 let catalog = OverlayCatalog::from_store(&store, &["AWS::Test::NoPrimary".to_string()]);
571
572 assert!(!catalog.primary_identifiers.contains_key("AWS::Test::NoPrimary"));
573 assert!(
574 !catalog.ref_returns.contains_key("AWS::Test::NoPrimary"),
575 "no ref_returns entry when primaryIdentifier is empty"
576 );
577 }
578
579 #[test]
580 fn catalog_all_property_getatt_types() {
581 let mut store = CompiledSchemaStore::new();
582 store
583 .apply_overlay(
584 "AWS::Test::AllProps",
585 &json!({
586 "properties": {
587 "Name": { "type": "string" },
588 "Port": { "type": "integer" },
589 "Tags": { "type": "array" },
590 "Config": { "type": "object" }
591 },
592 "readOnlyProperties": ["/properties/Name"]
593 }),
594 )
595 .expect("overlay applies");
596
597 let catalog = OverlayCatalog::from_store(&store, &["AWS::Test::AllProps".to_string()]);
598
599 let attr_types = catalog.getatt_attribute_types.get("AWS::Test::AllProps").expect("attr types");
600 assert_eq!(attr_types.get("Name"), Some(&"string".to_string()));
601 assert_eq!(attr_types.get("Port"), Some(&"integer".to_string()));
602 assert_eq!(attr_types.get("Tags"), Some(&"array".to_string()));
603 assert_eq!(attr_types.get("Config"), Some(&"object".to_string()));
604 }
605
606 #[test]
607 fn catalog_metadata_describes_top_level_properties_only() {
608 let mut store = CompiledSchemaStore::new();
609 store
610 .apply_overlay(
611 "AWS::Test::Nested",
612 &json!({
613 "properties": {
614 "Config": {
615 "type": "object",
616 "properties": {
617 "Name": { "type": "string", "maxLength": 32 },
618 "Port": { "type": "integer", "minimum": 1 }
619 },
620 "required": ["Name"],
621 "dependentRequired": { "Name": ["Port"] }
622 },
623 "Items": {
624 "type": "array",
625 "minItems": 1,
626 "uniqueItems": true,
627 "items": {
628 "type": "object",
629 "properties": {
630 "Key": { "type": "string" },
631 "Value": { "type": "string" }
632 },
633 "required": ["Key"]
634 }
635 },
636 "Document": { "type": ["object", "string"] },
637 "Mode": { "type": "string", "enumCaseInsensitive": ["fast", "slow"] },
638 "Ratio": { "type": "number", "minimum": 0.5, "maximum": 2.0 }
639 },
640 "additionalProperties": false
641 }),
642 )
643 .expect("overlay applies");
644
645 let catalog = OverlayCatalog::from_store(&store, &["AWS::Test::Nested".to_string()]);
646 let meta = catalog.schema_metadata.get("AWS::Test::Nested").expect("metadata");
647
648 assert_eq!(meta.property_types.get("Config"), Some(&"object".to_string()));
652 assert!(!meta.property_constraints.contains_key("Config"), "nested shape must not be repeated");
653 assert!(meta.dependent_required.is_empty(), "a nested dependency group is not lifted to the resource");
654
655 let items_c = meta.property_constraints.get("Items").expect("Items constraints");
657 assert_eq!(items_c.min_items, Some(1));
658 assert_eq!(items_c.unique_items, Some(true));
659 assert!(items_c.additional.is_empty());
660
661 assert_eq!(meta.property_types.get("Document"), Some(&"object".to_string()));
665 assert_eq!(meta.property_enums.get("Mode"), Some(&vec![json!("fast"), json!("slow")]));
666 let ratio_c = meta.property_constraints.get("Ratio").expect("Ratio constraints");
667 assert_eq!(ratio_c.minimum, Some(serde_json::Number::from_f64(0.5).expect("finite")));
668 assert_eq!(ratio_c.maximum, Some(serde_json::Number::from(2)));
669 }
670
671 #[test]
672 fn catalog_recursive_refs_terminate() {
673 let mut store = CompiledSchemaStore::new();
674 store
675 .apply_overlay(
676 "AWS::Test::Recursive",
677 &json!({
678 "properties": {
679 "Root": { "$ref": "#/definitions/TreeNode" }
680 },
681 "definitions": {
682 "TreeNode": {
683 "type": "object",
684 "properties": {
685 "Value": { "type": "string" },
686 "Children": {
687 "type": "array",
688 "items": { "$ref": "#/definitions/TreeNode" }
689 }
690 }
691 }
692 }
693 }),
694 )
695 .expect("overlay applies");
696
697 let catalog = OverlayCatalog::from_store(&store, &["AWS::Test::Recursive".to_string()]);
698 let meta = catalog.schema_metadata.get("AWS::Test::Recursive").expect("metadata");
699 assert!(meta.properties.contains(&"Root".to_string()));
701 assert_eq!(meta.property_types.get("Root"), Some(&"object".to_string()));
702 }
703
704 #[test]
705 fn catalog_ref_type_for_integer_primary_id() {
706 let mut store = CompiledSchemaStore::new();
707 store
708 .apply_overlay(
709 "AWS::Test::IntId",
710 &json!({
711 "properties": {
712 "Id": { "type": "integer" }
713 },
714 "primaryIdentifier": ["/properties/Id"]
715 }),
716 )
717 .expect("overlay applies");
718
719 let catalog = OverlayCatalog::from_store(&store, &["AWS::Test::IntId".to_string()]);
720 assert_eq!(catalog.ref_returns.get("AWS::Test::IntId"), Some(&"integer".to_string()));
721 }
722
723 #[test]
724 fn catalog_resolves_nested_ref_for_primary_id() {
725 let mut store = CompiledSchemaStore::new();
726 store
727 .apply_overlay(
728 "AWS::Test::NestedRef",
729 &json!({
730 "properties": {
731 "Id": { "$ref": "#/definitions/IdDef" }
732 },
733 "definitions": {
734 "IdDef": { "type": "string", "pattern": "^[a-z]+$" }
735 },
736 "primaryIdentifier": ["/properties/Id"]
737 }),
738 )
739 .expect("overlay applies");
740
741 let catalog = OverlayCatalog::from_store(&store, &["AWS::Test::NestedRef".to_string()]);
742 assert_eq!(
743 catalog.ref_returns.get("AWS::Test::NestedRef"),
744 Some(&"string".to_string()),
745 "ref type must resolve through $ref chain"
746 );
747 }
748
749 #[test]
750 fn catalog_multiple_primary_ids_returns_string() {
751 let mut store = CompiledSchemaStore::new();
752 store
753 .apply_overlay(
754 "AWS::Test::MultiId",
755 &json!({
756 "properties": {
757 "Namespace": { "type": "string" },
758 "Name": { "type": "string" }
759 },
760 "primaryIdentifier": ["/properties/Namespace", "/properties/Name"]
761 }),
762 )
763 .expect("overlay applies");
764
765 let catalog = OverlayCatalog::from_store(&store, &["AWS::Test::MultiId".to_string()]);
766 assert_eq!(
767 catalog.ref_returns.get("AWS::Test::MultiId"),
768 Some(&"string".to_string()),
769 "multiple primary identifiers must return 'string'"
770 );
771 }
772
773 #[test]
774 fn catalog_metadata_includes_format_constraint() {
775 let mut store = CompiledSchemaStore::new();
776 store
777 .apply_overlay(
778 "AWS::Test::Format",
779 &json!({
780 "properties": {
781 "Email": { "type": "string", "format": "email" },
782 "Arn": { "type": "string", "format": "AWS::EC2::VPC.Id" }
783 },
784 "additionalProperties": false
785 }),
786 )
787 .expect("overlay applies");
788
789 let catalog = OverlayCatalog::from_store(&store, &["AWS::Test::Format".to_string()]);
790 let meta = catalog.schema_metadata.get("AWS::Test::Format").expect("metadata");
791 let email_c = meta.property_constraints.get("Email").expect("Email constraints");
792 assert_eq!(email_c.format.as_deref(), Some("email"));
793 let arn_c = meta.property_constraints.get("Arn").expect("Arn constraints");
794 assert_eq!(arn_c.format.as_deref(), Some("AWS::EC2::VPC.Id"));
795 }
796
797 #[test]
798 fn catalog_metadata_dependent_required_and_excluded() {
799 let mut store = CompiledSchemaStore::new();
800 store
801 .apply_overlay(
802 "AWS::Test::Deps",
803 &json!({
804 "properties": {
805 "Mode": { "type": "string" },
806 "Config": { "type": "object" },
807 "Legacy": { "type": "boolean" }
808 },
809 "dependentRequired": { "Mode": ["Config"] },
810 "dependentExcluded": { "Mode": ["Legacy"] },
811 "additionalProperties": false
812 }),
813 )
814 .expect("overlay applies");
815
816 let catalog = OverlayCatalog::from_store(&store, &["AWS::Test::Deps".to_string()]);
817 let meta = catalog.schema_metadata.get("AWS::Test::Deps").expect("metadata");
818 assert_eq!(meta.dependent_required.get("Mode"), Some(&vec!["Config".to_string()]));
819 assert_eq!(meta.dependent_excluded.get("Mode"), Some(&vec!["Legacy".to_string()]));
820 }
821
822 #[test]
823 fn catalog_metadata_required_or_xor() {
824 let mut store = CompiledSchemaStore::new();
825 store
826 .apply_overlay(
827 "AWS::Test::OrXor",
828 &json!({
829 "properties": {
830 "A": { "type": "string" },
831 "B": { "type": "string" },
832 "C": { "type": "string" }
833 },
834 "requiredOr": ["A", "B"],
835 "requiredXor": ["B", "C"],
836 "additionalProperties": false
837 }),
838 )
839 .expect("overlay applies");
840
841 let catalog = OverlayCatalog::from_store(&store, &["AWS::Test::OrXor".to_string()]);
842 let meta = catalog.schema_metadata.get("AWS::Test::OrXor").expect("metadata");
843 assert_eq!(meta.required_or, vec!["A".to_string(), "B".to_string()]);
844 assert_eq!(meta.required_xor, vec!["B".to_string(), "C".to_string()]);
845 }
846
847 #[test]
855 fn full_catalog_derivation_matches_committed_artifact() {
856 let document: data_source::types::SchemaMetadataDocument =
857 serde_json::from_slice(&data_source::embedded::SCHEMA_METADATA_BYTES).expect("committed artifact parses");
858 let store = CompiledSchemaStore::new();
859 let mut checked = 0usize;
860 let mut mismatches: Vec<String> = Vec::new();
861 for (type_name, artifact_entry) in &document.schema_metadata {
862 let Some(schema) = store.get(type_name) else {
863 mismatches.push(format!("{type_name}: in the artifact but not in the compiled store"));
864 continue;
865 };
866 let derived = derive_schema_metadata(schema);
867 checked += 1;
868
869 let mut report = |field: &str, artifact: &dyn std::fmt::Debug, derived: &dyn std::fmt::Debug| {
870 mismatches
871 .push(format!("{type_name}: {field} diverges\n artifact: {artifact:?}\n derived: {derived:?}"));
872 };
873 if derived.properties != artifact_entry.properties {
874 report("properties", &artifact_entry.properties, &derived.properties);
875 }
876 if derived.required != artifact_entry.required {
877 report("required", &artifact_entry.required, &derived.required);
878 }
879 if derived.property_types != artifact_entry.property_types {
880 report("property_types", &artifact_entry.property_types, &derived.property_types);
881 }
882 if derived.property_enums != artifact_entry.property_enums {
883 report("property_enums", &artifact_entry.property_enums, &derived.property_enums);
884 }
885 let artifact_constraints = serde_json::to_value(&artifact_entry.property_constraints).expect("serializes");
886 let derived_constraints = serde_json::to_value(&derived.property_constraints).expect("serializes");
887 if artifact_constraints != derived_constraints {
888 report("property_constraints", &artifact_constraints, &derived_constraints);
889 }
890 if derived.dependent_required != artifact_entry.dependent_required {
891 report("dependent_required", &artifact_entry.dependent_required, &derived.dependent_required);
892 }
893 if derived.dependent_excluded != artifact_entry.dependent_excluded {
894 report("dependent_excluded", &artifact_entry.dependent_excluded, &derived.dependent_excluded);
895 }
896 if derived.required_or != artifact_entry.required_or {
897 report("required_or", &artifact_entry.required_or, &derived.required_or);
898 }
899 if derived.required_xor != artifact_entry.required_xor {
900 report("required_xor", &artifact_entry.required_xor, &derived.required_xor);
901 }
902 assert!(derived.additional.is_empty(), "{type_name}: derived entry must not carry unmodeled fields");
903 }
904 assert_eq!(
905 checked,
906 store.len(),
907 "every compiled schema must have an artifact entry (artifact has {}, store has {})",
908 document.schema_metadata.len(),
909 store.len()
910 );
911 assert!(
912 mismatches.is_empty(),
913 "{} bundled type(s) derive differently from the committed artifact:\n{}",
914 mismatches.len(),
915 mismatches.join("\n")
916 );
917 }
918
919 #[test]
926 fn full_catalog_derivation_matches_committed_getatt_primary_and_ref_artifacts() {
927 #[derive(serde::Deserialize)]
928 struct RefTypesArtifact {
929 ref_returns: HashMap<String, String>,
930 }
931 let getatt: data_source::types::GetattData =
932 serde_json::from_slice(&data_source::embedded::GETATT_ATTRIBUTES_BYTES).expect("getatt artifact parses");
933 let primary: data_source::types::PrimaryIdentifiers =
934 serde_json::from_slice(&data_source::embedded::PRIMARY_IDENTIFIERS_BYTES).expect("primary ids parse");
935 let refs: RefTypesArtifact =
936 serde_json::from_slice(&data_source::embedded::REF_TYPES_BYTES).expect("ref_types artifact parses");
937
938 let store = CompiledSchemaStore::new();
939 let type_names: Vec<String> = store.type_names().map(String::from).collect();
940 let catalog = OverlayCatalog::from_store(&store, &type_names);
941
942 let mut mismatches: Vec<String> = Vec::new();
943 for type_name in &type_names {
944 let artifact_attrs = getatt.getatt_attributes.get(type_name).cloned().unwrap_or_default();
945 for attr in catalog.getatt_attributes.get(type_name).into_iter().flatten() {
946 if !artifact_attrs.contains(attr) {
947 mismatches
948 .push(format!("{type_name}: derived GetAtt attribute '{attr}' missing from the artifact"));
949 }
950 }
951 let artifact_types = getatt.getatt_attribute_types.get(type_name).cloned().unwrap_or_default();
952 let derived_types = catalog.getatt_attribute_types.get(type_name).cloned().unwrap_or_default();
953 if artifact_types != derived_types {
954 mismatches.push(format!(
955 "{type_name}: getatt_attribute_types diverge\n artifact: {artifact_types:?}\n derived: {derived_types:?}"
956 ));
957 }
958 if primary.primary_identifiers.get(type_name) != catalog.primary_identifiers.get(type_name) {
959 mismatches.push(format!(
960 "{type_name}: primary_identifiers diverge\n artifact: {:?}\n derived: {:?}",
961 primary.primary_identifiers.get(type_name),
962 catalog.primary_identifiers.get(type_name)
963 ));
964 }
965 if refs.ref_returns.get(type_name) != catalog.ref_returns.get(type_name) {
966 mismatches.push(format!(
967 "{type_name}: ref_returns diverge\n artifact: {:?}\n derived: {:?}",
968 refs.ref_returns.get(type_name),
969 catalog.ref_returns.get(type_name)
970 ));
971 }
972 }
973 assert!(
974 mismatches.is_empty(),
975 "{} bundled type(s) derive differently from the committed artifacts:\n{}",
976 mismatches.len(),
977 mismatches.join("\n")
978 );
979 }
980}