use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
pub mod keywords {
pub const DEFINITIONS_REF_PREFIX: &str = "#/definitions/";
pub const PROPERTIES_PATH_PREFIX: &str = "/properties/";
pub const REF: &str = "$ref";
pub const PROPERTIES: &str = "properties";
pub const DEFINITIONS: &str = "definitions";
pub const PATTERN_PROPERTIES: &str = "patternProperties";
pub const SCHEMA_MAPS: &[&str] = &[PROPERTIES, DEFINITIONS, PATTERN_PROPERTIES];
pub const ALL_OF: &str = "allOf";
pub const ANY_OF: &str = "anyOf";
pub const ONE_OF: &str = "oneOf";
pub const COMPOSITION: &[&str] = &[ALL_OF, ANY_OF, ONE_OF];
pub const IF: &str = "if";
pub const THEN: &str = "then";
pub const ELSE: &str = "else";
pub const CONDITIONALS: &[&str] = &[IF, THEN, ELSE];
pub const READ_ONLY_PROPERTIES: &str = "readOnlyProperties";
pub const WRITE_ONLY_PROPERTIES: &str = "writeOnlyProperties";
pub const CREATE_ONLY_PROPERTIES: &str = "createOnlyProperties";
pub const DEPRECATED_PROPERTIES: &str = "deprecatedProperties";
pub const CONDITIONAL_CREATE_ONLY_PROPERTIES: &str = "conditionalCreateOnlyProperties";
pub const PRIMARY_IDENTIFIER: &str = "primaryIdentifier";
pub const METADATA_POINTER_ARRAYS: &[&str] = &[
READ_ONLY_PROPERTIES,
WRITE_ONLY_PROPERTIES,
CREATE_ONLY_PROPERTIES,
DEPRECATED_PROPERTIES,
CONDITIONAL_CREATE_ONLY_PROPERTIES,
PRIMARY_IDENTIFIER,
];
pub const REQUIRED: &str = "required";
pub const REQUIRED_OR: &str = "requiredOr";
pub const REQUIRED_XOR: &str = "requiredXor";
pub const STRING_ARRAY_CONSTRAINTS: &[&str] = &[REQUIRED, REQUIRED_OR, REQUIRED_XOR];
pub const DEPENDENT_REQUIRED: &str = "dependentRequired";
pub const DEPENDENT_EXCLUDED: &str = "dependentExcluded";
pub const DEPENDENCY_MAPS: &[&str] = &[DEPENDENT_REQUIRED, DEPENDENT_EXCLUDED];
pub const ENUM: &str = "enum";
pub const ENUM_CASE_INSENSITIVE: &str = "enumCaseInsensitive";
pub const NOT: &str = "not";
pub const ENUM_KEYWORDS: &[&str] = &[ENUM, ENUM_CASE_INSENSITIVE];
pub const MINIMUM: &str = "minimum";
pub const MAXIMUM: &str = "maximum";
pub const EXCLUSIVE_MINIMUM: &str = "exclusiveMinimum";
pub const EXCLUSIVE_MAXIMUM: &str = "exclusiveMaximum";
pub const NUMERIC_CONSTRAINTS: &[&str] = &[MINIMUM, MAXIMUM, EXCLUSIVE_MINIMUM, EXCLUSIVE_MAXIMUM];
pub const MIN_LENGTH: &str = "minLength";
pub const MAX_LENGTH: &str = "maxLength";
pub const MIN_ITEMS: &str = "minItems";
pub const MAX_ITEMS: &str = "maxItems";
pub const MIN_PROPERTIES: &str = "minProperties";
pub const MAX_PROPERTIES: &str = "maxProperties";
pub const U64_CONSTRAINTS: &[&str] =
&[MIN_LENGTH, MAX_LENGTH, MIN_ITEMS, MAX_ITEMS, MIN_PROPERTIES, MAX_PROPERTIES];
pub const ADDITIONAL_PROPERTIES: &str = "additionalProperties";
pub const UNIQUE_ITEMS: &str = "uniqueItems";
pub const BOOL_CONSTRAINTS: &[&str] = &[ADDITIONAL_PROPERTIES, UNIQUE_ITEMS];
pub const PATTERN: &str = "pattern";
pub const FORMAT: &str = "format";
pub const DESCRIPTION: &str = "description";
pub const REPLACEMENT_STRATEGY: &str = "replacementStrategy";
pub const DOCUMENTATION_URL: &str = "documentationUrl";
pub const SOURCE_URL: &str = "sourceUrl";
pub const STRING_CONSTRAINTS: &[&str] =
&[PATTERN, FORMAT, DESCRIPTION, REPLACEMENT_STRATEGY, DOCUMENTATION_URL, SOURCE_URL];
pub const TYPE: &str = "type";
pub const CONST: &str = "const";
pub const ITEMS: &str = "items";
pub const TYPE_NAME: &str = "typeName";
pub const UNREPRESENTED_VALIDATION_KEYWORDS: &[&str] =
&["propertyNames", "contains", "minContains", "maxContains", "additionalItems", "unevaluatedProperties"];
pub const MULTIPLE_OF: &str = "multipleOf";
pub const DEPENDENCIES: &str = "dependencies";
pub const RELATIONSHIP_REF: &str = "relationshipRef";
pub const REF_ANNOTATION_KEYWORDS: &[&str] = &[
"description",
"markdownDescription",
"title",
"examples",
"default",
"$comment",
"insertionOrder",
RELATIONSHIP_REF,
];
pub const COMPILE_SUB_READS_ALL_PROP_FIELDS: bool = true;
pub const COMPILE_CONDITION_SCHEMA_FIELDS: &[&str] = &[PROPERTIES, REQUIRED, TYPE, ANY_OF];
}
#[derive(Serialize, Deserialize)]
pub struct CompiledSchema {
pub type_name: String,
#[serde(default)]
pub properties: BTreeMap<String, PropSchema>,
#[serde(default)]
pub definitions: BTreeMap<String, PropSchema>,
#[serde(default)]
pub required: Vec<String>,
#[serde(default)]
pub additional_properties: Option<bool>,
#[serde(default)]
pub read_only_properties: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub write_only_properties: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub create_only_properties: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub deprecated_properties: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub conditional_create_only_properties: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub primary_identifier: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub replacement_strategy: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub documentation_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub all_of: Vec<SubSchema>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub any_of: Vec<SubSchema>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub one_of: Vec<SubSchema>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub if_then_else: Vec<IfThenElse>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub dependent_required: BTreeMap<String, Vec<String>>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub dependent_excluded: BTreeMap<String, Vec<String>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub required_or: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub required_xor: Vec<String>,
}
#[derive(Serialize, Deserialize, Default)]
pub struct IfThenElse {
pub condition: ConditionSchema,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub then_schema: Option<SubSchema>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub else_schema: Option<SubSchema>,
}
#[derive(Serialize, Deserialize, Default)]
pub struct ConditionSchema {
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub properties: BTreeMap<String, PropSchema>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub required: Vec<String>,
#[serde(default, rename = "type", skip_serializing_if = "Option::is_none")]
pub prop_type: Option<PropType>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub any_of: Vec<ConditionSchema>,
}
#[derive(Serialize, Deserialize, Default)]
pub struct PropSchema {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ref_name: Option<String>,
#[serde(default, rename = "type", skip_serializing_if = "Option::is_none")]
pub prop_type: Option<PropType>,
#[serde(default, rename = "enum", skip_serializing_if = "Vec::is_empty")]
pub enum_values: Vec<serde_json::Value>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub enum_case_insensitive: Vec<serde_json::Value>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub not_enum: Vec<serde_json::Value>,
#[serde(default, rename = "const", skip_serializing_if = "Option::is_none")]
pub const_value: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pattern: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub minimum: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub maximum: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub exclusive_minimum: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub exclusive_maximum: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub multiple_of: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min_length: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_length: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min_items: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_items: Option<u64>,
#[serde(default, skip_serializing_if = "skip_unless_true")]
pub unique_items: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min_properties: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_properties: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub format: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub properties: BTreeMap<String, PropSchema>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub required: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub additional_properties: Option<bool>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub pattern_properties: BTreeMap<String, PropSchema>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub items: Option<Box<PropSchema>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub all_of: Vec<SubSchema>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub any_of: Vec<SubSchema>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub one_of: Vec<SubSchema>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub if_then_else: Vec<IfThenElse>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub dependent_required: BTreeMap<String, Vec<String>>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub dependent_excluded: BTreeMap<String, Vec<String>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub required_or: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub required_xor: Vec<String>,
}
fn skip_unless_true(value: &Option<bool>) -> bool {
*value != Some(true)
}
pub type SubSchema = PropSchema;
#[derive(Serialize, Deserialize)]
#[serde(untagged)]
pub enum PropType {
Single(String),
Multi(Vec<String>),
}
fn convert_property_paths(raw: &serde_json::Value) -> Vec<String> {
raw.as_array()
.map(|arr| {
arr.iter()
.filter_map(|v| {
v.as_str()
.and_then(|s| s.strip_prefix(keywords::PROPERTIES_PATH_PREFIX))
.map(|s| s.replace('/', "."))
})
.collect()
})
.unwrap_or_default()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RefSiblings {
Enforce,
Ignore,
}
pub fn compile_schema(type_name: &str, raw: &serde_json::Value) -> CompiledSchema {
compile_schema_with(type_name, raw, RefSiblings::Enforce)
}
pub fn compile_schema_with(type_name: &str, raw: &serde_json::Value, ref_siblings: RefSiblings) -> CompiledSchema {
let mut defs = BTreeMap::new();
if let Some(d) = raw.get(keywords::DEFINITIONS).and_then(|v| v.as_object()) {
for (k, v) in d {
defs.insert(k.clone(), compile_prop_with(v, ref_siblings));
}
}
let mut props = BTreeMap::new();
if let Some(p) = raw.get(keywords::PROPERTIES).and_then(|v| v.as_object()) {
for (k, v) in p {
props.insert(k.clone(), compile_prop_with(v, ref_siblings));
}
}
let mut all_of = Vec::new();
let mut if_then_else = Vec::new();
if let Some(arr) = raw.get(keywords::ALL_OF).and_then(|v| v.as_array()) {
for item in arr {
if item.get(keywords::IF).is_some() {
if let Some(ite) = compile_if_then_else(item, ref_siblings) {
if_then_else.push(ite);
}
} else {
all_of.push(compile_sub(item, ref_siblings));
}
}
}
CompiledSchema {
type_name: type_name.to_string(),
properties: props,
definitions: defs,
required: str_arr(raw.get(keywords::REQUIRED)),
additional_properties: raw.get(keywords::ADDITIONAL_PROPERTIES).and_then(|v| v.as_bool()),
read_only_properties: convert_property_paths(
raw.get(keywords::READ_ONLY_PROPERTIES).unwrap_or(&serde_json::Value::Null),
),
write_only_properties: convert_property_paths(
raw.get(keywords::WRITE_ONLY_PROPERTIES).unwrap_or(&serde_json::Value::Null),
),
create_only_properties: convert_property_paths(
raw.get(keywords::CREATE_ONLY_PROPERTIES).unwrap_or(&serde_json::Value::Null),
),
deprecated_properties: convert_property_paths(
raw.get(keywords::DEPRECATED_PROPERTIES).unwrap_or(&serde_json::Value::Null),
),
conditional_create_only_properties: convert_property_paths(
raw.get(keywords::CONDITIONAL_CREATE_ONLY_PROPERTIES).unwrap_or(&serde_json::Value::Null),
),
primary_identifier: convert_property_paths(
raw.get(keywords::PRIMARY_IDENTIFIER).unwrap_or(&serde_json::Value::Null),
),
replacement_strategy: raw
.get(keywords::REPLACEMENT_STRATEGY)
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.map(String::from),
documentation_url: raw
.get(keywords::DOCUMENTATION_URL)
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.map(String::from),
source_url: raw.get(keywords::SOURCE_URL).and_then(|v| v.as_str()).filter(|s| !s.is_empty()).map(String::from),
description: raw
.get(keywords::DESCRIPTION)
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.map(String::from),
all_of,
any_of: compile_subs(raw.get(keywords::ANY_OF), ref_siblings),
one_of: compile_subs(raw.get(keywords::ONE_OF), ref_siblings),
if_then_else,
dependent_required: compile_dependent_required(raw),
dependent_excluded: str_map(raw.get(keywords::DEPENDENT_EXCLUDED)),
required_or: str_arr(raw.get(keywords::REQUIRED_OR)),
required_xor: str_arr(raw.get(keywords::REQUIRED_XOR)),
}
}
fn compile_dependent_required(raw: &serde_json::Value) -> BTreeMap<String, Vec<String>> {
let mut dep_req = str_map(raw.get(keywords::DEPENDENT_REQUIRED));
if let Some(deps_obj) = raw.get(keywords::DEPENDENCIES).and_then(|v| v.as_object()) {
for (trigger, value) in deps_obj {
if let Some(arr) = value.as_array() {
let names: Vec<String> = arr.iter().filter_map(|v| v.as_str().map(String::from)).collect();
if !names.is_empty() {
dep_req.entry(trigger.clone()).or_default().extend(names);
}
}
}
}
for deps in dep_req.values_mut() {
deps.sort();
deps.dedup();
}
dep_req
}
fn compile_if_then_else(raw: &serde_json::Value, ref_siblings: RefSiblings) -> Option<IfThenElse> {
let if_val = raw.get(keywords::IF)?;
let condition = compile_condition_schema(if_val, ref_siblings);
let then_schema = raw.get(keywords::THEN).map(|branch| compile_sub(branch, ref_siblings));
let else_schema = raw.get(keywords::ELSE).map(|branch| compile_sub(branch, ref_siblings));
if then_schema.is_none() && else_schema.is_none() {
return None;
}
Some(IfThenElse { condition, then_schema, else_schema })
}
fn compile_condition_schema(raw: &serde_json::Value, ref_siblings: RefSiblings) -> ConditionSchema {
let obj = match raw.as_object() {
Some(o) => o,
None => return ConditionSchema::default(),
};
let mut props = BTreeMap::new();
if let Some(p) = obj.get(keywords::PROPERTIES).and_then(|v| v.as_object()) {
for (k, v) in p {
props.insert(k.clone(), compile_prop_with(v, ref_siblings));
}
}
let any_of = obj
.get(keywords::ANY_OF)
.and_then(|v| v.as_array())
.map(|arr| arr.iter().map(|entry| compile_condition_schema(entry, ref_siblings)).collect())
.unwrap_or_default();
let prop_type = obj.get(keywords::TYPE).map(compile_prop_type);
ConditionSchema { properties: props, required: str_arr(obj.get(keywords::REQUIRED)), prop_type, any_of }
}
fn compile_prop_type(raw: &serde_json::Value) -> PropType {
match raw {
serde_json::Value::String(s) => PropType::Single(s.clone()),
serde_json::Value::Array(a) => PropType::Multi(a.iter().filter_map(|v| v.as_str().map(String::from)).collect()),
_ => PropType::Single("string".into()),
}
}
fn compile_prop_with(raw: &serde_json::Value, ref_siblings: RefSiblings) -> PropSchema {
let obj = match raw.as_object() {
Some(o) => o,
None => return PropSchema::default(),
};
let raw_ref = obj.get(keywords::REF).and_then(|v| v.as_str());
let ref_name = raw_ref.and_then(|ref_str| ref_str.strip_prefix(keywords::DEFINITIONS_REF_PREFIX).map(String::from));
if raw_ref.is_some() && ref_siblings == RefSiblings::Ignore {
return PropSchema { ref_name, ..Default::default() };
}
if ref_name.is_some() {
let has_constraint_siblings = obj.keys().any(|key| {
key != keywords::REF
&& !keywords::REF_ANNOTATION_KEYWORDS.contains(&key.as_str())
&& key != keywords::RELATIONSHIP_REF
});
if !has_constraint_siblings {
return PropSchema { ref_name, ..Default::default() };
}
}
let prop_type = obj.get(keywords::TYPE).map(compile_prop_type);
let mut sub_props = BTreeMap::new();
if let Some(p) = obj.get(keywords::PROPERTIES).and_then(|v| v.as_object()) {
for (k, v) in p {
sub_props.insert(k.clone(), compile_prop_with(v, ref_siblings));
}
}
let mut pat_props = BTreeMap::new();
if let Some(p) = obj.get(keywords::PATTERN_PROPERTIES).and_then(|v| v.as_object()) {
for (k, v) in p {
pat_props.insert(k.clone(), compile_prop_with(v, ref_siblings));
}
}
let items = obj.get(keywords::ITEMS).map(|v| Box::new(compile_prop_with(v, ref_siblings)));
let mut dep_req = str_map(obj.get(keywords::DEPENDENT_REQUIRED).cloned().as_ref());
if let Some(deps_obj) = obj.get(keywords::DEPENDENCIES).and_then(|v| v.as_object()) {
for (trigger, value) in deps_obj {
if let Some(arr) = value.as_array() {
let names: Vec<String> = arr.iter().filter_map(|v| v.as_str().map(String::from)).collect();
if !names.is_empty() {
dep_req.entry(trigger.clone()).or_default().extend(names);
}
}
}
}
for deps in dep_req.values_mut() {
deps.sort();
deps.dedup();
}
let mut if_then_else = Vec::new();
if obj.get(keywords::IF).is_some()
&& let Some(ite) = compile_if_then_else(raw, ref_siblings)
{
if_then_else.push(ite);
}
let mut all_of_branches = Vec::new();
if let Some(arr) = obj.get(keywords::ALL_OF).and_then(|v| v.as_array()) {
for item in arr {
if item.get(keywords::IF).is_some() {
if let Some(ite) = compile_if_then_else(item, ref_siblings) {
if_then_else.push(ite);
}
} else {
all_of_branches.push(compile_sub(item, ref_siblings));
}
}
}
PropSchema {
ref_name,
prop_type,
enum_values: obj.get(keywords::ENUM).and_then(|v| v.as_array()).cloned().unwrap_or_default(),
enum_case_insensitive: obj
.get(keywords::ENUM_CASE_INSENSITIVE)
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default(),
not_enum: obj
.get(keywords::NOT)
.and_then(|v| v.get(keywords::ENUM))
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default(),
const_value: obj.get(keywords::CONST).cloned(),
pattern: obj.get(keywords::PATTERN).and_then(|v| v.as_str()).map(String::from),
minimum: obj.get(keywords::MINIMUM).and_then(|v| v.as_f64()),
maximum: obj.get(keywords::MAXIMUM).and_then(|v| v.as_f64()),
exclusive_minimum: obj.get(keywords::EXCLUSIVE_MINIMUM).and_then(|v| v.as_f64()),
exclusive_maximum: obj.get(keywords::EXCLUSIVE_MAXIMUM).and_then(|v| v.as_f64()),
multiple_of: obj.get(keywords::MULTIPLE_OF).and_then(|v| v.as_f64()),
min_length: obj.get(keywords::MIN_LENGTH).and_then(|v| v.as_u64()),
max_length: obj.get(keywords::MAX_LENGTH).and_then(|v| v.as_u64()),
min_items: obj.get(keywords::MIN_ITEMS).and_then(|v| v.as_u64()),
max_items: obj.get(keywords::MAX_ITEMS).and_then(|v| v.as_u64()),
unique_items: obj.get(keywords::UNIQUE_ITEMS).and_then(|v| v.as_bool()),
min_properties: obj.get(keywords::MIN_PROPERTIES).and_then(|v| v.as_u64()),
max_properties: obj.get(keywords::MAX_PROPERTIES).and_then(|v| v.as_u64()),
format: obj.get(keywords::FORMAT).and_then(|v| v.as_str()).map(String::from),
description: obj
.get(keywords::DESCRIPTION)
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.map(String::from),
properties: sub_props,
required: str_arr(obj.get(keywords::REQUIRED).cloned().as_ref()),
additional_properties: obj.get(keywords::ADDITIONAL_PROPERTIES).and_then(|v| v.as_bool()),
pattern_properties: pat_props,
items,
all_of: all_of_branches,
any_of: compile_subs(obj.get(keywords::ANY_OF).cloned().as_ref(), ref_siblings),
one_of: compile_subs(obj.get(keywords::ONE_OF).cloned().as_ref(), ref_siblings),
if_then_else,
dependent_required: dep_req,
dependent_excluded: str_map(obj.get(keywords::DEPENDENT_EXCLUDED).cloned().as_ref()),
required_or: str_arr(obj.get(keywords::REQUIRED_OR).cloned().as_ref()),
required_xor: str_arr(obj.get(keywords::REQUIRED_XOR).cloned().as_ref()),
}
}
fn compile_sub(raw: &serde_json::Value, ref_siblings: RefSiblings) -> SubSchema {
compile_prop_with(raw, ref_siblings)
}
fn compile_subs(val: Option<&serde_json::Value>, ref_siblings: RefSiblings) -> Vec<SubSchema> {
let Some(arr) = val.and_then(|v| v.as_array()) else {
return Vec::new();
};
arr.iter().map(|entry| compile_sub(entry, ref_siblings)).collect()
}
fn str_arr(val: Option<&serde_json::Value>) -> Vec<String> {
val.and_then(|v| v.as_array())
.map(|a| a.iter().filter_map(|v| v.as_str().map(String::from)).collect())
.unwrap_or_default()
}
fn str_map(val: Option<&serde_json::Value>) -> BTreeMap<String, Vec<String>> {
val.and_then(|v| v.as_object())
.map(|m| m.iter().map(|(k, v)| (k.clone(), str_arr(Some(v)))).collect())
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn compiles_properties_definitions_and_refs() {
let compiled = compile_schema(
"AWS::Test::T",
&json!({
"properties": {
"Name": { "type": "string", "pattern": "^a", "minLength": 1 },
"Cfg": { "$ref": "#/definitions/Config" },
"Kinds": { "type": ["string", "null"] }
},
"definitions": {
"Config": {
"type": "object",
"required": ["Inner"],
"requiredOr": ["First", "Second"],
"requiredXor": ["Left", "Right"]
}
},
"required": ["Name"],
"additionalProperties": false
}),
);
assert_eq!(compiled.type_name, "AWS::Test::T");
assert_eq!(compiled.required, vec!["Name".to_string()]);
assert_eq!(compiled.additional_properties, Some(false));
assert_eq!(compiled.properties["Cfg"].ref_name.as_deref(), Some("Config"));
assert_eq!(compiled.definitions["Config"].required, vec!["Inner".to_string()]);
assert_eq!(compiled.definitions["Config"].required_or, vec!["First".to_string(), "Second".to_string()]);
assert_eq!(compiled.definitions["Config"].required_xor, vec!["Left".to_string(), "Right".to_string()]);
assert_eq!(compiled.properties["Name"].pattern.as_deref(), Some("^a"));
assert_eq!(compiled.properties["Name"].min_length, Some(1));
match compiled.properties["Kinds"].prop_type.as_ref().expect("a multi type is compiled") {
PropType::Multi(names) => assert_eq!(names, &vec!["string".to_string(), "null".to_string()]),
PropType::Single(name) => panic!("expected a multi type, got {name}"),
}
}
#[test]
fn converts_property_pointer_paths_to_dot_notation() {
let compiled = compile_schema(
"AWS::Test::T",
&json!({
"readOnlyProperties": ["/properties/Arn", "/properties/Nested/Id"],
"writeOnlyProperties": ["/properties/Secret"],
"createOnlyProperties": ["/properties/Name"],
"deprecatedProperties": ["/properties/Old"],
"conditionalCreateOnlyProperties": ["/properties/Maybe"],
"primaryIdentifier": ["/properties/Name"]
}),
);
assert_eq!(compiled.read_only_properties, vec!["Arn".to_string(), "Nested.Id".to_string()]);
assert_eq!(compiled.write_only_properties, vec!["Secret".to_string()]);
assert_eq!(compiled.create_only_properties, vec!["Name".to_string()]);
assert_eq!(compiled.deprecated_properties, vec!["Old".to_string()]);
assert_eq!(compiled.conditional_create_only_properties, vec!["Maybe".to_string()]);
assert_eq!(compiled.primary_identifier, vec!["Name".to_string()]);
}
#[test]
fn splits_all_of_into_plain_and_conditional_entries() {
let compiled = compile_schema(
"AWS::Test::T",
&json!({
"allOf": [
{ "required": ["Plain"] },
{ "if": { "properties": { "A": { "enum": ["x"] } } }, "then": { "required": ["B"] } },
{ "if": { "properties": { "A": { "enum": ["y"] } } } }
]
}),
);
assert_eq!(compiled.all_of.len(), 1, "plain entries stay in all_of");
assert_eq!(compiled.all_of[0].required, vec!["Plain".to_string()]);
assert_eq!(compiled.if_then_else.len(), 1, "an if with neither then nor else is dropped");
assert_eq!(compiled.if_then_else[0].then_schema.as_ref().expect("then branch").required, vec!["B".to_string()]);
}
#[test]
fn preserves_unique_items_presence() {
let compiled = compile_schema(
"AWS::Test::T",
&json!({
"properties": {
"Strict": { "type": "array", "uniqueItems": true },
"Relaxed": { "type": "array", "uniqueItems": false },
"Silent": { "type": "array" }
}
}),
);
assert_eq!(compiled.properties["Strict"].unique_items, Some(true));
assert_eq!(
compiled.properties["Relaxed"].unique_items,
Some(false),
"an explicit false must be distinguishable from an omitted keyword"
);
assert_eq!(compiled.properties["Silent"].unique_items, None);
}
#[test]
fn unique_items_serialization_only_emits_true() {
let compiled = compile_schema(
"AWS::Test::T",
&json!({
"properties": {
"Strict": { "uniqueItems": true },
"Relaxed": { "uniqueItems": false },
"Silent": {}
}
}),
);
let json = serde_json::to_value(&compiled).expect("a compiled schema serializes");
let properties = json.get("properties").and_then(|v| v.as_object()).expect("properties are serialized");
assert_eq!(properties["Strict"].get("unique_items"), Some(&json!(true)));
assert!(properties["Relaxed"].get("unique_items").is_none(), "explicit false must be omitted");
assert!(properties["Silent"].get("unique_items").is_none(), "an absent keyword must be omitted");
}
#[test]
fn compiles_both_enum_representations_and_constraint_keywords() {
let compiled = compile_schema(
"AWS::Test::T",
&json!({
"properties": {
"Exact": { "enum": ["a", "b"] },
"Insensitive": { "enumCaseInsensitive": ["a", "b"] },
"Excluded": { "not": { "enum": ["bad"] } },
"Fixed": { "const": 7 }
},
"dependentRequired": { "A": ["B"] },
"dependentExcluded": { "C": ["D"] },
"requiredOr": ["A", "B"],
"requiredXor": ["C", "D"]
}),
);
assert_eq!(compiled.properties["Exact"].enum_values, vec![json!("a"), json!("b")]);
assert_eq!(compiled.properties["Insensitive"].enum_case_insensitive, vec![json!("a"), json!("b")]);
assert_eq!(compiled.properties["Excluded"].not_enum, vec![json!("bad")]);
assert_eq!(compiled.properties["Fixed"].const_value, Some(json!(7)));
assert_eq!(compiled.dependent_required.get("A"), Some(&vec!["B".to_string()]));
assert_eq!(compiled.dependent_excluded.get("C"), Some(&vec!["D".to_string()]));
assert_eq!(compiled.required_or, vec!["A".to_string(), "B".to_string()]);
assert_eq!(compiled.required_xor, vec!["C".to_string(), "D".to_string()]);
}
#[test]
fn compiles_nested_properties_items_and_pattern_properties() {
let compiled = compile_schema(
"AWS::Test::T",
&json!({
"properties": {
"Cfg": { "type": "object", "properties": { "Inner": { "type": "string" } } },
"Arr": { "type": "array", "items": { "type": "string", "maxLength": 3 } },
"Map": { "type": "object", "patternProperties": { "^k$": { "type": "integer" } } }
}
}),
);
assert!(compiled.properties["Cfg"].properties["Inner"].prop_type.is_some(), "nested type is compiled");
assert_eq!(compiled.properties["Arr"].items.as_ref().expect("items").max_length, Some(3));
assert!(compiled.properties["Map"].pattern_properties.contains_key("^k$"));
}
#[test]
fn compile_sub_compiles_all_prop_schema_fields() {
let sub = compile_sub(
&json!({
"required": ["A"],
"properties": { "P": { "type": "string" } },
"additionalProperties": false,
"dependentRequired": { "A": ["B"] },
"dependentExcluded": { "C": ["D"] },
"type": "object",
"enum": ["x"],
"pattern": "^a",
"minimum": 1.0,
"multipleOf": 5.0,
"minLength": 2,
"anyOf": [{ "required": ["Z"] }]
}),
RefSiblings::Enforce,
);
assert_eq!(sub.required, vec!["A".to_string()]);
assert!(sub.properties.contains_key("P"));
assert_eq!(sub.additional_properties, Some(false));
assert_eq!(sub.dependent_required.get("A"), Some(&vec!["B".to_string()]));
assert_eq!(sub.dependent_excluded.get("C"), Some(&vec!["D".to_string()]));
assert!(sub.prop_type.is_some());
assert_eq!(sub.enum_values, vec![json!("x")]);
assert_eq!(sub.pattern.as_deref(), Some("^a"));
assert_eq!(sub.minimum, Some(1.0));
assert_eq!(sub.multiple_of, Some(5.0));
assert_eq!(sub.min_length, Some(2));
assert_eq!(sub.any_of.len(), 1);
}
#[test]
fn compile_condition_schema_fields_matches_behavior() {
let cond = compile_condition_schema(
&json!({
"properties": { "A": { "enum": ["x"] } },
"required": ["A"],
"type": "object",
"anyOf": [{ "properties": { "B": { "enum": ["y"] } } }]
}),
RefSiblings::Enforce,
);
assert!(cond.properties.contains_key("A"));
assert_eq!(cond.required, vec!["A".to_string()]);
assert_eq!(cond.any_of.len(), 1);
assert!(cond.prop_type.is_some(), "the condition's type must be compiled");
assert_eq!(
keywords::COMPILE_CONDITION_SCHEMA_FIELDS,
&[keywords::PROPERTIES, keywords::REQUIRED, keywords::TYPE, keywords::ANY_OF]
);
}
#[test]
fn numeric_constraints_compile_into_option_f64_fields() {
let compiled = compile_schema(
"AWS::Test::T",
&json!({
"properties": {
"P": {
"type": "number",
"minimum": 1.5,
"maximum": 99.9,
"exclusiveMinimum": 0.1,
"exclusiveMaximum": 100.0
}
}
}),
);
let p = &compiled.properties["P"];
assert_eq!(p.minimum, Some(1.5));
assert_eq!(p.maximum, Some(99.9));
assert_eq!(p.exclusive_minimum, Some(0.1));
assert_eq!(p.exclusive_maximum, Some(100.0));
}
#[test]
fn u64_constraints_compile_into_option_u64_fields() {
let compiled = compile_schema(
"AWS::Test::T",
&json!({
"properties": {
"P": {
"type": "string",
"minLength": 1,
"maxLength": 256
},
"A": {
"type": "array",
"minItems": 0,
"maxItems": 10
},
"O": {
"type": "object",
"minProperties": 1,
"maxProperties": 5
}
}
}),
);
assert_eq!(compiled.properties["P"].min_length, Some(1));
assert_eq!(compiled.properties["P"].max_length, Some(256));
assert_eq!(compiled.properties["A"].min_items, Some(0));
assert_eq!(compiled.properties["A"].max_items, Some(10));
assert_eq!(compiled.properties["O"].min_properties, Some(1));
assert_eq!(compiled.properties["O"].max_properties, Some(5));
}
#[test]
fn string_constraints_compile_into_option_string_fields() {
let compiled = compile_schema(
"AWS::Test::T",
&json!({
"properties": {
"P": { "type": "string", "pattern": "^x$", "format": "uri", "description": "desc" }
},
"replacementStrategy": "delete_then_create",
"documentationUrl": "https://docs",
"sourceUrl": "https://src"
}),
);
let p = &compiled.properties["P"];
assert_eq!(p.pattern.as_deref(), Some("^x$"));
assert_eq!(p.format.as_deref(), Some("uri"));
assert_eq!(p.description.as_deref(), Some("desc"));
assert_eq!(compiled.replacement_strategy.as_deref(), Some("delete_then_create"));
assert_eq!(compiled.documentation_url.as_deref(), Some("https://docs"));
assert_eq!(compiled.source_url.as_deref(), Some("https://src"));
}
#[test]
fn metadata_pointer_arrays_all_strip_properties_prefix() {
let compiled = compile_schema(
"AWS::Test::T",
&json!({
"readOnlyProperties": ["/properties/A"],
"writeOnlyProperties": ["/properties/B"],
"createOnlyProperties": ["/properties/C"],
"deprecatedProperties": ["/properties/D"],
"conditionalCreateOnlyProperties": ["/properties/E"],
"primaryIdentifier": ["/properties/F"]
}),
);
assert_eq!(compiled.read_only_properties, vec!["A"]);
assert_eq!(compiled.write_only_properties, vec!["B"]);
assert_eq!(compiled.create_only_properties, vec!["C"]);
assert_eq!(compiled.deprecated_properties, vec!["D"]);
assert_eq!(compiled.conditional_create_only_properties, vec!["E"]);
assert_eq!(compiled.primary_identifier, vec!["F"]);
}
#[test]
fn definitions_ref_prefix_is_used_in_compile_prop() {
let compiled = compile_schema(
"AWS::Test::T",
&json!({
"properties": { "P": { "$ref": "#/definitions/D" } },
"definitions": { "D": { "type": "object" } }
}),
);
assert_eq!(compiled.properties["P"].ref_name.as_deref(), Some("D"));
}
#[test]
fn schema_maps_slice_matches_recursive_compilation() {
let compiled = compile_schema(
"AWS::Test::T",
&json!({
"properties": { "P": { "type": "string" } },
"definitions": { "D": { "type": "object" } }
}),
);
assert!(compiled.properties.contains_key("P"));
assert!(compiled.definitions.contains_key("D"));
let compiled_prop = compile_schema(
"AWS::Test::T",
&json!({
"properties": {
"Map": { "type": "object", "patternProperties": { "^k$": { "type": "string" } } }
}
}),
);
assert!(compiled_prop.properties["Map"].pattern_properties.contains_key("^k$"));
}
#[test]
fn ref_siblings_enforce_keeps_constraints_beside_a_ref() {
let compiled = compile_schema_with(
"AWS::Test::T",
&json!({
"properties": { "P": { "$ref": "#/definitions/D", "maxLength": 3, "pattern": "^a" } },
"definitions": { "D": { "type": "string" } }
}),
RefSiblings::Enforce,
);
let prop = &compiled.properties["P"];
assert_eq!(prop.ref_name.as_deref(), Some("D"));
assert_eq!(prop.max_length, Some(3), "Enforce must keep constraint siblings");
assert_eq!(prop.pattern.as_deref(), Some("^a"));
}
#[test]
fn ref_siblings_ignore_drops_everything_beside_a_ref() {
let compiled = compile_schema_with(
"AWS::Test::T",
&json!({
"properties": {
"P": { "$ref": "#/definitions/D", "maxLength": 3, "pattern": "^a" },
"Q": { "$ref": "not-a-definitions-ref", "maxLength": 3 }
},
"definitions": { "D": { "type": "string" } }
}),
RefSiblings::Ignore,
);
let prop = &compiled.properties["P"];
assert_eq!(prop.ref_name.as_deref(), Some("D"));
assert_eq!(prop.max_length, None, "Ignore must drop constraint siblings");
assert_eq!(prop.pattern, None);
let unresolvable = &compiled.properties["Q"];
assert_eq!(unresolvable.ref_name, None);
assert_eq!(
unresolvable.max_length, None,
"a $ref outside definitions compiles to an unconstrained property under Ignore, matching the \
historical bundled artifact"
);
}
#[test]
fn condition_schema_type_is_compiled() {
let cond = compile_condition_schema(&json!({ "type": "object", "required": ["A"] }), RefSiblings::Enforce);
match cond.prop_type.as_ref().expect("the condition's type must be compiled") {
PropType::Single(name) => assert_eq!(name, "object"),
PropType::Multi(names) => panic!("expected a single type, got {names:?}"),
}
}
}