Skip to main content

schema_validator/
resource_schema.rs

1use crate::compiled::{CompiledSchema, PropSchema};
2use std::collections::{BTreeMap, BTreeSet, HashMap};
3
4/// JSON value categories accepted by a CloudFormation resource property.
5#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
6pub enum PropertyValueType {
7    Any,
8    Array,
9    Object,
10    Boolean,
11    Integer,
12    Number,
13    String,
14}
15
16/// Schema information needed to map an AWS CLI command into one resource.
17///
18/// This is intentionally narrower than the validator's compiled schema model:
19/// callers can select and type-check resource properties without depending on
20/// validation implementation details.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct ResourceSchemaMetadata {
23    pub type_name: String,
24    pub property_types: BTreeMap<String, BTreeSet<PropertyValueType>>,
25    pub required_properties: BTreeSet<String>,
26    pub read_only_properties: BTreeSet<String>,
27    pub primary_identifier_properties: BTreeSet<String>,
28}
29
30impl ResourceSchemaMetadata {
31    pub(crate) fn from_compiled(schema: &CompiledSchema) -> Self {
32        let property_types = schema
33            .properties
34            .iter()
35            .map(|(name, property)| (name.clone(), accepted_value_types(property, &schema.definitions)))
36            .collect();
37        Self {
38            type_name: schema.type_name.clone(),
39            property_types,
40            required_properties: schema.required.iter().cloned().collect(),
41            read_only_properties: schema.read_only_properties.iter().cloned().collect(),
42            primary_identifier_properties: schema.primary_identifier.iter().cloned().collect(),
43        }
44    }
45}
46
47const MAX_COMPOSITION_DEPTH: usize = 64;
48
49fn accepted_value_types(
50    property: &PropSchema,
51    definitions: &HashMap<String, PropSchema>,
52) -> BTreeSet<PropertyValueType> {
53    let mut accepted = BTreeSet::new();
54    collect_value_types(property, definitions, 0, &mut accepted);
55    accepted.remove(&PropertyValueType::Any);
56    if accepted.is_empty() {
57        accepted.insert(PropertyValueType::Any);
58    }
59    accepted
60}
61
62fn collect_value_types(
63    property: &PropSchema,
64    definitions: &HashMap<String, PropSchema>,
65    depth: usize,
66    accepted: &mut BTreeSet<PropertyValueType>,
67) {
68    if depth >= MAX_COMPOSITION_DEPTH {
69        accepted.insert(PropertyValueType::Any);
70        return;
71    }
72
73    let property = property.resolve(definitions);
74    if let Some(property_type) = &property.prop_type {
75        for name in property_type.names() {
76            match name {
77                "array" => {
78                    accepted.insert(PropertyValueType::Array);
79                }
80                "object" => {
81                    accepted.insert(PropertyValueType::Object);
82                }
83                "boolean" => {
84                    accepted.insert(PropertyValueType::Boolean);
85                }
86                "integer" => {
87                    accepted.insert(PropertyValueType::Integer);
88                }
89                "number" => {
90                    accepted.insert(PropertyValueType::Number);
91                }
92                "string" => {
93                    accepted.insert(PropertyValueType::String);
94                }
95                "null" => {}
96                _ => {
97                    accepted.insert(PropertyValueType::Any);
98                }
99            }
100        }
101    }
102    if property.items.is_some() || property.min_items.is_some() || property.max_items.is_some() {
103        accepted.insert(PropertyValueType::Array);
104    }
105    if !property.properties.is_empty()
106        || !property.pattern_properties.is_empty()
107        || property.additional_properties.is_some()
108        || property.min_properties.is_some()
109        || property.max_properties.is_some()
110    {
111        accepted.insert(PropertyValueType::Object);
112    }
113    for alternative in property.all_of.iter().chain(property.any_of.iter()).chain(property.one_of.iter()) {
114        collect_value_types(alternative, definitions, depth + 1, accepted);
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121    use crate::compiled::PropType;
122
123    #[test]
124    fn metadata_resolves_referenced_property_types() {
125        let mut definitions = HashMap::new();
126        definitions.insert(
127            "Configuration".to_string(),
128            PropSchema { prop_type: Some(PropType::Single("object".into())), ..Default::default() },
129        );
130        let schema = CompiledSchema {
131            type_name: "AWS::Test::Thing".into(),
132            properties: HashMap::from([(
133                "Configuration".into(),
134                PropSchema { ref_name: Some("Configuration".into()), ..Default::default() },
135            )]),
136            definitions,
137            required: vec!["Configuration".into()],
138            read_only_properties: vec!["Arn".into()],
139            primary_identifier: vec!["Name".into()],
140            ..Default::default()
141        };
142
143        let metadata = ResourceSchemaMetadata::from_compiled(&schema);
144
145        assert_eq!(metadata.property_types["Configuration"], BTreeSet::from([PropertyValueType::Object]));
146        assert!(metadata.required_properties.contains("Configuration"));
147        assert!(metadata.read_only_properties.contains("Arn"));
148        assert!(metadata.primary_identifier_properties.contains("Name"));
149    }
150
151    #[test]
152    fn metadata_unions_composed_property_types() {
153        let property = PropSchema {
154            one_of: vec![
155                PropSchema { prop_type: Some(PropType::Single("string".into())), ..Default::default() },
156                PropSchema { prop_type: Some(PropType::Single("integer".into())), ..Default::default() },
157            ],
158            ..Default::default()
159        };
160
161        assert_eq!(
162            accepted_value_types(&property, &HashMap::new()),
163            BTreeSet::from([PropertyValueType::Integer, PropertyValueType::String])
164        );
165    }
166
167    #[test]
168    fn metadata_uses_any_when_no_value_type_is_known() {
169        assert_eq!(
170            accepted_value_types(&PropSchema::default(), &HashMap::new()),
171            BTreeSet::from([PropertyValueType::Any])
172        );
173    }
174}