use super::{
ImageValue, RuntimeError, Value,
record::{Symbol, intern_symbol},
unwrap_type_value,
};
use smallvec::SmallVec;
use std::fmt::Write as _;
use std::sync::Arc;
#[derive(Clone)]
pub(crate) struct ValidationPlan {
kind: ValidationPlanKind,
}
#[derive(Clone)]
enum ValidationPlanKind {
Any,
Primitive(PrimitiveMask),
Enum(Box<[Arc<str>]>),
List(Box<ValidationPlan>),
Object(Box<[ValidationFieldPlan]>),
Union(Box<[ValidationPlan]>),
}
#[derive(Clone)]
struct ValidationFieldPlan {
symbol: Symbol,
name: Arc<str>,
required: bool,
plan: ValidationPlan,
}
#[derive(Clone, Copy, PartialEq, Eq)]
struct PrimitiveMask(u16);
impl PrimitiveMask {
const STRING: Self = Self(1 << 0);
const NUMBER: Self = Self(1 << 1);
const INTEGER: Self = Self(1 << 2);
const BOOLEAN: Self = Self(1 << 3);
const ARRAY: Self = Self(1 << 4);
const OBJECT: Self = Self(1 << 5);
const NULL: Self = Self(1 << 6);
fn from_schema_name(name: &str) -> Option<Self> {
Some(match name {
"string" => Self::STRING,
"number" => Self::NUMBER,
"integer" => Self::INTEGER,
"boolean" => Self::BOOLEAN,
"array" => Self::ARRAY,
"object" => Self::OBJECT,
"null" => Self::NULL,
_ => return None,
})
}
fn schema_name(self) -> &'static str {
match self {
Self::STRING => "string",
Self::NUMBER => "number",
Self::INTEGER => "integer",
Self::BOOLEAN => "boolean",
Self::ARRAY => "array",
Self::OBJECT => "object",
Self::NULL => "null",
_ => "value",
}
}
fn matches(self, value: &Value) -> bool {
let value_mask = match value {
Value::String(_) => Self::STRING.0,
Value::Number(number) if number.is_finite() && number.fract() == 0.0 => {
Self::NUMBER.0 | Self::INTEGER.0
}
Value::Number(number) if number.is_finite() => Self::NUMBER.0,
Value::Bool(_) => Self::BOOLEAN.0,
Value::List(_) => Self::ARRAY.0,
Value::Record(_) | Value::Image(_) | Value::Resource(_) => Self::OBJECT.0,
Value::Null => Self::NULL.0,
Value::Projected(value) => match value.value_type_name() {
"list" => Self::ARRAY.0,
_ => Self::OBJECT.0,
},
Value::Number(_) => 0,
};
self.0 & value_mask != 0
}
}
pub(crate) fn execute_validate_builtin(
value: Value,
schema: &Value,
) -> Result<Value, RuntimeError> {
let schema = unwrap_type_value(schema).ok_or_else(|| RuntimeError::TypeError {
message: "`validate` requires a Type literal as the second argument".to_string(),
})?;
let plan = compile_schema_value(schema);
execute_validation_plan(value, &plan)
}
pub(crate) fn execute_validation_plan(
value: Value,
plan: &ValidationPlan,
) -> Result<Value, RuntimeError> {
if plan.accepts(&value) {
return Ok(value);
}
let mut path = SmallVec::<[PathSegment<'_>; 8]>::new();
let message = plan.describe_failure(&value, &mut path);
Err(RuntimeError::ValueError {
message: format!("validation failed: {message}"),
})
}
pub(crate) fn compile_schema_value(schema: &Value) -> ValidationPlan {
let Some(schema_obj) = schema.as_record() else {
return ValidationPlan {
kind: ValidationPlanKind::Any,
};
};
if let Some(Value::List(variants)) = schema_obj.get("anyOf") {
return ValidationPlan {
kind: ValidationPlanKind::Union(
variants
.iter()
.map(compile_schema_value)
.collect::<Vec<_>>()
.into_boxed_slice(),
),
};
}
if let Some(Value::List(allowed)) = schema_obj.get("enum") {
return ValidationPlan {
kind: ValidationPlanKind::Enum(
allowed
.iter()
.filter_map(|value| match value {
Value::String(value) => Some(Arc::<str>::from(value.as_str())),
_ => None,
})
.collect::<Vec<_>>()
.into_boxed_slice(),
),
};
}
let schema_type = match schema_obj.get("type") {
Some(Value::String(expected)) => PrimitiveMask::from_schema_name(expected.as_str()),
_ => None,
};
match schema_type {
Some(PrimitiveMask::ARRAY) => {
let item_plan =
schema_obj
.get("items")
.map(compile_schema_value)
.unwrap_or(ValidationPlan {
kind: ValidationPlanKind::Any,
});
ValidationPlan {
kind: ValidationPlanKind::List(Box::new(item_plan)),
}
}
Some(PrimitiveMask::OBJECT) => ValidationPlan {
kind: ValidationPlanKind::Object(compile_object_fields(schema_obj)),
},
Some(kind) => ValidationPlan {
kind: ValidationPlanKind::Primitive(kind),
},
None => ValidationPlan {
kind: ValidationPlanKind::Any,
},
}
}
impl ValidationPlan {
fn accepts(&self, value: &Value) -> bool {
match &self.kind {
ValidationPlanKind::Any => true,
ValidationPlanKind::Primitive(expected) => expected.matches(value),
ValidationPlanKind::Enum(allowed) => {
let Value::String(value) = value else {
return false;
};
allowed.iter().any(|candidate| candidate.as_ref() == value)
}
ValidationPlanKind::List(item_plan) => {
let Value::List(items) = value else {
return false;
};
items.iter().all(|item| item_plan.accepts(item))
}
ValidationPlanKind::Object(fields) => match value {
Value::Record(record) => fields.iter().all(|field| {
record
.get_symbol(field.symbol)
.map_or(!field.required, |field_value| {
field.plan.accepts(field_value)
})
}),
Value::Image(image) => fields.iter().all(|field| {
image_field_value(image, field.name.as_ref())
.map_or(!field.required, |field_value| {
field.plan.accepts(&field_value)
})
}),
_ => false,
},
ValidationPlanKind::Union(variants) => {
variants.iter().any(|variant| variant.accepts(value))
}
}
}
fn describe_failure<'a>(
&'a self,
value: &Value,
path: &mut SmallVec<[PathSegment<'a>; 8]>,
) -> String {
match &self.kind {
ValidationPlanKind::Any => format!(
"{}: expected any, got {}",
format_schema_path(path),
schema_value_type_name(value)
),
ValidationPlanKind::Primitive(expected) => {
describe_primitive_failure(value, *expected, path)
}
ValidationPlanKind::Enum(allowed) => {
if !PrimitiveMask::STRING.matches(value) {
return describe_primitive_failure(value, PrimitiveMask::STRING, path);
}
let allowed = allowed
.iter()
.map(AsRef::as_ref)
.collect::<Vec<_>>()
.join(", ");
format!(
"{}: expected one of [{allowed}], got {value}",
format_schema_path(path)
)
}
ValidationPlanKind::List(item_plan) => {
if !PrimitiveMask::ARRAY.matches(value) {
return describe_primitive_failure(value, PrimitiveMask::ARRAY, path);
}
let Value::List(items) = value else {
return describe_primitive_failure(value, PrimitiveMask::ARRAY, path);
};
for (index, item) in items.iter().enumerate() {
path.push(PathSegment::Index(index));
if !item_plan.accepts(item) {
let message = item_plan.describe_failure(item, path);
path.pop();
return message;
}
path.pop();
}
describe_primitive_failure(value, PrimitiveMask::ARRAY, path)
}
ValidationPlanKind::Object(fields) => {
if !PrimitiveMask::OBJECT.matches(value) {
return describe_primitive_failure(value, PrimitiveMask::OBJECT, path);
}
for field in fields.iter() {
let field_value = plan_field_value(value, field);
let Some(field_value) = field_value else {
if field.required {
return format!(
"{}: missing required field `{}`",
format_schema_path(path),
field.name
);
}
continue;
};
if !field.plan.accepts(field_value.as_ref()) {
path.push(PathSegment::Field(field.name.as_ref()));
let message = field.plan.describe_failure(field_value.as_ref(), path);
path.pop();
return message;
}
}
describe_primitive_failure(value, PrimitiveMask::OBJECT, path)
}
ValidationPlanKind::Union(variants) => format!(
"{}: expected one of [{}], got {}",
format_schema_path(path),
variants
.iter()
.map(ValidationPlan::describe)
.collect::<Vec<_>>()
.join(", "),
schema_value_type_name(value)
),
}
}
fn describe(&self) -> String {
match &self.kind {
ValidationPlanKind::Any => "any".to_string(),
ValidationPlanKind::Primitive(kind) => kind.schema_name().to_string(),
ValidationPlanKind::Enum(values) => format!(
"enum[{}]",
values
.iter()
.map(AsRef::as_ref)
.collect::<Vec<_>>()
.join(", ")
),
ValidationPlanKind::List(_) => "array".to_string(),
ValidationPlanKind::Object(_) => "object".to_string(),
ValidationPlanKind::Union(variants) => variants
.iter()
.map(ValidationPlan::describe)
.collect::<Vec<_>>()
.join(" | "),
}
}
}
fn compile_object_fields(schema_obj: &super::Record) -> Box<[ValidationFieldPlan]> {
let required_symbols = match schema_obj.get("required") {
Some(Value::List(required)) => required
.iter()
.filter_map(|field| match field {
Value::String(name) => Some((intern_symbol(name.as_str()), name.as_str())),
_ => None,
})
.collect::<Vec<_>>(),
_ => Vec::new(),
};
let mut fields = match schema_obj.get("properties") {
Some(Value::Record(properties)) => properties
.entries
.iter()
.map(|entry| ValidationFieldPlan {
symbol: entry.symbol,
name: entry.name.clone(),
required: required_symbols
.iter()
.any(|(symbol, _)| *symbol == entry.symbol),
plan: compile_schema_value(&entry.value),
})
.collect::<Vec<_>>(),
_ => Vec::new(),
};
for (symbol, name) in required_symbols {
if fields.iter().any(|field| field.symbol == symbol) {
continue;
}
fields.push(ValidationFieldPlan {
symbol,
name: Arc::<str>::from(name),
required: true,
plan: ValidationPlan {
kind: ValidationPlanKind::Any,
},
});
}
fields.into_boxed_slice()
}
enum FieldValue<'a> {
Borrowed(&'a Value),
Owned(Value),
}
impl AsRef<Value> for FieldValue<'_> {
fn as_ref(&self) -> &Value {
match self {
Self::Borrowed(value) => value,
Self::Owned(value) => value,
}
}
}
fn plan_field_value<'a>(value: &'a Value, field: &ValidationFieldPlan) -> Option<FieldValue<'a>> {
match value {
Value::Record(record) => record.get_symbol(field.symbol).map(FieldValue::Borrowed),
Value::Image(image) => image_field_value(image, field.name.as_ref()).map(FieldValue::Owned),
_ => None,
}
}
fn describe_primitive_failure(
value: &Value,
expected: PrimitiveMask,
path: &[PathSegment<'_>],
) -> String {
format!(
"{}: expected {}, got {}",
format_schema_path(path),
expected.schema_name(),
schema_value_type_name(value)
)
}
#[derive(Clone, Copy)]
enum PathSegment<'a> {
Field(&'a str),
Index(usize),
}
fn format_schema_path(path: &[PathSegment<'_>]) -> String {
let mut formatted = "$".to_string();
for segment in path {
match segment {
PathSegment::Field(name) => {
formatted.push('.');
formatted.push_str(name);
}
PathSegment::Index(index) => {
write!(formatted, "[{index}]").expect("string writes should not fail");
}
}
}
formatted
}
fn schema_value_type_name(value: &Value) -> &'static str {
match value {
Value::Null => "null",
Value::Bool(_) => "boolean",
Value::Number(_) => "number",
Value::String(_) => "string",
Value::Image(_) => "object",
Value::Resource(_) => "object",
Value::List(_) => "array",
Value::Record(_) => "object",
Value::Projected(value) => match value.value_type_name() {
"list" => "array",
_ => "object",
},
}
}
fn image_field_value(image: &ImageValue, field: &str) -> Option<Value> {
match field {
"type" => Some(Value::String("image".into())),
"id" => Some(Value::String(image.id.clone().into())),
"label" => Some(Value::String(image.label.clone().into())),
"size" => Some(Value::Number(image.size as f64)),
"width" => Some(
image
.width
.map(|width| Value::Number(width as f64))
.unwrap_or(Value::Null),
),
"height" => Some(
image
.height
.map(|height| Value::Number(height as f64))
.unwrap_or(Value::Null),
),
_ => None,
}
}