use crate::HashMap;
use anyhow::{anyhow, bail, Result};
use derivre::RegexAst;
use indexmap::{IndexMap, IndexSet};
use serde_json::Value;
use std::mem;
use super::context::{Context, Draft, PreContext, ResourceRef};
use super::formats::lookup_format;
use super::numeric::Decimal;
use super::RetrieveWrapper;
const TYPES: [&str; 6] = ["null", "boolean", "number", "string", "array", "object"];
pub(crate) const IMPLEMENTED: [&str; 24] = [
"anyOf",
"oneOf",
"allOf",
"$ref",
"const",
"enum",
"type",
"items",
"additionalItems",
"prefixItems",
"minItems",
"maxItems",
"properties",
"additionalProperties",
"required",
"minLength",
"maxLength",
"pattern",
"format",
"minimum",
"maximum",
"exclusiveMinimum",
"exclusiveMaximum",
"multipleOf",
];
pub(crate) const META_AND_ANNOTATIONS: [&str; 15] = [
"$anchor",
"$defs",
"definitions",
"$schema",
"$id",
"id",
"$comment",
"title",
"description",
"default",
"readOnly",
"writeOnly",
"examples",
"contentMediaType",
"contentEncoding",
];
fn limited_str(node: &Value) -> String {
let s = node.to_string();
if s.len() > 100 {
format!("{}...", &s[..100])
} else {
s
}
}
#[derive(Debug, Clone)]
pub enum Schema {
Any,
Unsatisfiable {
reason: String,
},
Null,
Boolean,
Number {
minimum: Option<f64>,
maximum: Option<f64>,
exclusive_minimum: Option<f64>,
exclusive_maximum: Option<f64>,
multiple_of: Option<Decimal>,
integer: bool,
},
String {
min_length: u64,
max_length: Option<u64>,
regex: Option<RegexAst>,
},
Array {
min_items: u64,
max_items: Option<u64>,
prefix_items: Vec<Schema>,
items: Option<Box<Schema>>,
},
Object {
properties: IndexMap<String, Schema>,
additional_properties: Option<Box<Schema>>,
required: IndexSet<String>,
},
LiteralBool {
value: bool,
},
AnyOf {
options: Vec<Schema>,
},
OneOf {
options: Vec<Schema>,
},
Ref {
uri: String,
},
}
impl Schema {
pub fn false_schema() -> Schema {
Schema::Unsatisfiable {
reason: "schema is false".to_string(),
}
}
fn normalize(self) -> Schema {
match self {
Schema::AnyOf { options } => {
let mut unsats = Vec::new();
let mut valid = Vec::new();
for option in options.into_iter() {
match option {
Schema::Any => {
return Schema::Any;
}
Schema::Unsatisfiable { reason } => {
unsats.push(Schema::Unsatisfiable { reason })
}
Schema::AnyOf { options: nested } => valid.extend(nested),
other => valid.push(other),
}
}
if valid.is_empty() {
if let Some(unsat) = unsats.into_iter().next() {
return unsat;
}
return Schema::Unsatisfiable {
reason: "anyOf is empty".to_string(),
};
}
if valid.len() == 1 {
return valid.swap_remove(0);
}
Schema::AnyOf { options: valid }
}
Schema::OneOf { options } => {
let mut unsats = Vec::new();
let mut valid = Vec::new();
for option in options.into_iter() {
match option {
Schema::Unsatisfiable { reason } => {
unsats.push(Schema::Unsatisfiable { reason })
}
Schema::OneOf { options: nested } => valid.extend(nested),
other => valid.push(other),
}
}
if valid.is_empty() {
if let Some(unsat) = unsats.into_iter().next() {
return unsat;
}
return Schema::Unsatisfiable {
reason: "oneOf is empty".to_string(),
};
}
if valid.len() == 1 {
return valid.swap_remove(0);
}
if valid.iter().enumerate().all(|(i, x)| {
valid
.iter()
.skip(i + 1) .all(|y| x.is_verifiably_disjoint_from(y))
}) {
Schema::AnyOf { options: valid }
} else {
Schema::OneOf { options: valid }
}
}
other_schema => other_schema,
}
}
fn intersect(self, other: Schema, ctx: &Context) -> Result<Schema> {
ctx.increment()?;
let merged = match (self, other) {
(Schema::Any, schema1) => schema1,
(schema0, Schema::Any) => schema0,
(Schema::Unsatisfiable { reason }, _) => Schema::Unsatisfiable { reason },
(_, Schema::Unsatisfiable { reason }) => Schema::Unsatisfiable { reason },
(Schema::Ref { uri }, schema1) => intersect_ref(ctx, &uri, schema1, true)?,
(schema0, Schema::Ref { uri }) => intersect_ref(ctx, &uri, schema0, false)?,
(Schema::OneOf { options }, schema1) => Schema::OneOf {
options: options
.into_iter()
.map(|opt| opt.intersect(schema1.clone(), ctx))
.collect::<Result<Vec<_>>>()?,
},
(schema0, Schema::OneOf { options }) => Schema::OneOf {
options: options
.into_iter()
.map(|opt| schema0.clone().intersect(opt, ctx))
.collect::<Result<Vec<_>>>()?,
},
(Schema::AnyOf { options }, schema1) => Schema::AnyOf {
options: options
.into_iter()
.map(|opt| opt.intersect(schema1.clone(), ctx))
.collect::<Result<Vec<_>>>()?,
},
(schema0, Schema::AnyOf { options }) => Schema::AnyOf {
options: options
.into_iter()
.map(|opt| schema0.clone().intersect(opt, ctx))
.collect::<Result<Vec<_>>>()?,
},
(Schema::Null, Schema::Null) => Schema::Null,
(Schema::Boolean, Schema::Boolean) => Schema::Boolean,
(Schema::Boolean, Schema::LiteralBool { value }) => Schema::LiteralBool { value },
(Schema::LiteralBool { value }, Schema::Boolean) => Schema::LiteralBool { value },
(Schema::LiteralBool { value: value1 }, Schema::LiteralBool { value: value2 }) => {
if value1 == value2 {
Schema::LiteralBool { value: value1 }
} else {
Schema::Unsatisfiable {
reason: "incompatible boolean values".to_string(),
}
}
}
(
Schema::Number {
minimum: min1,
maximum: max1,
exclusive_minimum: emin1,
exclusive_maximum: emax1,
integer: int1,
multiple_of: mult1,
},
Schema::Number {
minimum: min2,
maximum: max2,
exclusive_minimum: emin2,
exclusive_maximum: emax2,
integer: int2,
multiple_of: mult2,
},
) => Schema::Number {
minimum: opt_max(min1, min2),
maximum: opt_min(max1, max2),
exclusive_minimum: opt_max(emin1, emin2),
exclusive_maximum: opt_min(emax1, emax2),
integer: int1 || int2,
multiple_of: match (mult1, mult2) {
(None, None) => None,
(None, Some(mult)) => Some(mult),
(Some(mult), None) => Some(mult),
(Some(mult1), Some(mult2)) => Some(mult1.lcm(&mult2)),
},
},
(
Schema::String {
min_length: min1,
max_length: max1,
regex: r1,
},
Schema::String {
min_length: min2,
max_length: max2,
regex: r2,
},
) => Schema::String {
min_length: min1.max(min2),
max_length: opt_min(max1, max2),
regex: match (r1, r2) {
(None, None) => None,
(None, Some(r)) => Some(r),
(Some(r), None) => Some(r),
(Some(r1), Some(r2)) => Some(RegexAst::And(vec![r1, r2])),
},
},
(
Schema::Array {
min_items: min1,
max_items: max1,
prefix_items: mut prefix1,
items: items1,
},
Schema::Array {
min_items: min2,
max_items: max2,
prefix_items: mut prefix2,
items: items2,
},
) => Schema::Array {
min_items: min1.max(min2),
max_items: opt_min(max1, max2),
prefix_items: {
let len = prefix1.len().max(prefix2.len());
prefix1.resize_with(len, || items1.as_deref().cloned().unwrap_or(Schema::Any));
prefix2.resize_with(len, || items2.as_deref().cloned().unwrap_or(Schema::Any));
prefix1
.into_iter()
.zip(prefix2.into_iter())
.map(|(item1, item2)| item1.intersect(item2, ctx))
.collect::<Result<Vec<_>>>()?
},
items: match (items1, items2) {
(None, None) => None,
(None, Some(item)) => Some(item),
(Some(item), None) => Some(item),
(Some(item1), Some(item2)) => Some(Box::new((*item1).intersect(*item2, ctx)?)),
},
},
(
Schema::Object {
properties: props1,
additional_properties: add1,
required: req1,
},
Schema::Object {
properties: mut props2,
additional_properties: add2,
required: req2,
},
) => {
let mut new_props = IndexMap::new();
for (key, prop1) in props1.into_iter() {
let prop2 = props2
.shift_remove(&key)
.or_else(|| add2.as_deref().cloned())
.unwrap_or(Schema::Any);
new_props.insert(key, prop1.intersect(prop2, ctx)?);
}
for (key, prop2) in props2.into_iter() {
let prop1 = add1.as_deref().cloned().unwrap_or(Schema::Any);
new_props.insert(key, prop1.intersect(prop2, ctx)?);
}
let mut required = req1;
required.extend(req2);
Schema::Object {
properties: new_props,
additional_properties: match (add1, add2) {
(None, None) => None,
(None, Some(add2)) => Some(add2),
(Some(add1), None) => Some(add1),
(Some(add1), Some(add2)) => Some(Box::new((*add1).intersect(*add2, ctx)?)),
},
required,
}
}
_ => Schema::Unsatisfiable {
reason: "incompatible types".to_string(),
},
};
Ok(merged.normalize())
}
fn is_verifiably_disjoint_from(&self, other: &Schema) -> bool {
match (self, other) {
(Schema::Unsatisfiable { .. }, _) => true,
(_, Schema::Unsatisfiable { .. }) => true,
(Schema::Any, _) => false,
(_, Schema::Any) => false,
(Schema::Boolean, Schema::LiteralBool { .. }) => false,
(Schema::LiteralBool { .. }, Schema::Boolean) => false,
(Schema::Ref { .. }, _) => false, (_, Schema::Ref { .. }) => false, (Schema::LiteralBool { value: value1 }, Schema::LiteralBool { value: value2 }) => {
value1 != value2
}
(Schema::AnyOf { options }, _) => options
.iter()
.all(|opt| opt.is_verifiably_disjoint_from(other)),
(_, Schema::AnyOf { options }) => options
.iter()
.all(|opt| self.is_verifiably_disjoint_from(opt)),
(Schema::OneOf { options }, _) => options
.iter()
.all(|opt| opt.is_verifiably_disjoint_from(other)),
(_, Schema::OneOf { options }) => options
.iter()
.all(|opt| self.is_verifiably_disjoint_from(opt)),
(
Schema::String {
regex: Some(RegexAst::Literal(lit1)),
..
},
Schema::String {
regex: Some(RegexAst::Literal(lit2)),
..
},
) => lit1 != lit2,
(
Schema::Object {
properties: props1,
required: req1,
additional_properties: add1,
},
Schema::Object {
properties: props2,
required: req2,
additional_properties: add2,
},
) => req1.union(req2).any(|key| {
let prop1 = props1
.get(key)
.unwrap_or(add1.as_deref().unwrap_or(&Schema::Any));
let prop2 = props2
.get(key)
.unwrap_or(add2.as_deref().unwrap_or(&Schema::Any));
prop1.is_verifiably_disjoint_from(prop2)
}),
_ => {
mem::discriminant(self) != mem::discriminant(other)
}
}
}
}
#[derive(Clone)]
pub struct SchemaBuilderOptions {
pub max_size: usize,
}
impl Default for SchemaBuilderOptions {
fn default() -> Self {
SchemaBuilderOptions { max_size: 50_000 }
}
}
pub fn build_schema(
contents: Value,
retriever: Option<RetrieveWrapper>,
) -> Result<(Schema, HashMap<String, Schema>)> {
if let Some(b) = contents.as_bool() {
if b {
return Ok((Schema::Any, HashMap::default()));
} else {
return Ok((Schema::false_schema(), HashMap::default()));
}
}
let pre_ctx = PreContext::new(contents, retriever)?;
let ctx = Context::new(&pre_ctx)?;
let root_resource = ctx.lookup_resource(&pre_ctx.base_uri)?;
let schema = compile_resource(&ctx, root_resource)?;
Ok((schema, ctx.take_defs()))
}
fn compile_resource(ctx: &Context, resource: ResourceRef) -> Result<Schema> {
let ctx = ctx.in_subresource(resource)?;
compile_contents(&ctx, resource.contents())
}
fn compile_contents(ctx: &Context, contents: &Value) -> Result<Schema> {
compile_contents_inner(ctx, contents).map(|schema| schema.normalize())
}
fn compile_contents_inner(ctx: &Context, contents: &Value) -> Result<Schema> {
if let Some(b) = contents.as_bool() {
if b {
return Ok(Schema::Any);
} else {
return Ok(Schema::false_schema());
}
}
let schemadict = contents
.as_object()
.ok_or_else(|| anyhow!("schema must be an object or boolean"))?;
let schemadict = schemadict
.iter()
.map(|(k, v)| (k.as_str(), v))
.collect::<HashMap<_, _>>();
compile_contents_map(ctx, schemadict)
}
fn only_meta_and_annotations(schemadict: &HashMap<&str, &Value>) -> bool {
schemadict.keys().all(|k| META_AND_ANNOTATIONS.contains(k))
}
fn compile_contents_map(ctx: &Context, mut schemadict: HashMap<&str, &Value>) -> Result<Schema> {
ctx.increment()?;
if only_meta_and_annotations(&schemadict) {
return Ok(Schema::Any);
}
let mut unimplemented_keys = schemadict
.keys()
.filter(|k| !ctx.is_valid_keyword(k))
.collect::<Vec<_>>();
if !unimplemented_keys.is_empty() {
unimplemented_keys.sort();
bail!("Unimplemented keys: {:?}", unimplemented_keys);
}
if let Some(instance) = schemadict.remove("const") {
let const_schema = compile_const(instance)?;
let siblings = compile_contents_map(ctx, schemadict)?;
return const_schema.intersect(siblings, ctx);
}
if let Some(instances) = schemadict.remove("enum") {
let instances = instances
.as_array()
.ok_or_else(|| anyhow!("enum must be an array"))?;
let siblings = compile_contents_map(ctx, schemadict)?;
if matches!(siblings, Schema::Unsatisfiable { .. }) {
return Ok(siblings);
}
let options = instances
.iter()
.map(compile_const)
.map(|res| res.and_then(|schema| schema.intersect(siblings.clone(), ctx)))
.collect::<Result<Vec<_>>>()?;
return Ok(Schema::AnyOf { options });
}
if let Some(all_of) = schemadict.remove("allOf") {
let all_of = all_of
.as_array()
.ok_or_else(|| anyhow!("allOf must be an array"))?;
let siblings = compile_contents_map(ctx, schemadict)?;
if matches!(siblings, Schema::Unsatisfiable { .. }) {
return Ok(siblings);
}
let options = all_of
.iter()
.map(|value| compile_resource(ctx, ctx.as_resource_ref(value)))
.collect::<Result<Vec<_>>>()?;
let merged = intersect(
ctx,
vec![siblings]
.into_iter()
.chain(options.into_iter())
.collect(),
)?;
return Ok(merged);
}
if let Some(any_of) = schemadict.remove("anyOf") {
let any_of = any_of
.as_array()
.ok_or_else(|| anyhow!("anyOf must be an array"))?;
let siblings = compile_contents_map(ctx, schemadict)?;
if matches!(siblings, Schema::Unsatisfiable { .. }) {
return Ok(siblings);
}
let options = any_of
.iter()
.map(|value| compile_resource(ctx, ctx.as_resource_ref(value)))
.map(|res| res.and_then(|schema| siblings.clone().intersect(schema, ctx)))
.collect::<Result<Vec<_>>>()?;
return Ok(Schema::AnyOf { options });
}
if let Some(one_of) = schemadict.remove("oneOf") {
let one_of = one_of
.as_array()
.ok_or_else(|| anyhow!("oneOf must be an array"))?;
let siblings = compile_contents_map(ctx, schemadict)?;
if matches!(siblings, Schema::Unsatisfiable { .. }) {
return Ok(siblings);
}
let options = one_of
.iter()
.map(|value| compile_resource(ctx, ctx.as_resource_ref(value)))
.map(|res| res.and_then(|schema| siblings.clone().intersect(schema, ctx)))
.collect::<Result<Vec<_>>>()?;
return Ok(Schema::OneOf { options }.normalize());
}
if let Some(reference) = schemadict.remove("$ref") {
let reference = reference
.as_str()
.ok_or_else(|| anyhow!("$ref must be a string, got {}", limited_str(reference)))?
.to_string();
let uri: String = ctx.normalize_ref(&reference)?;
let siblings = compile_contents_map(ctx, schemadict)?;
if matches!(siblings, Schema::Any) {
define_ref(ctx, &uri)?;
return Ok(Schema::Ref { uri });
} else {
return intersect_ref(ctx, &uri, siblings, false);
}
}
let types = match schemadict.remove("type") {
Some(Value::String(tp)) => {
return compile_type(ctx, tp, &schemadict);
}
Some(Value::Array(types)) => types
.iter()
.map(|tp| match tp.as_str() {
Some(tp) => Ok(tp.to_string()),
None => bail!("type must be a string"),
})
.collect::<Result<Vec<String>>>()?,
Some(_) => bail!("type must be a string or array"),
None => TYPES.iter().map(|s| s.to_string()).collect(),
};
let options = types
.iter()
.map(|tp| compile_type(ctx, tp, &schemadict))
.collect::<Result<Vec<Schema>>>()?;
Ok(Schema::AnyOf { options })
}
fn define_ref(ctx: &Context, ref_uri: &str) -> Result<()> {
if !ctx.been_seen(ref_uri) {
ctx.mark_seen(ref_uri);
let resource = ctx.lookup_resource(ref_uri)?;
let resolved_schema = compile_resource(ctx, resource)?;
ctx.insert_ref(ref_uri, resolved_schema);
}
Ok(())
}
fn intersect_ref(ctx: &Context, ref_uri: &str, schema: Schema, ref_first: bool) -> Result<Schema> {
define_ref(ctx, ref_uri)?;
let resolved_schema = ctx
.get_ref_cloned(ref_uri)
.ok_or_else(|| {
anyhow!(
"circular references with sibling keys are not supported: {}",
ref_uri
)
})?;
if ref_first {
resolved_schema.intersect(schema, ctx)
} else {
schema.intersect(resolved_schema, ctx)
}
}
fn compile_const(instance: &Value) -> Result<Schema> {
match instance {
Value::Null => Ok(Schema::Null),
Value::Bool(b) => Ok(Schema::LiteralBool { value: *b }),
Value::Number(n) => {
let value = n.as_f64().ok_or_else(|| {
anyhow!(
"Expected f64 for numeric const, got {}",
limited_str(instance)
)
})?;
Ok(Schema::Number {
minimum: Some(value),
maximum: Some(value),
exclusive_minimum: None,
exclusive_maximum: None,
integer: n.is_i64(),
multiple_of: None,
})
}
Value::String(s) => Ok(Schema::String {
min_length: 0,
max_length: None,
regex: Some(RegexAst::Literal(s.to_string())),
}),
Value::Array(items) => {
let prefix_items = items
.iter()
.map(compile_const)
.collect::<Result<Vec<Schema>>>()?;
Ok(Schema::Array {
min_items: prefix_items.len() as u64,
max_items: Some(prefix_items.len() as u64),
prefix_items,
items: Some(Box::new(Schema::false_schema())),
})
}
Value::Object(mapping) => {
let properties = mapping
.iter()
.map(|(k, v)| Ok((k.clone(), compile_const(v)?)))
.collect::<Result<IndexMap<String, Schema>>>()?;
let required = properties.keys().cloned().collect();
Ok(Schema::Object {
properties,
additional_properties: Some(Box::new(Schema::false_schema())),
required,
})
}
}
}
fn compile_type(ctx: &Context, tp: &str, schema: &HashMap<&str, &Value>) -> Result<Schema> {
ctx.increment()?;
let get = |key: &str| schema.get(key).copied();
match tp {
"null" => Ok(Schema::Null),
"boolean" => Ok(Schema::Boolean),
"number" | "integer" => compile_numeric(
get("minimum"),
get("maximum"),
get("exclusiveMinimum"),
get("exclusiveMaximum"),
tp == "integer",
get("multipleOf"),
),
"string" => compile_string(
get("minLength"),
get("maxLength"),
get("pattern"),
get("format"),
),
"array" => compile_array(
ctx,
get("minItems"),
get("maxItems"),
get("prefixItems"),
get("items"),
get("additionalItems"),
),
"object" => compile_object(
ctx,
get("properties"),
get("additionalProperties"),
get("required"),
),
_ => bail!("Invalid type: {}", tp),
}
}
fn compile_numeric(
minimum: Option<&Value>,
maximum: Option<&Value>,
exclusive_minimum: Option<&Value>,
exclusive_maximum: Option<&Value>,
integer: bool,
multiple_of: Option<&Value>,
) -> Result<Schema> {
let minimum = match minimum {
None => None,
Some(val) => Some(
val.as_f64()
.ok_or_else(|| anyhow!("Expected f64 for 'minimum', got {}", limited_str(val)))?,
),
};
let maximum = match maximum {
None => None,
Some(val) => Some(
val.as_f64()
.ok_or_else(|| anyhow!("Expected f64 for 'maximum', got {}", limited_str(val)))?,
),
};
let exclusive_minimum = match exclusive_minimum {
None | Some(Value::Bool(false)) => None,
Some(Value::Bool(true)) => minimum,
Some(value) => Some(value.as_f64().ok_or_else(|| {
anyhow!(
"Expected f64 for 'exclusiveMinimum', got {}",
limited_str(value)
)
})?),
};
let exclusive_maximum = match exclusive_maximum {
None | Some(Value::Bool(false)) => None,
Some(Value::Bool(true)) => maximum,
Some(value) => Some(value.as_f64().ok_or_else(|| {
anyhow!(
"Expected f64 for 'exclusiveMaximum', got {}",
limited_str(value)
)
})?),
};
let multiple_of = match multiple_of {
None => None,
Some(val) => {
let f = val.as_f64().ok_or_else(|| {
anyhow!("Expected f64 for 'multipleOf', got {}", limited_str(val))
})?;
Some(Decimal::try_from(f.abs())?)
}
};
Ok(Schema::Number {
minimum,
maximum,
exclusive_minimum,
exclusive_maximum,
integer,
multiple_of,
})
}
fn pattern_to_regex(pattern: &str) -> RegexAst {
let left_anchored = pattern.starts_with('^');
let right_anchored = pattern.ends_with('$');
let trimmed = pattern.trim_start_matches('^').trim_end_matches('$');
let mut result = String::new();
if !left_anchored {
result.push_str(".*");
}
result.push('(');
result.push_str(trimmed);
result.push(')');
if !right_anchored {
result.push_str(".*");
}
RegexAst::Regex(result)
}
fn compile_string(
min_length: Option<&Value>,
max_length: Option<&Value>,
pattern: Option<&Value>,
format: Option<&Value>,
) -> Result<Schema> {
let min_length = match min_length {
None => 0,
Some(val) => val
.as_u64()
.ok_or_else(|| anyhow!("Expected u64 for 'minLength', got {}", limited_str(val)))?,
};
let max_length = match max_length {
None => None,
Some(val) => Some(
val.as_u64()
.ok_or_else(|| anyhow!("Expected u64 for 'maxLength', got {}", limited_str(val)))?,
),
};
let pattern_rx = match pattern {
None => None,
Some(val) => Some({
let s = val
.as_str()
.ok_or_else(|| anyhow!("Expected string for 'pattern', got {}", limited_str(val)))?
.to_string();
pattern_to_regex(&s)
}),
};
let format_rx = match format {
None => None,
Some(val) => Some({
let key = val
.as_str()
.ok_or_else(|| anyhow!("Expected string for 'format', got {}", limited_str(val)))?
.to_string();
let fmt = lookup_format(&key).ok_or_else(|| anyhow!("Unknown format: {}", key))?;
pattern_to_regex(fmt)
}),
};
let regex = match (pattern_rx, format_rx) {
(None, None) => None,
(None, Some(fmt)) => Some(fmt),
(Some(pat), None) => Some(pat),
(Some(pat), Some(fmt)) => Some(RegexAst::And(vec![pat, fmt])),
};
Ok(Schema::String {
min_length,
max_length,
regex,
})
}
fn compile_array(
ctx: &Context,
min_items: Option<&Value>,
max_items: Option<&Value>,
prefix_items: Option<&Value>,
items: Option<&Value>,
additional_items: Option<&Value>,
) -> Result<Schema> {
let (prefix_items, items) = {
if ctx.draft <= Draft::Draft201909
|| additional_items.is_some()
|| matches!(items, Some(Value::Array(..)))
{
match (items, additional_items) {
(Some(Value::Array(..)), _) => (items, additional_items),
_ => (None, items),
}
} else {
(prefix_items, items)
}
};
let min_items = match min_items {
None => 0,
Some(val) => val
.as_u64()
.ok_or_else(|| anyhow!("Expected u64 for 'minItems', got {}", limited_str(val)))?,
};
let max_items = match max_items {
None => None,
Some(val) => Some(
val.as_u64()
.ok_or_else(|| anyhow!("Expected u64 for 'maxItems', got {}", limited_str(val)))?,
),
};
let prefix_items = match prefix_items {
None => vec![],
Some(val) => val
.as_array()
.ok_or_else(|| anyhow!("Expected array for 'prefixItems', got {}", limited_str(val)))?
.iter()
.map(|item| compile_resource(ctx, ctx.as_resource_ref(item)))
.collect::<Result<Vec<Schema>>>()?,
};
let items = match items {
None => None,
Some(val) => Some(Box::new(compile_resource(ctx, ctx.as_resource_ref(val))?)),
};
Ok(Schema::Array {
min_items,
max_items,
prefix_items,
items,
})
}
fn compile_object(
ctx: &Context,
properties: Option<&Value>,
additional_properties: Option<&Value>,
required: Option<&Value>,
) -> Result<Schema> {
let properties = match properties {
None => IndexMap::new(),
Some(val) => val
.as_object()
.ok_or_else(|| anyhow!("Expected object for 'properties', got {}", limited_str(val)))?
.iter()
.map(|(k, v)| compile_resource(ctx, ctx.as_resource_ref(v)).map(|v| (k.clone(), v)))
.collect::<Result<IndexMap<String, Schema>>>()?,
};
let additional_properties = match additional_properties {
None => None,
Some(val) => Some(Box::new(compile_resource(ctx, ctx.as_resource_ref(val))?)),
};
let required = match required {
None => IndexSet::new(),
Some(val) => val
.as_array()
.ok_or_else(|| anyhow!("Expected array for 'required', got {}", limited_str(val)))?
.iter()
.map(|item| {
item.as_str()
.ok_or_else(|| {
anyhow!(
"Expected string for 'required' item, got {}",
limited_str(item)
)
})
.map(|s| s.to_string())
})
.collect::<Result<IndexSet<String>>>()?,
};
Ok(Schema::Object {
properties,
additional_properties,
required,
})
}
fn intersect(ctx: &Context, schemas: Vec<Schema>) -> Result<Schema> {
let (schemas, unsatisfiable) = schemas
.into_iter()
.filter(|schema| !matches!(schema, Schema::Any))
.partition::<Vec<_>, _>(|schema| !matches!(schema, Schema::Unsatisfiable { .. }));
if let Some(schema) = unsatisfiable.into_iter().next() {
return Ok(schema);
}
let mut merged = Schema::Any;
for subschema in schemas.into_iter() {
merged = merged.intersect(subschema, ctx)?;
if matches!(merged, Schema::Unsatisfiable { .. }) {
break;
}
}
Ok(merged)
}
fn opt_max<T: PartialOrd>(a: Option<T>, b: Option<T>) -> Option<T> {
match (a, b) {
(Some(a), Some(b)) => {
if a >= b {
Some(a)
} else {
Some(b)
}
}
(Some(a), None) => Some(a),
(None, Some(b)) => Some(b),
(None, None) => None,
}
}
fn opt_min<T: PartialOrd>(a: Option<T>, b: Option<T>) -> Option<T> {
match (a, b) {
(Some(a), Some(b)) => {
if a <= b {
Some(a)
} else {
Some(b)
}
}
(Some(a), None) => Some(a),
(None, Some(b)) => Some(b),
(None, None) => None,
}
}
#[cfg(all(test, feature = "referencing"))]
mod test_retriever {
use crate::json::{Retrieve, RetrieveWrapper};
use super::{build_schema, Schema};
use serde_json::{json, Value};
use std::{fmt, sync::Arc};
#[derive(Debug, Clone)]
struct TestRetrieverError(String);
impl fmt::Display for TestRetrieverError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Could not retrieve URI: {}", self.0)
}
}
impl std::error::Error for TestRetrieverError {}
struct TestRetriever {
schemas: std::collections::HashMap<String, serde_json::Value>,
}
impl Retrieve for TestRetriever {
fn retrieve(&self, uri: &str) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
let key = uri;
match self.schemas.get(key) {
Some(schema) => Ok(schema.clone()),
None => Err(Box::new(TestRetrieverError(key.to_string()))),
}
}
}
#[test]
fn test_retriever() {
let key: &str = "http://example.com/schema";
let schema = json!({
"$ref": key
});
let retriever = TestRetriever {
schemas: vec![(
key.to_string(),
json!({
"type": "string"
}),
)]
.into_iter()
.collect(),
};
let wrapper = RetrieveWrapper::new(Arc::new(retriever));
let (schema, defs) = build_schema(schema, Some(wrapper)).unwrap();
match schema {
Schema::Ref { uri } => {
assert_eq!(uri, key);
}
_ => panic!("Unexpected schema: {:?}", schema),
}
assert_eq!(defs.len(), 1);
let val = defs.get(key).unwrap();
match val {
Schema::String {
min_length: 0,
max_length: None,
regex: None,
} => {}
_ => panic!("Unexpected schema: {:?}", val),
}
}
}