validator reorg

This commit is contained in:
2026-02-26 19:17:13 -05:00
parent 960a99034a
commit e14f53e7d9
16 changed files with 501 additions and 423 deletions

394
src/validator/compiler.rs Normal file
View File

@ -0,0 +1,394 @@
use crate::validator::schema::Schema;
use regex::Regex;
use serde_json::Value;
// use std::collections::HashMap;
use std::error::Error;
use std::sync::Arc;
/// Represents a compiled format validator
#[derive(Debug, Clone)]
pub enum CompiledFormat {
/// A simple function pointer validator
Func(fn(&Value) -> Result<(), Box<dyn Error + Send + Sync>>),
/// A regex-based validator
Regex(Regex),
}
/// A wrapper for compiled regex patterns
#[derive(Debug, Clone)]
pub struct CompiledRegex(pub Regex);
/// The Compiler is responsible for pre-calculating high-cost schema operations
pub struct Compiler;
impl Compiler {
/// Internal: Compiles formats and regexes in-place
fn compile_formats_and_regexes(schema: &mut Schema) {
// 1. Compile Format
if let Some(format_str) = &schema.format {
if let Some(fmt) = crate::validator::formats::FORMATS.get(format_str.as_str()) {
schema.compiled_format = Some(CompiledFormat::Func(fmt.func));
}
}
// 2. Compile Pattern (regex)
if let Some(pattern_str) = &schema.pattern {
if let Ok(re) = Regex::new(pattern_str) {
schema.compiled_pattern = Some(CompiledRegex(re));
}
}
// 2.5 Compile Pattern Properties
if let Some(pp) = &schema.pattern_properties {
let mut compiled_pp = Vec::new();
for (pattern, sub_schema) in pp {
if let Ok(re) = Regex::new(pattern) {
compiled_pp.push((CompiledRegex(re), sub_schema.clone()));
} else {
eprintln!(
"Invalid patternProperty regex in schema (compile time): {}",
pattern
);
}
}
if !compiled_pp.is_empty() {
schema.compiled_pattern_properties = Some(compiled_pp);
}
}
// 3. Recurse
Self::compile_recursive(schema);
}
fn normalize_dependencies(schema: &mut Schema) {
if let Some(deps) = schema.dependencies.take() {
for (key, dep) in deps {
match dep {
crate::validator::schema::Dependency::Props(props) => {
schema
.dependent_required
.get_or_insert_with(std::collections::BTreeMap::new)
.insert(key, props);
}
crate::validator::schema::Dependency::Schema(sub_schema) => {
schema
.dependent_schemas
.get_or_insert_with(std::collections::BTreeMap::new)
.insert(key, sub_schema);
}
}
}
}
}
fn compile_recursive(schema: &mut Schema) {
Self::normalize_dependencies(schema);
// Compile self
if let Some(format_str) = &schema.format {
if let Some(fmt) = crate::validator::formats::FORMATS.get(format_str.as_str()) {
schema.compiled_format = Some(CompiledFormat::Func(fmt.func));
}
}
if let Some(pattern_str) = &schema.pattern {
if let Ok(re) = Regex::new(pattern_str) {
schema.compiled_pattern = Some(CompiledRegex(re));
}
}
// Recurse
if let Some(defs) = &mut schema.definitions {
for s in defs.values_mut() {
Self::compile_recursive(Arc::make_mut(s));
}
}
if let Some(defs) = &mut schema.defs {
for s in defs.values_mut() {
Self::compile_recursive(Arc::make_mut(s));
}
}
if let Some(props) = &mut schema.properties {
for s in props.values_mut() {
Self::compile_recursive(Arc::make_mut(s));
}
}
if let Some(add_props) = &mut schema.additional_properties {
Self::compile_recursive(Arc::make_mut(add_props));
}
// ... Recurse logic ...
if let Some(items) = &mut schema.items {
Self::compile_recursive(Arc::make_mut(items));
}
if let Some(prefix_items) = &mut schema.prefix_items {
for s in prefix_items {
Self::compile_recursive(Arc::make_mut(s));
}
}
if let Some(not) = &mut schema.not {
Self::compile_recursive(Arc::make_mut(not));
}
if let Some(all_of) = &mut schema.all_of {
for s in all_of {
Self::compile_recursive(Arc::make_mut(s));
}
}
if let Some(any_of) = &mut schema.any_of {
for s in any_of {
Self::compile_recursive(Arc::make_mut(s));
}
}
if let Some(one_of) = &mut schema.one_of {
for s in one_of {
Self::compile_recursive(Arc::make_mut(s));
}
}
if let Some(s) = &mut schema.if_ {
Self::compile_recursive(Arc::make_mut(s));
}
if let Some(s) = &mut schema.then_ {
Self::compile_recursive(Arc::make_mut(s));
}
if let Some(s) = &mut schema.else_ {
Self::compile_recursive(Arc::make_mut(s));
}
if let Some(ds) = &mut schema.dependent_schemas {
for s in ds.values_mut() {
Self::compile_recursive(Arc::make_mut(s));
}
}
if let Some(pn) = &mut schema.property_names {
Self::compile_recursive(Arc::make_mut(pn));
}
}
/// Recursively traverses the schema tree to build the local registry index.
fn compile_index(
schema: &Arc<Schema>,
registry: &mut crate::validator::registry::Registry,
parent_base: Option<String>,
pointer: json_pointer::JsonPointer<String, Vec<String>>,
) {
// 1. Index using Parent Base (Path from Parent)
if let Some(base) = &parent_base {
// We use the pointer's string representation (e.g., "/definitions/foo")
// and append it to the base.
let fragment = pointer.to_string();
let ptr_uri = if fragment.is_empty() {
base.clone()
} else {
format!("{}#{}", base, fragment)
};
registry.insert(ptr_uri, schema.clone());
}
// 2. Determine Current Scope... (unchanged logic)
let mut current_base = parent_base.clone();
let mut child_pointer = pointer.clone();
if let Some(id) = &schema.obj.id {
let mut new_base = None;
if let Ok(_) = url::Url::parse(id) {
new_base = Some(id.clone());
} else if let Some(base) = &current_base {
if let Ok(base_url) = url::Url::parse(base) {
if let Ok(joined) = base_url.join(id) {
new_base = Some(joined.to_string());
}
}
} else {
new_base = Some(id.clone());
}
if let Some(base) = new_base {
// println!("DEBUG: Compiling index for path: {}", base); // Added println
registry.insert(base.clone(), schema.clone());
current_base = Some(base);
child_pointer = json_pointer::JsonPointer::new(vec![]); // Reset
}
}
// 3. Index by Anchor
if let Some(anchor) = &schema.obj.anchor {
if let Some(base) = &current_base {
let anchor_uri = format!("{}#{}", base, anchor);
registry.insert(anchor_uri, schema.clone());
}
}
// Index by Dynamic Anchor
if let Some(d_anchor) = &schema.obj.dynamic_anchor {
if let Some(base) = &current_base {
let anchor_uri = format!("{}#{}", base, d_anchor);
registry.insert(anchor_uri, schema.clone());
}
}
// 4. Recurse (unchanged logic structure, just passing registry)
if let Some(defs) = schema.defs.as_ref().or(schema.definitions.as_ref()) {
let segment = if schema.defs.is_some() {
"$defs"
} else {
"definitions"
};
for (key, sub_schema) in defs {
let mut sub = child_pointer.clone();
sub.push(segment.to_string());
let decoded_key = percent_encoding::percent_decode_str(key).decode_utf8_lossy();
sub.push(decoded_key.to_string());
Self::compile_index(sub_schema, registry, current_base.clone(), sub);
}
}
if let Some(props) = &schema.properties {
for (key, sub_schema) in props {
let mut sub = child_pointer.clone();
sub.push("properties".to_string());
sub.push(key.to_string());
Self::compile_index(sub_schema, registry, current_base.clone(), sub);
}
}
if let Some(items) = &schema.items {
let mut sub = child_pointer.clone();
sub.push("items".to_string());
Self::compile_index(items, registry, current_base.clone(), sub);
}
if let Some(prefix_items) = &schema.prefix_items {
for (i, sub_schema) in prefix_items.iter().enumerate() {
let mut sub = child_pointer.clone();
sub.push("prefixItems".to_string());
sub.push(i.to_string());
Self::compile_index(sub_schema, registry, current_base.clone(), sub);
}
}
if let Some(all_of) = &schema.all_of {
for (i, sub_schema) in all_of.iter().enumerate() {
let mut sub = child_pointer.clone();
sub.push("allOf".to_string());
sub.push(i.to_string());
Self::compile_index(sub_schema, registry, current_base.clone(), sub);
}
}
if let Some(any_of) = &schema.any_of {
for (i, sub_schema) in any_of.iter().enumerate() {
let mut sub = child_pointer.clone();
sub.push("anyOf".to_string());
sub.push(i.to_string());
Self::compile_index(sub_schema, registry, current_base.clone(), sub);
}
}
if let Some(one_of) = &schema.one_of {
for (i, sub_schema) in one_of.iter().enumerate() {
let mut sub = child_pointer.clone();
sub.push("oneOf".to_string());
sub.push(i.to_string());
Self::compile_index(sub_schema, registry, current_base.clone(), sub);
}
}
if let Some(not) = &schema.not {
let mut sub = child_pointer.clone();
sub.push("not".to_string());
Self::compile_index(not, registry, current_base.clone(), sub);
}
if let Some(if_) = &schema.if_ {
let mut sub = child_pointer.clone();
sub.push("if".to_string());
Self::compile_index(if_, registry, current_base.clone(), sub);
}
if let Some(then_) = &schema.then_ {
let mut sub = child_pointer.clone();
sub.push("then".to_string());
Self::compile_index(then_, registry, current_base.clone(), sub);
}
if let Some(else_) = &schema.else_ {
let mut sub = child_pointer.clone();
sub.push("else".to_string());
Self::compile_index(else_, registry, current_base.clone(), sub);
}
if let Some(deps) = &schema.dependent_schemas {
for (key, sub_schema) in deps {
let mut sub = child_pointer.clone();
sub.push("dependentSchemas".to_string());
sub.push(key.to_string());
Self::compile_index(sub_schema, registry, current_base.clone(), sub);
}
}
if let Some(pp) = &schema.pattern_properties {
for (key, sub_schema) in pp {
let mut sub = child_pointer.clone();
sub.push("patternProperties".to_string());
sub.push(key.to_string());
Self::compile_index(sub_schema, registry, current_base.clone(), sub);
}
}
if let Some(add_props) = &schema.additional_properties {
let mut sub = child_pointer.clone();
sub.push("additionalProperties".to_string());
Self::compile_index(add_props, registry, current_base.clone(), sub);
}
if let Some(contains) = &schema.contains {
let mut sub = child_pointer.clone();
sub.push("contains".to_string());
Self::compile_index(contains, registry, current_base.clone(), sub);
}
if let Some(property_names) = &schema.property_names {
let mut sub = child_pointer.clone();
sub.push("propertyNames".to_string());
Self::compile_index(property_names, registry, current_base.clone(), sub);
}
}
pub fn compile(mut root_schema: Schema, root_id: Option<String>) -> Arc<Schema> {
// 1. Compile in-place (formats/regexes/normalization)
Self::compile_formats_and_regexes(&mut root_schema);
// Apply root_id override if schema ID is missing
if let Some(rid) = &root_id {
if root_schema.obj.id.is_none() {
root_schema.obj.id = Some(rid.clone());
}
}
// 2. Build ID/Pointer Index
let mut registry = crate::validator::registry::Registry::new();
// We need a temporary Arc to satisfy compile_index recursion
// But we are modifying root_schema.
// This is tricky. compile_index takes &Arc<Schema>.
// We should build the index first, THEN attach it.
let root = Arc::new(root_schema);
// Default base_uri to ""
let base_uri = root_id
.clone()
.or_else(|| root.obj.id.clone())
.or(Some("".to_string()));
Self::compile_index(
&root,
&mut registry,
base_uri,
json_pointer::JsonPointer::new(vec![]),
);
// Also ensure root id is indexed if present
if let Some(rid) = root_id {
registry.insert(rid, root.clone());
}
// Now we need to attach this registry to the root schema.
// Since root is an Arc, we might need to recreate it if we can't mutate.
// Schema struct modifications require &mut.
let mut final_schema = Arc::try_unwrap(root).unwrap_or_else(|arc| (*arc).clone());
final_schema.obj.compiled_registry = Some(Arc::new(registry));
Arc::new(final_schema)
}
}

