1use serde::{Deserialize, Serialize};
2use std::collections::{BTreeMap, HashMap, HashSet};
3
4#[derive(Debug, Default, Serialize, Deserialize)]
5pub struct KnownResourceTypes {
6 pub known_resource_types: Vec<String>,
7}
8
9#[derive(Debug, Default, Clone, Serialize, Deserialize)]
11pub struct GetattData {
12 pub getatt_attributes: HashMap<String, Vec<String>>,
13 pub getatt_attribute_types: HashMap<String, HashMap<String, String>>,
14}
15
16#[derive(Debug, Default, Serialize, Deserialize)]
17pub struct StatefulResourceTypes {
18 pub stateful_resource_types: HashSet<String>,
19}
20
21#[derive(Debug, Default, Serialize, Deserialize)]
22pub struct RetentionPeriodRequirements {
23 pub retention_period_requirements: HashMap<String, Vec<String>>,
24}
25
26#[derive(Debug, Default, Serialize, Deserialize)]
27pub struct PrimaryIdentifiers {
28 pub primary_identifiers: HashMap<String, Vec<String>>,
29}
30
31pub const PRIMARY_IDENTIFIER_OVERRIDES: &[(&str, &[&str])] = &[("AWS::CodeBuild::Project", &["Name"])];
41
42pub fn primary_identifier_override(type_name: &str) -> Option<Vec<String>> {
44 PRIMARY_IDENTIFIER_OVERRIDES
45 .iter()
46 .find(|(overridden, _)| *overridden == type_name)
47 .map(|(_, properties)| properties.iter().map(|property| (*property).to_string()).collect())
48}
49
50#[derive(Debug, Default, Serialize, Deserialize)]
51pub struct IamActionResourcePatterns {
52 pub iam_action_resource_patterns: HashMap<String, Vec<String>>,
53}
54
55#[derive(Debug, Default, Serialize, Deserialize)]
56pub struct CodepipelineArtifactCounts {
57 pub codepipeline_action_artifact_counts: HashMap<String, ArtifactCountEntry>,
58}
59
60#[derive(Debug, Default, Clone, Serialize, Deserialize)]
61pub struct ArtifactCountEntry {
62 pub min_input: usize,
63 pub max_input: usize,
64 pub min_output: usize,
65 pub max_output: usize,
66}
67
68#[derive(Debug, Default, Serialize, Deserialize)]
69pub struct DeprecatedResourceTypes {
70 pub deprecated_resource_types: Vec<String>,
71}
72
73#[derive(Debug, Default, Serialize, Deserialize)]
74pub struct SensitivePorts {
75 pub sensitive_ports: Vec<u16>,
76}
77
78#[derive(Debug, Default, Serialize, Deserialize)]
79pub struct SecretsManagerArnFields {
80 pub secretsmanager_arn_fields: Vec<String>,
81}
82
83#[derive(Debug, Default, Clone, Serialize, Deserialize)]
92pub struct SchemaMetadataDocument {
93 pub schema_metadata: SchemaMetadataCatalog,
94}
95
96pub type SchemaMetadataCatalog = HashMap<String, SchemaMetadataEntry>;
98
99#[derive(Debug, Default, Clone, Serialize, Deserialize)]
111pub struct SchemaMetadataEntry {
112 #[serde(default)]
113 pub properties: Vec<String>,
114 #[serde(default)]
115 pub required: Vec<String>,
116 #[serde(default)]
119 pub property_types: HashMap<String, String>,
120 #[serde(default)]
123 pub property_enums: HashMap<String, Vec<serde_json::Value>>,
124 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
125 pub property_constraints: HashMap<String, SchemaPropertyConstraints>,
126 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
127 pub dependent_required: HashMap<String, Vec<String>>,
128 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
129 pub dependent_excluded: HashMap<String, Vec<String>>,
130 #[serde(default, skip_serializing_if = "Vec::is_empty")]
131 pub required_or: Vec<String>,
132 #[serde(default, skip_serializing_if = "Vec::is_empty")]
133 pub required_xor: Vec<String>,
134 #[serde(flatten, default)]
135 pub additional: BTreeMap<String, serde_json::Value>,
136}
137
138#[derive(Debug, Default, Clone, Serialize, Deserialize)]
147pub struct SchemaPropertyConstraints {
148 #[serde(default, skip_serializing_if = "Option::is_none")]
149 pub pattern: Option<String>,
150 #[serde(default, skip_serializing_if = "Option::is_none")]
151 pub minimum: Option<serde_json::Number>,
152 #[serde(default, skip_serializing_if = "Option::is_none")]
153 pub maximum: Option<serde_json::Number>,
154 #[serde(rename = "minLength", default, skip_serializing_if = "Option::is_none")]
155 pub min_length: Option<u64>,
156 #[serde(rename = "maxLength", default, skip_serializing_if = "Option::is_none")]
157 pub max_length: Option<u64>,
158 #[serde(rename = "minItems", default, skip_serializing_if = "Option::is_none")]
159 pub min_items: Option<u64>,
160 #[serde(rename = "maxItems", default, skip_serializing_if = "Option::is_none")]
161 pub max_items: Option<u64>,
162 #[serde(default, skip_serializing_if = "Option::is_none")]
163 pub format: Option<String>,
164 #[serde(rename = "uniqueItems", default, skip_serializing_if = "Option::is_none")]
165 pub unique_items: Option<bool>,
166 #[serde(flatten, default)]
167 pub additional: BTreeMap<String, serde_json::Value>,
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173
174 #[test]
175 fn required_document_fields_must_be_present() {
176 assert!(serde_json::from_str::<KnownResourceTypes>("{}").is_err());
177 assert!(serde_json::from_str::<GetattData>("{}").is_err());
178 assert!(serde_json::from_str::<StatefulResourceTypes>("{}").is_err());
179 assert!(serde_json::from_str::<RetentionPeriodRequirements>("{}").is_err());
180 assert!(serde_json::from_str::<PrimaryIdentifiers>("{}").is_err());
181 assert!(serde_json::from_str::<IamActionResourcePatterns>("{}").is_err());
182 assert!(serde_json::from_str::<CodepipelineArtifactCounts>("{}").is_err());
183 assert!(serde_json::from_str::<DeprecatedResourceTypes>("{}").is_err());
184 assert!(serde_json::from_str::<SensitivePorts>("{}").is_err());
185 assert!(serde_json::from_str::<SecretsManagerArnFields>("{}").is_err());
186 assert!(serde_json::from_str::<SchemaMetadataDocument>("{}").is_err());
187 }
188
189 #[test]
190 fn postcard_roundtrip_known_resource_types() {
191 let original =
192 KnownResourceTypes { known_resource_types: vec!["AWS::S3::Bucket".into(), "AWS::EC2::Instance".into()] };
193 let bytes = postcard::to_allocvec(&original).unwrap();
194 let restored: KnownResourceTypes = postcard::from_bytes(&bytes).unwrap();
195 assert_eq!(restored.known_resource_types.len(), 2);
196 assert!(restored.known_resource_types.contains(&"AWS::S3::Bucket".to_string()));
197 }
198
199 #[test]
200 fn postcard_roundtrip_getatt_data() {
201 let mut original = GetattData::default();
202 original.getatt_attributes.insert("AWS::S3::Bucket".into(), vec!["Arn".into(), "DomainName".into()]);
203 original
204 .getatt_attribute_types
205 .insert("AWS::EC2::CapacityReservation".into(), [("InstanceCount".into(), "integer".into())].into());
206 let bytes = postcard::to_allocvec(&original).unwrap();
207 let restored: GetattData = postcard::from_bytes(&bytes).unwrap();
208 assert_eq!(restored.getatt_attributes["AWS::S3::Bucket"].len(), 2);
209 assert_eq!(restored.getatt_attribute_types["AWS::EC2::CapacityReservation"]["InstanceCount"], "integer");
210 }
211
212 #[test]
213 fn postcard_roundtrip_stateful_resource_types() {
214 let original = StatefulResourceTypes {
215 stateful_resource_types: ["AWS::SQS::Queue".into(), "AWS::DynamoDB::Table".into()].into(),
216 };
217 let bytes = postcard::to_allocvec(&original).unwrap();
218 let restored: StatefulResourceTypes = postcard::from_bytes(&bytes).unwrap();
219 assert!(restored.stateful_resource_types.contains("AWS::SQS::Queue"));
220 }
221
222 #[test]
223 fn postcard_roundtrip_retention_period_requirements() {
224 let original = RetentionPeriodRequirements {
225 retention_period_requirements: [("AWS::SQS::Queue".into(), vec!["MessageRetentionPeriod".into()])].into(),
226 };
227 let bytes = postcard::to_allocvec(&original).unwrap();
228 let restored: RetentionPeriodRequirements = postcard::from_bytes(&bytes).unwrap();
229 assert_eq!(restored.retention_period_requirements["AWS::SQS::Queue"], vec!["MessageRetentionPeriod"]);
230 }
231
232 #[test]
238 fn committed_schema_metadata_typed_round_trip_is_lossless() {
239 let bytes = &crate::embedded::SCHEMA_METADATA_BYTES;
240 let original: serde_json::Value =
241 serde_json::from_slice(bytes).expect("committed schema_metadata must be valid JSON");
242 let document: SchemaMetadataDocument =
243 serde_json::from_slice(bytes).expect("committed schema_metadata must parse through the typed model");
244 assert!(!document.schema_metadata.is_empty(), "committed schema_metadata must not be empty");
245 let reserialized = serde_json::to_value(&document).expect("typed model must reserialize");
246 assert_eq!(
247 original, reserialized,
248 "typed round-trip of the committed schema_metadata artifact dropped or changed a field"
249 );
250 }
251
252 #[test]
256 fn committed_schema_metadata_has_no_unmodeled_fields() {
257 let document: SchemaMetadataDocument =
258 serde_json::from_slice(&crate::embedded::SCHEMA_METADATA_BYTES).expect("typed parse");
259 for (type_name, entry) in &document.schema_metadata {
260 assert!(
261 entry.additional.is_empty(),
262 "{type_name}: entry carries unmodeled fields {:?}",
263 entry.additional.keys().collect::<Vec<_>>()
264 );
265 for (prop, constraints) in &entry.property_constraints {
266 assert!(
267 constraints.additional.is_empty(),
268 "{type_name}.{prop}: constraint carries unmodeled fields {:?}",
269 constraints.additional.keys().collect::<Vec<_>>()
270 );
271 }
272 }
273 }
274
275 #[test]
279 fn unknown_fields_and_number_forms_survive_round_trip() {
280 let synthetic = serde_json::json!({
281 "schema_metadata": {
282 "AWS::Test::Synthetic": {
283 "properties": ["A", "B"],
284 "required": ["A"],
285 "property_types": {"A": "string", "B": "array"},
286 "property_enums": {},
287 "property_constraints": {
288 "A": {
289 "minLength": 1,
290 "maxLength": 64,
291 "minimum": 0,
292 "maximum": 3.5,
293 "pattern": "^x$",
294 "future_int": 42,
295 "future_decimal": 1.25,
296 "future_exponent": 1e10
297 },
298 "B": {
299 "minItems": 1,
300 "uniqueItems": true,
301 "future_items_int": 99
302 }
303 },
304 "future_entry_field": {"nested": [1, 2, 3]},
305 "future_entry_int": 123
306 }
307 }
308 });
309
310 let document: SchemaMetadataDocument =
311 serde_json::from_value(synthetic.clone()).expect("synthetic document parses");
312 let reserialized = serde_json::to_value(&document).expect("synthetic document reserializes");
313 assert_eq!(synthetic, reserialized, "an unknown field or number form was not preserved through the model");
314
315 let entry = &document.schema_metadata["AWS::Test::Synthetic"];
317 assert_eq!(entry.additional["future_entry_int"], serde_json::json!(123));
318 let a = &entry.property_constraints["A"];
319 assert_eq!(a.additional["future_int"], serde_json::json!(42));
320 assert_eq!(a.additional["future_exponent"], serde_json::json!(1e10));
321 assert_eq!(a.minimum, Some(serde_json::Number::from(0)));
322 let b = &entry.property_constraints["B"];
323 assert_eq!(b.min_items, Some(1));
324 assert_eq!(b.unique_items, Some(true));
325 assert_eq!(b.additional["future_items_int"], serde_json::json!(99));
326 }
327
328 #[test]
330 fn present_empty_base_fields_are_retained() {
331 let source = serde_json::json!({
332 "schema_metadata": {
333 "AWS::Test::Empty": {
334 "properties": [],
335 "required": [],
336 "property_types": {},
337 "property_enums": {}
338 }
339 }
340 });
341
342 let document: SchemaMetadataDocument = serde_json::from_value(source.clone()).expect("parses");
343 let reserialized = serde_json::to_value(&document).expect("reserializes");
344 assert_eq!(source, reserialized, "a present-empty base field was dropped during round-trip");
345
346 let entry = &reserialized["schema_metadata"]["AWS::Test::Empty"];
347 for field in ["properties", "required", "property_types", "property_enums"] {
348 assert!(entry.get(field).is_some(), "expected present-empty '{field}': {entry}");
349 }
350 for field in ["property_constraints", "dependent_required", "dependent_excluded", "required_or", "required_xor"]
351 {
352 assert!(entry.get(field).is_none(), "an absent optional field must stay absent: '{field}': {entry}");
353 }
354 }
355}