1use serde::{Deserialize, Serialize};
30use std::collections::BTreeMap;
31
32pub mod keywords {
38 pub const DEFINITIONS_REF_PREFIX: &str = "#/definitions/";
42
43 pub const PROPERTIES_PATH_PREFIX: &str = "/properties/";
46
47 pub const REF: &str = "$ref";
51
52 pub const PROPERTIES: &str = "properties";
58 pub const DEFINITIONS: &str = "definitions";
59 pub const PATTERN_PROPERTIES: &str = "patternProperties";
60
61 pub const SCHEMA_MAPS: &[&str] = &[PROPERTIES, DEFINITIONS, PATTERN_PROPERTIES];
64
65 pub const ALL_OF: &str = "allOf";
71 pub const ANY_OF: &str = "anyOf";
72 pub const ONE_OF: &str = "oneOf";
73
74 pub const COMPOSITION: &[&str] = &[ALL_OF, ANY_OF, ONE_OF];
76
77 pub const IF: &str = "if";
83 pub const THEN: &str = "then";
84 pub const ELSE: &str = "else";
85
86 pub const CONDITIONALS: &[&str] = &[IF, THEN, ELSE];
89
90 pub const READ_ONLY_PROPERTIES: &str = "readOnlyProperties";
96 pub const WRITE_ONLY_PROPERTIES: &str = "writeOnlyProperties";
97 pub const CREATE_ONLY_PROPERTIES: &str = "createOnlyProperties";
98 pub const DEPRECATED_PROPERTIES: &str = "deprecatedProperties";
99 pub const CONDITIONAL_CREATE_ONLY_PROPERTIES: &str = "conditionalCreateOnlyProperties";
100 pub const PRIMARY_IDENTIFIER: &str = "primaryIdentifier";
101
102 pub const METADATA_POINTER_ARRAYS: &[&str] = &[
104 READ_ONLY_PROPERTIES,
105 WRITE_ONLY_PROPERTIES,
106 CREATE_ONLY_PROPERTIES,
107 DEPRECATED_PROPERTIES,
108 CONDITIONAL_CREATE_ONLY_PROPERTIES,
109 PRIMARY_IDENTIFIER,
110 ];
111
112 pub const REQUIRED: &str = "required";
117 pub const REQUIRED_OR: &str = "requiredOr";
118 pub const REQUIRED_XOR: &str = "requiredXor";
119
120 pub const STRING_ARRAY_CONSTRAINTS: &[&str] = &[REQUIRED, REQUIRED_OR, REQUIRED_XOR];
123
124 pub const DEPENDENT_REQUIRED: &str = "dependentRequired";
129 pub const DEPENDENT_EXCLUDED: &str = "dependentExcluded";
130
131 pub const DEPENDENCY_MAPS: &[&str] = &[DEPENDENT_REQUIRED, DEPENDENT_EXCLUDED];
133
134 pub const ENUM: &str = "enum";
137 pub const ENUM_CASE_INSENSITIVE: &str = "enumCaseInsensitive";
138 pub const NOT: &str = "not";
139
140 pub const ENUM_KEYWORDS: &[&str] = &[ENUM, ENUM_CASE_INSENSITIVE];
142
143 pub const MINIMUM: &str = "minimum";
146 pub const MAXIMUM: &str = "maximum";
147 pub const EXCLUSIVE_MINIMUM: &str = "exclusiveMinimum";
148 pub const EXCLUSIVE_MAXIMUM: &str = "exclusiveMaximum";
149
150 pub const NUMERIC_CONSTRAINTS: &[&str] = &[MINIMUM, MAXIMUM, EXCLUSIVE_MINIMUM, EXCLUSIVE_MAXIMUM];
152
153 pub const MIN_LENGTH: &str = "minLength";
156 pub const MAX_LENGTH: &str = "maxLength";
157 pub const MIN_ITEMS: &str = "minItems";
158 pub const MAX_ITEMS: &str = "maxItems";
159 pub const MIN_PROPERTIES: &str = "minProperties";
160 pub const MAX_PROPERTIES: &str = "maxProperties";
161
162 pub const U64_CONSTRAINTS: &[&str] =
164 &[MIN_LENGTH, MAX_LENGTH, MIN_ITEMS, MAX_ITEMS, MIN_PROPERTIES, MAX_PROPERTIES];
165
166 pub const ADDITIONAL_PROPERTIES: &str = "additionalProperties";
169 pub const UNIQUE_ITEMS: &str = "uniqueItems";
170
171 pub const BOOL_CONSTRAINTS: &[&str] = &[ADDITIONAL_PROPERTIES, UNIQUE_ITEMS];
173
174 pub const PATTERN: &str = "pattern";
177 pub const FORMAT: &str = "format";
178 pub const REPLACEMENT_STRATEGY: &str = "replacementStrategy";
179 pub const DOCUMENTATION_URL: &str = "documentationUrl";
180 pub const SOURCE_URL: &str = "sourceUrl";
181
182 pub const STRING_CONSTRAINTS: &[&str] = &[PATTERN, FORMAT, REPLACEMENT_STRATEGY, DOCUMENTATION_URL, SOURCE_URL];
187
188 pub const TYPE: &str = "type";
191 pub const CONST: &str = "const";
192 pub const ITEMS: &str = "items";
193 pub const TYPE_NAME: &str = "typeName";
194
195 pub const UNREPRESENTED_VALIDATION_KEYWORDS: &[&str] =
202 &["propertyNames", "contains", "minContains", "maxContains", "additionalItems", "unevaluatedProperties"];
203
204 pub const MULTIPLE_OF: &str = "multipleOf";
207
208 pub const DEPENDENCIES: &str = "dependencies";
214
215 pub const RELATIONSHIP_REF: &str = "relationshipRef";
218
219 pub const REF_ANNOTATION_KEYWORDS: &[&str] = &[
225 "description",
226 "markdownDescription",
227 "title",
228 "examples",
229 "default",
230 "$comment",
231 "insertionOrder",
232 RELATIONSHIP_REF,
233 ];
234
235 pub const COMPILE_SUB_READS_ALL_PROP_FIELDS: bool = true;
246
247 pub const COMPILE_CONDITION_SCHEMA_FIELDS: &[&str] = &[PROPERTIES, REQUIRED, TYPE, ANY_OF];
255}
256
257#[derive(Serialize, Deserialize)]
258pub struct CompiledSchema {
259 pub type_name: String,
260 #[serde(default)]
261 pub properties: BTreeMap<String, PropSchema>,
262 #[serde(default)]
263 pub definitions: BTreeMap<String, PropSchema>,
264 #[serde(default)]
265 pub required: Vec<String>,
266 #[serde(default)]
267 pub additional_properties: Option<bool>,
268 #[serde(default)]
269 pub read_only_properties: Vec<String>,
270 #[serde(default, skip_serializing_if = "Vec::is_empty")]
271 pub write_only_properties: Vec<String>,
272 #[serde(default, skip_serializing_if = "Vec::is_empty")]
273 pub create_only_properties: Vec<String>,
274 #[serde(default, skip_serializing_if = "Vec::is_empty")]
275 pub deprecated_properties: Vec<String>,
276 #[serde(default, skip_serializing_if = "Vec::is_empty")]
277 pub conditional_create_only_properties: Vec<String>,
278 #[serde(default, skip_serializing_if = "Vec::is_empty")]
279 pub primary_identifier: Vec<String>,
280 #[serde(default, skip_serializing_if = "Option::is_none")]
281 pub replacement_strategy: Option<String>,
282 #[serde(default, skip_serializing_if = "Option::is_none")]
283 pub documentation_url: Option<String>,
284 #[serde(default, skip_serializing_if = "Option::is_none")]
285 pub source_url: Option<String>,
286 #[serde(default, skip_serializing_if = "Vec::is_empty")]
287 pub all_of: Vec<SubSchema>,
288 #[serde(default, skip_serializing_if = "Vec::is_empty")]
289 pub any_of: Vec<SubSchema>,
290 #[serde(default, skip_serializing_if = "Vec::is_empty")]
291 pub one_of: Vec<SubSchema>,
292 #[serde(default, skip_serializing_if = "Vec::is_empty")]
293 pub if_then_else: Vec<IfThenElse>,
294 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
295 pub dependent_required: BTreeMap<String, Vec<String>>,
296 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
297 pub dependent_excluded: BTreeMap<String, Vec<String>>,
298 #[serde(default, skip_serializing_if = "Vec::is_empty")]
299 pub required_or: Vec<String>,
300 #[serde(default, skip_serializing_if = "Vec::is_empty")]
301 pub required_xor: Vec<String>,
302}
303
304#[derive(Serialize, Deserialize, Default)]
305pub struct IfThenElse {
306 pub condition: ConditionSchema,
307 #[serde(default, skip_serializing_if = "Option::is_none")]
308 pub then_schema: Option<SubSchema>,
309 #[serde(default, skip_serializing_if = "Option::is_none")]
310 pub else_schema: Option<SubSchema>,
311}
312
313#[derive(Serialize, Deserialize, Default)]
314pub struct ConditionSchema {
315 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
316 pub properties: BTreeMap<String, PropSchema>,
317 #[serde(default, skip_serializing_if = "Vec::is_empty")]
318 pub required: Vec<String>,
319 #[serde(default, rename = "type", skip_serializing_if = "Option::is_none")]
324 pub prop_type: Option<PropType>,
325 #[serde(default, skip_serializing_if = "Vec::is_empty")]
326 pub any_of: Vec<ConditionSchema>,
327}
328
329#[derive(Serialize, Deserialize, Default)]
330pub struct PropSchema {
331 #[serde(default, skip_serializing_if = "Option::is_none")]
332 pub ref_name: Option<String>,
333 #[serde(default, rename = "type", skip_serializing_if = "Option::is_none")]
334 pub prop_type: Option<PropType>,
335 #[serde(default, rename = "enum", skip_serializing_if = "Vec::is_empty")]
336 pub enum_values: Vec<serde_json::Value>,
337 #[serde(default, skip_serializing_if = "Vec::is_empty")]
338 pub enum_case_insensitive: Vec<serde_json::Value>,
339 #[serde(default, skip_serializing_if = "Vec::is_empty")]
340 pub not_enum: Vec<serde_json::Value>,
341 #[serde(default, rename = "const", skip_serializing_if = "Option::is_none")]
342 pub const_value: Option<serde_json::Value>,
343 #[serde(default, skip_serializing_if = "Option::is_none")]
344 pub pattern: Option<String>,
345 #[serde(default, skip_serializing_if = "Option::is_none")]
346 pub minimum: Option<f64>,
347 #[serde(default, skip_serializing_if = "Option::is_none")]
348 pub maximum: Option<f64>,
349 #[serde(default, skip_serializing_if = "Option::is_none")]
350 pub exclusive_minimum: Option<f64>,
351 #[serde(default, skip_serializing_if = "Option::is_none")]
352 pub exclusive_maximum: Option<f64>,
353 #[serde(default, skip_serializing_if = "Option::is_none")]
354 pub multiple_of: Option<f64>,
355 #[serde(default, skip_serializing_if = "Option::is_none")]
356 pub min_length: Option<u64>,
357 #[serde(default, skip_serializing_if = "Option::is_none")]
358 pub max_length: Option<u64>,
359 #[serde(default, skip_serializing_if = "Option::is_none")]
360 pub min_items: Option<u64>,
361 #[serde(default, skip_serializing_if = "Option::is_none")]
362 pub max_items: Option<u64>,
363 #[serde(default, skip_serializing_if = "skip_unless_true")]
368 pub unique_items: Option<bool>,
369 #[serde(default, skip_serializing_if = "Option::is_none")]
370 pub min_properties: Option<u64>,
371 #[serde(default, skip_serializing_if = "Option::is_none")]
372 pub max_properties: Option<u64>,
373 #[serde(default, skip_serializing_if = "Option::is_none")]
374 pub format: Option<String>,
375 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
376 pub properties: BTreeMap<String, PropSchema>,
377 #[serde(default, skip_serializing_if = "Vec::is_empty")]
378 pub required: Vec<String>,
379 #[serde(default, skip_serializing_if = "Option::is_none")]
380 pub additional_properties: Option<bool>,
381 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
382 pub pattern_properties: BTreeMap<String, PropSchema>,
383 #[serde(default, skip_serializing_if = "Option::is_none")]
384 pub items: Option<Box<PropSchema>>,
385 #[serde(default, skip_serializing_if = "Vec::is_empty")]
386 pub all_of: Vec<SubSchema>,
387 #[serde(default, skip_serializing_if = "Vec::is_empty")]
388 pub any_of: Vec<SubSchema>,
389 #[serde(default, skip_serializing_if = "Vec::is_empty")]
390 pub one_of: Vec<SubSchema>,
391 #[serde(default, skip_serializing_if = "Vec::is_empty")]
392 pub if_then_else: Vec<IfThenElse>,
393 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
394 pub dependent_required: BTreeMap<String, Vec<String>>,
395 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
396 pub dependent_excluded: BTreeMap<String, Vec<String>>,
397 #[serde(default, skip_serializing_if = "Vec::is_empty")]
398 pub required_or: Vec<String>,
399 #[serde(default, skip_serializing_if = "Vec::is_empty")]
400 pub required_xor: Vec<String>,
401}
402fn skip_unless_true(value: &Option<bool>) -> bool {
403 *value != Some(true)
404}
405
406pub type SubSchema = PropSchema;
410
411#[derive(Serialize, Deserialize)]
412#[serde(untagged)]
413pub enum PropType {
414 Single(String),
415 Multi(Vec<String>),
416}
417
418fn convert_property_paths(raw: &serde_json::Value) -> Vec<String> {
420 raw.as_array()
421 .map(|arr| {
422 arr.iter()
423 .filter_map(|v| {
424 v.as_str()
425 .and_then(|s| s.strip_prefix(keywords::PROPERTIES_PATH_PREFIX))
426 .map(|s| s.replace('/', "."))
427 })
428 .collect()
429 })
430 .unwrap_or_default()
431}
432
433#[derive(Debug, Clone, Copy, PartialEq, Eq)]
443pub enum RefSiblings {
444 Enforce,
446 Ignore,
448}
449
450pub fn compile_schema(type_name: &str, raw: &serde_json::Value) -> CompiledSchema {
454 compile_schema_with(type_name, raw, RefSiblings::Enforce)
455}
456
457pub fn compile_schema_with(type_name: &str, raw: &serde_json::Value, ref_siblings: RefSiblings) -> CompiledSchema {
467 let mut defs = BTreeMap::new();
468 if let Some(d) = raw.get(keywords::DEFINITIONS).and_then(|v| v.as_object()) {
469 for (k, v) in d {
470 defs.insert(k.clone(), compile_prop_with(v, ref_siblings));
471 }
472 }
473 let mut props = BTreeMap::new();
474 if let Some(p) = raw.get(keywords::PROPERTIES).and_then(|v| v.as_object()) {
475 for (k, v) in p {
476 props.insert(k.clone(), compile_prop_with(v, ref_siblings));
477 }
478 }
479
480 let mut all_of = Vec::new();
481 let mut if_then_else = Vec::new();
482 if let Some(arr) = raw.get(keywords::ALL_OF).and_then(|v| v.as_array()) {
483 for item in arr {
484 if item.get(keywords::IF).is_some() {
485 if let Some(ite) = compile_if_then_else(item, ref_siblings) {
486 if_then_else.push(ite);
487 }
488 } else {
489 all_of.push(compile_sub(item, ref_siblings));
490 }
491 }
492 }
493
494 CompiledSchema {
495 type_name: type_name.to_string(),
496 properties: props,
497 definitions: defs,
498 required: str_arr(raw.get(keywords::REQUIRED)),
499 additional_properties: raw.get(keywords::ADDITIONAL_PROPERTIES).and_then(|v| v.as_bool()),
500 read_only_properties: convert_property_paths(
501 raw.get(keywords::READ_ONLY_PROPERTIES).unwrap_or(&serde_json::Value::Null),
502 ),
503 write_only_properties: convert_property_paths(
504 raw.get(keywords::WRITE_ONLY_PROPERTIES).unwrap_or(&serde_json::Value::Null),
505 ),
506 create_only_properties: convert_property_paths(
507 raw.get(keywords::CREATE_ONLY_PROPERTIES).unwrap_or(&serde_json::Value::Null),
508 ),
509 deprecated_properties: convert_property_paths(
510 raw.get(keywords::DEPRECATED_PROPERTIES).unwrap_or(&serde_json::Value::Null),
511 ),
512 conditional_create_only_properties: convert_property_paths(
513 raw.get(keywords::CONDITIONAL_CREATE_ONLY_PROPERTIES).unwrap_or(&serde_json::Value::Null),
514 ),
515 primary_identifier: convert_property_paths(
516 raw.get(keywords::PRIMARY_IDENTIFIER).unwrap_or(&serde_json::Value::Null),
517 ),
518 replacement_strategy: raw
519 .get(keywords::REPLACEMENT_STRATEGY)
520 .and_then(|v| v.as_str())
521 .filter(|s| !s.is_empty())
522 .map(String::from),
523 documentation_url: raw
524 .get(keywords::DOCUMENTATION_URL)
525 .and_then(|v| v.as_str())
526 .filter(|s| !s.is_empty())
527 .map(String::from),
528 source_url: raw.get(keywords::SOURCE_URL).and_then(|v| v.as_str()).filter(|s| !s.is_empty()).map(String::from),
529 all_of,
530 any_of: compile_subs(raw.get(keywords::ANY_OF), ref_siblings),
531 one_of: compile_subs(raw.get(keywords::ONE_OF), ref_siblings),
532 if_then_else,
533 dependent_required: compile_dependent_required(raw),
534 dependent_excluded: str_map(raw.get(keywords::DEPENDENT_EXCLUDED)),
535 required_or: str_arr(raw.get(keywords::REQUIRED_OR)),
536 required_xor: str_arr(raw.get(keywords::REQUIRED_XOR)),
537 }
538}
539
540fn compile_dependent_required(raw: &serde_json::Value) -> BTreeMap<String, Vec<String>> {
543 let mut dep_req = str_map(raw.get(keywords::DEPENDENT_REQUIRED));
544 if let Some(deps_obj) = raw.get(keywords::DEPENDENCIES).and_then(|v| v.as_object()) {
545 for (trigger, value) in deps_obj {
546 if let Some(arr) = value.as_array() {
547 let names: Vec<String> = arr.iter().filter_map(|v| v.as_str().map(String::from)).collect();
548 if !names.is_empty() {
549 dep_req.entry(trigger.clone()).or_default().extend(names);
550 }
551 }
552 }
553 }
554 for deps in dep_req.values_mut() {
555 deps.sort();
556 deps.dedup();
557 }
558 dep_req
559}
560
561fn compile_if_then_else(raw: &serde_json::Value, ref_siblings: RefSiblings) -> Option<IfThenElse> {
562 let if_val = raw.get(keywords::IF)?;
563 let condition = compile_condition_schema(if_val, ref_siblings);
564 let then_schema = raw.get(keywords::THEN).map(|branch| compile_sub(branch, ref_siblings));
565 let else_schema = raw.get(keywords::ELSE).map(|branch| compile_sub(branch, ref_siblings));
566 if then_schema.is_none() && else_schema.is_none() {
567 return None;
568 }
569 Some(IfThenElse { condition, then_schema, else_schema })
570}
571
572fn compile_condition_schema(raw: &serde_json::Value, ref_siblings: RefSiblings) -> ConditionSchema {
573 let obj = match raw.as_object() {
574 Some(o) => o,
575 None => return ConditionSchema::default(),
576 };
577 let mut props = BTreeMap::new();
578 if let Some(p) = obj.get(keywords::PROPERTIES).and_then(|v| v.as_object()) {
579 for (k, v) in p {
580 props.insert(k.clone(), compile_prop_with(v, ref_siblings));
581 }
582 }
583 let any_of = obj
584 .get(keywords::ANY_OF)
585 .and_then(|v| v.as_array())
586 .map(|arr| arr.iter().map(|entry| compile_condition_schema(entry, ref_siblings)).collect())
587 .unwrap_or_default();
588 let prop_type = obj.get(keywords::TYPE).map(compile_prop_type);
589 ConditionSchema { properties: props, required: str_arr(obj.get(keywords::REQUIRED)), prop_type, any_of }
590}
591
592fn compile_prop_type(raw: &serde_json::Value) -> PropType {
596 match raw {
597 serde_json::Value::String(s) => PropType::Single(s.clone()),
598 serde_json::Value::Array(a) => PropType::Multi(a.iter().filter_map(|v| v.as_str().map(String::from)).collect()),
599 _ => PropType::Single("string".into()),
600 }
601}
602
603fn compile_prop_with(raw: &serde_json::Value, ref_siblings: RefSiblings) -> PropSchema {
604 let obj = match raw.as_object() {
605 Some(o) => o,
606 None => return PropSchema::default(),
607 };
608 let raw_ref = obj.get(keywords::REF).and_then(|v| v.as_str());
615 let ref_name = raw_ref.and_then(|ref_str| ref_str.strip_prefix(keywords::DEFINITIONS_REF_PREFIX).map(String::from));
616 if raw_ref.is_some() && ref_siblings == RefSiblings::Ignore {
617 return PropSchema { ref_name, ..Default::default() };
618 }
619 if ref_name.is_some() {
621 let has_constraint_siblings = obj.keys().any(|key| {
622 key != keywords::REF
623 && !keywords::REF_ANNOTATION_KEYWORDS.contains(&key.as_str())
624 && key != keywords::RELATIONSHIP_REF
625 });
626 if !has_constraint_siblings {
627 return PropSchema { ref_name, ..Default::default() };
628 }
629 }
630
631 let prop_type = obj.get(keywords::TYPE).map(compile_prop_type);
632 let mut sub_props = BTreeMap::new();
633 if let Some(p) = obj.get(keywords::PROPERTIES).and_then(|v| v.as_object()) {
634 for (k, v) in p {
635 sub_props.insert(k.clone(), compile_prop_with(v, ref_siblings));
636 }
637 }
638 let mut pat_props = BTreeMap::new();
639 if let Some(p) = obj.get(keywords::PATTERN_PROPERTIES).and_then(|v| v.as_object()) {
640 for (k, v) in p {
641 pat_props.insert(k.clone(), compile_prop_with(v, ref_siblings));
642 }
643 }
644 let items = obj.get(keywords::ITEMS).map(|v| Box::new(compile_prop_with(v, ref_siblings)));
645
646 let mut dep_req = str_map(obj.get(keywords::DEPENDENT_REQUIRED).cloned().as_ref());
648 if let Some(deps_obj) = obj.get(keywords::DEPENDENCIES).and_then(|v| v.as_object()) {
649 for (trigger, value) in deps_obj {
650 if let Some(arr) = value.as_array() {
651 let names: Vec<String> = arr.iter().filter_map(|v| v.as_str().map(String::from)).collect();
652 if !names.is_empty() {
653 dep_req.entry(trigger.clone()).or_default().extend(names);
654 }
655 }
656 }
659 }
660 for deps in dep_req.values_mut() {
662 deps.sort();
663 deps.dedup();
664 }
665
666 let mut if_then_else = Vec::new();
668 if obj.get(keywords::IF).is_some()
669 && let Some(ite) = compile_if_then_else(raw, ref_siblings)
670 {
671 if_then_else.push(ite);
672 }
673
674 let mut all_of_branches = Vec::new();
676 if let Some(arr) = obj.get(keywords::ALL_OF).and_then(|v| v.as_array()) {
677 for item in arr {
678 if item.get(keywords::IF).is_some() {
679 if let Some(ite) = compile_if_then_else(item, ref_siblings) {
680 if_then_else.push(ite);
681 }
682 } else {
683 all_of_branches.push(compile_sub(item, ref_siblings));
684 }
685 }
686 }
687
688 PropSchema {
689 ref_name,
690 prop_type,
691 enum_values: obj.get(keywords::ENUM).and_then(|v| v.as_array()).cloned().unwrap_or_default(),
692 enum_case_insensitive: obj
693 .get(keywords::ENUM_CASE_INSENSITIVE)
694 .and_then(|v| v.as_array())
695 .cloned()
696 .unwrap_or_default(),
697 not_enum: obj
698 .get(keywords::NOT)
699 .and_then(|v| v.get(keywords::ENUM))
700 .and_then(|v| v.as_array())
701 .cloned()
702 .unwrap_or_default(),
703 const_value: obj.get(keywords::CONST).cloned(),
704 pattern: obj.get(keywords::PATTERN).and_then(|v| v.as_str()).map(String::from),
705 minimum: obj.get(keywords::MINIMUM).and_then(|v| v.as_f64()),
706 maximum: obj.get(keywords::MAXIMUM).and_then(|v| v.as_f64()),
707 exclusive_minimum: obj.get(keywords::EXCLUSIVE_MINIMUM).and_then(|v| v.as_f64()),
708 exclusive_maximum: obj.get(keywords::EXCLUSIVE_MAXIMUM).and_then(|v| v.as_f64()),
709 multiple_of: obj.get(keywords::MULTIPLE_OF).and_then(|v| v.as_f64()),
710 min_length: obj.get(keywords::MIN_LENGTH).and_then(|v| v.as_u64()),
711 max_length: obj.get(keywords::MAX_LENGTH).and_then(|v| v.as_u64()),
712 min_items: obj.get(keywords::MIN_ITEMS).and_then(|v| v.as_u64()),
713 max_items: obj.get(keywords::MAX_ITEMS).and_then(|v| v.as_u64()),
714 unique_items: obj.get(keywords::UNIQUE_ITEMS).and_then(|v| v.as_bool()),
715 min_properties: obj.get(keywords::MIN_PROPERTIES).and_then(|v| v.as_u64()),
716 max_properties: obj.get(keywords::MAX_PROPERTIES).and_then(|v| v.as_u64()),
717 format: obj.get(keywords::FORMAT).and_then(|v| v.as_str()).map(String::from),
718 properties: sub_props,
719 required: str_arr(obj.get(keywords::REQUIRED).cloned().as_ref()),
720 additional_properties: obj.get(keywords::ADDITIONAL_PROPERTIES).and_then(|v| v.as_bool()),
721 pattern_properties: pat_props,
722 items,
723 all_of: all_of_branches,
724 any_of: compile_subs(obj.get(keywords::ANY_OF).cloned().as_ref(), ref_siblings),
725 one_of: compile_subs(obj.get(keywords::ONE_OF).cloned().as_ref(), ref_siblings),
726 if_then_else,
727 dependent_required: dep_req,
728 dependent_excluded: str_map(obj.get(keywords::DEPENDENT_EXCLUDED).cloned().as_ref()),
729 required_or: str_arr(obj.get(keywords::REQUIRED_OR).cloned().as_ref()),
730 required_xor: str_arr(obj.get(keywords::REQUIRED_XOR).cloned().as_ref()),
731 }
732}
733
734fn compile_sub(raw: &serde_json::Value, ref_siblings: RefSiblings) -> SubSchema {
735 compile_prop_with(raw, ref_siblings)
736}
737
738fn compile_subs(val: Option<&serde_json::Value>, ref_siblings: RefSiblings) -> Vec<SubSchema> {
739 let Some(arr) = val.and_then(|v| v.as_array()) else {
740 return Vec::new();
741 };
742 arr.iter().map(|entry| compile_sub(entry, ref_siblings)).collect()
743}
744
745fn str_arr(val: Option<&serde_json::Value>) -> Vec<String> {
746 val.and_then(|v| v.as_array())
747 .map(|a| a.iter().filter_map(|v| v.as_str().map(String::from)).collect())
748 .unwrap_or_default()
749}
750
751fn str_map(val: Option<&serde_json::Value>) -> BTreeMap<String, Vec<String>> {
752 val.and_then(|v| v.as_object())
753 .map(|m| m.iter().map(|(k, v)| (k.clone(), str_arr(Some(v)))).collect())
754 .unwrap_or_default()
755}
756
757#[cfg(test)]
758mod tests {
759 use super::*;
760 use serde_json::json;
761
762 #[test]
763 fn compiles_properties_definitions_and_refs() {
764 let compiled = compile_schema(
765 "AWS::Test::T",
766 &json!({
767 "properties": {
768 "Name": { "type": "string", "pattern": "^a", "minLength": 1 },
769 "Cfg": { "$ref": "#/definitions/Config" },
770 "Kinds": { "type": ["string", "null"] }
771 },
772 "definitions": {
773 "Config": {
774 "type": "object",
775 "required": ["Inner"],
776 "requiredOr": ["First", "Second"],
777 "requiredXor": ["Left", "Right"]
778 }
779 },
780 "required": ["Name"],
781 "additionalProperties": false
782 }),
783 );
784 assert_eq!(compiled.type_name, "AWS::Test::T");
785 assert_eq!(compiled.required, vec!["Name".to_string()]);
786 assert_eq!(compiled.additional_properties, Some(false));
787 assert_eq!(compiled.properties["Cfg"].ref_name.as_deref(), Some("Config"));
788 assert_eq!(compiled.definitions["Config"].required, vec!["Inner".to_string()]);
789 assert_eq!(compiled.definitions["Config"].required_or, vec!["First".to_string(), "Second".to_string()]);
790 assert_eq!(compiled.definitions["Config"].required_xor, vec!["Left".to_string(), "Right".to_string()]);
791 assert_eq!(compiled.properties["Name"].pattern.as_deref(), Some("^a"));
792 assert_eq!(compiled.properties["Name"].min_length, Some(1));
793 match compiled.properties["Kinds"].prop_type.as_ref().expect("a multi type is compiled") {
794 PropType::Multi(names) => assert_eq!(names, &vec!["string".to_string(), "null".to_string()]),
795 PropType::Single(name) => panic!("expected a multi type, got {name}"),
796 }
797 }
798
799 #[test]
800 fn converts_property_pointer_paths_to_dot_notation() {
801 let compiled = compile_schema(
802 "AWS::Test::T",
803 &json!({
804 "readOnlyProperties": ["/properties/Arn", "/properties/Nested/Id"],
805 "writeOnlyProperties": ["/properties/Secret"],
806 "createOnlyProperties": ["/properties/Name"],
807 "deprecatedProperties": ["/properties/Old"],
808 "conditionalCreateOnlyProperties": ["/properties/Maybe"],
809 "primaryIdentifier": ["/properties/Name"]
810 }),
811 );
812 assert_eq!(compiled.read_only_properties, vec!["Arn".to_string(), "Nested.Id".to_string()]);
813 assert_eq!(compiled.write_only_properties, vec!["Secret".to_string()]);
814 assert_eq!(compiled.create_only_properties, vec!["Name".to_string()]);
815 assert_eq!(compiled.deprecated_properties, vec!["Old".to_string()]);
816 assert_eq!(compiled.conditional_create_only_properties, vec!["Maybe".to_string()]);
817 assert_eq!(compiled.primary_identifier, vec!["Name".to_string()]);
818 }
819
820 #[test]
821 fn splits_all_of_into_plain_and_conditional_entries() {
822 let compiled = compile_schema(
823 "AWS::Test::T",
824 &json!({
825 "allOf": [
826 { "required": ["Plain"] },
827 { "if": { "properties": { "A": { "enum": ["x"] } } }, "then": { "required": ["B"] } },
828 { "if": { "properties": { "A": { "enum": ["y"] } } } }
829 ]
830 }),
831 );
832 assert_eq!(compiled.all_of.len(), 1, "plain entries stay in all_of");
833 assert_eq!(compiled.all_of[0].required, vec!["Plain".to_string()]);
834 assert_eq!(compiled.if_then_else.len(), 1, "an if with neither then nor else is dropped");
835 assert_eq!(compiled.if_then_else[0].then_schema.as_ref().expect("then branch").required, vec!["B".to_string()]);
836 }
837
838 #[test]
839 fn preserves_unique_items_presence() {
840 let compiled = compile_schema(
841 "AWS::Test::T",
842 &json!({
843 "properties": {
844 "Strict": { "type": "array", "uniqueItems": true },
845 "Relaxed": { "type": "array", "uniqueItems": false },
846 "Silent": { "type": "array" }
847 }
848 }),
849 );
850 assert_eq!(compiled.properties["Strict"].unique_items, Some(true));
851 assert_eq!(
852 compiled.properties["Relaxed"].unique_items,
853 Some(false),
854 "an explicit false must be distinguishable from an omitted keyword"
855 );
856 assert_eq!(compiled.properties["Silent"].unique_items, None);
857 }
858
859 #[test]
860 fn unique_items_serialization_only_emits_true() {
861 let compiled = compile_schema(
864 "AWS::Test::T",
865 &json!({
866 "properties": {
867 "Strict": { "uniqueItems": true },
868 "Relaxed": { "uniqueItems": false },
869 "Silent": {}
870 }
871 }),
872 );
873 let json = serde_json::to_value(&compiled).expect("a compiled schema serializes");
874 let properties = json.get("properties").and_then(|v| v.as_object()).expect("properties are serialized");
875 assert_eq!(properties["Strict"].get("unique_items"), Some(&json!(true)));
876 assert!(properties["Relaxed"].get("unique_items").is_none(), "explicit false must be omitted");
877 assert!(properties["Silent"].get("unique_items").is_none(), "an absent keyword must be omitted");
878 }
879
880 #[test]
881 fn compiles_both_enum_representations_and_constraint_keywords() {
882 let compiled = compile_schema(
883 "AWS::Test::T",
884 &json!({
885 "properties": {
886 "Exact": { "enum": ["a", "b"] },
887 "Insensitive": { "enumCaseInsensitive": ["a", "b"] },
888 "Excluded": { "not": { "enum": ["bad"] } },
889 "Fixed": { "const": 7 }
890 },
891 "dependentRequired": { "A": ["B"] },
892 "dependentExcluded": { "C": ["D"] },
893 "requiredOr": ["A", "B"],
894 "requiredXor": ["C", "D"]
895 }),
896 );
897 assert_eq!(compiled.properties["Exact"].enum_values, vec![json!("a"), json!("b")]);
898 assert_eq!(compiled.properties["Insensitive"].enum_case_insensitive, vec![json!("a"), json!("b")]);
899 assert_eq!(compiled.properties["Excluded"].not_enum, vec![json!("bad")]);
900 assert_eq!(compiled.properties["Fixed"].const_value, Some(json!(7)));
901 assert_eq!(compiled.dependent_required.get("A"), Some(&vec!["B".to_string()]));
902 assert_eq!(compiled.dependent_excluded.get("C"), Some(&vec!["D".to_string()]));
903 assert_eq!(compiled.required_or, vec!["A".to_string(), "B".to_string()]);
904 assert_eq!(compiled.required_xor, vec!["C".to_string(), "D".to_string()]);
905 }
906
907 #[test]
908 fn compiles_nested_properties_items_and_pattern_properties() {
909 let compiled = compile_schema(
910 "AWS::Test::T",
911 &json!({
912 "properties": {
913 "Cfg": { "type": "object", "properties": { "Inner": { "type": "string" } } },
914 "Arr": { "type": "array", "items": { "type": "string", "maxLength": 3 } },
915 "Map": { "type": "object", "patternProperties": { "^k$": { "type": "integer" } } }
916 }
917 }),
918 );
919 assert!(compiled.properties["Cfg"].properties["Inner"].prop_type.is_some(), "nested type is compiled");
920 assert_eq!(compiled.properties["Arr"].items.as_ref().expect("items").max_length, Some(3));
921 assert!(compiled.properties["Map"].pattern_properties.contains_key("^k$"));
922 }
923
924 #[test]
927 fn compile_sub_compiles_all_prop_schema_fields() {
928 let sub = compile_sub(
931 &json!({
932 "required": ["A"],
933 "properties": { "P": { "type": "string" } },
934 "additionalProperties": false,
935 "dependentRequired": { "A": ["B"] },
936 "dependentExcluded": { "C": ["D"] },
937 "type": "object",
938 "enum": ["x"],
939 "pattern": "^a",
940 "minimum": 1.0,
941 "multipleOf": 5.0,
942 "minLength": 2,
943 "anyOf": [{ "required": ["Z"] }]
944 }),
945 RefSiblings::Enforce,
946 );
947 assert_eq!(sub.required, vec!["A".to_string()]);
948 assert!(sub.properties.contains_key("P"));
949 assert_eq!(sub.additional_properties, Some(false));
950 assert_eq!(sub.dependent_required.get("A"), Some(&vec!["B".to_string()]));
951 assert_eq!(sub.dependent_excluded.get("C"), Some(&vec!["D".to_string()]));
952 assert!(sub.prop_type.is_some());
953 assert_eq!(sub.enum_values, vec![json!("x")]);
954 assert_eq!(sub.pattern.as_deref(), Some("^a"));
955 assert_eq!(sub.minimum, Some(1.0));
956 assert_eq!(sub.multiple_of, Some(5.0));
957 assert_eq!(sub.min_length, Some(2));
958 assert_eq!(sub.any_of.len(), 1);
959 }
960
961 #[test]
962 fn compile_condition_schema_fields_matches_behavior() {
963 let cond = compile_condition_schema(
966 &json!({
967 "properties": { "A": { "enum": ["x"] } },
968 "required": ["A"],
969 "type": "object",
970 "anyOf": [{ "properties": { "B": { "enum": ["y"] } } }]
971 }),
972 RefSiblings::Enforce,
973 );
974 assert!(cond.properties.contains_key("A"));
975 assert_eq!(cond.required, vec!["A".to_string()]);
976 assert_eq!(cond.any_of.len(), 1);
977 assert!(cond.prop_type.is_some(), "the condition's type must be compiled");
978 assert_eq!(
979 keywords::COMPILE_CONDITION_SCHEMA_FIELDS,
980 &[keywords::PROPERTIES, keywords::REQUIRED, keywords::TYPE, keywords::ANY_OF]
981 );
982 }
983
984 #[test]
985 fn numeric_constraints_compile_into_option_f64_fields() {
986 let compiled = compile_schema(
987 "AWS::Test::T",
988 &json!({
989 "properties": {
990 "P": {
991 "type": "number",
992 "minimum": 1.5,
993 "maximum": 99.9,
994 "exclusiveMinimum": 0.1,
995 "exclusiveMaximum": 100.0
996 }
997 }
998 }),
999 );
1000 let p = &compiled.properties["P"];
1001 assert_eq!(p.minimum, Some(1.5));
1002 assert_eq!(p.maximum, Some(99.9));
1003 assert_eq!(p.exclusive_minimum, Some(0.1));
1004 assert_eq!(p.exclusive_maximum, Some(100.0));
1005 }
1006
1007 #[test]
1008 fn u64_constraints_compile_into_option_u64_fields() {
1009 let compiled = compile_schema(
1010 "AWS::Test::T",
1011 &json!({
1012 "properties": {
1013 "P": {
1014 "type": "string",
1015 "minLength": 1,
1016 "maxLength": 256
1017 },
1018 "A": {
1019 "type": "array",
1020 "minItems": 0,
1021 "maxItems": 10
1022 },
1023 "O": {
1024 "type": "object",
1025 "minProperties": 1,
1026 "maxProperties": 5
1027 }
1028 }
1029 }),
1030 );
1031 assert_eq!(compiled.properties["P"].min_length, Some(1));
1032 assert_eq!(compiled.properties["P"].max_length, Some(256));
1033 assert_eq!(compiled.properties["A"].min_items, Some(0));
1034 assert_eq!(compiled.properties["A"].max_items, Some(10));
1035 assert_eq!(compiled.properties["O"].min_properties, Some(1));
1036 assert_eq!(compiled.properties["O"].max_properties, Some(5));
1037 }
1038
1039 #[test]
1040 fn string_constraints_compile_into_option_string_fields() {
1041 let compiled = compile_schema(
1042 "AWS::Test::T",
1043 &json!({
1044 "description": "resource documentation",
1045 "properties": {
1046 "P": { "type": "string", "pattern": "^x$", "format": "uri", "description": "desc" }
1047 },
1048 "replacementStrategy": "delete_then_create",
1049 "documentationUrl": "https://docs",
1050 "sourceUrl": "https://src"
1051 }),
1052 );
1053 let p = &compiled.properties["P"];
1054 assert_eq!(p.pattern.as_deref(), Some("^x$"));
1055 assert_eq!(p.format.as_deref(), Some("uri"));
1056 assert_eq!(compiled.replacement_strategy.as_deref(), Some("delete_then_create"));
1057 assert_eq!(compiled.documentation_url.as_deref(), Some("https://docs"));
1058 assert_eq!(compiled.source_url.as_deref(), Some("https://src"));
1059 let serialized = serde_json::to_string(&compiled).expect("compiled schema serializes");
1062 assert!(!serialized.contains("description"), "description must not reach the compiled schema: {serialized}");
1063 }
1064
1065 #[test]
1066 fn metadata_pointer_arrays_all_strip_properties_prefix() {
1067 let compiled = compile_schema(
1070 "AWS::Test::T",
1071 &json!({
1072 "readOnlyProperties": ["/properties/A"],
1073 "writeOnlyProperties": ["/properties/B"],
1074 "createOnlyProperties": ["/properties/C"],
1075 "deprecatedProperties": ["/properties/D"],
1076 "conditionalCreateOnlyProperties": ["/properties/E"],
1077 "primaryIdentifier": ["/properties/F"]
1078 }),
1079 );
1080 assert_eq!(compiled.read_only_properties, vec!["A"]);
1081 assert_eq!(compiled.write_only_properties, vec!["B"]);
1082 assert_eq!(compiled.create_only_properties, vec!["C"]);
1083 assert_eq!(compiled.deprecated_properties, vec!["D"]);
1084 assert_eq!(compiled.conditional_create_only_properties, vec!["E"]);
1085 assert_eq!(compiled.primary_identifier, vec!["F"]);
1086 }
1087
1088 #[test]
1089 fn definitions_ref_prefix_is_used_in_compile_prop() {
1090 let compiled = compile_schema(
1091 "AWS::Test::T",
1092 &json!({
1093 "properties": { "P": { "$ref": "#/definitions/D" } },
1094 "definitions": { "D": { "type": "object" } }
1095 }),
1096 );
1097 assert_eq!(compiled.properties["P"].ref_name.as_deref(), Some("D"));
1098 }
1099
1100 #[test]
1101 fn schema_maps_slice_matches_recursive_compilation() {
1102 let compiled = compile_schema(
1104 "AWS::Test::T",
1105 &json!({
1106 "properties": { "P": { "type": "string" } },
1107 "definitions": { "D": { "type": "object" } }
1108 }),
1109 );
1110 assert!(compiled.properties.contains_key("P"));
1111 assert!(compiled.definitions.contains_key("D"));
1112 let compiled_prop = compile_schema(
1114 "AWS::Test::T",
1115 &json!({
1116 "properties": {
1117 "Map": { "type": "object", "patternProperties": { "^k$": { "type": "string" } } }
1118 }
1119 }),
1120 );
1121 assert!(compiled_prop.properties["Map"].pattern_properties.contains_key("^k$"));
1122 }
1123
1124 #[test]
1125 fn ref_siblings_enforce_keeps_constraints_beside_a_ref() {
1126 let compiled = compile_schema_with(
1127 "AWS::Test::T",
1128 &json!({
1129 "properties": { "P": { "$ref": "#/definitions/D", "maxLength": 3, "pattern": "^a" } },
1130 "definitions": { "D": { "type": "string" } }
1131 }),
1132 RefSiblings::Enforce,
1133 );
1134 let prop = &compiled.properties["P"];
1135 assert_eq!(prop.ref_name.as_deref(), Some("D"));
1136 assert_eq!(prop.max_length, Some(3), "Enforce must keep constraint siblings");
1137 assert_eq!(prop.pattern.as_deref(), Some("^a"));
1138 }
1139
1140 #[test]
1141 fn ref_siblings_ignore_drops_everything_beside_a_ref() {
1142 let compiled = compile_schema_with(
1147 "AWS::Test::T",
1148 &json!({
1149 "properties": {
1150 "P": { "$ref": "#/definitions/D", "maxLength": 3, "pattern": "^a" },
1151 "Q": { "$ref": "not-a-definitions-ref", "maxLength": 3 }
1152 },
1153 "definitions": { "D": { "type": "string" } }
1154 }),
1155 RefSiblings::Ignore,
1156 );
1157 let prop = &compiled.properties["P"];
1158 assert_eq!(prop.ref_name.as_deref(), Some("D"));
1159 assert_eq!(prop.max_length, None, "Ignore must drop constraint siblings");
1160 assert_eq!(prop.pattern, None);
1161 let unresolvable = &compiled.properties["Q"];
1162 assert_eq!(unresolvable.ref_name, None);
1163 assert_eq!(
1164 unresolvable.max_length, None,
1165 "a $ref outside definitions compiles to an unconstrained property under Ignore, matching the \
1166 historical bundled artifact"
1167 );
1168 }
1169
1170 #[test]
1171 fn condition_schema_type_is_compiled() {
1172 let cond = compile_condition_schema(&json!({ "type": "object", "required": ["A"] }), RefSiblings::Enforce);
1173 match cond.prop_type.as_ref().expect("the condition's type must be compiled") {
1174 PropType::Single(name) => assert_eq!(name, "object"),
1175 PropType::Multi(names) => panic!("expected a single type, got {names:?}"),
1176 }
1177 }
1178}