118
src/validator/context.rs Normal file
View File

@ -0,0 +1,118 @@
use crate::validator::schema::Schema;
use crate::validator::Validator;
use crate::validator::error::ValidationError;
use crate::validator::instance::ValidationInstance;
use crate::validator::result::ValidationResult;
use std::collections::HashSet;
pub struct ValidationContext<'a, I: ValidationInstance<'a>> {
pub validator: &'a Validator,
pub root: &'a Schema,
pub schema: &'a Schema,
pub instance: I,
pub path: String,
pub depth: usize,
pub scope: Vec<String>,
pub overrides: HashSet<String>,
pub extensible: bool,
pub reporter: bool,
}
impl<'a, I: ValidationInstance<'a>> ValidationContext<'a, I> {
pub fn new(
validator: &'a Validator,
root: &'a Schema,
schema: &'a Schema,
instance: I,
scope: Vec<String>,
overrides: HashSet<String>,
extensible: bool,
reporter: bool,
) -> Self {
let effective_extensible = schema.extensible.unwrap_or(extensible);
Self {
validator,
root,
schema,
instance,
path: String::new(),
depth: 0,
scope,
overrides,
extensible: effective_extensible,
reporter,
}
}
pub fn derive(
&self,
schema: &'a Schema,
instance: I,
path: &str,
scope: Vec<String>,
overrides: HashSet<String>,
extensible: bool,
reporter: bool,
) -> Self {
let effective_extensible = schema.extensible.unwrap_or(extensible);
Self {
validator: self.validator,
root: self.root,
schema,
instance,
path: path.to_string(),
depth: self.depth + 1,
scope,
overrides,
extensible: effective_extensible,
reporter,
}
}
pub fn derive_for_schema(&self, schema: &'a Schema, reporter: bool) -> Self {
self.derive(
schema,
self.instance,
&self.path,
self.scope.clone(),
HashSet::new(),
self.extensible,
reporter,
)
}
pub fn validate(&self) -> Result<ValidationResult, ValidationError> {
let mut effective_scope = self.scope.clone();
if let Some(id) = &self.schema.obj.id {
let current_base = self.scope.last().map(|s| s.as_str()).unwrap_or("");
let mut new_base = id.clone().to_string();
if !current_base.is_empty() {
if let Ok(base_url) = url::Url::parse(current_base) {
if let Ok(joined) = base_url.join(id) {
new_base = joined.to_string();
}
}
}
effective_scope.push(new_base);
let shadow = ValidationContext {
validator: self.validator,
root: self.root,
schema: self.schema,
instance: self.instance,
path: self.path.clone(),
depth: self.depth,
scope: effective_scope,
overrides: self.overrides.clone(),
extensible: self.extensible,
reporter: self.reporter,
};
return shadow.validate_scoped();
}
self.validate_scoped()
}
}

6
src/validator/error.rs Normal file
View File

@ -0,0 +1,6 @@
#[derive(Debug, Clone, serde::Serialize)]
pub struct ValidationError {
pub code: String,
pub message: String,
pub path: String,
}

875
src/validator/formats.rs Normal file
View File

