schema_validator/
resource_schema.rs1use crate::compiled::{CompiledSchema, PropSchema};
2use std::collections::{BTreeMap, BTreeSet, HashMap};
3
4#[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#[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}