1use crate::compiled::{CompiledSchema, PropSchema};
11use serde::{Deserialize, Serialize};
12use std::collections::{HashMap, HashSet};
13use std::sync::LazyLock;
14
15pub(crate) static GETATT_RETURN_TYPE_OVERRIDES: LazyLock<HashMap<String, HashMap<String, String>>> =
21 LazyLock::new(|| {
22 let artifact: serde_json::Value =
23 serde_json::from_slice(&data_source::embedded::GETATT_RETURN_TYPE_OVERRIDES_BYTES)
24 .expect("Embedded getatt_return_type_overrides must be valid JSON");
25 let overrides = artifact
26 .get("getatt_return_type_overrides")
27 .expect("Embedded getatt_return_type_overrides must contain getatt_return_type_overrides");
28 let overrides: HashMap<String, HashMap<String, String>> = serde_json::from_value(overrides.clone())
29 .expect("Embedded getatt_return_type_overrides must contain a valid override map");
30 assert!(!overrides.is_empty(), "Embedded getatt_return_type_overrides must not be empty");
31 overrides
32 });
33
34pub(crate) fn apply_getatt_return_type_overrides(type_name: &str, attr_types: &mut HashMap<String, String>) {
37 if let Some(corrections) = GETATT_RETURN_TYPE_OVERRIDES.get(type_name) {
38 for (attribute, return_type) in corrections {
39 if attr_types.contains_key(attribute) {
40 attr_types.insert(attribute.clone(), return_type.clone());
41 }
42 }
43 }
44}
45
46#[doc(hidden)]
52#[derive(Debug, Clone, Default)]
53pub struct OverlayCatalog {
54 pub type_names: Vec<String>,
56 pub getatt_attributes: HashMap<String, Vec<String>>,
58 pub getatt_attribute_types: HashMap<String, HashMap<String, String>>,
62 pub primary_identifiers: HashMap<String, Vec<String>>,
66 pub ref_returns: HashMap<String, String>,
70 pub schema_metadata: HashMap<String, SchemaMetadataEntry>,
75}
76
77#[derive(Debug, Clone, Default, Serialize, Deserialize)]
80pub struct SchemaMetadataEntry {
81 #[serde(default, skip_serializing_if = "Vec::is_empty")]
83 pub properties: Vec<String>,
84 #[serde(default, skip_serializing_if = "Vec::is_empty")]
86 pub required: Vec<String>,
87 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
89 pub property_types: HashMap<String, String>,
90 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
92 pub property_enums: HashMap<String, Vec<serde_json::Value>>,
93 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
97 pub property_constraints: HashMap<String, serde_json::Value>,
98 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
100 pub dependent_required: HashMap<String, Vec<String>>,
101 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
103 pub dependent_excluded: HashMap<String, Vec<String>>,
104 #[serde(default, skip_serializing_if = "Vec::is_empty")]
106 pub required_or: Vec<String>,
107 #[serde(default, skip_serializing_if = "Vec::is_empty")]
109 pub required_xor: Vec<String>,
110}
111
112impl OverlayCatalog {
113 pub fn is_empty(&self) -> bool {
115 self.type_names.is_empty()
116 }
117
118 pub fn from_store(store: &crate::store::CompiledSchemaStore, overlaid_type_names: &[String]) -> Self {
121 if overlaid_type_names.is_empty() {
122 return Self::default();
123 }
124
125 let mut type_names: Vec<String> = overlaid_type_names.to_vec();
126 type_names.sort();
127 type_names.dedup();
128
129 let mut getatt_attributes: HashMap<String, Vec<String>> = HashMap::new();
130 let mut getatt_attribute_types: HashMap<String, HashMap<String, String>> = HashMap::new();
131 let mut primary_identifiers: HashMap<String, Vec<String>> = HashMap::new();
132 let mut ref_returns: HashMap<String, String> = HashMap::new();
133 let mut schema_metadata: HashMap<String, SchemaMetadataEntry> = HashMap::new();
134
135 for type_name in &type_names {
136 let Some(schema) = store.get(type_name) else {
137 continue;
138 };
139
140 let attrs = derive_getatt_attributes(schema);
142 if !attrs.is_empty() {
143 getatt_attributes.insert(type_name.clone(), attrs.clone());
144 }
145
146 let mut attr_types = derive_getatt_attribute_types(schema, &attrs);
151 apply_getatt_return_type_overrides(type_name, &mut attr_types);
152 if !attr_types.is_empty() {
153 getatt_attribute_types.insert(type_name.clone(), attr_types);
154 }
155
156 let read_only_set: HashSet<&str> = schema.read_only_properties.iter().map(|s| s.as_str()).collect();
159 if let Some(pids) = derive_primary_identifiers(schema, &read_only_set) {
160 primary_identifiers.insert(type_name.clone(), pids);
161 }
162
163 if let Some(ref_type) = derive_ref_return_type(schema, &read_only_set) {
167 ref_returns.insert(type_name.clone(), ref_type);
168 }
169
170 let entry = derive_schema_metadata(schema);
172 schema_metadata.insert(type_name.clone(), entry);
173 }
174
175 OverlayCatalog {
176 type_names,
177 getatt_attributes,
178 getatt_attribute_types,
179 primary_identifiers,
180 ref_returns,
181 schema_metadata,
182 }
183 }
184}
185
186fn derive_getatt_attributes(schema: &CompiledSchema) -> Vec<String> {
193 let mut attrs: Vec<String> = schema.read_only_properties.clone();
194 attrs.sort();
195 attrs.dedup();
196 attrs
197}
198
199fn derive_getatt_attribute_types(schema: &CompiledSchema, read_only_attrs: &[String]) -> HashMap<String, String> {
203 let mut types = HashMap::new();
204
205 for (name, prop) in &schema.properties {
208 let resolved = prop.resolve(&schema.definitions);
209 if let Some(pt) = resolved.prop_type.as_ref().and_then(|p| p.primary()) {
210 types.insert(name.clone(), pt.to_string());
211 }
212 }
213
214 for attr in read_only_attrs {
216 if attr.contains('.')
217 && let Some(prop_type) = resolve_property_type(schema, attr)
218 {
219 types.insert(attr.clone(), prop_type);
220 }
221 }
222
223 types
224}
225
226fn derive_primary_identifiers(schema: &CompiledSchema, read_only_set: &HashSet<&str>) -> Option<Vec<String>> {
231 if schema.primary_identifier.is_empty() {
232 return None;
233 }
234
235 if schema.primary_identifier.iter().any(|p| read_only_set.contains(p.as_str())) {
237 return None;
238 }
239
240 let props: Vec<String> = schema
242 .primary_identifier
243 .iter()
244 .filter_map(|p| {
245 if p.contains('.') {
247 return None;
248 }
249 Some(p.clone())
250 })
251 .collect();
252
253 if props.is_empty() || props.len() != schema.primary_identifier.len() {
255 return None;
256 }
257
258 Some(props)
259}
260
261fn derive_ref_return_type(schema: &CompiledSchema, read_only_set: &HashSet<&str>) -> Option<String> {
267 if schema.primary_identifier.is_empty() {
268 return None;
269 }
270
271 if schema.primary_identifier.len() > 1 {
273 return Some("string".to_string());
274 }
275
276 let id_prop = &schema.primary_identifier[0];
278 if read_only_set.contains(id_prop.as_str()) {
279 return Some("string".to_string());
280 }
281
282 match resolve_property_type(schema, id_prop) {
284 Some(t) => Some(t),
285 None => Some("string".to_string()),
286 }
287}
288
289pub(crate) fn resolve_property_type(schema: &CompiledSchema, path: &str) -> Option<String> {
293 if path.is_empty() {
294 return None;
295 }
296 let parts: Vec<&str> = path.split('.').collect();
297 resolve_nested_property_type(schema, &parts, &schema.properties, &mut HashSet::new())
298}
299
300fn resolve_nested_property_type(
303 schema: &CompiledSchema,
304 remaining_parts: &[&str],
305 properties: &HashMap<String, PropSchema>,
306 visited_refs: &mut HashSet<String>,
307) -> Option<String> {
308 if remaining_parts.is_empty() {
309 return None;
310 }
311 let prop = properties.get(remaining_parts[0])?;
312
313 if let Some(ref ref_name) = prop.ref_name {
315 if visited_refs.contains(ref_name) {
316 return None;
317 }
318 visited_refs.insert(ref_name.clone());
319 }
320
321 let resolved = prop.resolve(&schema.definitions);
322 if remaining_parts.len() == 1 {
323 return resolved.prop_type.as_ref().and_then(|pt| pt.primary()).map(|s| s.to_string());
324 }
325 resolve_nested_property_type(schema, &remaining_parts[1..], &resolved.properties, visited_refs)
326}
327
328fn derive_schema_metadata(schema: &CompiledSchema) -> SchemaMetadataEntry {
332 let mut properties: Vec<String> = schema.properties.keys().cloned().collect();
333 properties.sort();
334
335 let required = schema.required.clone();
336
337 let mut property_types: HashMap<String, String> = HashMap::new();
338 let mut property_enums: HashMap<String, Vec<serde_json::Value>> = HashMap::new();
339 let mut property_constraints: HashMap<String, serde_json::Value> = HashMap::new();
340
341 let mut visiting: HashSet<String> = HashSet::new();
342
343 for (name, prop) in &schema.properties {
344 let ref_name = prop.ref_name.clone();
346 if let Some(ref rn) = ref_name {
347 if visiting.contains(rn) {
348 continue;
349 }
350 visiting.insert(rn.clone());
351 }
352
353 let resolved = prop.resolve(&schema.definitions);
354 if let Some(pt) = resolved.prop_type.as_ref().and_then(|p| p.primary()) {
356 property_types.insert(name.clone(), pt.to_string());
357 }
358 if !resolved.enum_values.is_empty() {
360 property_enums.insert(name.clone(), resolved.enum_values.clone());
361 } else if !resolved.enum_case_insensitive.is_empty() {
362 property_enums.insert(name.clone(), resolved.enum_case_insensitive.clone());
363 }
364 let constraint = build_property_constraint(&resolved, &schema.definitions, &mut visiting);
366 if !constraint.is_null() {
367 property_constraints.insert(name.clone(), constraint);
368 }
369
370 if let Some(ref rn) = ref_name {
371 visiting.remove(rn);
372 }
373 }
374
375 SchemaMetadataEntry {
376 properties,
377 required,
378 property_types,
379 property_enums,
380 property_constraints,
381 dependent_required: schema.dependent_required.clone(),
382 dependent_excluded: schema.dependent_excluded.clone(),
383 required_or: schema.required_or.clone(),
384 required_xor: schema.required_xor.clone(),
385 }
386}
387
388fn string_list_map_to_value(values: &HashMap<String, Vec<String>>) -> serde_json::Value {
389 let mut keys: Vec<&String> = values.keys().collect();
390 keys.sort();
391 let mut object = serde_json::Map::new();
392 for key in keys {
393 object.insert(
394 key.clone(),
395 serde_json::Value::Array(values[key].iter().cloned().map(serde_json::Value::String).collect()),
396 );
397 }
398 serde_json::Value::Object(object)
399}
400
401fn f64_to_json(v: f64) -> serde_json::Value {
405 if v.fract() == 0.0 && v.abs() < (i64::MAX as f64) { serde_json::json!(v as i64) } else { serde_json::json!(v) }
406}
407
408fn build_property_constraint(
411 prop: &PropSchema,
412 definitions: &HashMap<String, PropSchema>,
413 visiting: &mut HashSet<String>,
414) -> serde_json::Value {
415 let mut obj = serde_json::Map::new();
416
417 if let Some(ref v) = prop.pattern {
418 obj.insert("pattern".into(), serde_json::json!(v));
419 }
420 if let Some(v) = prop.minimum {
421 obj.insert("minimum".into(), f64_to_json(v));
422 }
423 if let Some(v) = prop.maximum {
424 obj.insert("maximum".into(), f64_to_json(v));
425 }
426 if let Some(v) = prop.min_length {
427 obj.insert("minLength".into(), serde_json::json!(v));
428 }
429 if let Some(v) = prop.max_length {
430 obj.insert("maxLength".into(), serde_json::json!(v));
431 }
432 if let Some(v) = prop.min_items {
433 obj.insert("minItems".into(), serde_json::json!(v));
434 }
435 if let Some(v) = prop.max_items {
436 obj.insert("maxItems".into(), serde_json::json!(v));
437 }
438 if let Some(ref v) = prop.format {
439 obj.insert("format".into(), serde_json::json!(v));
440 }
441 if let Some(true) = prop.unique_items {
442 obj.insert("uniqueItems".into(), serde_json::json!(true));
443 }
444
445 if !prop.properties.is_empty() {
447 let sub = build_nested_metadata(&prop.properties, &prop.required, definitions, visiting);
448 obj.insert("sub_properties".into(), sub);
449 if !prop.dependent_required.is_empty() {
450 obj.insert("dependent_required".into(), string_list_map_to_value(&prop.dependent_required));
451 }
452 if !prop.dependent_excluded.is_empty() {
453 obj.insert("dependent_excluded".into(), string_list_map_to_value(&prop.dependent_excluded));
454 }
455 }
456
457 if let Some(ref items_schema) = prop.items {
459 let item_ref_name = items_schema.ref_name.clone();
460 let skip_items = item_ref_name.as_ref().map(|n| visiting.contains(n)).unwrap_or(false);
461 if !skip_items {
462 if let Some(ref rn) = item_ref_name {
463 visiting.insert(rn.clone());
464 }
465 let resolved_items = items_schema.resolve(definitions);
466 let mut item_obj = serde_json::Map::new();
467 if let Some(pt) = resolved_items.prop_type.as_ref().and_then(|p| p.primary()) {
468 item_obj.insert("type".into(), serde_json::Value::String(pt.to_string()));
469 }
470 if !resolved_items.properties.is_empty() {
471 let nested =
472 build_nested_metadata(&resolved_items.properties, &resolved_items.required, definitions, visiting);
473 item_obj.insert("schema".into(), nested);
474 }
475 if !resolved_items.dependent_required.is_empty() {
476 item_obj
477 .insert("dependent_required".into(), string_list_map_to_value(&resolved_items.dependent_required));
478 }
479 if !resolved_items.dependent_excluded.is_empty() {
480 item_obj
481 .insert("dependent_excluded".into(), string_list_map_to_value(&resolved_items.dependent_excluded));
482 }
483 if !item_obj.is_empty() {
484 obj.insert("items".into(), serde_json::Value::Object(item_obj));
485 }
486 if let Some(ref rn) = item_ref_name {
487 visiting.remove(rn);
488 }
489 }
490 }
491
492 if obj.is_empty() { serde_json::Value::Null } else { serde_json::Value::Object(obj) }
493}
494
495fn build_nested_metadata(
498 properties: &HashMap<String, PropSchema>,
499 required: &[String],
500 definitions: &HashMap<String, PropSchema>,
501 visiting: &mut HashSet<String>,
502) -> serde_json::Value {
503 let mut props: Vec<String> = properties.keys().cloned().collect();
504 props.sort();
505 let req: Vec<String> = required.to_vec();
506 let mut pt: serde_json::Map<String, serde_json::Value> = serde_json::Map::new();
507 let mut pe: serde_json::Map<String, serde_json::Value> = serde_json::Map::new();
508 let mut pc: serde_json::Map<String, serde_json::Value> = serde_json::Map::new();
509
510 for (pn, ps) in properties {
511 let ref_name = ps.ref_name.clone();
512 if let Some(ref rn) = ref_name {
513 if visiting.contains(rn) {
514 continue;
515 }
516 visiting.insert(rn.clone());
517 }
518
519 let resolved = ps.resolve(definitions);
520 if let Some(t) = resolved.prop_type.as_ref().and_then(|p| p.primary()) {
521 pt.insert(pn.clone(), serde_json::Value::String(t.to_string()));
522 }
523 if !resolved.enum_values.is_empty() {
524 pe.insert(pn.clone(), serde_json::json!(resolved.enum_values));
525 } else if !resolved.enum_case_insensitive.is_empty() {
526 pe.insert(pn.clone(), serde_json::json!(resolved.enum_case_insensitive));
527 }
528 let constraints = build_property_constraint(&resolved, definitions, visiting);
529 if !constraints.is_null() {
530 pc.insert(pn.clone(), constraints);
531 }
532
533 if let Some(ref rn) = ref_name {
534 visiting.remove(rn);
535 }
536 }
537
538 let mut obj = serde_json::json!({
539 "properties": props,
540 "required": req,
541 "property_types": pt,
542 "property_enums": pe,
543 });
544 if !pc.is_empty() {
545 obj["property_constraints"] = serde_json::Value::Object(pc);
546 }
547 obj
548}
549
550#[cfg(test)]
551mod tests {
552 use super::*;
553 use crate::compiled::PropType;
554 use crate::store::CompiledSchemaStore;
555 use serde_json::json;
556
557 #[test]
558 fn resolve_property_type_returns_none_for_empty_path() {
559 let schema = CompiledSchema {
560 properties: HashMap::from([(
561 String::new(),
562 PropSchema { prop_type: Some(PropType::Single("string".into())), ..Default::default() },
563 )]),
564 ..Default::default()
565 };
566
567 assert_eq!(
568 resolve_property_type(&schema, ""),
569 None,
570 "an empty path must not resolve an empty-string property name"
571 );
572 }
573
574 #[test]
575 fn empty_overlay_produces_empty_catalog() {
576 let catalog = OverlayCatalog::from_store(&CompiledSchemaStore::new(), &[]);
577 assert!(catalog.is_empty());
578 assert!(catalog.type_names.is_empty());
579 }
580
581 #[test]
582 fn catalog_for_existing_type_extracts_getatt_and_primary_id() {
583 let store = CompiledSchemaStore::new();
584 let catalog = OverlayCatalog::from_store(&store, &["AWS::S3::Bucket".to_string()]);
585 assert_eq!(catalog.type_names, vec!["AWS::S3::Bucket".to_string()]);
586 assert!(
587 catalog.getatt_attributes.contains_key("AWS::S3::Bucket"),
588 "S3 Bucket must have GetAtt attributes derived from read_only_properties"
589 );
590 let attr_types = catalog.getatt_attribute_types.get("AWS::S3::Bucket").expect("must have attr types");
592 assert!(
593 attr_types.len() > catalog.getatt_attributes.get("AWS::S3::Bucket").unwrap().len(),
594 "attribute types must include more entries than just readOnly attributes"
595 );
596 }
597
598 #[test]
599 fn catalog_type_names_are_sorted_and_deduped() {
600 let store = CompiledSchemaStore::new();
601 let catalog = OverlayCatalog::from_store(
602 &store,
603 &["AWS::Lambda::Function".to_string(), "AWS::S3::Bucket".to_string(), "AWS::Lambda::Function".to_string()],
604 );
605 assert_eq!(catalog.type_names, vec!["AWS::Lambda::Function".to_string(), "AWS::S3::Bucket".to_string()]);
606 }
607
608 #[test]
609 fn catalog_schema_metadata_includes_properties_and_types() {
610 let store = CompiledSchemaStore::new();
611 let catalog = OverlayCatalog::from_store(&store, &["AWS::S3::Bucket".to_string()]);
612 let entry = catalog.schema_metadata.get("AWS::S3::Bucket").expect("S3 metadata must exist");
613 assert!(!entry.properties.is_empty(), "S3 Bucket must have properties");
614 assert!(!entry.property_types.is_empty(), "S3 Bucket must have property types");
615 }
616
617 #[test]
618 fn catalog_for_unknown_type_is_skipped() {
619 let store = CompiledSchemaStore::new();
620 let catalog = OverlayCatalog::from_store(&store, &["AWS::Fake::NotExist".to_string()]);
621 assert_eq!(catalog.type_names, vec!["AWS::Fake::NotExist".to_string()]);
622 assert!(!catalog.getatt_attributes.contains_key("AWS::Fake::NotExist"));
623 assert!(!catalog.schema_metadata.contains_key("AWS::Fake::NotExist"));
624 }
625
626 #[test]
627 fn catalog_derives_overlay_only_type_metadata() {
628 let mut store = CompiledSchemaStore::new();
629 store
630 .apply_overlay(
631 "AWS::Test::OverlayOnly",
632 &json!({
633 "properties": {
634 "Name": { "type": "string", "minLength": 1, "maxLength": 64 },
635 "Count": { "type": "integer", "enum": [1, 2, 3] },
636 "Arn": { "type": "string" }
637 },
638 "required": ["Name"],
639 "readOnlyProperties": ["/properties/Arn"],
640 "primaryIdentifier": ["/properties/Name"],
641 "additionalProperties": false
642 }),
643 )
644 .expect("overlay applies");
645
646 let catalog = OverlayCatalog::from_store(&store, &["AWS::Test::OverlayOnly".to_string()]);
647
648 let attrs = catalog.getatt_attributes.get("AWS::Test::OverlayOnly").expect("attrs");
650 assert_eq!(attrs, &vec!["Arn".to_string()]);
651
652 let attr_types = catalog.getatt_attribute_types.get("AWS::Test::OverlayOnly").expect("attr types");
654 assert_eq!(attr_types.get("Arn"), Some(&"string".to_string()));
655 assert_eq!(attr_types.get("Name"), Some(&"string".to_string()));
656 assert_eq!(attr_types.get("Count"), Some(&"integer".to_string()));
657
658 let pids = catalog.primary_identifiers.get("AWS::Test::OverlayOnly").expect("pids");
660 assert_eq!(pids, &vec!["Name".to_string()]);
661
662 assert_eq!(catalog.ref_returns.get("AWS::Test::OverlayOnly"), Some(&"string".to_string()));
664
665 let meta = catalog.schema_metadata.get("AWS::Test::OverlayOnly").expect("schema metadata");
667 assert!(meta.properties.contains(&"Name".to_string()));
668 assert!(meta.properties.contains(&"Count".to_string()));
669 assert_eq!(meta.required, vec!["Name".to_string()]);
670 assert_eq!(meta.property_types.get("Name"), Some(&"string".to_string()));
671 assert_eq!(meta.property_types.get("Count"), Some(&"integer".to_string()));
672 assert_eq!(meta.property_enums.get("Count"), Some(&vec![json!(1), json!(2), json!(3)]));
673
674 let name_constraints = meta.property_constraints.get("Name").expect("Name constraints");
676 assert_eq!(name_constraints.get("minLength"), Some(&json!(1)));
677 assert_eq!(name_constraints.get("maxLength"), Some(&json!(64)));
678 }
679
680 #[test]
681 fn catalog_readonly_primary_excludes_entry() {
682 let mut store = CompiledSchemaStore::new();
683 store
684 .apply_overlay(
685 "AWS::Test::ReadOnlyPrimary",
686 &json!({
687 "properties": {
688 "Id": { "type": "string" },
689 "Name": { "type": "string" }
690 },
691 "readOnlyProperties": ["/properties/Id"],
692 "primaryIdentifier": ["/properties/Id"]
693 }),
694 )
695 .expect("overlay applies");
696
697 let catalog = OverlayCatalog::from_store(&store, &["AWS::Test::ReadOnlyPrimary".to_string()]);
698
699 assert!(
701 !catalog.primary_identifiers.contains_key("AWS::Test::ReadOnlyPrimary"),
702 "readOnly primary identifier must exclude the whole entry"
703 );
704 assert_eq!(
707 catalog.ref_returns.get("AWS::Test::ReadOnlyPrimary"),
708 Some(&"string".to_string()),
709 "readOnly primary identifier produces 'string' ref type"
710 );
711 }
712
713 #[test]
714 fn catalog_no_primary_no_ref_returns_entry() {
715 let mut store = CompiledSchemaStore::new();
716 store
717 .apply_overlay(
718 "AWS::Test::NoPrimary",
719 &json!({
720 "properties": {
721 "Name": { "type": "string" }
722 }
723 }),
724 )
725 .expect("overlay applies");
726
727 let catalog = OverlayCatalog::from_store(&store, &["AWS::Test::NoPrimary".to_string()]);
728
729 assert!(!catalog.primary_identifiers.contains_key("AWS::Test::NoPrimary"));
730 assert!(
731 !catalog.ref_returns.contains_key("AWS::Test::NoPrimary"),
732 "no ref_returns entry when primaryIdentifier is empty"
733 );
734 }
735
736 #[test]
737 fn catalog_all_property_getatt_types() {
738 let mut store = CompiledSchemaStore::new();
739 store
740 .apply_overlay(
741 "AWS::Test::AllProps",
742 &json!({
743 "properties": {
744 "Name": { "type": "string" },
745 "Port": { "type": "integer" },
746 "Tags": { "type": "array" },
747 "Config": { "type": "object" }
748 },
749 "readOnlyProperties": ["/properties/Name"]
750 }),
751 )
752 .expect("overlay applies");
753
754 let catalog = OverlayCatalog::from_store(&store, &["AWS::Test::AllProps".to_string()]);
755
756 let attr_types = catalog.getatt_attribute_types.get("AWS::Test::AllProps").expect("attr types");
757 assert_eq!(attr_types.get("Name"), Some(&"string".to_string()));
758 assert_eq!(attr_types.get("Port"), Some(&"integer".to_string()));
759 assert_eq!(attr_types.get("Tags"), Some(&"array".to_string()));
760 assert_eq!(attr_types.get("Config"), Some(&"object".to_string()));
761 }
762
763 #[test]
764 fn catalog_nested_metadata_and_items() {
765 let mut store = CompiledSchemaStore::new();
766 store
767 .apply_overlay(
768 "AWS::Test::Nested",
769 &json!({
770 "properties": {
771 "Config": {
772 "type": "object",
773 "properties": {
774 "Name": { "type": "string", "maxLength": 32 },
775 "Port": { "type": "integer", "minimum": 1 }
776 },
777 "required": ["Name"]
778 },
779 "Items": {
780 "type": "array",
781 "items": {
782 "type": "object",
783 "properties": {
784 "Key": { "type": "string" },
785 "Value": { "type": "string" }
786 },
787 "required": ["Key"]
788 }
789 }
790 },
791 "additionalProperties": false
792 }),
793 )
794 .expect("overlay applies");
795
796 let catalog = OverlayCatalog::from_store(&store, &["AWS::Test::Nested".to_string()]);
797 let meta = catalog.schema_metadata.get("AWS::Test::Nested").expect("metadata");
798
799 let config_c = meta.property_constraints.get("Config").expect("Config constraints");
801 let sub = config_c.get("sub_properties").expect("sub_properties for Config");
802 assert!(sub["properties"].as_array().unwrap().contains(&json!("Name")));
803 assert!(sub["properties"].as_array().unwrap().contains(&json!("Port")));
804 assert!(sub["required"].as_array().unwrap().contains(&json!("Name")));
805 assert_eq!(sub["property_types"]["Name"], json!("string"));
806 assert_eq!(sub["property_types"]["Port"], json!("integer"));
807 let name_c = sub["property_constraints"]["Name"].as_object().expect("Name constraints in sub");
808 assert_eq!(name_c.get("maxLength"), Some(&json!(32)));
809 let port_c = sub["property_constraints"]["Port"].as_object().expect("Port constraints in sub");
810 assert_eq!(port_c.get("minimum"), Some(&json!(1)));
811
812 let items_c = meta.property_constraints.get("Items").expect("Items constraints");
814 let items = items_c.get("items").expect("items sub-schema");
815 assert_eq!(items["type"], json!("object"));
816 let items_schema = items.get("schema").expect("items.schema");
817 assert!(items_schema["properties"].as_array().unwrap().contains(&json!("Key")));
818 assert!(items_schema["required"].as_array().unwrap().contains(&json!("Key")));
819 }
820
821 #[test]
822 fn catalog_recursive_refs_terminate() {
823 let mut store = CompiledSchemaStore::new();
824 store
825 .apply_overlay(
826 "AWS::Test::Recursive",
827 &json!({
828 "properties": {
829 "Root": { "$ref": "#/definitions/TreeNode" }
830 },
831 "definitions": {
832 "TreeNode": {
833 "type": "object",
834 "properties": {
835 "Value": { "type": "string" },
836 "Children": {
837 "type": "array",
838 "items": { "$ref": "#/definitions/TreeNode" }
839 }
840 }
841 }
842 }
843 }),
844 )
845 .expect("overlay applies");
846
847 let catalog = OverlayCatalog::from_store(&store, &["AWS::Test::Recursive".to_string()]);
848 let meta = catalog.schema_metadata.get("AWS::Test::Recursive").expect("metadata");
849 assert!(meta.properties.contains(&"Root".to_string()));
851 assert_eq!(meta.property_types.get("Root"), Some(&"object".to_string()));
852 }
853
854 #[test]
855 fn catalog_ref_type_for_integer_primary_id() {
856 let mut store = CompiledSchemaStore::new();
857 store
858 .apply_overlay(
859 "AWS::Test::IntId",
860 &json!({
861 "properties": {
862 "Id": { "type": "integer" }
863 },
864 "primaryIdentifier": ["/properties/Id"]
865 }),
866 )
867 .expect("overlay applies");
868
869 let catalog = OverlayCatalog::from_store(&store, &["AWS::Test::IntId".to_string()]);
870 assert_eq!(catalog.ref_returns.get("AWS::Test::IntId"), Some(&"integer".to_string()));
871 }
872
873 #[test]
874 fn catalog_resolves_nested_ref_for_primary_id() {
875 let mut store = CompiledSchemaStore::new();
876 store
877 .apply_overlay(
878 "AWS::Test::NestedRef",
879 &json!({
880 "properties": {
881 "Id": { "$ref": "#/definitions/IdDef" }
882 },
883 "definitions": {
884 "IdDef": { "type": "string", "pattern": "^[a-z]+$" }
885 },
886 "primaryIdentifier": ["/properties/Id"]
887 }),
888 )
889 .expect("overlay applies");
890
891 let catalog = OverlayCatalog::from_store(&store, &["AWS::Test::NestedRef".to_string()]);
892 assert_eq!(
893 catalog.ref_returns.get("AWS::Test::NestedRef"),
894 Some(&"string".to_string()),
895 "ref type must resolve through $ref chain"
896 );
897 }
898
899 #[test]
900 fn catalog_multiple_primary_ids_returns_string() {
901 let mut store = CompiledSchemaStore::new();
902 store
903 .apply_overlay(
904 "AWS::Test::MultiId",
905 &json!({
906 "properties": {
907 "Namespace": { "type": "string" },
908 "Name": { "type": "string" }
909 },
910 "primaryIdentifier": ["/properties/Namespace", "/properties/Name"]
911 }),
912 )
913 .expect("overlay applies");
914
915 let catalog = OverlayCatalog::from_store(&store, &["AWS::Test::MultiId".to_string()]);
916 assert_eq!(
917 catalog.ref_returns.get("AWS::Test::MultiId"),
918 Some(&"string".to_string()),
919 "multiple primary identifiers must return 'string'"
920 );
921 }
922
923 #[test]
924 fn catalog_metadata_includes_format_constraint() {
925 let mut store = CompiledSchemaStore::new();
926 store
927 .apply_overlay(
928 "AWS::Test::Format",
929 &json!({
930 "properties": {
931 "Email": { "type": "string", "format": "email" },
932 "Arn": { "type": "string", "format": "AWS::EC2::VPC.Id" }
933 },
934 "additionalProperties": false
935 }),
936 )
937 .expect("overlay applies");
938
939 let catalog = OverlayCatalog::from_store(&store, &["AWS::Test::Format".to_string()]);
940 let meta = catalog.schema_metadata.get("AWS::Test::Format").expect("metadata");
941 let email_c = meta.property_constraints.get("Email").expect("Email constraints");
942 assert_eq!(email_c.get("format"), Some(&json!("email")));
943 let arn_c = meta.property_constraints.get("Arn").expect("Arn constraints");
944 assert_eq!(arn_c.get("format"), Some(&json!("AWS::EC2::VPC.Id")));
945 }
946
947 #[test]
948 fn catalog_metadata_dependent_required_and_excluded() {
949 let mut store = CompiledSchemaStore::new();
950 store
951 .apply_overlay(
952 "AWS::Test::Deps",
953 &json!({
954 "properties": {
955 "Mode": { "type": "string" },
956 "Config": { "type": "object" },
957 "Legacy": { "type": "boolean" }
958 },
959 "dependentRequired": { "Mode": ["Config"] },
960 "dependentExcluded": { "Mode": ["Legacy"] },
961 "additionalProperties": false
962 }),
963 )
964 .expect("overlay applies");
965
966 let catalog = OverlayCatalog::from_store(&store, &["AWS::Test::Deps".to_string()]);
967 let meta = catalog.schema_metadata.get("AWS::Test::Deps").expect("metadata");
968 assert_eq!(meta.dependent_required.get("Mode"), Some(&vec!["Config".to_string()]));
969 assert_eq!(meta.dependent_excluded.get("Mode"), Some(&vec!["Legacy".to_string()]));
970 }
971
972 #[test]
973 fn catalog_metadata_required_or_xor() {
974 let mut store = CompiledSchemaStore::new();
975 store
976 .apply_overlay(
977 "AWS::Test::OrXor",
978 &json!({
979 "properties": {
980 "A": { "type": "string" },
981 "B": { "type": "string" },
982 "C": { "type": "string" }
983 },
984 "requiredOr": ["A", "B"],
985 "requiredXor": ["B", "C"],
986 "additionalProperties": false
987 }),
988 )
989 .expect("overlay applies");
990
991 let catalog = OverlayCatalog::from_store(&store, &["AWS::Test::OrXor".to_string()]);
992 let meta = catalog.schema_metadata.get("AWS::Test::OrXor").expect("metadata");
993 assert_eq!(meta.required_or, vec!["A".to_string(), "B".to_string()]);
994 assert_eq!(meta.required_xor, vec!["B".to_string(), "C".to_string()]);
995 }
996}