@ -0,0 +1,875 @@
use std::{
collections::HashMap,
error::Error,
net::{Ipv4Addr, Ipv6Addr},
};
use lazy_static::lazy_static;
use percent_encoding::percent_decode_str;
use serde_json::Value;
use url::Url;
// use crate::ecma; // Assuming ecma is not yet available, stubbing regex for now
/// Defines format for `format` keyword.
#[derive(Clone, Copy)]
pub struct Format {
/// Name of the format
pub name: &'static str,
/// validates given value.
pub func: fn(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>>, // Ensure thread safety if needed
}
lazy_static! {
pub(crate) static ref FORMATS: HashMap<&'static str, Format> = {
let mut m = HashMap::<&'static str, Format>::new();
// Helper to register formats
let mut register = |name, func| m.insert(name, Format { name, func });
// register("regex", validate_regex); // Stubbed
register("ipv4", validate_ipv4);
register("ipv6", validate_ipv6);
register("hostname", validate_hostname);
register("idn-hostname", validate_idn_hostname);
register("email", validate_email);
register("idn-email", validate_idn_email);
register("date", validate_date);
register("time", validate_time);
register("date-time", validate_date_time);
register("duration", validate_duration);
register("period", validate_period);
register("json-pointer", validate_json_pointer);
register("relative-json-pointer", validate_relative_json_pointer);
register("uuid", validate_uuid);
register("uri", validate_uri);
register("iri", validate_iri);
register("uri-reference", validate_uri_reference);
register("iri-reference", validate_iri_reference);
register("uri-template", validate_uri_template);
m
};
}
/*
fn validate_regex(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
let Value::String(s) = v else {
return Ok(());
};
// ecma::convert(s).map(|_| ())
Ok(())
}
*/
fn validate_ipv4(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
let Value::String(s) = v else {
return Ok(());
};
s.parse::<Ipv4Addr>()?;
Ok(())
}
fn validate_ipv6(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
let Value::String(s) = v else {
return Ok(());
};
s.parse::<Ipv6Addr>()?;
Ok(())
}
fn validate_date(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
let Value::String(s) = v else {
return Ok(());
};
check_date(s)?;
Ok(())
}
fn matches_char(s: &str, index: usize, ch: char) -> bool {
s.is_char_boundary(index) && s[index..].starts_with(ch)
}
// see https://datatracker.ietf.org/doc/html/rfc3339#section-5.6
fn check_date(s: &str) -> Result<(), Box<dyn Error + Send + Sync>> {
// yyyy-mm-dd
if s.len() != 10 {
Err("must be 10 characters long")?;
}
if !matches_char(s, 4, '-') || !matches_char(s, 7, '-') {
Err("missing hyphen in correct place")?;
}
let mut ymd = s.splitn(3, '-').filter_map(|t| t.parse::<usize>().ok());
let (Some(y), Some(m), Some(d)) = (ymd.next(), ymd.next(), ymd.next()) else {
Err("non-positive year/month/day")?
};
if !matches!(m, 1..=12) {
Err(format!("{m} months in year"))?;
}
if !matches!(d, 1..=31) {
Err(format!("{d} days in month"))?;
}
match m {
2 => {
let mut feb_days = 28;
if y % 4 == 0 && (y % 100 != 0 || y % 400 == 0) {
feb_days += 1; // leap year
};
if d > feb_days {
Err(format!("february has {feb_days} days only"))?;
}
}
4 | 6 | 9 | 11 => {
if d > 30 {
Err("month has 30 days only")?;
}
}
_ => {}
}
Ok(())
}
fn validate_time(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
let Value::String(s) = v else {
return Ok(());
};
check_time(s)
}
fn check_time(mut str: &str) -> Result<(), Box<dyn Error + Send + Sync>> {
// min: hh:mm:ssZ
if str.len() < 9 {
Err("less than 9 characters long")?
}
if !matches_char(str, 2, ':') || !matches_char(str, 5, ':') {
Err("missing colon in correct place")?
}
// parse hh:mm:ss
if !str.is_char_boundary(8) {
Err("contains non-ascii char")?
}
let mut hms = (str[..8])
.splitn(3, ':')
.filter_map(|t| t.parse::<usize>().ok());
let (Some(mut h), Some(mut m), Some(s)) = (hms.next(), hms.next(), hms.next()) else {
Err("non-positive hour/min/sec")?
};
if h > 23 || m > 59 || s > 60 {
Err("hour/min/sec out of range")?
}
str = &str[8..];
// parse sec-frac if present
if let Some(rem) = str.strip_prefix('.') {
let n_digits = rem.chars().take_while(char::is_ascii_digit).count();
if n_digits == 0 {
Err("no digits in second fraction")?;
}
str = &rem[n_digits..];
}
if str != "z" && str != "Z" {
// parse time-numoffset
if str.len() != 6 {
Err("offset must be 6 characters long")?;
}
let sign: isize = match str.chars().next() {
Some('+') => -1,
Some('-') => 1,
_ => return Err("offset must begin with plus/minus")?,
};
str = &str[1..];
if !matches_char(str, 2, ':') {
Err("missing colon in offset at correct place")?
}
let mut zhm = str.splitn(2, ':').filter_map(|t| t.parse::<usize>().ok());
let (Some(zh), Some(zm)) = (zhm.next(), zhm.next()) else {
Err("non-positive hour/min in offset")?
};
if zh > 23 || zm > 59 {
Err("hour/min in offset out of range")?
}
// apply timezone
let mut hm = (h * 60 + m) as isize + sign * (zh * 60 + zm) as isize;
if hm < 0 {
hm += 24 * 60;
debug_assert!(hm >= 0);
}
let hm = hm as usize;
(h, m) = (hm / 60, hm % 60);
}
// check leap second
if !(s < 60 || (h == 23 && m == 59)) {
Err("invalid leap second")?
}
Ok(())
}
fn validate_date_time(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
let Value::String(s) = v else {
return Ok(());
};
check_date_time(s)
}
fn check_date_time(s: &str) -> Result<(), Box<dyn Error + Send + Sync>> {
// min: yyyy-mm-ddThh:mm:ssZ
if s.len() < 20 {
Err("less than 20 characters long")?;
}
if !s.is_char_boundary(10) || !s[10..].starts_with(['t', 'T']) {
Err("11th character must be t or T")?;
}
if let Err(e) = check_date(&s[..10]) {
Err(format!("invalid date element: {e}"))?;
}
if let Err(e) = check_time(&s[11..]) {
Err(format!("invalid time element: {e}"))?;
}
Ok(())
}
fn validate_duration(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
let Value::String(s) = v else {
return Ok(());
};
check_duration(s)?;
Ok(())
}
// see https://datatracker.ietf.org/doc/html/rfc3339#appendix-A
fn check_duration(s: &str) -> Result<(), Box<dyn Error + Send + Sync>> {
// must start with 'P'
let Some(s) = s.strip_prefix('P') else {
Err("must start with P")?
};
if s.is_empty() {
Err("nothing after P")?
}
// dur-week
if let Some(s) = s.strip_suffix('W') {
if s.is_empty() {
Err("no number in week")?
}
if !s.chars().all(|c| c.is_ascii_digit()) {
Err("invalid week")?
}
return Ok(());
}
static UNITS: [&str; 2] = ["YMD", "HMS"];
for (i, s) in s.split('T').enumerate() {
let mut s = s;
if i != 0 && s.is_empty() {
Err("no time elements")?
}
let Some(mut units) = UNITS.get(i).cloned() else {
Err("more than one T")?
};
while !s.is_empty() {
let digit_count = s.chars().take_while(char::is_ascii_digit).count();
if digit_count == 0 {
Err("missing number")?
}
s = &s[digit_count..];
let Some(unit) = s.chars().next() else {
Err("missing unit")?
};
let Some(j) = units.find(unit) else {
if UNITS[i].contains(unit) {
Err(format!("unit {unit} out of order"))?
}
Err(format!("invalid unit {unit}"))?
};
units = &units[j + 1..];
s = &s[1..];
}
}
Ok(())
}
// see https://datatracker.ietf.org/doc/html/rfc3339#appendix-A
fn validate_period(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
let Value::String(s) = v else {
return Ok(());
};
let Some(slash) = s.find('/') else {
Err("missing slash")?
};
let (start, end) = (&s[..slash], &s[slash + 1..]);
if start.starts_with('P') {
if let Err(e) = check_duration(start) {
Err(format!("invalid start duration: {e}"))?
}
if let Err(e) = check_date_time(end) {
Err(format!("invalid end date-time: {e}"))?
}
} else {
if let Err(e) = check_date_time(start) {
Err(format!("invalid start date-time: {e}"))?
}
if end.starts_with('P') {
if let Err(e) = check_duration(end) {
Err(format!("invalid end duration: {e}"))?;
}
} else if let Err(e) = check_date_time(end) {
Err(format!("invalid end date-time: {e}"))?;
}
}
Ok(())
}
fn validate_hostname(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
let Value::String(s) = v else {
return Ok(());
};
check_hostname(s)?;
Ok(())
}
// see https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names
fn check_hostname(s: &str) -> Result<(), Box<dyn Error + Send + Sync>> {
// entire hostname (including the delimiting dots but not a trailing dot) has a maximum of 253 ASCII characters
if s.len() > 253 {
Err("more than 253 characters long")?
}
// Hostnames are composed of series of labels concatenated with dots, as are all domain names
for label in s.split('.') {
// Each label must be from 1 to 63 characters long
if !matches!(label.len(), 1..=63) {
Err("label must be 1 to 63 characters long")?;
}
// labels must not start or end with a hyphen
if label.starts_with('-') {
Err("label starts with hyphen")?;
}
if label.ends_with('-') {
Err("label ends with hyphen")?;
}
// labels may contain only the ASCII letters 'a' through 'z' (in a case-insensitive manner),
// the digits '0' through '9', and the hyphen ('-')
if let Some(ch) = label
.chars()
.find(|c| !matches!(c, 'a'..='z' | 'A'..='Z' | '0'..='9' | '-'))
{
Err(format!("invalid character {ch:?}"))?;
}
// labels must not contain "--" in 3rd and 4th position unless they start with "xn--"
if label.len() >= 4 && &label[2..4] == "--" {
if !label.starts_with("xn--") {
Err("label has -- in 3rd/4th position but does not start with xn--")?;
} else {
let (unicode, errors) = idna::domain_to_unicode(label);
if let Err(_) = errors {
Err("invalid punycode")?;
}
check_unicode_idn_constraints(&unicode).map_err(|e| format!("invalid punycode/IDN: {e}"))?;
}
}
}
Ok(())
}
fn validate_idn_hostname(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
let Value::String(s) = v else {
return Ok(());
};
check_idn_hostname(s)?;
Ok(())
}
static DISALLOWED: [char; 10] = [
'\u{0640}', // ARABIC TATWEEL
'\u{07FA}', // NKO LAJANYALAN
'\u{302E}', // HANGUL SINGLE DOT TONE MARK
'\u{302F}', // HANGUL DOUBLE DOT TONE MARK
'\u{3031}', // VERTICAL KANA REPEAT MARK
'\u{3032}', // VERTICAL KANA REPEAT WITH VOICED SOUND MARK
'\u{3033}', // VERTICAL KANA REPEAT MARK UPPER HALF
'\u{3034}', // VERTICAL KANA REPEAT WITH VOICED SOUND MARK UPPER HA
'\u{3035}', // VERTICAL KANA REPEAT MARK LOWER HALF
'\u{303B}', // VERTICAL IDEOGRAPHIC ITERATION MARK
];
fn check_idn_hostname(s: &str) -> Result<(), Box<dyn Error + Send + Sync>> {
let s = idna::domain_to_ascii_strict(s).map_err(|e| format!("idna error: {:?}", e))?;
let (unicode, errors) = idna::domain_to_unicode(&s);
if let Err(e) = errors {
Err(format!("idna decoding error: {:?}", e))?;
}
check_unicode_idn_constraints(&unicode)?;
check_hostname(&s)?;
Ok(())
}
fn check_unicode_idn_constraints(unicode: &str) -> Result<(), Box<dyn Error + Send + Sync>> {
// see https://www.rfc-editor.org/rfc/rfc5892#section-2.6
{
if unicode.contains(DISALLOWED) {
Err("contains disallowed character")?;
}
}
// unicode string must not contain "--" in 3rd and 4th position
// and must not start and end with a '-'
// see https://www.rfc-editor.org/rfc/rfc5891#section-4.2.3.1
{
let count: usize = unicode
.chars()
.skip(2)
.take(2)
.map(|c| if c == '-' { 1 } else { 0 })
.sum();
if count == 2 {
Err("unicode string must not contain '--' in 3rd and 4th position")?;
}
}
// MIDDLE DOT is allowed between 'l' characters only
// see https://www.rfc-editor.org/rfc/rfc5892#appendix-A.3
{
let middle_dot = '\u{00b7}';
let mut s = unicode;
while let Some(i) = s.find(middle_dot) {
let prefix = &s[..i];
let suffix = &s[i + middle_dot.len_utf8()..];
if !prefix.ends_with('l') || !suffix.ends_with('l') {
Err("MIDDLE DOT is allowed between 'l' characters only")?;
}
s = suffix;
}
}
// Greek KERAIA must be followed by Greek character
// see https://www.rfc-editor.org/rfc/rfc5892#appendix-A.4
{
let keralia = '\u{0375}';
let greek = '\u{0370}'..='\u{03FF}';
let mut s = unicode;
while let Some(i) = s.find(keralia) {
let suffix = &s[i + keralia.len_utf8()..];
if !suffix.starts_with(|c| greek.contains(&c)) {
Err("Greek KERAIA must be followed by Greek character")?;
}
s = suffix;
}
}
// Hebrew GERESH must be preceded by Hebrew character
// see https://www.rfc-editor.org/rfc/rfc5892#appendix-A.5
//
// Hebrew GERSHAYIM must be preceded by Hebrew character
// see https://www.rfc-editor.org/rfc/rfc5892#appendix-A.6
{
let geresh = '\u{05F3}';
let gereshayim = '\u{05F4}';
let hebrew = '\u{0590}'..='\u{05FF}';
for ch in [geresh, gereshayim] {
let mut s = unicode;
while let Some(i) = s.find(ch) {
let prefix = &s[..i];
if !prefix.ends_with(|c| hebrew.contains(&c)) {
if i == 0 {
Err("Hebrew GERESH must be preceded by Hebrew character")?;
} else {
Err("Hebrew GERESHYIM must be preceded by Hebrew character")?;
}
}
let suffix = &s[i + ch.len_utf8()..];
s = suffix;
}
}
}
// KATAKANA MIDDLE DOT must be with Hiragana, Katakana, or Han
// see https://www.rfc-editor.org/rfc/rfc5892#appendix-A.7
{
let katakana_middle_dot = '\u{30FB}';
if unicode.contains(katakana_middle_dot) {
let hiragana = '\u{3040}'..='\u{309F}';
let katakana = '\u{30A0}'..='\u{30FF}';
let han = '\u{4E00}'..='\u{9FFF}'; // https://en.wikipedia.org/wiki/CJK_Unified_Ideographs_(Unicode_block): is this range correct??
if unicode.contains(|c| hiragana.contains(&c))
|| unicode.contains(|c| c != katakana_middle_dot && katakana.contains(&c))
|| unicode.contains(|c| han.contains(&c))
{
// ok
} else {
Err("KATAKANA MIDDLE DOT must be with Hiragana, Katakana, or Han")?;
}
}
}
// ARABIC-INDIC DIGITS and Extended Arabic-Indic Digits cannot be mixed
// see https://www.rfc-editor.org/rfc/rfc5892#appendix-A.8
// see https://www.rfc-editor.org/rfc/rfc5892#appendix-A.9
{
let arabic_indic_digits = '\u{0660}'..='\u{0669}';
let extended_arabic_indic_digits = '\u{06F0}'..='\u{06F9}';
if unicode.contains(|c| arabic_indic_digits.contains(&c))
&& unicode.contains(|c| extended_arabic_indic_digits.contains(&c))
{
Err("ARABIC-INDIC DIGITS and Extended Arabic-Indic Digits cannot be mixed")?;
}
}
// ZERO WIDTH JOINER must be preceded by Virama
// see https://www.rfc-editor.org/rfc/rfc5892#appendix-A.2
{
let zero_width_jointer = '\u{200D}';
static VIRAMA: [char; 61] = [
'\u{094D}',
'\u{09CD}',
'\u{0A4D}',
'\u{0ACD}',
'\u{0B4D}',
'\u{0BCD}',
'\u{0C4D}',
'\u{0CCD}',
'\u{0D3B}',
'\u{0D3C}',
'\u{0D4D}',
'\u{0DCA}',
'\u{0E3A}',
'\u{0EBA}',
'\u{0F84}',
'\u{1039}',
'\u{103A}',
'\u{1714}',
'\u{1734}',
'\u{17D2}',
'\u{1A60}',
'\u{1B44}',
'\u{1BAA}',
'\u{1BAB}',
'\u{1BF2}',
'\u{1BF3}',
'\u{2D7F}',
'\u{A806}',
'\u{A82C}',
'\u{A8C4}',
'\u{A953}',
'\u{A9C0}',
'\u{AAF6}',
'\u{ABED}',
'\u{10A3F}',
'\u{11046}',
'\u{1107F}',
'\u{110B9}',
'\u{11133}',
'\u{11134}',
'\u{111C0}',
'\u{11235}',
'\u{112EA}',
'\u{1134D}',
'\u{11442}',
'\u{114C2}',
'\u{115BF}',
'\u{1163F}',
'\u{116B6}',
'\u{1172B}',
'\u{11839}',
'\u{1193D}',
'\u{1193E}',
'\u{119E0}',
'\u{11A34}',
'\u{11A47}',
'\u{11A99}',
'\u{11C3F}',
'\u{11D44}',
'\u{11D45}',
'\u{11D97}',
]; // https://www.compart.com/en/unicode/combining/9
let mut s = unicode;
while let Some(i) = s.find(zero_width_jointer) {
let prefix = &s[..i];
if !prefix.ends_with(VIRAMA) {
Err("ZERO WIDTH JOINER must be preceded by Virama")?;
}
let suffix = &s[i + zero_width_jointer.len_utf8()..];
s = suffix;
}
}
Ok(())
}
fn validate_email(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
let Value::String(s) = v else {
return Ok(());
};
check_email(s)?;
Ok(())
}
// see https://en.wikipedia.org/wiki/Email_address
fn check_email(s: &str) -> Result<(), Box<dyn Error + Send + Sync>> {
// entire email address to be no more than 254 characters long
if s.len() > 254 {
Err("more than 254 characters long")?
}
// email address is generally recognized as having two parts joined with an at-sign
let Some(at) = s.rfind('@') else {
Err("missing @")?
};
let (local, domain) = (&s[..at], &s[at + 1..]);
// local part may be up to 64 characters long
if local.len() > 64 {
Err("local part more than 64 characters long")?
}
if local.len() > 1 && local.starts_with('"') && local.ends_with('"') {
// quoted
let local = &local[1..local.len() - 1];
if local.contains(['\\', '"']) {
Err("backslash and quote not allowed within quoted local part")?
}
} else {
// unquoted
if local.starts_with('.') {
Err("starts with dot")?
}
if local.ends_with('.') {
Err("ends with dot")?
}
// consecutive dots not allowed
if local.contains("..") {
Err("consecutive dots")?
}
// check allowd chars
if let Some(ch) = local
.chars()
.find(|c| !(c.is_ascii_alphanumeric() || ".!#$%&'*+-/=?^_`{|}~".contains(*c)))
{
Err(format!("invalid character {ch:?}"))?
}
}
// domain if enclosed in brackets, must match an IP address
if domain.starts_with('[') && domain.ends_with(']') {
let s = &domain[1..domain.len() - 1];
if let Some(s) = s.strip_prefix("IPv6:") {
if let Err(e) = s.parse::<Ipv6Addr>() {
Err(format!("invalid ipv6 address: {e}"))?
}
return Ok(());
}
if let Err(e) = s.parse::<Ipv4Addr>() {
Err(format!("invalid ipv4 address: {e}"))?
}
return Ok(());
}
// domain must match the requirements for a hostname
if let Err(e) = check_hostname(domain) {
Err(format!("invalid domain: {e}"))?
}
Ok(())
}
fn validate_idn_email(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
let Value::String(s) = v else {
return Ok(());
};
let Some(at) = s.rfind('@') else {
Err("missing @")?
};
let (local, domain) = (&s[..at], &s[at + 1..]);
let local = idna::domain_to_ascii_strict(local).map_err(|e| format!("idna error: {:?}", e))?;
let domain = idna::domain_to_ascii_strict(domain).map_err(|e| format!("idna error: {:?}", e))?;
if let Err(e) = check_idn_hostname(&domain) {
Err(format!("invalid domain: {e}"))?
}
check_email(&format!("{local}@{domain}"))
}
fn validate_json_pointer(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
let Value::String(s) = v else {
return Ok(());
};
check_json_pointer(s)?;
Ok(())
}
// see https://www.rfc-editor.org/rfc/rfc6901#section-3
fn check_json_pointer(s: &str) -> Result<(), Box<dyn Error + Send + Sync>> {
if s.is_empty() {
return Ok(());
}
if !s.starts_with('/') {
Err("not starting with slash")?;
}
for token in s.split('/').skip(1) {
let mut chars = token.chars();
while let Some(ch) = chars.next() {
if ch == '~' {
if !matches!(chars.next(), Some('0' | '1')) {
Err("~ must be followed by 0 or 1")?;
}
} else if !matches!(ch, '\x00'..='\x2E' | '\x30'..='\x7D' | '\x7F'..='\u{10FFFF}') {
Err("contains disallowed character")?;
}
}
}
Ok(())
}
// see https://tools.ietf.org/html/draft-handrews-relative-json-pointer-01#section-3
fn validate_relative_json_pointer(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
let Value::String(s) = v else {
return Ok(());
};
// start with non-negative-integer
let num_digits = s.chars().take_while(char::is_ascii_digit).count();
if num_digits == 0 {
Err("must start with non-negative integer")?;
}
if num_digits > 1 && s.starts_with('0') {
Err("starts with zero")?;
}
let s = &s[num_digits..];
// followed by either json-pointer or '#'
if s == "#" {
return Ok(());
}
if let Err(e) = check_json_pointer(s) {
Err(format!("invalid json-pointer element: {e}"))?;
}
Ok(())
}
// see https://datatracker.ietf.org/doc/html/rfc4122#page-4
fn validate_uuid(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
let Value::String(s) = v else {
return Ok(());
};
static HEX_GROUPS: [usize; 5] = [8, 4, 4, 4, 12];
let mut i = 0;
for group in s.split('-') {
if i >= HEX_GROUPS.len() {
Err("more than 5 elements")?;
}
if group.len() != HEX_GROUPS[i] {
Err(format!(
"element {} must be {} characters long",
i + 1,
HEX_GROUPS[i]
))?;
}
if let Some(ch) = group.chars().find(|c| !c.is_ascii_hexdigit()) {
Err(format!("non-hex character {ch:?}"))?;
}
i += 1;
}
if i != HEX_GROUPS.len() {
Err("must have 5 elements")?;
}
Ok(())
}
fn validate_uri(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
let Value::String(s) = v else {
return Ok(());
};
if fluent_uri::UriRef::parse(s.as_str()).map_err(|e| e.to_string())?.scheme().is_none() {
Err("relative url")?;
};
Ok(())
}
fn validate_iri(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
let Value::String(s) = v else {
return Ok(());
};
match Url::parse(s) {
Ok(_) => Ok(()),
Err(url::ParseError::RelativeUrlWithoutBase) => Err("relative url")?,
Err(e) => Err(e)?,
}
}
lazy_static! {
static ref TEMP_URL: Url = Url::parse("http://temp.com").unwrap();
}
fn parse_uri_reference(s: &str) -> Result<Url, Box<dyn Error + Send + Sync>> {
if s.contains('\\') {
Err("contains \\\\")?;
}
Ok(TEMP_URL.join(s)?)
}
fn validate_uri_reference(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
let Value::String(s) = v else {
return Ok(());
};
fluent_uri::UriRef::parse(s.as_str()).map_err(|e| e.to_string())?;
Ok(())
}
fn validate_iri_reference(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
let Value::String(s) = v else {
return Ok(());
};
parse_uri_reference(s)?;
Ok(())
}
fn validate_uri_template(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
let Value::String(s) = v else {
return Ok(());
};
let url = parse_uri_reference(s)?;
let path = url.path();
// path we got has curly bases percent encoded
let path = percent_decode_str(path).decode_utf8()?;
// ensure curly brackets are not nested and balanced
for part in path.as_ref().split('/') {
let mut want = true;
for got in part
.chars()
.filter(|c| matches!(c, '{' | '}'))
.map(|c| c == '{')
{
if got != want {
Err("nested curly braces")?;
}
want = !want;
}
if !want {
Err("no matching closing brace")?
}
}
Ok(())
}

