use std::cmp::Ordering;
use std::ops::ControlFlow;
use std::sync::Arc;
use crate::error::SchemaError;
use crate::graph::schema::types::value_type_name;
use crate::graph::schema::{Constraints, JsonType, PropertySchema};
use crate::value::json_path::JsonPathSegment;
use crate::value::physical::{PhysicalValue, lower};
use crate::value::{ElementProperty, JsonPath, Number};
pub(crate) struct NestedCtx<'a> {
pub(crate) element_type: &'static str,
pub(crate) label: &'a str,
pub(crate) additional_default: bool,
pub(crate) full: bool,
}
pub(crate) fn render(path: &[JsonPathSegment]) -> String {
match path {
[JsonPathSegment::Key(key)] => key.to_string(),
_ => JsonPath(Arc::new(path.to_vec())).to_string(),
}
}
fn mismatch(
ctx: &NestedCtx<'_>,
path: &[JsonPathSegment],
expected: String,
value: &ElementProperty,
) -> SchemaError {
SchemaError::TypeMismatch {
element_type: ctx.element_type,
label: ctx.label.to_string(),
property_name: render(path),
expected_type: expected,
actual_type: value_type_name(value),
}
}
fn kind_of(value: &ElementProperty, format_uuid: bool) -> Option<JsonType> {
Some(match value {
ElementProperty::Null => JsonType::Null,
ElementProperty::Boolean(_) => JsonType::Boolean,
ElementProperty::Number(n) if n.is_i64() => JsonType::Integer,
ElementProperty::Number(_) => JsonType::Number,
ElementProperty::String(_) | ElementProperty::Compressed(_) if !format_uuid => {
JsonType::String
}
ElementProperty::Uuid(_) if format_uuid => JsonType::String,
ElementProperty::String(_) | ElementProperty::Compressed(_) | ElementProperty::Uuid(_) => {
return None;
}
ElementProperty::Array(_) => JsonType::Array,
ElementProperty::Object(_) | ElementProperty::Map(_) => JsonType::Object,
ElementProperty::Vertex(_) | ElementProperty::Edge(_) => return None,
})
}
pub(crate) fn admits_type(node: &PropertySchema, value: &ElementProperty) -> bool {
match &node.types {
None => !matches!(value, ElementProperty::Vertex(_) | ElementProperty::Edge(_)),
Some(types) => {
let uuid_string = node.is_uuid_string();
kind_of(value, uuid_string).is_some_and(|kind| types.contains(&kind))
}
}
}
pub(crate) trait Sink: Sized {
fn report(&mut self, error: SchemaError) -> ControlFlow<()>;
fn fresh(&self) -> Self;
fn is_clean(&self) -> bool;
fn absorb(&mut self, other: Self) -> ControlFlow<()>;
}
#[derive(Default)]
pub(crate) struct FailFast(Option<SchemaError>);
impl FailFast {
pub(crate) fn into_result(self) -> Result<(), SchemaError> {
self.0.map_or(Ok(()), Err)
}
}
impl Sink for FailFast {
fn report(&mut self, error: SchemaError) -> ControlFlow<()> {
if self.0.is_none() {
self.0 = Some(error);
}
ControlFlow::Break(())
}
fn fresh(&self) -> Self {
FailFast(None)
}
fn is_clean(&self) -> bool {
self.0.is_none()
}
fn absorb(&mut self, other: Self) -> ControlFlow<()> {
match other.0 {
Some(error) => self.report(error),
None => ControlFlow::Continue(()),
}
}
}
impl Sink for Vec<SchemaError> {
fn report(&mut self, error: SchemaError) -> ControlFlow<()> {
self.push(error);
ControlFlow::Continue(())
}
fn fresh(&self) -> Self {
Vec::new()
}
fn is_clean(&self) -> bool {
self.is_empty()
}
fn absorb(&mut self, other: Self) -> ControlFlow<()> {
self.extend(other);
ControlFlow::Continue(())
}
}
pub(crate) fn validate_nested(
ctx: &NestedCtx<'_>,
path: &mut Vec<JsonPathSegment>,
node: &PropertySchema,
value: &ElementProperty,
) -> Result<(), SchemaError> {
let mut sink = FailFast::default();
let _ = walk(ctx, path, node, value, &mut sink);
sink.into_result()
}
pub(crate) fn walk<S: Sink>(
ctx: &NestedCtx<'_>,
path: &mut Vec<JsonPathSegment>,
node: &PropertySchema,
value: &ElementProperty,
sink: &mut S,
) -> ControlFlow<()> {
if !admits_type(node, value) {
return sink.report(mismatch(ctx, path, node.non_null_type_name(), value));
}
match value {
ElementProperty::Array(items) => {
if let Some(item_node) = &node.items {
for (index, item) in items.iter().enumerate() {
path.push(JsonPathSegment::Index(index as i64));
let flow = walk(ctx, path, item_node, item, sink);
path.pop();
flow?;
}
}
}
ElementProperty::Object(map) => {
for (key, field) in map.iter() {
path.push(JsonPathSegment::Key(key.clone()));
let flow = match node.properties.get(key.as_str()) {
Some(declared) => walk(ctx, path, declared, field, sink),
None if ctx.full
&& !node.admits_additional_properties(ctx.additional_default) =>
{
sink.report(SchemaError::UndeclaredProperty {
element_type: ctx.element_type,
label: ctx.label.to_string(),
property_name: render(path),
})
}
None => ControlFlow::Continue(()),
};
path.pop();
flow?;
}
if ctx.full {
for name in &node.required {
if !map.contains_key(name.as_str()) {
path.push(JsonPathSegment::Key(name.as_str().into()));
let property_name = render(path);
path.pop();
sink.report(SchemaError::MissingRequiredProperty {
element_type: ctx.element_type,
label: ctx.label.to_string(),
property_name,
})?;
}
}
}
}
_ => {}
}
if ctx.full
&& let Some(constraints) = node.constraints.as_deref()
&& let Err(error) = check_constraints(ctx.label, &render(path), constraints, value)
{
sink.report(error)?;
}
if let Some(members) = &node.any_of {
walk_any_of(ctx, path, node, members, value, sink)?;
}
ControlFlow::Continue(())
}
fn walk_any_of<S: Sink>(
ctx: &NestedCtx<'_>,
path: &mut Vec<JsonPathSegment>,
node: &PropertySchema,
members: &[PropertySchema],
value: &ElementProperty,
sink: &mut S,
) -> ControlFlow<()> {
let mut first_same_kind: Option<S> = None;
for member in members {
let mut trial = sink.fresh();
let _ = walk(ctx, path, member, value, &mut trial);
if trial.is_clean() {
return ControlFlow::Continue(());
}
let same_kind = admits_type(member, value)
&& matches!(
value,
ElementProperty::Object(_) | ElementProperty::Array(_)
);
if same_kind && first_same_kind.is_none() {
first_same_kind = Some(trial);
}
}
match first_same_kind {
Some(trial) => sink.absorb(trial),
None => sink.report(mismatch(ctx, path, node.non_null_type_name(), value)),
}
}
pub(crate) fn compare_numbers(a: &Number, b: &Number) -> Option<Ordering> {
match (a.as_i64(), b.as_i64()) {
(Some(x), Some(y)) => Some(x.cmp(&y)),
_ => {
let x = a.as_f64().or_else(|| a.as_i64().map(|i| i as f64))?;
let y = b.as_f64().or_else(|| b.as_i64().map(|i| i as f64))?;
x.partial_cmp(&y)
}
}
}
pub(crate) fn number_text(n: &Number) -> String {
match n.as_i64() {
Some(i) => i.to_string(),
None => n.as_f64().map(|f| f.to_string()).unwrap_or_default(),
}
}
pub(crate) fn json_equal(a: &ElementProperty, b: &ElementProperty) -> bool {
fn eq(a: &PhysicalValue<'_>, b: &PhysicalValue<'_>) -> bool {
match (a, b) {
(PhysicalValue::Int(x), PhysicalValue::Int(y)) => x == y,
(PhysicalValue::Int(x), PhysicalValue::Float(y))
| (PhysicalValue::Float(y), PhysicalValue::Int(x)) => (*x as f64) == *y,
(PhysicalValue::Float(x), PhysicalValue::Float(y)) => x == y,
(PhysicalValue::Array(x), PhysicalValue::Array(y)) => {
x.len() == y.len() && x.iter().zip(y).all(|(x, y)| eq(x, y))
}
(PhysicalValue::Object(x), PhysicalValue::Object(y)) => {
x.len() == y.len()
&& x.iter().all(|(key, value)| {
y.iter()
.find(|(other, _)| other == key)
.is_some_and(|(_, other)| eq(value, other))
})
}
(a, b) => a == b,
}
}
eq(&lower(a), &lower(b))
}
fn preview(value: &ElementProperty) -> String {
crate::traversal::step::barrier_steps::numeric::preview(value)
}
pub(crate) fn check_constraints(
label: &str,
property_name: &str,
constraints: &Constraints,
value: &ElementProperty,
) -> Result<(), SchemaError> {
let violation = |rule: String, actual: String| SchemaError::ConstraintViolation {
label: label.to_string(),
property_name: property_name.to_string(),
constraint_rule: rule,
actual_value: actual,
};
match value {
ElementProperty::String(_) | ElementProperty::Compressed(_) => value
.with_text(|s| -> Result<(), SchemaError> {
let length = s.chars().count() as u64;
if let Some(min) = constraints.min_length
&& length < min
{
return Err(violation(
format!("length >= {min}"),
format!("length {length}"),
));
}
if let Some(max) = constraints.max_length
&& length > max
{
return Err(violation(
format!("length <= {max}"),
format!("length {length}"),
));
}
if let Some(pattern) = &constraints.pattern
&& !pattern.is_match(s)
{
return Err(violation(
format!("matches pattern '{pattern}'"),
s.to_string(),
));
}
Ok(())
})
.unwrap_or(Ok(()))?,
ElementProperty::Number(n) => {
let bounds = [
(
&constraints.minimum,
">=",
[Ordering::Greater, Ordering::Equal],
),
(
&constraints.exclusive_minimum,
">",
[Ordering::Greater, Ordering::Greater],
),
(
&constraints.maximum,
"<=",
[Ordering::Less, Ordering::Equal],
),
(
&constraints.exclusive_maximum,
"<",
[Ordering::Less, Ordering::Less],
),
];
for (bound, op, allowed) in bounds {
if let Some(bound) = bound {
let ok = compare_numbers(n, bound).is_some_and(|o| allowed.contains(&o));
if !ok {
return Err(violation(
format!("value {op} {}", number_text(bound)),
number_text(n),
));
}
}
}
}
ElementProperty::Array(items) => {
let len = items.len() as u64;
if let Some(min) = constraints.min_items
&& len < min
{
return Err(violation(format!("items >= {min}"), format!("items {len}")));
}
if let Some(max) = constraints.max_items
&& len > max
{
return Err(violation(format!("items <= {max}"), format!("items {len}")));
}
}
_ => {}
}
if let Some(values) = &constraints.enum_values
&& !values.iter().any(|allowed| json_equal(allowed, value))
{
let listed: Vec<String> = values.iter().take(10).map(preview).collect();
let more = if values.len() > 10 { ", …" } else { "" };
return Err(violation(
format!("one of [{}{more}]", listed.join(", ")),
preview(value),
));
}
if let Some(expected) = &constraints.const_value
&& !json_equal(expected, value)
{
return Err(violation(
format!("equals {}", preview(expected)),
preview(value),
));
}
Ok(())
}
pub(crate) fn declared_node_at<'a>(
ctx: &NestedCtx<'_>,
full_path: &[JsonPathSegment],
node: &'a PropertySchema,
depth: usize,
value: &ElementProperty,
) -> Result<Option<&'a PropertySchema>, SchemaError> {
let Some(segment) = full_path.get(depth) else {
return Ok(Some(node));
};
let own = match segment {
JsonPathSegment::Key(key) => node.properties.get(key.as_str()),
JsonPathSegment::Index(_) => node.items.as_deref(),
};
if let Some(child) = own {
return declared_node_at(ctx, full_path, child, depth + 1, value);
}
let Some(members) = &node.any_of else {
return Ok(None);
};
let mut candidates: Vec<&'a PropertySchema> = Vec::new();
for member in members {
if let Some(found) = declared_node_at(ctx, full_path, member, depth, value)?
&& !candidates.contains(&found)
{
candidates.push(found);
}
}
match candidates.len() {
0 => Ok(None),
1 => Ok(candidates.pop()),
_ => {
let types_only = NestedCtx {
element_type: ctx.element_type,
label: ctx.label,
additional_default: true,
full: false,
};
let exact = candidates.iter().find(|candidate| {
validate_nested(&types_only, &mut Vec::new(), candidate, value).is_ok()
});
match exact {
Some(candidate) => Ok(Some(*candidate)),
None => Err(mismatch(
ctx,
full_path,
format!(
"union<{}>",
candidates
.iter()
.map(|c| c.non_null_type_name())
.collect::<Vec<_>>()
.join("|")
),
value,
)),
}
}
}
}