1use crate::compiled::CompiledSchema;
2use crate::overlay::{self, SchemaOverlayError};
3use data_source::embedded::*;
4use std::collections::{BTreeSet, HashMap};
5use template_model::regions::AWS_REGIONS;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum OverlayOutcome {
15 Merged,
17 Inserted,
19}
20
21pub struct CompiledSchemaStore {
22 schemas: HashMap<String, CompiledSchema>,
23 region_types: HashMap<String, HashMap<String, bool>>,
24 ref_types: RefTypeStore,
25 lifecycle: LifecycleStore,
26 extensions: ExtensionStore,
27 region_enums: RegionEnumStore,
28}
29
30impl Default for CompiledSchemaStore {
31 fn default() -> Self {
32 Self::new()
33 }
34}
35
36impl CompiledSchemaStore {
37 pub fn new() -> Self {
38 let schemas: HashMap<String, CompiledSchema> =
39 serde_json::from_slice(&COMPILED_SCHEMAS_BYTES).expect("Embedded compiled schemas must be valid JSON");
40 assert!(!schemas.is_empty(), "Embedded compiled schemas must not be empty");
41 let ref_types = RefTypeStore::load(&REF_TYPES_BYTES);
42 let lifecycle = LifecycleStore::load(&RESOURCE_LIFECYCLE_BYTES, &LAMBDA_RUNTIMES_BYTES);
43 let mut extensions = ExtensionStore::load(&EXTENSIONS_BYTES);
44 extensions.remap_keys(&schemas);
45 let region_enums =
46 RegionEnumStore::load(®ION_ENUMS_BYTES).expect("Embedded region_enums data must be valid and populated");
47 let mut store = CompiledSchemaStore {
48 schemas,
49 region_types: HashMap::new(),
50 ref_types,
51 lifecycle,
52 extensions,
53 region_enums,
54 };
55 store
59 .load_region_data(®ION_RESOURCE_TYPES_BYTES)
60 .expect("Embedded region_resource_types data must be valid and populated");
61 assert!(
62 store.has_region_data(),
63 "Embedded region-availability data (region_resource_types) is empty; the build is missing regional data"
64 );
65 assert!(
66 store.region_enums.has_data(),
67 "Embedded regional enum data (region_enums) is empty; the build is missing regional data"
68 );
69 store
70 }
71
72 pub fn load_region_data(&mut self, json_bytes: &[u8]) -> Result<(), String> {
73 let wrapper: serde_json::Value = serde_json::from_slice(json_bytes)
74 .map_err(|error| format!("region_resource_types must be valid JSON: {error}"))?;
75 let regions = wrapper
76 .get("region_resource_types")
77 .and_then(|value| value.as_object())
78 .ok_or_else(|| "region_resource_types must contain a region_resource_types object".to_string())?;
79 if regions.is_empty() {
80 return Err("region_resource_types must not be empty".to_string());
81 }
82
83 let mut parsed_regions = HashMap::new();
84 for (region, types) in regions {
85 let type_object =
86 types.as_object().ok_or_else(|| format!("region_resource_types entry '{region}' must be an object"))?;
87 if type_object.is_empty() {
88 return Err(format!("region_resource_types entry '{region}' must not be empty"));
89 }
90 let type_map = type_object.keys().map(|resource_type| (resource_type.clone(), true)).collect();
91 parsed_regions.insert(region.clone(), type_map);
92 }
93 self.region_types.extend(parsed_regions);
94 Ok(())
95 }
96
97 pub fn get(&self, type_name: &str) -> Option<&CompiledSchema> {
98 self.schemas.get(type_name)
99 }
100
101 pub fn apply_overlay(
118 &mut self,
119 type_name: &str,
120 raw: &serde_json::Value,
121 ) -> Result<OverlayOutcome, SchemaOverlayError> {
122 let overlay = overlay::compile(type_name, raw)?;
123 overlay::validate_schema(&overlay)?;
124 match self.schemas.get(type_name) {
125 Some(existing) => {
126 let mut merged = existing.clone();
127 overlay::merge_into(&mut merged, overlay);
128 overlay::validate_schema(&merged)?;
129 overlay::warn_removed_required(existing, &merged);
130 overlay::warn_dangling_refs(&merged);
131 self.ref_types.update_from_schema(&merged);
132 self.schemas.insert(type_name.to_string(), merged);
133 Ok(OverlayOutcome::Merged)
134 }
135 None => {
136 overlay::warn_dangling_refs(&overlay);
137 self.ref_types.update_from_schema(&overlay);
138 self.schemas.insert(type_name.to_string(), overlay);
139 Ok(OverlayOutcome::Inserted)
140 }
141 }
142 }
143
144 #[cfg(test)]
148 pub(crate) fn insert_schema(&mut self, schema: CompiledSchema) {
149 self.schemas.insert(schema.type_name.clone(), schema);
150 }
151
152 pub fn len(&self) -> usize {
153 self.schemas.len()
154 }
155
156 pub fn is_available_in_region(&self, type_name: &str, region: &str) -> bool {
157 self.region_types.get(region).map(|types| types.contains_key(type_name)).unwrap_or(true)
158 }
159
160 pub fn is_known_in_any_region(&self, type_name: &str) -> bool {
165 self.region_types.values().any(|types| types.contains_key(type_name))
166 }
167
168 pub fn has_region_data(&self) -> bool {
169 !self.region_types.is_empty()
170 }
171
172 pub fn ref_types(&self) -> &RefTypeStore {
173 &self.ref_types
174 }
175
176 pub fn lifecycle(&self) -> &LifecycleStore {
177 &self.lifecycle
178 }
179
180 pub fn extensions(&self) -> &ExtensionStore {
181 &self.extensions
182 }
183
184 pub fn region_enums(&self) -> &RegionEnumStore {
185 &self.region_enums
186 }
187}
188
189pub struct RefTypeStore {
190 ref_returns: HashMap<String, String>,
191 getatt_returns: HashMap<String, HashMap<String, String>>,
192 format_compatible_types: HashMap<String, Vec<String>>,
193}
194
195impl RefTypeStore {
196 fn load(bytes: &[u8]) -> Self {
197 let json: serde_json::Value = serde_json::from_slice(bytes).expect("Embedded ref_types must be valid JSON");
198 let ref_returns: HashMap<String, String> = json
199 .get("ref_returns")
200 .and_then(|v| serde_json::from_value(v.clone()).ok())
201 .expect("Embedded ref_types must contain ref_returns");
202 let getatt_returns: HashMap<String, HashMap<String, String>> = json
203 .get("getatt_returns")
204 .and_then(|v| serde_json::from_value(v.clone()).ok())
205 .expect("Embedded ref_types must contain getatt_returns");
206 let format_compatible_types: HashMap<String, Vec<String>> = json
207 .get("format_compatible_types")
208 .and_then(|v| serde_json::from_value(v.clone()).ok())
209 .expect("Embedded ref_types must contain format_compatible_types");
210 assert!(!ref_returns.is_empty(), "Embedded ref_returns must not be empty");
211 assert!(!getatt_returns.is_empty(), "Embedded getatt_returns must not be empty");
212 assert!(!format_compatible_types.is_empty(), "Embedded format_compatible_types must not be empty");
213 RefTypeStore { ref_returns, getatt_returns, format_compatible_types }
214 }
215
216 pub fn update_from_schema(&mut self, schema: &crate::compiled::CompiledSchema) {
225 let type_name = &schema.type_name;
226 let read_only_set: std::collections::HashSet<&str> =
227 schema.read_only_properties.iter().map(|s| s.as_str()).collect();
228
229 self.ref_returns.remove(type_name);
233 if !schema.primary_identifier.is_empty() {
234 let ref_type = if schema.primary_identifier.len() > 1 {
235 "string".to_string()
236 } else {
237 let id_prop = &schema.primary_identifier[0];
238 if read_only_set.contains(id_prop.as_str()) {
239 "string".to_string()
240 } else {
241 crate::catalog::resolve_property_type(schema, id_prop).unwrap_or_else(|| "string".to_string())
242 }
243 };
244 self.ref_returns.insert(type_name.clone(), ref_type);
245 }
246
247 let mut attr_map: HashMap<String, String> = HashMap::new();
251 for (name, prop) in &schema.properties {
252 let resolved = prop.resolve(&schema.definitions);
253 if let Some(pt) = resolved.prop_type.as_ref().and_then(|p| p.primary()) {
254 attr_map.insert(name.clone(), pt.to_string());
255 }
256 }
257 for attr in &schema.read_only_properties {
258 if attr.contains('.')
259 && let Some(prop_type) = crate::catalog::resolve_property_type(schema, attr)
260 {
261 attr_map.insert(attr.clone(), prop_type);
262 }
263 }
264 crate::catalog::apply_getatt_return_type_overrides(type_name, &mut attr_map);
267 if !attr_map.is_empty() {
268 self.getatt_returns.insert(type_name.clone(), attr_map);
269 } else {
270 self.getatt_returns.remove(type_name);
271 }
272 }
273
274 pub fn ref_type_for(&self, resource_type: &str) -> Option<&str> {
275 self.ref_returns.get(resource_type).map(|s| s.as_str())
276 }
277
278 pub fn getatt_type_for(&self, resource_type: &str, attribute: &str) -> Option<&str> {
279 self.getatt_returns.get(resource_type).and_then(|attrs| attrs.get(attribute)).map(|s| s.as_str())
280 }
281
282 pub fn format_compatible_types(&self, format: &str) -> &[String] {
283 self.format_compatible_types.get(format).map(|v| v.as_slice()).unwrap_or(&[])
284 }
285}
286
287pub struct LifecycleEntry {
288 pub status: String,
289 pub date: Option<String>,
290}
291
292pub struct LifecycleStore {
293 resource_lifecycle: HashMap<String, LifecycleEntry>,
294 deprecated_runtimes: Vec<String>,
295 create_blocked_runtimes: Vec<String>,
296 eol_runtimes: Vec<String>,
297 runtime_lifecycle: HashMap<String, RuntimeLifecycle>,
298}
299
300#[derive(Clone)]
303pub struct RuntimeLifecycle {
304 pub deprecated: String,
305 pub create_block: String,
306 pub update_block: String,
307 pub successor: Option<String>,
308}
309
310impl LifecycleStore {
311 fn load(lifecycle_bytes: &[u8], runtimes_bytes: &[u8]) -> Self {
312 let lifecycle_document: serde_json::Value =
313 serde_json::from_slice(lifecycle_bytes).expect("Embedded resource_lifecycle must be valid JSON");
314 let lifecycle_entries = lifecycle_document
315 .get("resource_lifecycle")
316 .and_then(|value| value.as_object())
317 .expect("Embedded resource_lifecycle must contain a resource_lifecycle object");
318 assert!(!lifecycle_entries.is_empty(), "Embedded resource_lifecycle must not be empty");
319 let mut resource_lifecycle = HashMap::new();
320 for (type_name, entry) in lifecycle_entries {
321 let status = entry
322 .get("status")
323 .and_then(|value| value.as_str())
324 .expect("Embedded resource lifecycle entries must contain a status");
325 assert!(!status.is_empty(), "Embedded resource lifecycle status must not be empty");
326 let date = entry.get("date").and_then(|value| value.as_str()).map(String::from);
327 resource_lifecycle.insert(type_name.clone(), LifecycleEntry { status: status.to_string(), date });
328 }
329
330 let runtime_document: serde_json::Value =
331 serde_json::from_slice(runtimes_bytes).expect("Embedded lambda_runtimes must be valid JSON");
332 let runtime_data = runtime_document
333 .get("lambda_runtimes")
334 .and_then(|value| value.as_object())
335 .expect("Embedded lambda_runtimes must contain a lambda_runtimes object");
336 let parse_runtime_list = |field: &str| -> Vec<String> {
337 runtime_data
338 .get(field)
339 .and_then(|value| value.as_array())
340 .unwrap_or_else(|| panic!("Embedded lambda_runtimes must contain a '{}' array", field))
341 .iter()
342 .map(|value| {
343 value
344 .as_str()
345 .unwrap_or_else(|| panic!("Embedded lambda_runtimes '{}' entries must be strings", field))
346 .to_string()
347 })
348 .collect()
349 };
350 let current_runtimes = parse_runtime_list("current");
351 let deprecated_runtimes = parse_runtime_list("deprecated");
352 let create_blocked_runtimes = parse_runtime_list("create_blocked");
353 let eol_runtimes = parse_runtime_list("eol");
354 assert!(
355 !current_runtimes.is_empty()
356 || !deprecated_runtimes.is_empty()
357 || !create_blocked_runtimes.is_empty()
358 || !eol_runtimes.is_empty(),
359 "Embedded lambda_runtimes lists must not all be empty"
360 );
361
362 let lifecycle_dates = runtime_data
363 .get("lifecycle")
364 .and_then(|value| value.as_object())
365 .expect("Embedded lambda_runtimes must contain a lifecycle object");
366 assert!(!lifecycle_dates.is_empty(), "Embedded lambda runtime lifecycle must not be empty");
367 let mut runtime_lifecycle = HashMap::new();
368 for (runtime, dates) in lifecycle_dates {
369 let required_date = |field: &str| {
370 dates
371 .get(field)
372 .and_then(|value| value.as_str())
373 .unwrap_or_else(|| panic!("Embedded runtime lifecycle must contain a '{}' string", field))
374 .to_string()
375 };
376 runtime_lifecycle.insert(
377 runtime.clone(),
378 RuntimeLifecycle {
379 deprecated: required_date("deprecated"),
380 create_block: required_date("create_block"),
381 update_block: required_date("update_block"),
382 successor: dates.get("successor").and_then(|value| value.as_str()).map(String::from),
383 },
384 );
385 }
386
387 LifecycleStore {
388 resource_lifecycle,
389 deprecated_runtimes,
390 create_blocked_runtimes,
391 eol_runtimes,
392 runtime_lifecycle,
393 }
394 }
395
396 pub fn runtime_lifecycle(&self, runtime: &str) -> Option<&RuntimeLifecycle> {
397 self.runtime_lifecycle.get(runtime)
398 }
399
400 pub fn resource_lifecycle(&self, type_name: &str) -> Option<&LifecycleEntry> {
401 self.resource_lifecycle.get(type_name)
402 }
403
404 pub fn is_runtime_deprecated(&self, runtime: &str) -> bool {
405 self.deprecated_runtimes.iter().any(|r| r == runtime)
406 }
407
408 pub fn is_runtime_eol(&self, runtime: &str) -> bool {
409 self.eol_runtimes.iter().any(|r| r == runtime)
410 }
411
412 pub fn is_runtime_create_blocked(&self, runtime: &str) -> bool {
413 self.create_blocked_runtimes.iter().any(|r| r == runtime)
414 }
415}
416
417pub struct ExtensionStore {
418 extensions: HashMap<String, Vec<serde_json::Value>>,
419}
420
421impl ExtensionStore {
422 fn load(bytes: &[u8]) -> Self {
423 let json: HashMap<String, serde_json::Value> =
424 serde_json::from_slice(bytes).expect("Embedded extensions must be valid JSON");
425 assert!(!json.is_empty(), "Embedded extensions must not be empty");
426 let mut extensions = HashMap::new();
427 for (type_name, value) in json {
428 let fragments = value.as_array().expect("Embedded extension entries must be arrays");
429 assert!(!fragments.is_empty(), "Embedded extension arrays must not be empty");
430 extensions.insert(type_name, fragments.clone());
431 }
432 ExtensionStore { extensions }
433 }
434
435 pub fn remap_keys(&mut self, known_types: &HashMap<String, CompiledSchema>) {
439 let lowercase_to_canonical: HashMap<String, String> =
440 known_types.keys().map(|k| (k.to_lowercase(), k.clone())).collect();
441 let old_keys: Vec<String> = self.extensions.keys().cloned().collect();
442 for key in old_keys {
443 if known_types.contains_key(&key) {
444 continue;
445 }
446 if let Some(canonical) = lowercase_to_canonical.get(&key.to_lowercase())
447 && let Some(val) = self.extensions.remove(&key)
448 {
449 self.extensions.insert(canonical.clone(), val);
450 }
451 }
452 }
453
454 pub fn get(&self, type_name: &str) -> Option<&[serde_json::Value]> {
455 self.extensions.get(type_name).map(|v| v.as_slice())
456 }
457}
458
459pub struct RegionEnumStore {
460 enums: HashMap<String, HashMap<String, Vec<String>>>,
461}
462
463impl RegionEnumStore {
464 fn load(bytes: &[u8]) -> Result<Self, String> {
465 let enums: HashMap<String, HashMap<String, Vec<String>>> =
466 serde_json::from_slice(bytes).map_err(|error| format!("region_enums must be valid JSON: {error}"))?;
467 if enums.is_empty() {
468 return Err("region_enums must not be empty".to_string());
469 }
470 if !enums.values().all(|regions| !regions.is_empty() && regions.values().all(|values| !values.is_empty())) {
471 return Err("region_enums entries must not be empty".to_string());
472 }
473 Ok(RegionEnumStore { enums })
474 }
475
476 pub fn get(&self, resource_type: &str, prop_name: &str, region: &str) -> Option<&[String]> {
478 let key = format!("{}::{}", resource_type, prop_name);
479 self.enums.get(&key).and_then(|regions| regions.get(region)).map(|v| v.as_slice())
480 }
481
482 pub fn allowed_values(&self, resource_type: &str, prop_name: &str, region: Option<&str>) -> Option<Vec<&str>> {
488 let key = format!("{}::{}", resource_type, prop_name);
489 let regions = self.enums.get(&key)?;
490 match region {
491 Some(region) => regions.get(region).map(|v| v.iter().map(String::as_str).collect()),
492 None => {
493 let mut union: BTreeSet<&str> = BTreeSet::new();
494 for region in AWS_REGIONS {
495 if let Some(values) = regions.get(*region) {
496 union.extend(values.iter().map(String::as_str));
497 }
498 }
499 (!union.is_empty()).then(|| union.into_iter().collect())
500 }
501 }
502 }
503
504 pub fn has_data(&self) -> bool {
505 !self.enums.is_empty()
506 }
507}
508
509#[cfg(test)]
510mod tests {
511 use super::*;
512 use serde_json::json;
513
514 #[test]
515 fn store_loads_nonempty() {
516 let store = CompiledSchemaStore::new();
517 assert!(store.len() > 0, "store should have schemas, got {}", store.len());
518 }
519
520 #[test]
521 fn store_get_known_type() {
522 let store = CompiledSchemaStore::new();
523 let schema = store.get("AWS::S3::Bucket").expect("expected AWS::S3::Bucket schema");
524 assert_eq!(schema.type_name, "AWS::S3::Bucket");
525 }
526
527 #[test]
528 fn store_get_unknown_type_returns_none() {
529 let store = CompiledSchemaStore::new();
530 assert!(store.get("AWS::Fake::NonExistent").is_none(), "unknown type should return None");
531 }
532
533 #[test]
534 fn store_new_always_has_region_data() {
535 let store = CompiledSchemaStore::new();
536 assert!(store.has_region_data(), "embedded region data must be present");
537 }
538
539 #[test]
540 fn store_load_region_data_filters_types() {
541 let mut store = CompiledSchemaStore::new();
542 let region_json = json!({
543 "region_resource_types": {
544 "us-east-1": { "AWS::S3::Bucket": true, "AWS::EC2::Instance": true },
545 "eu-west-1": { "AWS::S3::Bucket": true }
546 }
547 });
548 store.load_region_data(serde_json::to_vec(®ion_json).unwrap().as_slice()).expect("valid region data");
549 assert!(store.has_region_data());
550 assert!(store.is_available_in_region("AWS::S3::Bucket", "us-east-1"));
551 assert!(store.is_available_in_region("AWS::EC2::Instance", "us-east-1"));
552 assert!(!store.is_available_in_region("AWS::EC2::Instance", "eu-west-1"));
553 }
554
555 #[test]
556 fn store_unknown_region_defaults_to_available() {
557 let mut store = CompiledSchemaStore::new();
558 let region_json = json!({
559 "region_resource_types": {
560 "us-east-1": { "AWS::S3::Bucket": true }
561 }
562 });
563 store.load_region_data(serde_json::to_vec(®ion_json).unwrap().as_slice()).expect("valid region data");
564 assert!(store.is_available_in_region("AWS::S3::Bucket", "ap-southeast-99"));
565 }
566
567 #[test]
568 fn store_load_region_data_invalid_json_returns_error() {
569 let mut store = CompiledSchemaStore::new();
570
571 let error = store.load_region_data(b"not json").expect_err("invalid JSON must fail");
572
573 assert!(error.contains("must be valid JSON"), "unexpected error: {error}");
574 }
575
576 #[test]
577 fn store_load_region_data_wrong_structure_returns_error() {
578 let mut store = CompiledSchemaStore::new();
579
580 let error = store.load_region_data(b"{}").expect_err("a missing wrapper must fail");
581
582 assert!(error.contains("must contain a region_resource_types object"), "unexpected error: {error}");
583 }
584
585 #[test]
586 fn ref_type_store_loads_from_embedded_data() {
587 let store = CompiledSchemaStore::new();
588 let rt = store.ref_types();
589 let vpc_ref = rt.ref_type_for("AWS::EC2::VPC");
590 assert_eq!(vpc_ref, Some("string"), "expected Ref to VPC to return string");
591 }
592
593 #[test]
594 fn ref_type_unknown_resource_returns_none() {
595 let store = CompiledSchemaStore::new();
596 assert_eq!(store.ref_types().ref_type_for("AWS::Fake::Thing"), None, "unknown type should have no ref type");
597 }
598
599 #[test]
600 fn getatt_type_for_known_attribute() {
601 let store = CompiledSchemaStore::new();
602 let rt = store.ref_types();
603 let sg = rt.getatt_type_for("AWS::EC2::VPC", "DefaultSecurityGroup");
604 assert!(sg.is_some(), "expected GetAtt type for VPC.DefaultSecurityGroup");
605 }
606
607 #[test]
608 fn getatt_type_unknown_attribute_returns_none() {
609 let store = CompiledSchemaStore::new();
610 assert!(store.ref_types().getatt_type_for("AWS::EC2::VPC", "FakeAttr").is_none());
611 }
612
613 #[test]
614 fn format_compatible_types_vpc_id() {
615 let store = CompiledSchemaStore::new();
616 let compatible = store.ref_types().format_compatible_types("AWS::EC2::VPC.Id");
617 assert!(
618 compatible.iter().any(|t| t == "AWS::EC2::VPC"),
619 "expected AWS::EC2::VPC in format-compatible types for VPC.Id, got: {:?}",
620 compatible
621 );
622 }
623
624 #[test]
625 fn format_compatible_types_unknown_format_empty() {
626 let store = CompiledSchemaStore::new();
627 assert!(store.ref_types().format_compatible_types("FakeFormat").is_empty());
628 }
629
630 #[test]
631 fn lifecycle_store_loads() {
632 let store = CompiledSchemaStore::new();
633 let lc = store.lifecycle();
634 let entry = lc.resource_lifecycle("AWS::CodeStar::GitHubRepository");
635 assert!(entry.is_some(), "expected lifecycle entry for AWS::CodeStar::GitHubRepository");
636 assert_eq!(entry.unwrap().status, "shutdown");
637 }
638
639 #[test]
640 fn lifecycle_unknown_type_returns_none() {
641 let store = CompiledSchemaStore::new();
642 assert!(store.lifecycle().resource_lifecycle("AWS::S3::Bucket").is_none());
643 }
644
645 #[test]
646 fn runtime_eol_detection() {
647 let store = CompiledSchemaStore::new();
648 let lc = store.lifecycle();
649 assert!(lc.is_runtime_eol("python2.7"), "expected python2.7 to be EOL");
650 assert!(!lc.is_runtime_eol("python3.12"), "python3.12 should not be EOL");
651 }
652
653 #[test]
654 fn runtime_deprecated_detection() {
655 let store = CompiledSchemaStore::new();
656 let lc = store.lifecycle();
657 assert!(!lc.is_runtime_deprecated("python3.12"), "python3.12 should not be deprecated");
658 }
659
660 #[test]
661 fn extension_store_remap_keys() {
662 let mut extensions = ExtensionStore { extensions: HashMap::new() };
663 extensions.extensions.insert("Aws::S3::Bucket".into(), vec![json!({"test": true})]);
664
665 let mut known = HashMap::new();
666 known.insert(
667 "AWS::S3::Bucket".into(),
668 CompiledSchema { type_name: "AWS::S3::Bucket".into(), ..Default::default() },
669 );
670
671 extensions.remap_keys(&known);
672 assert!(extensions.get("AWS::S3::Bucket").is_some(), "expected remapped key");
673 assert!(extensions.get("Aws::S3::Bucket").is_none(), "old key should be removed");
674 }
675
676 #[test]
677 fn extension_store_remap_preserves_canonical_keys() {
678 let mut extensions = ExtensionStore { extensions: HashMap::new() };
679 extensions.extensions.insert("AWS::S3::Bucket".into(), vec![json!({"test": true})]);
680
681 let mut known = HashMap::new();
682 known.insert(
683 "AWS::S3::Bucket".into(),
684 CompiledSchema { type_name: "AWS::S3::Bucket".into(), ..Default::default() },
685 );
686
687 extensions.remap_keys(&known);
688 assert!(extensions.get("AWS::S3::Bucket").is_some(), "canonical key should be preserved");
689 }
690
691 #[test]
692 fn region_enum_get_unknown_returns_none() {
693 let store = CompiledSchemaStore::new();
694 assert!(store.region_enums().get("AWS::Fake::Type", "FakeProp", "us-east-1").is_none());
695 }
696
697 #[test]
698 fn region_enum_store_rejects_empty_document() {
699 let error = RegionEnumStore::load(b"{}").err().expect("empty required enum data must fail");
700
701 assert!(error.contains("must not be empty"), "unexpected error: {error}");
702 }
703
704 #[test]
705 fn region_enum_store_from_valid_data() {
706 let data = json!({
707 "AWS::EC2::Instance::InstanceType": {
708 "us-east-1": ["t2.micro", "t3.micro"],
709 "eu-west-1": ["t2.micro"]
710 }
711 });
712 let re = RegionEnumStore::load(serde_json::to_vec(&data).unwrap().as_slice()).expect("valid region enums");
713 assert!(re.has_data());
714 let vals =
715 re.get("AWS::EC2::Instance", "InstanceType", "us-east-1").expect("expected enum values for us-east-1");
716 assert_eq!(vals, &["t2.micro", "t3.micro"]);
717 assert_eq!(
718 re.get("AWS::EC2::Instance", "InstanceType", "ap-south-1"),
719 None,
720 "ap-south-1 should have no enum values"
721 );
722 }
723
724 fn region_enum_fixture() -> RegionEnumStore {
725 let data = json!({
728 "AWS::EC2::Instance::InstanceType": {
729 "us-east-1": ["t2.micro"],
730 "eu-west-1": ["t2.micro", "t3.micro"],
731 "description": ["should.be.ignored"]
732 }
733 });
734 RegionEnumStore::load(serde_json::to_vec(&data).unwrap().as_slice()).expect("valid region enum fixture")
735 }
736
737 #[test]
738 fn allowed_values_with_region_is_that_region_only() {
739 let re = region_enum_fixture();
740 let vals = re.allowed_values("AWS::EC2::Instance", "InstanceType", Some("us-east-1")).expect("us-east-1 entry");
741 assert!(vals.contains(&"t2.micro"));
742 assert!(!vals.contains(&"t3.micro"), "t3.micro is not valid in us-east-1");
743 }
744
745 #[test]
746 fn allowed_values_without_region_unions_all_regions() {
747 let re = region_enum_fixture();
748 let vals = re.allowed_values("AWS::EC2::Instance", "InstanceType", None).expect("union across regions");
749 assert!(vals.contains(&"t2.micro"));
750 assert!(vals.contains(&"t3.micro"), "t3.micro is valid in eu-west-1, so present in the union");
751 assert!(!vals.contains(&"should.be.ignored"), "the synthetic 'description' key must not contribute");
752 }
753
754 #[test]
755 fn allowed_values_unknown_configured_region_is_none() {
756 let re = region_enum_fixture();
757 assert!(re.allowed_values("AWS::EC2::Instance", "InstanceType", Some("ap-south-1")).is_none());
758 }
759
760 #[test]
761 fn allowed_values_unknown_property_is_none() {
762 let re = region_enum_fixture();
763 assert!(re.allowed_values("AWS::Fake::Type", "FakeProp", None).is_none());
764 assert!(re.allowed_values("AWS::Fake::Type", "FakeProp", Some("us-east-1")).is_none());
765 }
766}