98
src/validator/instance.rs Normal file
View File

@ -0,0 +1,98 @@
use serde_json::Value;
use std::collections::HashSet;
use std::ptr::NonNull;
pub trait ValidationInstance<'a>: Copy + Clone {
fn as_value(&self) -> &'a Value;
fn child_at_key(&self, key: &str) -> Option<Self>;
fn child_at_index(&self, idx: usize) -> Option<Self>;
fn prune_object(&self, _keys: &HashSet<String>) {}
fn prune_array(&self, _indices: &HashSet<usize>) {}
}
#[derive(Clone, Copy)]
pub struct ReadOnlyInstance<'a>(pub &'a Value);
impl<'a> ValidationInstance<'a> for ReadOnlyInstance<'a> {
fn as_value(&self) -> &'a Value {
self.0
}
fn child_at_key(&self, key: &str) -> Option<Self> {
self.0.get(key).map(ReadOnlyInstance)
}
fn child_at_index(&self, idx: usize) -> Option<Self> {
self.0.get(idx).map(ReadOnlyInstance)
}
}
#[derive(Clone, Copy)]
pub struct MutableInstance {
ptr: NonNull<Value>,
}
impl MutableInstance {
pub fn new(val: &mut Value) -> Self {
Self {
ptr: NonNull::from(val),
}
}
}
impl<'a> ValidationInstance<'a> for MutableInstance {
fn as_value(&self) -> &'a Value {
unsafe { self.ptr.as_ref() }
}
fn child_at_key(&self, key: &str) -> Option<Self> {
unsafe {
if let Some(obj) = self.ptr.as_ref().as_object() {
if obj.contains_key(key) {
let parent_mut = &mut *self.ptr.as_ptr();
if let Some(child_val) = parent_mut.get_mut(key) {
return Some(MutableInstance::new(child_val));
}
}
}
None
}
}
fn child_at_index(&self, idx: usize) -> Option<Self> {
unsafe {
if let Some(arr) = self.ptr.as_ref().as_array() {
if idx < arr.len() {
let parent_mut = &mut *self.ptr.as_ptr();
if let Some(child_val) = parent_mut.get_mut(idx) {
return Some(MutableInstance::new(child_val));
}
}
}
None
}
}
fn prune_object(&self, keys: &HashSet<String>) {
unsafe {
let val_mut = &mut *self.ptr.as_ptr();
if let Some(obj) = val_mut.as_object_mut() {
obj.retain(|k, _| keys.contains(k));
}
}
}
fn prune_array(&self, indices: &HashSet<usize>) {
unsafe {
let val_mut = &mut *self.ptr.as_ptr();
if let Some(arr) = val_mut.as_array_mut() {
let mut i = 0;
arr.retain(|_| {
let keep = indices.contains(&i);
i += 1;
keep
});
}
}
}
}

