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