use crate::json::Value;
use alloc::{collections::BTreeSet, format, string::String, vec::Vec};
#[derive(Clone, Copy, Debug)]
#[non_exhaustive]
pub enum Schema {
Any,
Null,
Boolean,
Number,
Integer {
min: i64,
max: i64,
},
Unsigned {
min: u64,
max: u64,
},
String {
min: usize,
max: usize,
choices: &'static [&'static str],
},
Array {
items: &'static Schema,
min: usize,
max: usize,
},
Object {
properties: &'static [Property],
additional: bool,
},
OneOf(&'static [Schema]),
}
#[derive(Clone, Copy, Debug)]
pub struct Property {
pub name: &'static str,
pub schema: &'static Schema,
pub required: bool,
}
pub static EMPTY_OBJECT: Schema = Schema::Object {
properties: &[],
additional: false,
};
pub static OBJECT: Schema = Schema::Object {
properties: &[],
additional: true,
};
pub static ANY: Schema = Schema::Any;
pub static BOOLEAN: Schema = Schema::Boolean;
pub static U64: Schema = Schema::Unsigned {
min: 0,
max: u64::MAX,
};
pub static STRING: Schema = Schema::String {
min: 0,
max: 4096,
choices: &[],
};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Violation {
pub path: String,
pub reason: &'static str,
}
impl Schema {
pub fn validate(&self, value: &Value) -> Result<(), Violation> {
self.validate_at(value, String::new(), 0)
}
fn validate_at(&self, value: &Value, path: String, depth: usize) -> Result<(), Violation> {
let bad = |reason| {
Err(Violation {
path: path.clone(),
reason,
})
};
if depth > 32 {
return bad("schema_depth_limit");
}
match self {
Self::Any => Ok(()),
Self::Null if matches!(value, Value::Null) => Ok(()),
Self::Boolean if matches!(value, Value::Bool(_)) => Ok(()),
Self::Number if matches!(value, Value::Number(_)) => Ok(()),
Self::Integer { min, max } => match value.as_i64() {
Some(v) if v >= *min && v <= *max => Ok(()),
Some(_) => bad("out_of_range"),
_ => bad("expected_integer"),
},
Self::Unsigned { min, max } => match value.as_u64() {
Some(v) if v >= *min && v <= *max => Ok(()),
Some(_) => bad("out_of_range"),
_ => bad("expected_unsigned_integer"),
},
Self::String { min, max, choices } => {
let Some(text) = value.as_str() else {
return bad("expected_string");
};
let count = text.chars().count();
if count < *min || count > *max {
return bad("string_length");
}
if !choices.is_empty() && !choices.contains(&text) {
return bad("unknown_choice");
}
Ok(())
}
Self::Array { items, min, max } => {
let Some(values) = value.as_array() else {
return bad("expected_array");
};
if values.len() < *min || values.len() > *max {
return bad("array_length");
}
for (index, item) in values.iter().enumerate() {
items.validate_at(item, format!("{path}/{index}"), depth + 1)?;
}
Ok(())
}
Self::Object {
properties,
additional,
} => {
let Some(fields) = value.as_object() else {
return bad("expected_object");
};
for property in *properties {
match value.get(property.name) {
Some(v) => property.schema.validate_at(
v,
pointer(&path, property.name),
depth + 1,
)?,
None if property.required => {
return Err(Violation {
path: pointer(&path, property.name),
reason: "required",
})
}
None => (),
}
}
let mut seen = BTreeSet::new();
for (name, _) in fields.iter() {
if !seen.insert(name.as_str()) {
return Err(Violation {
path: pointer(&path, name),
reason: "duplicate_key",
});
}
if !*additional && !properties.iter().any(|p| p.name == name) {
return Err(Violation {
path: pointer(&path, name),
reason: "unknown_property",
});
}
}
Ok(())
}
Self::OneOf(options) => {
let mut matches = 0;
for option in *options {
if option.validate_at(value, path.clone(), depth + 1).is_ok() {
matches += 1;
}
}
if matches == 1 {
Ok(())
} else {
bad("one_of")
}
}
_ => bad("type_mismatch"),
}
}
pub fn to_json(&self) -> Value {
self.to_json_at(0)
}
fn to_json_at(&self, depth: usize) -> Value {
if depth > 32 {
return Value::Bool(false);
}
let typed = |name: &str| Value::object([("type", Value::string(name))]);
match self {
Self::Any => Value::object([]),
Self::Null => typed("null"),
Self::Boolean => typed("boolean"),
Self::Number => typed("number"),
Self::Integer { min, max } => Value::object([
("type", Value::string("integer")),
("minimum", Value::i64(*min)),
("maximum", Value::i64(*max)),
]),
Self::Unsigned { min, max } => Value::object([
("type", Value::string("integer")),
("minimum", Value::u64(*min)),
("maximum", Value::u64(*max)),
]),
Self::String { min, max, choices } => {
let mut value = Value::object([
("type", Value::string("string")),
("minLength", Value::u64(*min as u64)),
("maxLength", Value::u64(*max as u64)),
]);
if !choices.is_empty() {
value.insert(
"enum",
Value::Array(choices.iter().map(|s| Value::string(*s)).collect()),
);
}
value
}
Self::Array { items, min, max } => Value::object([
("type", Value::string("array")),
("items", items.to_json_at(depth + 1)),
("minItems", Value::u64(*min as u64)),
("maxItems", Value::u64(*max as u64)),
]),
Self::Object {
properties,
additional,
} => {
let members = properties
.iter()
.map(|p| (String::from(p.name), p.schema.to_json_at(depth + 1)))
.collect();
let required: Vec<Value> = properties
.iter()
.filter(|p| p.required)
.map(|p| Value::string(p.name))
.collect();
Value::object([
("type", Value::string("object")),
("properties", Value::Object(members)),
("required", Value::Array(required)),
("additionalProperties", Value::Bool(*additional)),
])
}
Self::OneOf(options) => Value::object([(
"oneOf",
Value::Array(options.iter().map(|s| s.to_json_at(depth + 1)).collect()),
)]),
}
}
}
fn pointer(prefix: &str, name: &str) -> String {
format!("{prefix}/{}", name.replace('~', "~0").replace('/', "~1"))
}