269
src/validator/mod.rs Normal file
View File

@ -0,0 +1,269 @@
pub mod compiler;
pub mod context;
pub mod error;
pub mod formats;
pub mod instance;
pub mod registry;
pub mod result;
pub mod rules;
pub mod schema;
pub mod util;
pub use context::ValidationContext;
pub use error::ValidationError;
pub use instance::{MutableInstance, ReadOnlyInstance};
pub use result::ValidationResult;
use crate::validator::registry::Registry;
use crate::validator::schema::Schema;
use serde_json::Value;
use std::collections::HashSet;
use std::sync::Arc;
pub enum ResolvedRef<'a> {
Local(&'a Schema),
Global(&'a Schema, &'a Schema),
}
pub struct Validator {
pub registry: Registry,
pub families: std::collections::HashMap<String, Arc<Schema>>,
}
impl Validator {
pub fn from_punc_definition(
enums: Option<&Value>,
types: Option<&Value>,
puncs: Option<&Value>,
) -> Self {
let mut registry = Registry::new();
let mut families = std::collections::HashMap::new();
let mut family_map: std::collections::HashMap<String, std::collections::HashSet<String>> =
std::collections::HashMap::new();
if let Some(Value::Array(arr)) = types {
for item in arr {
if let Some(name) = item.get("name").and_then(|v| v.as_str()) {
if let Some(hierarchy) = item.get("hierarchy").and_then(|v| v.as_array()) {
for ancestor in hierarchy {
if let Some(anc_str) = ancestor.as_str() {
family_map
.entry(anc_str.to_string())
.or_default()
.insert(name.to_string());
}
}
}
}
}
}
for (family_name, members) in family_map {
let object_refs: Vec<Value> = members
.iter()
.map(|s| serde_json::json!({ "$ref": s }))
.collect();
let schema_json = serde_json::json!({
"oneOf": object_refs
});
if let Ok(schema) = serde_json::from_value::<Schema>(schema_json) {
let compiled = crate::validator::compiler::Compiler::compile(schema, None);
families.insert(family_name, compiled);
}
}
let mut cache_items = |items_val: Option<&Value>| {
if let Some(Value::Array(arr)) = items_val {
for item in arr {
if let Some(Value::Array(schemas)) = item.get("schemas") {
for schema_val in schemas {
if let Ok(schema) = serde_json::from_value::<Schema>(schema_val.clone()) {
registry.add(schema);
}
}
}
}
}
};
cache_items(enums);
cache_items(types);
cache_items(puncs);
Self { registry, families }
}
pub fn get_schema_ids(&self) -> Vec<String> {
self.registry.schemas.keys().cloned().collect()
}
pub fn check_type(t: &str, val: &Value) -> bool {
if let Value::String(s) = val {
if s.is_empty() {
return true;
}
}
match t {
"null" => val.is_null(),
"boolean" => val.is_boolean(),
"string" => val.is_string(),
"number" => val.is_number(),
"integer" => crate::validator::util::is_integer(val),
"object" => val.is_object(),
"array" => val.is_array(),
_ => true,
}
}
pub fn resolve_ref<'a>(
&'a self,
root: &'a Schema,
ref_string: &str,
scope: &str,
) -> Option<(ResolvedRef<'a>, String)> {
if ref_string.starts_with('#') {
if let Some(indexrs) = &root.obj.compiled_registry {
if let Some(s) = indexrs.schemas.get(ref_string) {
return Some((ResolvedRef::Local(s.as_ref()), ref_string.to_string()));
}
}
}
if let Ok(base) = url::Url::parse(scope) {
if let Ok(joined) = base.join(ref_string) {
let joined_str = joined.to_string();
if let Some(indexrs) = &root.obj.compiled_registry {
if let Some(s) = indexrs.schemas.get(&joined_str) {
return Some((ResolvedRef::Local(s.as_ref() as &Schema), joined_str));
}
}
if let Ok(decoded) = percent_encoding::percent_decode_str(&joined_str).decode_utf8() {
let decoded_str = decoded.to_string();
if decoded_str != joined_str {
if let Some(indexrs) = &root.obj.compiled_registry {
if let Some(s) = indexrs.schemas.get(&decoded_str) {
return Some((ResolvedRef::Local(s.as_ref() as &Schema), decoded_str));
}
}
}
}
if let Some(s) = self.registry.schemas.get(&joined_str) {
return Some((ResolvedRef::Global(s.as_ref(), s.as_ref()), joined_str));
}
}
} else {
if ref_string.starts_with('#') {
let joined_str = format!("{}{}", scope, ref_string);
if let Some(indexrs) = &root.obj.compiled_registry {
if let Some(s) = indexrs.schemas.get(&joined_str) {
return Some((ResolvedRef::Local(s.as_ref() as &Schema), joined_str));
}
}
if let Ok(decoded) = percent_encoding::percent_decode_str(&joined_str).decode_utf8() {
let decoded_str = decoded.to_string();
if decoded_str != joined_str {
if let Some(indexrs) = &root.obj.compiled_registry {
if let Some(s) = indexrs.schemas.get(&decoded_str) {
return Some((ResolvedRef::Local(s.as_ref() as &Schema), decoded_str));
}
}
}
}
if let Some(s) = self.registry.schemas.get(&joined_str) {
return Some((ResolvedRef::Global(s.as_ref(), s.as_ref()), joined_str));
}
}
}
if let Ok(parsed) = url::Url::parse(ref_string) {
let absolute = parsed.to_string();
if let Some(indexrs) = &root.obj.compiled_registry {
if let Some(s) = indexrs.schemas.get(&absolute) {
return Some((ResolvedRef::Local(s.as_ref()), absolute));
}
}
let resource_base = if let Some((base, _)) = absolute.split_once('#') {
base
} else {
&absolute
};
if let Some(compiled) = self.registry.schemas.get(resource_base) {
if let Some(indexrs) = &compiled.obj.compiled_registry {
if let Some(s) = indexrs.schemas.get(&absolute) {
return Some((ResolvedRef::Global(compiled.as_ref(), s.as_ref()), absolute));
}
}
}
}
if let Some(compiled) = self.registry.schemas.get(ref_string) {
return Some((
ResolvedRef::Global(compiled.as_ref(), compiled.as_ref()),
ref_string.to_string(),
));
}
None
}
pub fn validate(
&self,
schema_id: &str,
instance: &Value,
) -> Result<ValidationResult, ValidationError> {
if let Some(schema) = self.registry.schemas.get(schema_id) {
let ctx = ValidationContext::new(
self,
schema,
schema,
ReadOnlyInstance(instance),
vec![],
HashSet::new(),
false,
false,
);
ctx.validate()
} else {
Err(ValidationError {
code: "SCHEMA_NOT_FOUND".to_string(),
message: format!("Schema {} not found", schema_id),
path: "".to_string(),
})
}
}
pub fn mask(
&self,
schema_id: &str,
instance: &mut Value,
) -> Result<ValidationResult, ValidationError> {
if let Some(schema) = self.registry.schemas.get(schema_id) {
let ctx = ValidationContext::new(
self,
schema,
schema,
MutableInstance::new(instance),
vec![],
HashSet::new(),
false,
false,
);
let res = ctx.validate()?;
Ok(res)
} else {
Err(ValidationError {
code: "SCHEMA_NOT_FOUND".to_string(),
message: format!("Schema {} not found", schema_id),
path: "".to_string(),
})
}
}
}

