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