50
src/validator/registry.rs Normal file
View File

@ -0,0 +1,50 @@
use crate::validator::schema::Schema;
use lazy_static::lazy_static;
use std::collections::HashMap;
use std::sync::RwLock;
lazy_static! {
pub static ref REGISTRY: RwLock<Registry> = RwLock::new(Registry::new());
}
use std::sync::Arc;
#[derive(Debug, Clone, Default)]
pub struct Registry {
pub schemas: HashMap<String, Arc<Schema>>,
}
impl Registry {
pub fn new() -> Self {
Registry {
schemas: HashMap::new(),
}
}
pub fn add(&mut self, schema: crate::validator::schema::Schema) {
let id = schema
.obj
.id
.clone()
.expect("Schema must have an $id to be registered");
let compiled = crate::validator::compiler::Compiler::compile(schema, Some(id.clone()));
self.schemas.insert(id, compiled);
}
pub fn insert(&mut self, id: String, schema: Arc<Schema>) {
// We allow overwriting for now to support re-compilation in tests/dev
self.schemas.insert(id, schema);
}
pub fn get(&self, id: &str) -> Option<Arc<Schema>> {
self.schemas.get(id).cloned()
}
pub fn clear(&mut self) {
self.schemas.clear();
}
pub fn len(&self) -> usize {
self.schemas.len()
}
}

27
src/validator/result.rs Normal file
View File

@ -0,0 +1,27 @@
use crate::validator::error::ValidationError;
use std::collections::HashSet;
#[derive(Debug, Default, Clone, serde::Serialize)]
pub struct ValidationResult {
pub errors: Vec<ValidationError>,
#[serde(skip)]
pub evaluated_keys: HashSet<String>,
#[serde(skip)]
pub evaluated_indices: HashSet<usize>,
}
impl ValidationResult {
pub fn new() -> Self {
Self::default()
}
pub fn merge(&mut self, other: ValidationResult) {
self.errors.extend(other.errors);
self.evaluated_keys.extend(other.evaluated_keys);
self.evaluated_indices.extend(other.evaluated_indices);
}
pub fn is_valid(&self) -> bool {
self.errors.is_empty()
}
}

1008
src/validator/rules.rs Normal file

File diff suppressed because it is too large Load Diff

222
src/validator/schema.rs Normal file
View File

@ -0,0 +1,222 @@
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::BTreeMap;
use std::sync::Arc;
// Schema mirrors the Go Punc Generator's schema struct for consistency.
// It is an order-preserving representation of a JSON Schema.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SchemaObject {
// Core Schema Keywords
#[serde(rename = "$id")]
pub id: Option<String>,
#[serde(rename = "$ref")]
pub ref_string: Option<String>,
#[serde(rename = "$anchor")]
pub anchor: Option<String>,
#[serde(rename = "$dynamicAnchor")]
pub dynamic_anchor: Option<String>,
#[serde(rename = "$dynamicRef")]
pub dynamic_ref: Option<String>,
/*
Note: The `Ref` field in the Go struct is a pointer populated by the linker.
In Rust, we might handle this differently (e.g., separate lookup or Rc/Arc),
so we omit the direct recursive `Ref` field for now and rely on `ref_string`.
*/
pub description: Option<String>,
pub title: Option<String>,
#[serde(default)] // Allow missing type
#[serde(rename = "type")]
pub type_: Option<SchemaTypeOrArray>, // Handles string or array of strings
// Object Keywords
pub properties: Option<BTreeMap<String, Arc<Schema>>>,
#[serde(rename = "patternProperties")]
pub pattern_properties: Option<BTreeMap<String, Arc<Schema>>>,
#[serde(rename = "additionalProperties")]
pub additional_properties: Option<Arc<Schema>>,
#[serde(rename = "$family")]
pub family: Option<String>,
pub required: Option<Vec<String>>,
// dependencies can be schema dependencies or property dependencies
pub dependencies: Option<BTreeMap<String, Dependency>>,
// Definitions (for $ref resolution)
#[serde(rename = "$defs")]
pub defs: Option<BTreeMap<String, Arc<Schema>>>,
#[serde(rename = "definitions")]
pub definitions: Option<BTreeMap<String, Arc<Schema>>>,
// Array Keywords
#[serde(rename = "items")]
pub items: Option<Arc<Schema>>,
#[serde(rename = "prefixItems")]
pub prefix_items: Option<Vec<Arc<Schema>>>,
// String Validation
#[serde(rename = "minLength")]
pub min_length: Option<f64>,
#[serde(rename = "maxLength")]
pub max_length: Option<f64>,
pub pattern: Option<String>,
// Array Validation
#[serde(rename = "minItems")]
pub min_items: Option<f64>,
#[serde(rename = "maxItems")]
pub max_items: Option<f64>,
#[serde(rename = "uniqueItems")]
pub unique_items: Option<bool>,
#[serde(rename = "contains")]
pub contains: Option<Arc<Schema>>,
#[serde(rename = "minContains")]
pub min_contains: Option<f64>,
#[serde(rename = "maxContains")]
pub max_contains: Option<f64>,
// Object Validation
#[serde(rename = "minProperties")]
pub min_properties: Option<f64>,
#[serde(rename = "maxProperties")]
pub max_properties: Option<f64>,
#[serde(rename = "propertyNames")]
pub property_names: Option<Arc<Schema>>,
#[serde(rename = "dependentRequired")]
pub dependent_required: Option<BTreeMap<String, Vec<String>>>,
#[serde(rename = "dependentSchemas")]
pub dependent_schemas: Option<BTreeMap<String, Arc<Schema>>>,
// Numeric Validation
pub format: Option<String>,
#[serde(rename = "enum")]
pub enum_: Option<Vec<Value>>, // `enum` is a reserved keyword in Rust
#[serde(
default,
rename = "const",
deserialize_with = "crate::validator::util::deserialize_some"
)]
pub const_: Option<Value>,
// Numeric Validation
#[serde(rename = "multipleOf")]
pub multiple_of: Option<f64>,
pub minimum: Option<f64>,
pub maximum: Option<f64>,
#[serde(rename = "exclusiveMinimum")]
pub exclusive_minimum: Option<f64>,
#[serde(rename = "exclusiveMaximum")]
pub exclusive_maximum: Option<f64>,
// Combining Keywords
#[serde(rename = "allOf")]
pub all_of: Option<Vec<Arc<Schema>>>,
#[serde(rename = "anyOf")]
pub any_of: Option<Vec<Arc<Schema>>>,
#[serde(rename = "oneOf")]
pub one_of: Option<Vec<Arc<Schema>>>,
#[serde(rename = "not")]
pub not: Option<Arc<Schema>>,
#[serde(rename = "if")]
pub if_: Option<Arc<Schema>>,
#[serde(rename = "then")]
pub then_: Option<Arc<Schema>>,
#[serde(rename = "else")]
pub else_: Option<Arc<Schema>>,
// Custom Vocabularies
pub form: Option<Vec<String>>,
pub display: Option<Vec<String>>,
#[serde(rename = "enumNames")]
pub enum_names: Option<Vec<String>>,
pub control: Option<String>,
pub actions: Option<BTreeMap<String, Action>>,
pub computer: Option<String>,
#[serde(default)]
pub extensible: Option<bool>,
// Compiled Fields (Hidden from JSON/Serde)
#[serde(skip)]
pub compiled_format: Option<crate::validator::compiler::CompiledFormat>,
#[serde(skip)]
pub compiled_pattern: Option<crate::validator::compiler::CompiledRegex>,
#[serde(skip)]
pub compiled_pattern_properties: Option<Vec<(crate::validator::compiler::CompiledRegex, Arc<Schema>)>>,
#[serde(skip)]
pub compiled_registry: Option<Arc<crate::validator::registry::Registry>>,
}
#[derive(Debug, Clone, Serialize)]
pub struct Schema {
#[serde(flatten)]
pub obj: SchemaObject,
#[serde(skip)]
pub always_fail: bool,
}
impl Default for Schema {
fn default() -> Self {
Schema {
obj: SchemaObject::default(),
always_fail: false,
}
}
}
impl std::ops::Deref for Schema {
type Target = SchemaObject;
fn deref(&self) -> &Self::Target {
&self.obj
}
}
impl std::ops::DerefMut for Schema {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.obj
}
}
impl<'de> Deserialize<'de> for Schema {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let v: Value = Deserialize::deserialize(deserializer)?;
if let Some(b) = v.as_bool() {
let mut obj = SchemaObject::default();
if b {
obj.extensible = Some(true);
}
return Ok(Schema {
obj,
always_fail: !b,
});
}
let obj: SchemaObject = serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?;
Ok(Schema {
obj,
always_fail: false,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum SchemaTypeOrArray {
Single(String),
Multiple(Vec<String>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Action {
pub navigate: Option<String>,
pub punc: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Dependency {
Props(Vec<String>),
Schema(Arc<Schema>),
}

323
src/validator/util.rs Normal file
View File

@ -0,0 +1,323 @@
use serde::Deserialize;
use std::fs;
#[derive(Debug, Deserialize)]
struct TestSuite {
#[allow(dead_code)]
description: String,
schema: Option<serde_json::Value>,
// Support JSPG-style test suites with explicit types/enums/puncs
types: Option<serde_json::Value>,
enums: Option<serde_json::Value>,
puncs: Option<serde_json::Value>,
tests: Vec<TestCase>,
}
#[derive(Debug, Deserialize)]
struct TestCase {
description: String,
data: serde_json::Value,
valid: bool,
// Support explicit schema ID target for test case
schema_id: Option<String>,
// Expected output for masking tests
#[allow(dead_code)]
expected: Option<serde_json::Value>,
}
// use crate::validator::registry::REGISTRY; // No longer used directly for tests!
use crate::validator::Validator;
use serde_json::Value;
pub fn deserialize_some<'de, D>(deserializer: D) -> Result<Option<Value>, D::Error>
where
D: serde::Deserializer<'de>,
{
let v = Value::deserialize(deserializer)?;
Ok(Some(v))
}
pub fn run_test_file_at_index(path: &str, index: usize) -> Result<(), String> {
let content =
fs::read_to_string(path).unwrap_or_else(|_| panic!("Failed to read file: {}", path));
let suite: Vec<TestSuite> = serde_json::from_str(&content)
.unwrap_or_else(|e| panic!("Failed to parse JSON in {}: {}", path, e));
if index >= suite.len() {
panic!("Index {} out of bounds for file {}", index, path);
}
let group = &suite[index];
let mut failures = Vec::<String>::new();
// Create Validator Instance and parse enums, types, and puncs automatically
let mut validator = Validator::from_punc_definition(
group.enums.as_ref(),
group.types.as_ref(),
group.puncs.as_ref(),
);
// 3. Register root 'schemas' if present (generic test support)
// Some tests use a raw 'schema' or 'schemas' field at the group level
if let Some(schema_val) = &group.schema {
match serde_json::from_value::<crate::validator::schema::Schema>(schema_val.clone()) {
Ok(mut schema) => {
let id_clone = schema.obj.id.clone();
if id_clone.is_some() {
validator.registry.add(schema);
} else {
// Fallback ID if none provided in schema
let id = format!("test:{}:{}", path, index);
schema.obj.id = Some(id);
validator.registry.add(schema);
}
}
Err(e) => {
eprintln!(
"DEBUG: FAILED to deserialize group schema for index {}: {}",
index, e
);
}
}
}
// 4. Run Tests
for (_test_index, test) in group.tests.iter().enumerate() {
let mut schema_id = test.schema_id.clone();
// If no explicit schema_id, try to infer from the single schema in the group
if schema_id.is_none() {
if let Some(s) = &group.schema {
// If 'schema' is a single object, use its ID or "root"
if let Some(obj) = s.as_object() {
if let Some(id_val) = obj.get("$id") {
schema_id = id_val.as_str().map(|s| s.to_string());
}
}
if schema_id.is_none() {
schema_id = Some(format!("test:{}:{}", path, index));
}
}
}
// Default to the first punc if present (for puncs.json style)
if schema_id.is_none() {
if let Some(Value::Array(puncs)) = &group.puncs {
if let Some(first_punc) = puncs.first() {
if let Some(Value::Array(schemas)) = first_punc.get("schemas") {
if let Some(first_schema) = schemas.first() {
if let Some(id) = first_schema.get("$id").and_then(|v| v.as_str()) {
schema_id = Some(id.to_string());
}
}
}
}
}
}
if let Some(sid) = schema_id {
let result = validator.validate(&sid, &test.data);
let (got_valid, _errors) = match &result {
Ok(res) => (res.is_valid(), &res.errors),
Err(_e) => {
// If we encounter an execution error (e.g. Schema Not Found),
// we treat it as a test failure.
(false, &vec![])
}
};
if let Some(expected) = &test.expected {
// Masking Test
let mut data_for_mask = test.data.clone();
match validator.mask(&sid, &mut data_for_mask) {
Ok(_) => {
if !equals(&data_for_mask, expected) {
let msg = format!(
"Masking Test '{}' failed.\nExpected: {:?}\nGot: {:?}",
test.description, expected, data_for_mask
);
eprintln!("{}", msg);
failures.push(msg);
}
}
Err(e) => {
let msg = format!(
"Masking Test '{}' failed with execution error: {:?}",
test.description, e
);
eprintln!("{}", msg);
failures.push(msg);
}
}
} else {
// Standard Validation Test
if got_valid != test.valid {
let error_msg = match &result {
Ok(res) => format!("{:?}", res.errors),
Err(e) => format!("Execution Error: {:?}", e),
};
failures.push(format!(
"[{}] Test '{}' failed. Expected: {}, Got: {}. Errors: {}",
group.description, test.description, test.valid, got_valid, error_msg
));
}
}
} else {
failures.push(format!(
"[{}] Test '{}' skipped: No schema ID found.",
group.description, test.description
));
}
}
if !failures.is_empty() {
return Err(failures.join("\n"));
}
Ok(())
}
pub fn run_test_file(path: &str) -> Result<(), String> {
let content =
fs::read_to_string(path).unwrap_or_else(|_| panic!("Failed to read file: {}", path));
let suite: Vec<TestSuite> = serde_json::from_str(&content)
.unwrap_or_else(|e| panic!("Failed to parse JSON in {}: {}", path, e));
let mut failures = Vec::<String>::new();
for (group_index, group) in suite.into_iter().enumerate() {
// Create Validator Instance and parse enums, types, and puncs automatically
let mut validator = Validator::from_punc_definition(
group.enums.as_ref(),
group.types.as_ref(),
group.puncs.as_ref(),
);
let unique_id = format!("test:{}:{}", path, group_index);
// Register main 'schema' if present (Standard style)
if let Some(ref schema_val) = group.schema {
let mut schema: crate::validator::schema::Schema =
serde_json::from_value(schema_val.clone()).expect("Failed to parse test schema");
// If schema has no ID, assign unique_id and use add() or manual insert?
// Compiler needs ID. Registry::add needs ID.
if schema.obj.id.is_none() {
schema.obj.id = Some(unique_id.clone());
}
validator.registry.add(schema);
}
for test in group.tests {
// Use explicit schema_id from test, or default to unique_id
let schema_id = test.schema_id.as_deref().unwrap_or(&unique_id).to_string();
let result = validator.validate(&schema_id, &test.data);
if test.valid {
match result {
Ok(res) => {
if !res.is_valid() {
let msg = format!(
"Test failed (expected valid): {}\nSchema: {:?}\nData: {:?}\nErrors: {:?}",
test.description,
group.schema, // We might need to find the actual schema used if schema_id is custom
test.data,
res.errors
);
eprintln!("{}", msg);
failures.push(msg);
}
}
Err(e) => {
let msg = format!(
"Test failed (expected valid) but got execution error: {}\nSchema: {:?}\nData: {:?}\nError: {:?}",
test.description, group.schema, test.data, e
);
eprintln!("{}", msg);
failures.push(msg);
}
}
} else {
match result {
Ok(res) => {
if res.is_valid() {
let msg = format!(
"Test failed (expected invalid): {}\nSchema: {:?}\nData: {:?}",
test.description, group.schema, test.data
);
eprintln!("{}", msg);
failures.push(msg);
}
}
Err(_) => {
// Expected invalid, got error (which implies invalid/failure), so this is PASS.
}
}
}
}
}
if !failures.is_empty() {
return Err(format!(
"{} tests failed in file {}:\n\n{}",
failures.len(),
path,
failures.join("\n\n")
));
}
Ok(())
}
pub fn is_integer(v: &Value) -> bool {
match v {
Value::Number(n) => {
n.is_i64() || n.is_u64() || n.as_f64().filter(|n| n.fract() == 0.0).is_some()
}
_ => false,
}
}
/// serde_json treats 0 and 0.0 not equal. so we cannot simply use v1==v2
pub fn equals(v1: &Value, v2: &Value) -> bool {
// eprintln!("Comparing {:?} with {:?}", v1, v2);
match (v1, v2) {
(Value::Null, Value::Null) => true,
(Value::Bool(b1), Value::Bool(b2)) => b1 == b2,
(Value::Number(n1), Value::Number(n2)) => {
if let (Some(n1), Some(n2)) = (n1.as_u64(), n2.as_u64()) {
return n1 == n2;
}
if let (Some(n1), Some(n2)) = (n1.as_i64(), n2.as_i64()) {
return n1 == n2;
}
if let (Some(n1), Some(n2)) = (n1.as_f64(), n2.as_f64()) {
return (n1 - n2).abs() < f64::EPSILON;
}
false
}
(Value::String(s1), Value::String(s2)) => s1 == s2,
(Value::Array(arr1), Value::Array(arr2)) => {
if arr1.len() != arr2.len() {
return false;
}
arr1.iter().zip(arr2).all(|(e1, e2)| equals(e1, e2))
}
(Value::Object(obj1), Value::Object(obj2)) => {
if obj1.len() != obj2.len() {
return false;
}
for (k1, v1) in obj1 {
if let Some(v2) = obj2.get(k1) {
if !equals(v1, v2) {
return false;
}
} else {
return false;
}
}
true
}
_ => false,
}
}