use crate::grammar::pda::{Alt, CompiledGrammar, GrammarBuilder, Symbol};
use serde_json::Value;
use std::collections::HashMap;
const MAX_ARRAY_CARDINALITY: usize = 4096;
const MAX_SCHEMA_DEPTH: usize = 512;
const MAX_OBJECT_PROPERTIES: usize = 4096;
const MAX_TRIE_DEPTH: usize = 1024;
const MAX_STRING_LITERALS: usize = 4096;
const MAX_STRING_LITERAL_BYTES: usize = 1024 * 1024;
const MAX_PREFIX_ITEMS: usize = 4096;
const MAX_ANYOF_BRANCHES: usize = 4096;
#[derive(Debug, Clone, PartialEq)]
pub struct SchemaError(pub String);
impl std::fmt::Display for SchemaError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "JSON Schema compile error: {}", self.0)
}
}
impl std::error::Error for SchemaError {}
pub fn compile_json_schema(schema: &Value) -> Result<CompiledGrammar, SchemaError> {
guard_schema_string_bytes(schema)?;
let mut ctx = CompileCtx::new(schema)?;
let root_id = ctx.builder.reserve("root");
let root_alts = ctx.compile_schema(schema, &[])?;
ctx.builder.set_alts(root_id, root_alts);
ctx.resolve_pending()?;
let mut grammar = ctx.builder.build();
if root_id != 0 {
grammar.rules.swap(0, root_id);
for rule in &mut grammar.rules {
for alt in &mut rule.alts {
for sym in alt.iter_mut() {
if let crate::grammar::pda::Symbol::NonTerminal(rid) = sym {
if *rid == root_id {
*rid = 0;
} else if *rid == 0 {
*rid = root_id;
}
}
}
}
}
}
Ok(grammar)
}
struct CompileCtx<'a> {
defs: HashMap<String, &'a Value>,
builder: GrammarBuilder,
enum_counter: usize,
array_counter: usize,
object_counter: usize,
depth: usize,
}
impl<'a> CompileCtx<'a> {
fn new(doc_root: &'a Value) -> Result<Self, SchemaError> {
let mut defs = HashMap::new();
let mut defs_name_bytes: usize = 0;
if let Some(defs_map) = doc_root.get("$defs").and_then(Value::as_object) {
for (k, v) in defs_map {
defs_name_bytes = defs_name_bytes.saturating_add(k.len());
if defs_name_bytes > MAX_STRING_LITERAL_BYTES {
return Err(SchemaError(format!(
"schema definition name cumulative byte length exceeds the supported limit ({MAX_STRING_LITERAL_BYTES})"
)));
}
defs.insert(k.clone(), v);
}
}
if let Some(defs_map) = doc_root.get("definitions").and_then(Value::as_object) {
for (k, v) in defs_map {
defs_name_bytes = defs_name_bytes.saturating_add(k.len());
if defs_name_bytes > MAX_STRING_LITERAL_BYTES {
return Err(SchemaError(format!(
"schema definition name cumulative byte length exceeds the supported limit ({MAX_STRING_LITERAL_BYTES})"
)));
}
defs.entry(k.clone()).or_insert(v);
}
}
let mut builder = GrammarBuilder::new();
register_builtins(&mut builder);
Ok(Self {
defs,
builder,
enum_counter: 0,
array_counter: 0,
object_counter: 0,
depth: 0,
})
}
fn compile_schema(
&mut self,
schema: &'a Value,
path: &[&str],
) -> Result<Vec<Alt>, SchemaError> {
self.depth += 1;
if self.depth > MAX_SCHEMA_DEPTH {
self.depth -= 1;
return Err(SchemaError(format!(
"schema nesting / $ref chain exceeds the supported depth ({MAX_SCHEMA_DEPTH})"
)));
}
let result = self.compile_schema_inner(schema, path);
self.depth -= 1;
result
}
#[inline(never)]
fn compile_any_of(
&mut self,
any_of: &'a [Value],
path: &[&str],
) -> Result<Vec<Alt>, SchemaError> {
let mut flat_subs: Vec<&'a Value> = Vec::new();
flatten_any_of_branches(any_of, 0, &mut flat_subs)?;
let mut broad_string = false;
let mut literals: Vec<String> = Vec::new();
let mut byte_total: usize = 0;
let mut other_subs: Vec<&'a Value> = Vec::new();
for sub in flat_subs.iter().copied() {
match string_class_of(sub)? {
Some(StrClass::Broad) => broad_string = true,
Some(StrClass::Literals(values)) => {
fold_literals_into(&mut literals, &mut byte_total, values)?;
}
None => {
if sub.get("$ref").is_some() {
match self.ref_string_contribution(sub)? {
RefStr::Broad => broad_string = true,
RefStr::Literals(values) => {
if !values.is_empty() {
fold_literals_into(&mut literals, &mut byte_total, values)?;
}
}
RefStr::Dead => {
}
RefStr::NotString => {
other_subs.push(sub);
}
}
} else {
let folded = fold_string_members(sub)?;
if !folded.is_empty() {
fold_literals_into(&mut literals, &mut byte_total, folded)?;
}
other_subs.push(sub);
}
}
}
}
let mut merged: Vec<Alt> = Vec::new();
if broad_string {
match self.builder.rule_id("json_string") {
Some(id) => merged.push(vec![Symbol::NonTerminal(id)]),
None => merged.push(vec![Symbol::Terminal(b'"'), Symbol::Terminal(b'"')]),
}
} else if !literals.is_empty() {
if literals.len() > MAX_STRING_LITERALS {
return Err(SchemaError(format!(
"string literal count ({}) exceeds the supported limit ({MAX_STRING_LITERALS})",
literals.len()
)));
}
literals.sort();
literals.dedup();
let mut seqs: Vec<Vec<u8>> = Vec::with_capacity(literals.len());
for s in &literals {
let json_repr = serde_json::to_string(s)
.map_err(|e| SchemaError(format!("cannot JSON-encode string value: {e}")))?;
let inner = &json_repr[1..json_repr.len() - 1];
let mut seq: Vec<u8> = inner.bytes().collect();
seq.push(b'"');
seqs.push(seq);
}
let trie_root_id = self.compile_trie_literals(&seqs)?;
merged.push(vec![
Symbol::Terminal(b'"'),
Symbol::NonTerminal(trie_root_id),
]);
}
for sub in other_subs {
merged.extend(self.compile_schema(sub, path)?);
}
Ok(merged)
}
fn compile_schema_inner(
&mut self,
schema: &'a Value,
_path: &[&str],
) -> Result<Vec<Alt>, SchemaError> {
if let Some(ref_str) = schema.get("$ref").and_then(Value::as_str) {
return self.compile_ref(ref_str);
}
if let Some(values) = schema.get("enum").and_then(Value::as_array) {
let type_is_string = schema.get("type").and_then(Value::as_str) == Some("string");
let all_strings = values.iter().all(Value::is_string);
if type_is_string || all_strings {
return self.compile_string_type(schema);
}
return compile_enum(values);
}
if let Some(v) = schema.get("const") {
return compile_const(v);
}
if let Some(any_of) = schema
.get("anyOf")
.or_else(|| schema.get("oneOf"))
.and_then(Value::as_array)
{
return self.compile_any_of(any_of, _path);
}
match schema.get("type").and_then(Value::as_str) {
Some("object") => self.compile_object(schema),
Some("array") => self.compile_array(schema),
Some("string") => self.compile_string_type(schema),
Some("number") => {
let id = self
.builder
.rule_id("json_number")
.ok_or_else(|| SchemaError("builtin json_number missing".into()))?;
Ok(vec![vec![Symbol::NonTerminal(id)]])
}
Some("integer") => {
let id = self
.builder
.rule_id("json_integer")
.ok_or_else(|| SchemaError("builtin json_integer missing".into()))?;
Ok(vec![vec![Symbol::NonTerminal(id)]])
}
Some("boolean") => {
let id = self
.builder
.rule_id("json_boolean")
.ok_or_else(|| SchemaError("builtin json_boolean missing".into()))?;
Ok(vec![vec![Symbol::NonTerminal(id)]])
}
Some("null") => {
let id = self
.builder
.rule_id("json_null")
.ok_or_else(|| SchemaError("builtin json_null missing".into()))?;
Ok(vec![vec![Symbol::NonTerminal(id)]])
}
Some(other) => Err(SchemaError(format!("unsupported type: {other}"))),
None => {
Ok(self.any_value_alts())
}
}
}
fn compile_ref(&mut self, ref_str: &str) -> Result<Vec<Alt>, SchemaError> {
let name = extract_ref_name(ref_str)?;
let target: &'a Value = self
.defs
.get(name)
.copied()
.ok_or_else(|| SchemaError(format!("$ref not found: {ref_str}")))?;
let rule_name = format!("def_{name}");
if let Some(id) = self.builder.rule_id(&rule_name) {
return Ok(vec![vec![Symbol::NonTerminal(id)]]);
}
let id = self.builder.reserve(&rule_name);
let alts = self.compile_schema(target, &[])?;
self.builder.set_alts(id, alts);
Ok(vec![vec![Symbol::NonTerminal(id)]])
}
fn ref_string_contribution(&self, sub: &'a Value) -> Result<RefStr, SchemaError> {
let mut narrowing: Option<Vec<String>> = None;
let mut chain_forces_string = ref_sub_forces_string(sub);
intersect_narrowing(&mut narrowing, node_string_narrowing(sub)?);
let mut current = sub;
for _ in 0..=MAX_SCHEMA_DEPTH {
let Some(ref_str) = current.get("$ref").and_then(Value::as_str) else {
return Ok(combine_ref_str(
string_class_of(current)?,
narrowing,
chain_forces_string,
));
};
let Ok(name) = extract_ref_name(ref_str) else {
return Ok(RefStr::NotString);
};
let Some(next) = self.defs.get(name).copied() else {
return Ok(RefStr::NotString);
};
current = next;
chain_forces_string |= ref_sub_forces_string(current);
intersect_narrowing(&mut narrowing, node_string_narrowing(current)?);
}
Ok(RefStr::NotString)
}
fn compile_object(&mut self, schema: &'a Value) -> Result<Vec<Alt>, SchemaError> {
let properties_map = schema.get("properties").and_then(Value::as_object);
if let Some(m) = properties_map
&& m.len() > MAX_OBJECT_PROPERTIES
{
return Err(SchemaError(format!(
"object property count ({}) exceeds the supported limit ({MAX_OBJECT_PROPERTIES})",
m.len()
)));
}
let properties = properties_map.map(|m| m.iter().collect::<Vec<_>>());
let required_arr = schema.get("required").and_then(Value::as_array);
if let Some(a) = required_arr
&& a.len() > MAX_OBJECT_PROPERTIES
{
return Err(SchemaError(format!(
"object required count ({}) exceeds the supported limit ({MAX_OBJECT_PROPERTIES})",
a.len()
)));
}
let required: Vec<&str> = required_arr
.map(|a| a.iter().filter_map(|v| v.as_str()).collect::<Vec<_>>())
.unwrap_or_default();
let ws_id = self.builder.rule_id("ws").unwrap();
match properties {
None => {
Ok(vec![empty_object_as_alt_with_ws(ws_id)])
}
Some(props) => {
let obj_idx = self.object_counter;
self.object_counter += 1;
let mut props_info: Vec<(usize, usize, Vec<u8>, bool)> =
Vec::with_capacity(props.len());
let mut key_bytes_total: usize = 0;
for (key, val_schema) in &props {
let key_str: &str = key.as_str();
let is_req = required.contains(&key_str);
guard_literal_bytes(key_str)?;
key_bytes_total = key_bytes_total.saturating_add(key_str.len());
if key_bytes_total > MAX_STRING_LITERAL_BYTES {
return Err(SchemaError(format!(
"object property key cumulative byte length exceeds the supported limit ({MAX_STRING_LITERAL_BYTES})"
)));
}
let key_bytes = serde_json::to_string(key_str)
.map_err(|e| SchemaError(format!("cannot JSON-encode property key: {e}")))?
.into_bytes();
let val_rule_name = format!("prop_val_{obj_idx}_{key_str}");
let val_id = if let Some(id) = self.builder.rule_id(&val_rule_name) {
id
} else {
let id = self.builder.reserve(&val_rule_name);
let val_alts = self.compile_schema(val_schema, &[])?;
self.builder.set_alts(id, val_alts);
id
};
let pair_rule_name = format!("pair_{obj_idx}_{key_str}");
let pair_id = if let Some(id) = self.builder.rule_id(&pair_rule_name) {
id
} else {
let id = self.builder.reserve(&pair_rule_name);
let mut pair_alt = json_string_literal(key_str)?;
pair_alt.push(Symbol::NonTerminal(ws_id));
pair_alt.push(Symbol::Terminal(b':'));
pair_alt.push(Symbol::NonTerminal(ws_id));
pair_alt.push(Symbol::NonTerminal(val_id));
self.builder.set_alts(id, vec![pair_alt]);
id
};
props_info.push((pair_id, val_id, key_bytes, is_req));
}
props_info.sort_by_key(|&(_, _, _, is_req)| if is_req { 0usize } else { 1 });
let tail_id = self.build_object_tail(obj_idx, &props_info, 0, false, ws_id);
let main_alt = vec![
Symbol::Terminal(b'{'),
Symbol::NonTerminal(ws_id),
Symbol::NonTerminal(tail_id),
Symbol::NonTerminal(ws_id),
Symbol::Terminal(b'}'),
];
Ok(vec![main_alt])
}
}
}
fn build_object_tail(
&mut self,
obj_idx: usize,
props: &[(usize, usize, Vec<u8>, bool)], i: usize,
started: bool,
ws_id: usize,
) -> usize {
let started_char = if started { 's' } else { 'f' };
let rule_name = format!("obj_{obj_idx}_tail_{i}_{started_char}");
if let Some(id) = self.builder.rule_id(&rule_name) {
return id;
}
if i == props.len() {
let id = self.builder.reserve(&rule_name);
self.builder.set_alts(id, vec![vec![]]); return id;
}
let id = self.builder.reserve(&rule_name);
let (pair_id, val_id, ref key_bytes, is_required) = props[i];
let alts: Vec<Alt> = if is_required {
let next_id = self.build_object_tail(obj_idx, props, i + 1, true, ws_id);
if started {
vec![vec![
Symbol::Terminal(b','),
Symbol::NonTerminal(ws_id),
Symbol::NonTerminal(pair_id),
Symbol::NonTerminal(next_id),
]]
} else {
vec![vec![
Symbol::NonTerminal(pair_id),
Symbol::NonTerminal(next_id),
]]
}
} else {
let next_started_id = self.build_object_tail(obj_idx, props, i + 1, true, ws_id);
let next_same_id = self.build_object_tail(obj_idx, props, i + 1, started, ws_id);
let emit_alt: Alt = if started {
let mut alt: Alt = vec![Symbol::Terminal(b','), Symbol::NonTerminal(ws_id)];
for &byte in key_bytes.iter() {
alt.push(Symbol::Terminal(byte));
}
alt.push(Symbol::NonTerminal(ws_id));
alt.push(Symbol::Terminal(b':'));
alt.push(Symbol::NonTerminal(ws_id));
alt.push(Symbol::NonTerminal(val_id));
alt.push(Symbol::NonTerminal(next_started_id));
alt
} else {
vec![
Symbol::NonTerminal(pair_id),
Symbol::NonTerminal(next_started_id),
]
};
let skip_alt: Alt = vec![Symbol::NonTerminal(next_same_id)];
vec![emit_alt, skip_alt]
};
self.builder.set_alts(id, alts);
id
}
fn compile_array(&mut self, schema: &'a Value) -> Result<Vec<Alt>, SchemaError> {
let ws_id = self.builder.rule_id("ws").unwrap();
let min_items = schema.get("minItems").and_then(Value::as_u64).unwrap_or(0) as usize;
let max_items = schema
.get("maxItems")
.and_then(Value::as_u64)
.map(|v| v as usize);
if min_items > MAX_ARRAY_CARDINALITY {
return Err(SchemaError(format!(
"array schema minItems ({min_items}) exceeds the supported limit ({MAX_ARRAY_CARDINALITY})"
)));
}
if let Some(max) = max_items
&& max > MAX_ARRAY_CARDINALITY
{
return Err(SchemaError(format!(
"array schema maxItems ({max}) exceeds the supported limit ({MAX_ARRAY_CARDINALITY})"
)));
}
if let Some(max) = max_items
&& max < min_items
{
return Err(SchemaError(format!(
"array schema maxItems ({max}) < minItems ({min_items})"
)));
}
let prefix_items = schema
.get("prefixItems")
.and_then(Value::as_array)
.map(Vec::as_slice);
if let Some(prefix_schemas) = prefix_items {
if !prefix_schemas.is_empty() {
let n = self.array_counter;
self.array_counter += 1;
let p = prefix_schemas.len();
if p > MAX_PREFIX_ITEMS {
return Err(SchemaError(format!(
"prefixItems length ({p}) exceeds the supported limit ({MAX_PREFIX_ITEMS})"
)));
}
if let Some(m) = max_items
&& m < p
{
return Err(SchemaError(format!(
"array maxItems ({m}) < prefixItems length ({p})"
)));
}
if min_items > p {
return Err(SchemaError(format!(
"array minItems ({min_items}) > prefixItems length ({p}) is not supported with prefixItems"
)));
}
let mut pos_ids: Vec<usize> = Vec::with_capacity(p);
for (i, pos_schema) in prefix_schemas.iter().enumerate() {
let pos_rule = format!("arr_{n}_prefix_{i}");
let pos_id = self.builder.reserve(&pos_rule);
let pos_alts = self.compile_schema(pos_schema, &[])?;
self.builder.set_alts(pos_id, pos_alts);
pos_ids.push(pos_id);
}
let mut alt: Alt = vec![Symbol::Terminal(b'[')];
alt.push(Symbol::NonTerminal(ws_id));
for (i, &pid) in pos_ids.iter().enumerate() {
if i > 0 {
alt.push(Symbol::Terminal(b','));
alt.push(Symbol::NonTerminal(ws_id));
}
alt.push(Symbol::NonTerminal(pid));
alt.push(Symbol::NonTerminal(ws_id));
}
if let Some(items_schema) = schema.get("items") {
let item_rule = format!("arr_{n}_item");
let item_id = self.builder.reserve(&item_rule);
let item_alts = self.compile_schema(items_schema, &[])?;
self.builder.set_alts(item_id, item_alts);
let slack = max_items.map(|m| m - p);
let tail_id = self.build_bounded_tail(n, 1, slack, item_id, ws_id);
alt.push(Symbol::NonTerminal(tail_id));
}
alt.push(Symbol::Terminal(b']'));
return Ok(vec![alt]);
}
}
if let Some(items_schema) = schema.get("items") {
let n = self.array_counter;
self.array_counter += 1;
let item_rule = format!("arr_{n}_item");
let item_id = self.builder.reserve(&item_rule);
let item_alts = self.compile_schema(items_schema, &[])?;
self.builder.set_alts(item_id, item_alts);
let alt = self.build_cardinality_array_alt(n, item_id, ws_id, min_items, max_items);
return Ok(vec![alt]);
}
let any_val_rule = "any_json_value";
let any_id = if let Some(id) = self.builder.rule_id(any_val_rule) {
id
} else {
let id = self.builder.reserve(any_val_rule);
let any_alts = self.any_value_alts();
self.builder.set_alts(id, any_alts);
id
};
if min_items > 0 || max_items.is_some() {
let n = self.array_counter;
self.array_counter += 1;
let alt = self.build_cardinality_array_alt(n, any_id, ws_id, min_items, max_items);
return Ok(vec![alt]);
}
let tail_name = "any_arr_tail";
let tail_id = if let Some(id) = self.builder.rule_id(tail_name) {
id
} else {
let id = self.builder.reserve(tail_name);
let tail_alts = vec![
vec![
Symbol::NonTerminal(ws_id),
Symbol::Terminal(b','),
Symbol::NonTerminal(ws_id),
Symbol::NonTerminal(any_id),
Symbol::NonTerminal(id), ],
vec![],
];
self.builder.set_alts(id, tail_alts);
id
};
let body_name = "any_arr_body";
let body_id = if let Some(id) = self.builder.rule_id(body_name) {
id
} else {
let id = self.builder.reserve(body_name);
self.builder.set_alts(
id,
vec![
vec![Symbol::NonTerminal(any_id), Symbol::NonTerminal(tail_id)],
vec![],
],
);
id
};
let mut alt: Alt = vec![Symbol::Terminal(b'[')];
alt.push(Symbol::NonTerminal(ws_id));
alt.push(Symbol::NonTerminal(body_id));
alt.push(Symbol::NonTerminal(ws_id));
alt.push(Symbol::Terminal(b']'));
Ok(vec![alt])
}
fn build_cardinality_array_alt(
&mut self,
n: usize,
item_id: usize,
ws_id: usize,
min_items: usize,
max_items: Option<usize>,
) -> Alt {
let mut alt: Alt = vec![Symbol::Terminal(b'[')];
alt.push(Symbol::NonTerminal(ws_id));
let slack = max_items.map(|m| m - min_items);
match (min_items, max_items) {
(0, Some(0)) => {
}
(0, _) => {
if slack != Some(0) {
let body_id = self.build_bounded_tail(n, 0, slack, item_id, ws_id);
alt.push(Symbol::NonTerminal(body_id));
}
}
(min, _) => {
alt.push(Symbol::NonTerminal(item_id));
alt.push(Symbol::NonTerminal(ws_id));
for _ in 1..min {
alt.push(Symbol::Terminal(b','));
alt.push(Symbol::NonTerminal(ws_id));
alt.push(Symbol::NonTerminal(item_id));
alt.push(Symbol::NonTerminal(ws_id));
}
if slack != Some(0) {
let tail_id = self.build_bounded_tail(n, 1, slack, item_id, ws_id);
alt.push(Symbol::NonTerminal(tail_id));
}
}
}
alt.push(Symbol::Terminal(b']'));
alt
}
fn build_bounded_tail(
&mut self,
arr_n: usize,
depth: usize,
slack: Option<usize>,
item_id: usize,
ws_id: usize,
) -> usize {
match slack {
None => {
let rule_name = format!("arr_{arr_n}_tail_{depth}");
let id = self.builder.reserve(&rule_name);
let recurse_id = id; let body_alt = if depth == 0 {
let cont_name = format!("arr_{arr_n}_tail_{}", depth + 1);
let cont_id = self.builder.reserve(&cont_name);
let cont_alt = vec![
Symbol::Terminal(b','),
Symbol::NonTerminal(ws_id),
Symbol::NonTerminal(item_id),
Symbol::NonTerminal(ws_id),
Symbol::NonTerminal(cont_id),
];
self.builder
.set_alts(cont_id, vec![cont_alt, vec![] ]);
vec![
Symbol::NonTerminal(item_id),
Symbol::NonTerminal(ws_id),
Symbol::NonTerminal(cont_id),
]
} else {
vec![
Symbol::Terminal(b','),
Symbol::NonTerminal(ws_id),
Symbol::NonTerminal(item_id),
Symbol::NonTerminal(ws_id),
Symbol::NonTerminal(recurse_id),
]
};
self.builder
.set_alts(id, vec![body_alt, vec![] ]);
id
}
Some(0) => {
let rule_name = format!("arr_{arr_n}_opt_{depth}_0");
let id = self.builder.reserve(&rule_name);
self.builder.set_alts(id, vec![vec![] ]);
id
}
Some(k) => {
let rule_name = format!("arr_{arr_n}_opt_{depth}_{k}");
let id = self.builder.reserve(&rule_name);
let inner_id =
self.build_bounded_tail(arr_n, depth + 1, Some(k - 1), item_id, ws_id);
let body_alt: Alt = if depth == 0 {
vec![
Symbol::NonTerminal(item_id),
Symbol::NonTerminal(ws_id),
Symbol::NonTerminal(inner_id),
]
} else {
vec![
Symbol::Terminal(b','),
Symbol::NonTerminal(ws_id),
Symbol::NonTerminal(item_id),
Symbol::NonTerminal(ws_id),
Symbol::NonTerminal(inner_id),
]
};
self.builder
.set_alts(id, vec![body_alt, vec![] ]);
id
}
}
}
fn compile_string_type(&mut self, schema: &Value) -> Result<Vec<Alt>, SchemaError> {
if let Some(values) = schema.get("enum").and_then(Value::as_array) {
if values.len() > MAX_STRING_LITERALS {
return Err(SchemaError(format!(
"string enum literal count ({}) exceeds the supported limit ({MAX_STRING_LITERALS})",
values.len()
)));
}
let mut str_values: Vec<&str> = values.iter().filter_map(|v| v.as_str()).collect();
if let Some(c) = schema.get("const") {
match c.as_str() {
Some(cs) => str_values.retain(|s| *s == cs),
None => str_values.clear(),
}
}
if !str_values.is_empty() {
let mut pre_byte_total: usize = 0;
for s in &str_values {
let json_repr = serde_json::to_string(s)
.map_err(|e| SchemaError(format!("cannot JSON-encode enum value: {e}")))?;
pre_byte_total = pre_byte_total.saturating_add(json_repr.len() - 1);
if pre_byte_total > MAX_STRING_LITERAL_BYTES {
return Err(SchemaError(format!(
"string literal encoded byte length exceeds the supported limit ({MAX_STRING_LITERAL_BYTES})"
)));
}
}
let mut seqs: Vec<Vec<u8>> = Vec::with_capacity(str_values.len());
for s in &str_values {
let json_repr = serde_json::to_string(s)
.map_err(|e| SchemaError(format!("cannot JSON-encode enum value: {e}")))?;
let inner = &json_repr[1..json_repr.len() - 1];
let mut seq: Vec<u8> = inner.bytes().collect();
seq.push(b'"');
seqs.push(seq);
}
let trie_root_id = self.compile_trie_literals(&seqs)?;
return Ok(vec![vec![
Symbol::Terminal(b'"'),
Symbol::NonTerminal(trie_root_id),
]]);
}
return Ok(Vec::new());
}
if let Some(id) = self.builder.rule_id("json_string") {
Ok(vec![vec![Symbol::NonTerminal(id)]])
} else {
Ok(vec![vec![Symbol::Terminal(b'"'), Symbol::Terminal(b'"')]])
}
}
fn any_value_alts(&mut self) -> Vec<Alt> {
let Some(ws_id) = self.builder.rule_id("ws") else {
return vec![];
};
let mut alts = Vec::new();
if let Some(id) = self.builder.rule_id("json_string") {
alts.push(vec![Symbol::NonTerminal(id)]);
}
if let Some(id) = self.builder.rule_id("json_number") {
alts.push(vec![Symbol::NonTerminal(id)]);
}
if let Some(id) = self.builder.rule_id("json_boolean") {
alts.push(vec![Symbol::NonTerminal(id)]);
}
if let Some(id) = self.builder.rule_id("json_null") {
alts.push(vec![Symbol::NonTerminal(id)]);
}
alts.push(empty_object_as_alt_with_ws(ws_id));
alts.push(vec![
Symbol::Terminal(b'['),
Symbol::NonTerminal(ws_id),
Symbol::Terminal(b']'),
]);
alts
}
fn compile_trie_literals(&mut self, seqs: &[Vec<u8>]) -> Result<usize, SchemaError> {
if seqs.len() > MAX_STRING_LITERALS {
return Err(SchemaError(format!(
"string literal count ({}) exceeds the supported limit ({MAX_STRING_LITERALS})",
seqs.len()
)));
}
let total_encoded_bytes: usize = seqs.iter().map(Vec::len).sum();
if total_encoded_bytes > MAX_STRING_LITERAL_BYTES {
return Err(SchemaError(format!(
"string literal encoded byte length ({total_encoded_bytes}) exceeds the supported limit ({MAX_STRING_LITERAL_BYTES})"
)));
}
let n = self.enum_counter;
self.enum_counter += 1;
let mut node_counter = 0usize;
build_trie_node(&mut self.builder, seqs, n, &mut node_counter, 0)
}
fn resolve_pending(&self) -> Result<(), SchemaError> {
Ok(())
}
}
fn register_builtins(b: &mut GrammarBuilder) {
let ws_id = b.reserve("ws");
let ws_tail = b.reserve("ws_tail");
b.set_alts(
ws_tail,
vec![
vec![Symbol::Terminal(b' '), Symbol::NonTerminal(ws_tail)],
vec![Symbol::Terminal(b'\t'), Symbol::NonTerminal(ws_tail)],
vec![Symbol::Terminal(b'\n'), Symbol::NonTerminal(ws_tail)],
vec![Symbol::Terminal(b'\r'), Symbol::NonTerminal(ws_tail)],
vec![], ],
);
b.set_alts(ws_id, vec![vec![Symbol::NonTerminal(ws_tail)]]);
let str_id = b.reserve("json_string");
let str_inner_id = b.reserve("json_string_inner");
let str_char_id = b.reserve("json_string_char");
let mut char_alts: Vec<Alt> = Vec::new();
char_alts.push(vec![Symbol::Terminal(b'\\'), Symbol::AnyByte]);
for byte in 0u8..=255 {
if byte != b'"' && byte != b'\\' {
char_alts.push(vec![Symbol::Terminal(byte)]);
}
}
b.set_alts(str_char_id, char_alts);
b.set_alts(
str_inner_id,
vec![
vec![
Symbol::NonTerminal(str_char_id),
Symbol::NonTerminal(str_inner_id),
],
vec![], ],
);
b.set_alts(
str_id,
vec![vec![
Symbol::Terminal(b'"'),
Symbol::NonTerminal(str_inner_id),
Symbol::Terminal(b'"'),
]],
);
let num_id = b.reserve("json_number");
let digit_id = b.reserve("json_digit");
let digit_alts: Vec<Alt> = (b'0'..=b'9')
.map(|byte| vec![Symbol::Terminal(byte)])
.collect();
b.set_alts(digit_id, digit_alts);
let digits_id = b.reserve("json_digits");
let digit_tail_id = b.reserve("json_digit_tail");
b.set_alts(
digit_tail_id,
vec![
vec![
Symbol::NonTerminal(digit_id),
Symbol::NonTerminal(digit_tail_id),
],
vec![], ],
);
b.set_alts(
digits_id,
vec![vec![
Symbol::NonTerminal(digit_id),
Symbol::NonTerminal(digit_tail_id),
]],
);
let nonzero_id = b.reserve("json_nonzero");
let nonzero_alts: Vec<Alt> = (b'1'..=b'9')
.map(|byte| vec![Symbol::Terminal(byte)])
.collect();
b.set_alts(nonzero_id, nonzero_alts);
let int_part_id = b.reserve("json_int_part");
b.set_alts(
int_part_id,
vec![
vec![Symbol::Terminal(b'0')],
vec![
Symbol::NonTerminal(nonzero_id),
Symbol::NonTerminal(digit_tail_id),
],
],
);
let opt_sign_id = b.reserve("json_opt_sign");
b.set_alts(opt_sign_id, vec![vec![Symbol::Terminal(b'-')], vec![]]);
let opt_frac_id = b.reserve("json_opt_frac");
b.set_alts(
opt_frac_id,
vec![
vec![Symbol::Terminal(b'.'), Symbol::NonTerminal(digits_id)],
vec![],
],
);
let exp_sign_id = b.reserve("json_exp_sign");
b.set_alts(
exp_sign_id,
vec![
vec![Symbol::Terminal(b'+')],
vec![Symbol::Terminal(b'-')],
vec![],
],
);
let opt_exp_id = b.reserve("json_opt_exp");
b.set_alts(
opt_exp_id,
vec![
vec![
Symbol::Terminal(b'e'),
Symbol::NonTerminal(exp_sign_id),
Symbol::NonTerminal(digits_id),
],
vec![
Symbol::Terminal(b'E'),
Symbol::NonTerminal(exp_sign_id),
Symbol::NonTerminal(digits_id),
],
vec![],
],
);
b.set_alts(
num_id,
vec![vec![
Symbol::NonTerminal(opt_sign_id),
Symbol::NonTerminal(int_part_id),
Symbol::NonTerminal(opt_frac_id),
Symbol::NonTerminal(opt_exp_id),
]],
);
let int_id = b.reserve("json_integer");
b.set_alts(
int_id,
vec![vec![
Symbol::NonTerminal(opt_sign_id),
Symbol::NonTerminal(int_part_id),
]],
);
let bool_id = b.reserve("json_boolean");
b.set_alts(bool_id, vec![bytes_to_alt(b"true"), bytes_to_alt(b"false")]);
let null_id = b.reserve("json_null");
b.set_alts(null_id, vec![bytes_to_alt(b"null")]);
}
fn bytes_to_alt(bytes: &[u8]) -> Alt {
bytes.iter().map(|&b| Symbol::Terminal(b)).collect()
}
fn empty_object_as_alt_with_ws(ws_id: usize) -> Alt {
vec![
Symbol::Terminal(b'{'),
Symbol::NonTerminal(ws_id),
Symbol::Terminal(b'}'),
]
}
fn build_trie_node(
builder: &mut GrammarBuilder,
seqs: &[Vec<u8>],
enum_n: usize,
node_counter: &mut usize,
depth: usize,
) -> Result<usize, SchemaError> {
if depth >= MAX_TRIE_DEPTH {
return Err(SchemaError(format!(
"enum/const shared-prefix nesting exceeds the supported depth ({MAX_TRIE_DEPTH})"
)));
}
let k = *node_counter;
*node_counter += 1;
let rule_name = if k == 0 {
format!("str_enum_{enum_n}")
} else {
format!("str_enum_{enum_n}_{k}")
};
let id = builder.reserve(&rule_name);
let mut groups: Vec<(u8, Vec<Vec<u8>>)> = Vec::new();
for seq in seqs {
let Some((&first, tail)) = seq.split_first() else {
return Err(SchemaError(
"trie: empty literal sequence (prefix-free invariant violated)".into(),
));
};
match groups.iter_mut().find(|(b, _)| *b == first) {
Some(entry) => entry.1.push(tail.to_vec()),
None => groups.push((first, vec![tail.to_vec()])),
}
}
let mut alts: Vec<Alt> = Vec::with_capacity(groups.len());
for (byte, tails) in groups {
if tails.iter().all(Vec::is_empty) {
alts.push(vec![Symbol::Terminal(byte)]);
} else if tails.len() == 1 {
let tail = &tails[0];
let mut alt = Vec::with_capacity(tail.len() + 1);
alt.push(Symbol::Terminal(byte));
alt.extend(tail.iter().map(|&b| Symbol::Terminal(b)));
alts.push(alt);
} else {
let child_id = build_trie_node(builder, &tails, enum_n, node_counter, depth + 1)?;
alts.push(vec![Symbol::Terminal(byte), Symbol::NonTerminal(child_id)]);
}
}
builder.set_alts(id, alts);
Ok(id)
}
fn compile_enum(values: &[Value]) -> Result<Vec<Alt>, SchemaError> {
if values.len() > MAX_STRING_LITERALS {
return Err(SchemaError(format!(
"enum value count ({}) exceeds the supported limit ({MAX_STRING_LITERALS})",
values.len()
)));
}
let mut pre_byte_total: usize = 0;
for v in values {
let json_repr = serde_json::to_string(v)
.map_err(|e| SchemaError(format!("cannot JSON-encode enum value: {e}")))?;
pre_byte_total = pre_byte_total.saturating_add(json_repr.len());
if pre_byte_total > MAX_STRING_LITERAL_BYTES {
return Err(SchemaError(format!(
"enum value encoded byte length exceeds the supported limit ({MAX_STRING_LITERAL_BYTES})"
)));
}
}
let alts = values
.iter()
.map(json_value_to_alt)
.collect::<Result<Vec<_>, _>>()?;
Ok(alts)
}
fn compile_const(v: &Value) -> Result<Vec<Alt>, SchemaError> {
Ok(vec![json_value_to_alt(v)?])
}
enum StrClass {
Broad,
Literals(Vec<String>),
}
fn string_class_of(sub: &Value) -> Result<Option<StrClass>, SchemaError> {
let Some(obj) = sub.as_object() else {
return Ok(None);
};
if obj.contains_key("$ref") {
return Ok(None);
}
let type_is_string = match obj.get("type") {
None => false,
Some(Value::String(s)) if s == "string" => true,
_ => return Ok(None),
};
let const_val = obj.get("const");
let enum_arr = obj.get("enum").and_then(Value::as_array);
if let Some(arr) = enum_arr
&& arr.len() > MAX_STRING_LITERALS
{
return Err(SchemaError(format!(
"anyOf string literal count ({}) exceeds the supported limit ({MAX_STRING_LITERALS})",
arr.len()
)));
}
match (const_val, enum_arr) {
(Some(c), enum_opt) => {
let Some(cs) = c.as_str() else {
return Ok(if type_is_string {
Some(StrClass::Literals(Vec::new()))
} else {
None
});
};
Ok(match enum_opt {
Some(arr) if !arr.iter().any(|v| v == c) => Some(StrClass::Literals(Vec::new())),
_ => {
guard_literal_bytes(cs)?;
Some(StrClass::Literals(vec![cs.to_string()]))
}
})
}
(None, Some(arr)) => {
if type_is_string {
let mut lits = Vec::new();
for v in arr {
if let Some(s) = v.as_str() {
guard_literal_bytes(s)?;
lits.push(s.to_string());
}
}
Ok(Some(StrClass::Literals(lits)))
} else if !arr.is_empty() && arr.iter().all(Value::is_string) {
let mut lits = Vec::new();
for v in arr {
if let Some(s) = v.as_str() {
guard_literal_bytes(s)?;
lits.push(s.to_string());
}
}
Ok(Some(StrClass::Literals(lits)))
} else {
Ok(None)
}
}
(None, None) => Ok(if type_is_string {
Some(StrClass::Broad)
} else {
None
}),
}
}
enum RefStr {
NotString,
Dead,
Broad,
Literals(Vec<String>),
}
fn combine_ref_str(
terminal: Option<StrClass>,
narrowing: Option<Vec<String>>,
chain_forces_string: bool,
) -> RefStr {
match terminal {
None if chain_forces_string => RefStr::Dead,
None => RefStr::NotString,
Some(StrClass::Broad) => match narrowing {
None => RefStr::Broad,
Some(set) => RefStr::Literals(set),
},
Some(StrClass::Literals(term_lits)) => match narrowing {
None => RefStr::Literals(term_lits),
Some(set) => RefStr::Literals(intersect_string_sets(&term_lits, &set)),
},
}
}
fn node_string_narrowing(node: &Value) -> Result<Option<Vec<String>>, SchemaError> {
let Some(obj) = node.as_object() else {
return Ok(None);
};
let const_val = obj.get("const");
let enum_arr = obj.get("enum").and_then(Value::as_array);
if let Some(arr) = enum_arr
&& arr.len() > MAX_STRING_LITERALS
{
return Err(SchemaError(format!(
"anyOf string literal count ({}) exceeds the supported limit ({MAX_STRING_LITERALS})",
arr.len()
)));
}
let set: Vec<String> = match (const_val, enum_arr) {
(Some(c), enum_opt) => match c.as_str() {
None => Vec::new(),
Some(cs) => match enum_opt {
Some(arr) if !arr.iter().any(|v| v == c) => Vec::new(),
_ => vec![cs.to_string()],
},
},
(None, Some(arr)) => arr
.iter()
.filter_map(Value::as_str)
.map(str::to_string)
.collect(),
(None, None) => return Ok(None),
};
for s in &set {
guard_literal_bytes(s)?;
}
Ok(Some(set))
}
fn intersect_narrowing(acc: &mut Option<Vec<String>>, new: Option<Vec<String>>) {
let Some(new_set) = new else {
return;
};
match acc {
None => *acc = Some(new_set),
Some(cur) => cur.retain(|s| new_set.contains(s)),
}
}
fn intersect_string_sets(a: &[String], b: &[String]) -> Vec<String> {
a.iter().filter(|s| b.contains(s)).cloned().collect()
}
fn ref_sub_forces_string(sub: &Value) -> bool {
let Some(obj) = sub.as_object() else {
return false;
};
match obj.get("type") {
Some(Value::String(s)) if s == "string" => return true,
Some(Value::Array(arr))
if !arr.is_empty() && arr.iter().all(|v| v.as_str() == Some("string")) =>
{
return true;
}
_ => {}
}
if let Some(c) = obj.get("const") {
return c.is_string();
}
if let Some(arr) = obj.get("enum").and_then(Value::as_array) {
return !arr.is_empty() && arr.iter().all(Value::is_string);
}
false
}
fn as_pure_nested_union(sub: &Value) -> Option<&[Value]> {
let obj = sub.as_object()?;
if obj.len() != 1 {
return None;
}
let arr = obj.get("anyOf")?;
arr.as_array().map(Vec::as_slice)
}
fn flatten_any_of_branches<'v>(
any_of: &'v [Value],
depth: usize,
out: &mut Vec<&'v Value>,
) -> Result<(), SchemaError> {
if depth > MAX_SCHEMA_DEPTH {
return Err(SchemaError(format!(
"anyOf/oneOf nesting depth exceeds the supported depth ({MAX_SCHEMA_DEPTH})"
)));
}
for sub in any_of {
match as_pure_nested_union(sub) {
Some(inner) => flatten_any_of_branches(inner, depth + 1, out)?,
None => out.push(sub),
}
if out.len() > MAX_ANYOF_BRANCHES {
return Err(SchemaError(format!(
"anyOf/oneOf branch count ({}) exceeds the supported limit ({MAX_ANYOF_BRANCHES})",
out.len()
)));
}
}
Ok(())
}
fn fold_literals_into(
literals: &mut Vec<String>,
byte_total: &mut usize,
values: Vec<String>,
) -> Result<(), SchemaError> {
let total = literals.len() + values.len();
if total > MAX_STRING_LITERALS {
return Err(SchemaError(format!(
"anyOf string literal count ({total}) exceeds the supported limit ({MAX_STRING_LITERALS})"
)));
}
for s in &values {
let json_repr = serde_json::to_string(s)
.map_err(|e| SchemaError(format!("cannot JSON-encode string value: {e}")))?;
*byte_total = byte_total.saturating_add(json_repr.len() - 1);
if *byte_total > MAX_STRING_LITERAL_BYTES {
return Err(SchemaError(format!(
"string literal encoded byte length exceeds the supported limit ({MAX_STRING_LITERAL_BYTES})"
)));
}
}
literals.extend(values);
Ok(())
}
fn fold_string_members(sub: &Value) -> Result<Vec<String>, SchemaError> {
let Some(obj) = sub.as_object() else {
return Ok(Vec::new());
};
if obj.contains_key("$ref") || obj.contains_key("type") || obj.contains_key("const") {
return Ok(Vec::new());
}
let Some(arr) = obj.get("enum").and_then(Value::as_array) else {
return Ok(Vec::new());
};
if arr.len() > MAX_STRING_LITERALS {
return Err(SchemaError(format!(
"anyOf string literal count ({}) exceeds the supported limit ({MAX_STRING_LITERALS})",
arr.len()
)));
}
let has_string = arr.iter().any(Value::is_string);
let has_non_string = arr.iter().any(|v| !v.is_string());
if !(has_string && has_non_string) {
return Ok(Vec::new());
}
let mut lits = Vec::new();
for v in arr {
if let Some(s) = v.as_str() {
guard_literal_bytes(s)?;
lits.push(s.to_string());
}
}
Ok(lits)
}
fn guard_literal_bytes(s: &str) -> Result<(), SchemaError> {
if s.len() > MAX_STRING_LITERAL_BYTES {
return Err(SchemaError(format!(
"string literal byte length ({}) exceeds the supported limit ({MAX_STRING_LITERAL_BYTES})",
s.len()
)));
}
Ok(())
}
fn guard_schema_string_bytes(v: &Value) -> Result<(), SchemaError> {
match v {
Value::String(s) => guard_literal_bytes(s),
Value::Array(items) => {
for item in items {
guard_schema_string_bytes(item)?;
}
Ok(())
}
Value::Object(map) => {
for (key, val) in map {
guard_literal_bytes(key)?;
guard_schema_string_bytes(val)?;
}
Ok(())
}
_ => Ok(()),
}
}
fn json_value_to_alt(v: &Value) -> Result<Alt, SchemaError> {
let json_str = serde_json::to_string(v)
.map_err(|e| SchemaError(format!("cannot serialize enum value: {e}")))?;
guard_literal_bytes(&json_str)?;
Ok(json_str.bytes().map(Symbol::Terminal).collect())
}
fn json_string_literal(key: &str) -> Result<Alt, SchemaError> {
let json_repr = serde_json::to_string(key)
.map_err(|e| SchemaError(format!("cannot JSON-encode property key: {e}")))?;
guard_literal_bytes(&json_repr)?;
Ok(json_repr.bytes().map(Symbol::Terminal).collect())
}
fn extract_ref_name(ref_str: &str) -> Result<&str, SchemaError> {
let parts: Vec<&str> = ref_str.split('/').collect();
match parts.as_slice() {
["#", "$defs", name] | ["#", "definitions", name] => Ok(name),
_ => Err(SchemaError(format!(
"unsupported $ref format: {ref_str} (only #/$defs/Name supported)"
))),
}
}
pub fn compile(schema: &Value) -> Result<CompiledGrammar, SchemaError> {
compile_json_schema(schema)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::grammar::pda::{GrammarState, SimResult, simulate_token};
fn compile_ok(schema_json: &str) -> CompiledGrammar {
let v: Value = serde_json::from_str(schema_json).unwrap();
compile(&v).unwrap()
}
fn accepts(grammar: &CompiledGrammar, input: &[u8]) -> bool {
let state = GrammarState::initial();
let (result, final_state) = simulate_token(&state, grammar, input);
result == SimResult::Accept && final_state.is_complete()
}
fn rejects(grammar: &CompiledGrammar, input: &[u8]) -> bool {
!accepts(grammar, input)
}
#[test]
fn compile_null_schema() {
let g = compile_ok(r#"{"type":"null"}"#);
assert!(accepts(&g, b"null"));
assert!(rejects(&g, b"true"));
}
#[test]
fn compile_boolean_schema() {
let g = compile_ok(r#"{"type":"boolean"}"#);
assert!(accepts(&g, b"true"));
assert!(accepts(&g, b"false"));
assert!(rejects(&g, b"null"));
}
#[test]
fn compile_integer_schema() {
let g = compile_ok(r#"{"type":"integer"}"#);
assert!(accepts(&g, b"42"));
assert!(accepts(&g, b"-7"));
assert!(accepts(&g, b"0"));
}
#[test]
fn compile_string_schema() {
let g = compile_ok(r#"{"type":"string"}"#);
assert!(accepts(&g, b"\"hello\""));
assert!(accepts(&g, b"\"\""));
}
#[test]
fn compile_string_enum() {
let g = compile_ok(r#"{"type":"string","enum":["foo","bar"]}"#);
assert!(accepts(&g, b"\"foo\""));
assert!(accepts(&g, b"\"bar\""));
assert!(rejects(&g, b"\"baz\""));
}
#[test]
fn compile_const_value() {
let g = compile_ok(r#"{"const":"hello"}"#);
assert!(accepts(&g, b"\"hello\""));
assert!(rejects(&g, b"\"world\""));
}
#[test]
fn compile_const_integer() {
let g = compile_ok(r#"{"const":42}"#);
assert!(accepts(&g, b"42"));
assert!(rejects(&g, b"43"));
}
#[test]
fn compile_any_of() {
let g = compile_ok(r#"{"anyOf":[{"type":"boolean"},{"type":"null"}]}"#);
assert!(accepts(&g, b"true"));
assert!(accepts(&g, b"false"));
assert!(accepts(&g, b"null"));
}
#[test]
fn compile_simple_object() {
let g = compile_ok(
r#"{
"type": "object",
"properties": {
"name": {"type": "string"}
},
"required": ["name"]
}"#,
);
assert!(accepts(&g, b"{\"name\":\"Alice\"}"));
}
#[test]
fn compile_empty_object() {
let g = compile_ok(r#"{"type":"object"}"#);
assert!(accepts(&g, b"{}"));
}
#[test]
fn empty_object_accepts_interior_whitespace() {
let g = compile_ok(r#"{"type":"object"}"#);
assert!(accepts(&g, b"{}"));
assert!(
accepts(&g, b"{ }"),
"empty no-properties object should accept interior whitespace"
);
}
#[test]
fn distinct_objects_sharing_a_key_do_not_alias() {
let g = compile_ok(
r#"{
"type":"object",
"properties":{
"a":{"type":"object","properties":{"x":{"type":"string"}},"required":["x"]},
"b":{"type":"object","properties":{"x":{"type":"integer"}},"required":["x"]}
},
"required":["a","b"]
}"#,
);
assert!(
accepts(&g, b"{\"a\":{\"x\":\"s\"},\"b\":{\"x\":42}}"),
"b.x must accept an integer; the string rule for a.x must not alias it"
);
assert!(
rejects(&g, b"{\"a\":{\"x\":42},\"b\":{\"x\":42}}"),
"a.x must still reject an integer (string-typed)"
);
}
#[test]
fn compile_array_any() {
let g = compile_ok(r#"{"type":"array"}"#);
assert!(accepts(&g, b"[]"));
assert!(accepts(&g, b"[5]"));
assert!(accepts(&g, b"[1,2]"));
assert!(accepts(&g, br#"[{}]"#));
assert!(accepts(&g, b"[true]"));
}
#[test]
fn any_array_rejects_trailing_comma() {
let g = compile_ok(r#"{"type":"array"}"#);
assert!(accepts(&g, b"[]"));
assert!(accepts(&g, b"[5]"));
assert!(accepts(&g, b"[1,2]"));
assert!(rejects(&g, b"[1,]"));
assert!(rejects(&g, b"[1,2,]"));
assert!(rejects(&g, b"[,]"));
}
#[test]
fn typed_array_rejects_trailing_comma() {
let g = compile_ok(r#"{"type":"array","items":{"type":"integer"}}"#);
assert!(accepts(&g, b"[]"));
assert!(accepts(&g, b"[5]"));
assert!(accepts(&g, b"[1,2]"));
assert!(rejects(&g, b"[1,]"));
assert!(rejects(&g, b"[1,2,]"));
}
#[test]
fn string_enum_shared_prefix_foo_food() {
let g = compile_ok(r#"{"type":"string","enum":["foo","food"]}"#);
assert!(accepts(&g, b"\"foo\""), "\"foo\" must be accepted");
assert!(accepts(&g, b"\"food\""), "\"food\" must be accepted");
assert!(rejects(&g, b"\"foe\""), "\"foe\" must be rejected");
assert!(rejects(&g, b"\"fooo\""), "\"fooo\" must be rejected");
assert!(rejects(&g, b"\"fo\""), "\"fo\" must be rejected");
}
#[test]
fn string_enum_bare_no_type_shared_prefix() {
let g = compile_ok(r#"{"enum":["foo","food"]}"#);
assert!(accepts(&g, b"\"foo\""), "\"foo\" must be accepted");
assert!(accepts(&g, b"\"food\""), "\"food\" must be accepted");
assert!(rejects(&g, b"\"foe\""), "\"foe\" must be rejected");
}
#[test]
fn anyof_const_shared_prefix_abc_abd() {
let g = compile_ok(r#"{"anyOf":[{"const":"abc"},{"const":"abd"}]}"#);
assert!(accepts(&g, b"\"abc\""), "\"abc\" must be accepted");
assert!(accepts(&g, b"\"abd\""), "\"abd\" must be accepted");
assert!(rejects(&g, b"\"abe\""), "\"abe\" must be rejected");
assert!(rejects(&g, b"\"ab\""), "\"ab\" must be rejected");
assert!(rejects(&g, b"\"abcd\""), "\"abcd\" must be rejected");
}
#[test]
fn anyof_broad_string_with_shared_prefix_consts() {
let g = compile_ok(r#"{"anyOf":[{"type":"string"},{"const":"abc"},{"const":"abd"}]}"#);
assert!(
accepts(&g, b"\"zzz\""),
"broad string branch must accept an arbitrary string"
);
assert!(accepts(&g, b"\"abc\""), "\"abc\" must be accepted");
assert!(accepts(&g, b"\"abd\""), "\"abd\" must be accepted");
assert!(rejects(&g, b"123"), "a non-string must still be rejected");
}
#[test]
fn anyof_shared_prefix_consts_with_integer() {
let g = compile_ok(r#"{"anyOf":[{"const":"abc"},{"const":"abd"},{"type":"integer"}]}"#);
assert!(accepts(&g, b"\"abc\""), "\"abc\" must be accepted");
assert!(
accepts(&g, b"\"abd\""),
"\"abd\" must be accepted via the trie"
);
assert!(
accepts(&g, b"123"),
"the integer branch must remain reachable"
);
assert!(
rejects(&g, b"\"abe\""),
"a non-member string must be rejected"
);
}
#[test]
fn anyof_broad_string_after_consts() {
let g = compile_ok(r#"{"anyOf":[{"const":"abc"},{"const":"abd"},{"type":"string"}]}"#);
assert!(
accepts(&g, b"\"abe\""),
"broad string (listed last) must accept a string that diverges from the consts"
);
assert!(accepts(&g, b"\"zzz\""), "arbitrary string must be accepted");
assert!(accepts(&g, b"\"abc\""), "\"abc\" must be accepted");
assert!(accepts(&g, b"\"abd\""), "\"abd\" must be accepted");
assert!(rejects(&g, b"123"), "a non-string must still be rejected");
}
#[test]
fn anyof_narrower_string_enum_sibling() {
let g = compile_ok(
r#"{"anyOf":[{"const":"abc"},{"const":"abd"},{"type":"string","enum":["abe","abf"]}]}"#,
);
assert!(accepts(&g, b"\"abc\""), "const \"abc\" must be accepted");
assert!(
accepts(&g, b"\"abd\""),
"const \"abd\" must be accepted via the combined trie"
);
assert!(
accepts(&g, b"\"abe\""),
"enum member \"abe\" must be accepted"
);
assert!(
accepts(&g, b"\"abf\""),
"enum member \"abf\" must be accepted"
);
assert!(
rejects(&g, b"\"abg\""),
"a string in no branch must be rejected (no over-accept)"
);
}
#[test]
fn anyof_numeric_enum_sibling() {
let g = compile_ok(r#"{"anyOf":[{"const":"abc"},{"const":"abd"},{"enum":[1,2]}]}"#);
assert!(
accepts(&g, b"\"abd\""),
"\"abd\" must be accepted via the trie"
);
assert!(accepts(&g, b"1"), "numeric enum member 1 must be reachable");
assert!(accepts(&g, b"2"), "numeric enum member 2 must be reachable");
assert!(
rejects(&g, b"\"abe\""),
"a non-member string must be rejected"
);
}
#[test]
fn anyof_typed_mixed_enum_folded() {
let g = compile_ok(r#"{"anyOf":[{"const":"abc"},{"type":"string","enum":["abe",1]}]}"#);
assert!(accepts(&g, b"\"abc\""), "const \"abc\" must be accepted");
assert!(
accepts(&g, b"\"abe\""),
"the type:string enum's string member \"abe\" must be folded and accepted"
);
assert!(
rejects(&g, b"\"abd\""),
"a string in no branch must be rejected (no over-accept)"
);
assert!(
rejects(&g, b"\"zzz\""),
"the dead integer member must not widen the branch to any-string"
);
assert!(
rejects(&g, b"1"),
"the integer enum member is unsatisfiable under type:string"
);
}
#[test]
fn anyof_typed_enum_no_string_members_rejected() {
let g = compile_ok(r#"{"anyOf":[{"type":"integer"},{"type":"string","enum":[1,2]}]}"#);
assert!(accepts(&g, b"7"), "the integer branch must stay reachable");
assert!(
rejects(&g, b"\"zzz\""),
"the unsatisfiable type:string + numeric-enum branch must not widen to any-string"
);
}
#[test]
fn anyof_const_conflicting_enum_rejected() {
let g = compile_ok(
r#"{"anyOf":[{"type":"integer"},{"type":"string","const":"x","enum":["y"]}]}"#,
);
assert!(accepts(&g, b"7"), "the integer branch must stay reachable");
assert!(
rejects(&g, b"\"x\""),
"const \"x\" conflicts with enum [\"y\"] — empty intersection, reject"
);
assert!(
rejects(&g, b"\"y\""),
"enum member \"y\" conflicts with const \"x\" — empty intersection, reject"
);
}
#[test]
fn anyof_const_compatible_enum_folds_to_const() {
let g = compile_ok(
r#"{"anyOf":[{"type":"integer"},{"type":"string","const":"x","enum":["x","y"]}]}"#,
);
assert!(accepts(&g, b"7"), "the integer branch must stay reachable");
assert!(
accepts(&g, b"\"x\""),
"const \"x\" is in the enum — accepted"
);
assert!(
rejects(&g, b"\"y\""),
"\"y\" is in the enum but excluded by const \"x\" — reject"
);
}
#[test]
fn standalone_typed_enum_no_string_members_rejects_all() {
let g = compile_ok(r#"{"type":"string","enum":[1,2]}"#);
assert!(
rejects(&g, b"\"zzz\""),
"no string satisfies type:string + enum [1,2]"
);
assert!(rejects(&g, b"1"), "the numeric member is not a string");
}
#[test]
fn standalone_string_const_enum_intersection() {
let conflict = compile_ok(r#"{"type":"string","const":"x","enum":["y"]}"#);
assert!(
rejects(&conflict, b"\"x\""),
"const conflicts with enum — reject \"x\""
);
assert!(
rejects(&conflict, b"\"y\""),
"const conflicts with enum — reject \"y\""
);
let compatible = compile_ok(r#"{"type":"string","const":"x","enum":["x","y"]}"#);
assert!(
accepts(&compatible, b"\"x\""),
"const \"x\" is in the enum — accept"
);
assert!(
rejects(&compatible, b"\"y\""),
"\"y\" excluded by const \"x\" — reject"
);
}
#[test]
fn typed_string_enum_empty_and_duplicate_members() {
let empty = compile_ok(r#"{"type":"string","enum":[]}"#);
assert!(rejects(&empty, b"\"zzz\""), "empty enum accepts nothing");
let dups = compile_ok(r#"{"type":"string","enum":["dup","dup"]}"#);
assert!(
accepts(&dups, b"\"dup\""),
"a duplicated member still accepts once"
);
assert!(rejects(&dups, b"\"other\""), "no over-accept from dedup");
let in_anyof = compile_ok(r#"{"anyOf":[{"type":"integer"},{"type":"string","enum":[]}]}"#);
assert!(accepts(&in_anyof, b"7"), "integer branch reachable");
assert!(
rejects(&in_anyof, b"\"zzz\""),
"empty typed enum contributes no string"
);
}
#[test]
fn anyof_ref_to_broad_string_folds_and_accepts() {
let g = compile_ok(
r##"{"$defs":{"S":{"type":"string"}},"anyOf":[{"const":"abc"},{"const":"abd"},{"$ref":"#/$defs/S"}]}"##,
);
assert!(accepts(&g, b"\"abc\""), "const \"abc\" still accepted");
assert!(accepts(&g, b"\"abd\""), "const \"abd\" still accepted");
assert!(
accepts(&g, b"\"zzz\""),
"issue #473 fix: $ref->broad-string sibling must now be reachable"
);
assert!(rejects(&g, b"123"), "a non-string must still be rejected");
}
#[test]
fn anyof_ref_to_string_literal_folds_into_trie() {
let g = compile_ok(
r##"{"$defs":{"S":{"const":"zzz"}},"anyOf":[{"const":"abc"},{"$ref":"#/$defs/S"}]}"##,
);
assert!(accepts(&g, b"\"abc\""), "const \"abc\" accepted");
assert!(
accepts(&g, b"\"zzz\""),
"issue #473 fix: $ref->const literal must be folded into the trie"
);
assert!(
rejects(&g, b"\"abd\""),
"a string in neither literal must be rejected (no over-accept)"
);
}
#[test]
fn anyof_ref_to_non_string_stays_other() {
let g = compile_ok(
r##"{"$defs":{"N":{"type":"integer"}},"anyOf":[{"const":"abc"},{"$ref":"#/$defs/N"}]}"##,
);
assert!(accepts(&g, b"\"abc\""), "const \"abc\" still accepted");
assert!(
accepts(&g, b"7"),
"the ref'd integer branch stays reachable"
);
assert!(
rejects(&g, b"\"zzz\""),
"a non-member string must still be rejected"
);
}
#[test]
fn anyof_ref_to_non_string_with_string_forcing_sibling_drops() {
let g = compile_ok(
r##"{"$defs":{"N":{"type":"integer"}},"anyOf":[{"const":"abc"},{"$ref":"#/$defs/N","const":"y"}]}"##,
);
assert!(
accepts(&g, b"\"abc\""),
"the peer const \"abc\" still accepted"
);
assert!(
rejects(&g, b"\"y\""),
"issue #473: a string const sibling on a $ref->integer branch must not over-accept the string"
);
assert!(
rejects(&g, b"7"),
"the string ∧ integer conjunction is empty — the number must be rejected too (no over-accept)"
);
let g = compile_ok(
r##"{"$defs":{"N":{"type":"integer"}},"anyOf":[{"const":"abc"},{"$ref":"#/$defs/N","type":"string"}]}"##,
);
assert!(
rejects(&g, b"\"zzz\""),
"type:string sibling on a $ref->integer branch must not over-accept"
);
assert!(
rejects(&g, b"7"),
"empty conjunction rejects the number too"
);
let g = compile_ok(
r##"{"$defs":{"N":{"type":"integer"}},"anyOf":[{"const":"abc"},{"$ref":"#/$defs/N","enum":["y","z"]}]}"##,
);
assert!(
rejects(&g, b"\"y\""),
"all-string enum sibling on a $ref->integer branch must not over-accept"
);
assert!(
rejects(&g, b"7"),
"empty conjunction rejects the number too"
);
}
#[test]
fn anyof_ref_chain_intermediate_string_forcing_node_drops() {
let g = compile_ok(
r##"{"$defs":{"S":{"$ref":"#/$defs/N","type":"string"},"N":{"type":"integer"}},"anyOf":[{"const":"ok"},{"$ref":"#/$defs/S"}]}"##,
);
assert!(
accepts(&g, b"\"ok\""),
"the peer const \"ok\" still accepted"
);
assert!(
rejects(&g, b"\"zzz\""),
"issue #473: an intermediate type:string node on a $ref->integer chain must not over-accept the string"
);
assert!(
rejects(&g, b"7"),
"the intermediate string ∧ integer conjunction is empty — the number must be rejected too"
);
let g = compile_ok(
r##"{"$defs":{"S":{"$ref":"#/$defs/N","const":"y"},"N":{"type":"integer"}},"anyOf":[{"const":"ok"},{"$ref":"#/$defs/S"}]}"##,
);
assert!(
rejects(&g, b"\"y\""),
"intermediate string const on a $ref->integer chain must not over-accept"
);
assert!(
rejects(&g, b"7"),
"empty conjunction rejects the number too"
);
let g = compile_ok(
r##"{"$defs":{"S":{"$ref":"#/$defs/N","enum":["y","z"]},"N":{"type":"integer"}},"anyOf":[{"const":"ok"},{"$ref":"#/$defs/S"}]}"##,
);
assert!(
rejects(&g, b"\"y\""),
"intermediate all-string enum on a $ref->integer chain must not over-accept"
);
assert!(
rejects(&g, b"7"),
"empty conjunction rejects the number too"
);
}
#[test]
fn anyof_ref_type_array_string_only_forces_drop() {
let g = compile_ok(
r##"{"$defs":{"N":{"type":"integer"}},"anyOf":[{"const":"ok"},{"$ref":"#/$defs/N","type":["string"]}]}"##,
);
assert!(
accepts(&g, b"\"ok\""),
"the peer const \"ok\" still accepted"
);
assert!(
rejects(&g, b"\"zzz\""),
"a string-only type array on a $ref->integer branch must not over-accept the string"
);
assert!(
rejects(&g, b"7"),
"the string ∧ integer conjunction is empty — the number must be rejected too"
);
let g = compile_ok(
r##"{"$defs":{"S":{"$ref":"#/$defs/N","type":["string"]},"N":{"type":"integer"}},"anyOf":[{"const":"ok"},{"$ref":"#/$defs/S"}]}"##,
);
assert!(
rejects(&g, b"\"zzz\""),
"an intermediate string-only type array must not over-accept"
);
assert!(
rejects(&g, b"7"),
"empty conjunction rejects the number too"
);
}
#[test]
fn anyof_ref_type_array_with_nonstring_member_stays_reachable() {
let g = compile_ok(
r##"{"$defs":{"N":{"type":"integer"}},"anyOf":[{"const":"ok"},{"$ref":"#/$defs/N","type":["string","integer"]}]}"##,
);
assert!(
accepts(&g, b"\"ok\""),
"the peer const \"ok\" still accepted"
);
assert!(
accepts(&g, b"7"),
"a type array admitting integer is not string-forcing — the integer branch stays reachable"
);
}
#[test]
fn anyof_ref_to_integer_with_numeric_enum_sibling_stays_reachable() {
let g = compile_ok(
r##"{"$defs":{"N":{"type":"integer"}},"anyOf":[{"const":"abc"},{"$ref":"#/$defs/N","enum":[1,2]}]}"##,
);
assert!(
accepts(&g, b"\"abc\""),
"the peer const \"abc\" still accepted"
);
assert!(
accepts(&g, b"7"),
"a numeric-enum sibling is not string-forcing — the integer branch stays reachable"
);
assert!(
rejects(&g, b"\"z\""),
"a non-member string is still rejected"
);
}
#[test]
fn anyof_ref_to_broad_string_with_enum_sibling_narrows() {
let g = compile_ok(
r##"{"$defs":{"T":{"type":"string"}},"anyOf":[{"const":"foo"},{"$ref":"#/$defs/T","enum":["abc"]}]}"##,
);
assert!(
accepts(&g, b"\"foo\""),
"the peer const \"foo\" still accepted"
);
assert!(
accepts(&g, b"\"abc\""),
"issue #473: the sibling enum member is reachable (terminal-broad intersect the enum)"
);
assert!(
rejects(&g, b"\"zzz\""),
"the enum-narrowed branch must reject a non-member string (no over-accept)"
);
}
#[test]
fn anyof_ref_chain_collects_intermediate_enum_narrowing() {
let g = compile_ok(
r##"{"$defs":{"S":{"$ref":"#/$defs/T","enum":["abc"]},"T":{"type":"string"}},"anyOf":[{"const":"foo"},{"$ref":"#/$defs/S"}]}"##,
);
assert!(
accepts(&g, b"\"foo\""),
"the peer const \"foo\" still accepted"
);
assert!(
accepts(&g, b"\"abc\""),
"issue #473: an enum on an intermediate chain node narrows the folded set"
);
assert!(
rejects(&g, b"\"zzz\""),
"the chain-collected narrowing must reject a non-member string (no over-accept)"
);
}
#[test]
fn anyof_ref_to_missing_def_still_errors() {
let schema_json = r##"{"anyOf":[{"const":"abc"},{"$ref":"#/$defs/Missing"}]}"##;
let v: Value = serde_json::from_str(schema_json).unwrap();
let err = compile(&v).expect_err("an unresolvable $ref must still fail to compile");
assert!(
err.0.contains("$ref not found"),
"expected the pre-existing $ref-not-found error, got: {}",
err.0
);
}
#[test]
fn anyof_nested_anyof_string_folds_and_accepts() {
let g = compile_ok(r#"{"anyOf":[{"const":"abc"},{"anyOf":[{"type":"string"}]}]}"#);
assert!(accepts(&g, b"\"abc\""), "const \"abc\" still accepted");
assert!(
accepts(&g, b"\"zzz\""),
"issue #473 fix: nested-anyOf broad string must now be reachable"
);
}
#[test]
fn anyof_nested_oneof_not_flattened_known_limitation() {
let g = compile_ok(r#"{"anyOf":[{"const":"abc"},{"oneOf":[{"type":"string"}]}]}"#);
assert!(accepts(&g, b"\"abc\""), "const \"abc\" still accepted");
assert!(
rejects(&g, b"\"zzz\""),
"KNOWN LIMITATION #473: a nested oneOf is deliberately not flattened (XOR unenforceable)"
);
}
#[test]
fn anyof_nested_oneof_overlap_no_over_accept() {
let g = compile_ok(
r#"{"anyOf":[{"const":"abc"},{"oneOf":[{"type":"string"},{"const":"zzz"}]}]}"#,
);
assert!(accepts(&g, b"\"abc\""), "const \"abc\" still accepted");
assert!(
rejects(&g, b"\"zzz\""),
"issue #473: overlapping nested oneOf must not over-accept the shared value \"zzz\""
);
}
#[test]
fn anyof_nested_anyof_with_sibling_key_stays_unflattened() {
let g = compile_ok(
r#"{"anyOf":[{"const":"abc"},{"anyOf":[{"type":"string"}],"description":"nested"}]}"#,
);
assert!(accepts(&g, b"\"abc\""), "const \"abc\" still accepted");
assert!(
rejects(&g, b"\"zzz\""),
"a nested union with a sibling key is deliberately not flattened"
);
}
#[test]
fn anyof_untyped_mixed_enum_folds_string_member() {
let g = compile_ok(r#"{"anyOf":[{"const":"abc"},{"enum":["abe",1]}]}"#);
assert!(accepts(&g, b"\"abc\""), "const \"abc\" still accepted");
assert!(
accepts(&g, b"\"abe\""),
"issue #473 fix: untyped mixed-enum string member must now be reachable"
);
assert!(
accepts(&g, b"1"),
"the non-string enum member 1 stays reachable"
);
assert!(
rejects(&g, b"\"abd\""),
"a string in no branch must still be rejected (no over-accept)"
);
}
#[test]
fn anyof_untyped_mixed_enum_multiple_members_all_reachable() {
let g = compile_ok(r#"{"anyOf":[{"enum":["abe","abf",1,null]}]}"#);
assert!(accepts(&g, b"\"abe\""), "string member \"abe\" reachable");
assert!(accepts(&g, b"\"abf\""), "string member \"abf\" reachable");
assert!(accepts(&g, b"1"), "non-string member 1 reachable");
assert!(accepts(&g, b"null"), "non-string member null reachable");
assert!(
rejects(&g, b"\"abg\""),
"a string not in the enum must be rejected (no over-accept)"
);
}
#[test]
fn anyof_untyped_sibling_shadowed_known_limitation() {
let g = compile_ok(r#"{"anyOf":[{"const":"abc"},{}]}"#);
assert!(accepts(&g, b"\"abc\""), "const \"abc\" still accepted");
assert!(
rejects(&g, b"\"zzz\""),
"KNOWN LIMITATION #473: untyped empty-schema string inputs shadowed by the const trie"
);
}
#[test]
fn anyof_mixed_type_array_sibling_shadowed_known_limitation() {
let g = compile_ok(r#"{"anyOf":[{"const":"abc"},{"type":["string","number"]}]}"#);
assert!(accepts(&g, b"\"abc\""), "const \"abc\" still accepted");
assert!(
accepts(&g, b"42"),
"a non-string alternative from the mixed-type branch stays reachable"
);
assert!(
rejects(&g, b"\"zzz\""),
"KNOWN LIMITATION #473: mixed-type-array sibling's string inputs shadowed by the const trie"
);
}
#[test]
fn anyof_nested_union_flatten_bypass_rejected_by_branch_cap() {
let over = MAX_ANYOF_BRANCHES + 1;
let inner_branches: String = (0..over)
.map(|_| r#"{"type":"boolean"}"#.to_string())
.collect::<Vec<_>>()
.join(",");
let nested_union = format!("{{\"anyOf\":[{inner_branches}]}}");
let schema_json = format!("{{\"anyOf\":[{{\"const\":\"abc\"}},{nested_union}]}}");
let v: Value = serde_json::from_str(&schema_json).unwrap();
let err = compile(&v).expect_err(
"flattened branch count over the cap must be rejected even though the raw top-level count is only 2",
);
assert!(
err.0.contains("anyOf/oneOf branch count"),
"expected the anyOf branch-count cap error, got: {}",
err.0
);
}
#[test]
fn anyof_nested_union_flatten_depth_capped() {
let mut inner = {
let mut m = serde_json::Map::new();
m.insert("type".to_string(), Value::String("boolean".to_string()));
Value::Object(m)
};
for _ in 0..(MAX_SCHEMA_DEPTH + 50) {
let mut m = serde_json::Map::new();
m.insert("anyOf".to_string(), Value::Array(vec![inner]));
inner = Value::Object(m);
}
let err =
compile(&inner).expect_err("anyOf/oneOf nesting depth over the cap must be rejected");
assert!(
err.0.contains("anyOf/oneOf nesting depth"),
"expected the flatten depth-cap error, got: {}",
err.0
);
}
#[test]
fn string_enum_three_way_shared_prefix() {
let g = compile_ok(r#"{"type":"string","enum":["foo","food","foot"]}"#);
assert!(accepts(&g, b"\"foo\""), "\"foo\" must be accepted");
assert!(accepts(&g, b"\"food\""), "\"food\" must be accepted");
assert!(accepts(&g, b"\"foot\""), "\"foot\" must be accepted");
assert!(rejects(&g, b"\"fooz\""), "\"fooz\" must be rejected");
assert!(rejects(&g, b"\"fo\""), "\"fo\" must be rejected");
}
#[test]
fn string_enum_prefix_pair_a_ab() {
let g = compile_ok(r#"{"type":"string","enum":["a","ab"]}"#);
assert!(accepts(&g, b"\"a\""), "\"a\" must be accepted");
assert!(accepts(&g, b"\"ab\""), "\"ab\" must be accepted");
assert!(rejects(&g, b"\"abc\""), "\"abc\" must be rejected");
assert!(rejects(&g, b"\"b\""), "\"b\" must be rejected");
}
#[test]
fn string_enum_escaped_values() {
let g = compile_ok(r#"{"type":"string","enum":["a\\b","a\\c"]}"#);
assert!(
accepts(&g, b"\"a\\\\b\""),
"literal-backslash value a\\b must be accepted"
);
assert!(
accepts(&g, b"\"a\\\\c\""),
"literal-backslash value a\\c must be accepted"
);
assert!(
rejects(&g, b"\"a\\\\d\""),
"a\\d (not in enum) must be rejected"
);
assert!(
rejects(&g, b"\"a\""),
"bare a (no backslash) must be rejected"
);
}
#[test]
fn string_enum_single_value() {
let g = compile_ok(r#"{"type":"string","enum":["solo"]}"#);
assert!(accepts(&g, b"\"solo\""), "\"solo\" must be accepted");
assert!(rejects(&g, b"\"sol\""), "prefix \"sol\" must be rejected");
assert!(
rejects(&g, b"\"soloo\""),
"extension \"soloo\" must be rejected"
);
}
#[test]
fn string_enum_disjoint() {
let g = compile_ok(r#"{"type":"string","enum":["cat","dog"]}"#);
assert!(accepts(&g, b"\"cat\""), "\"cat\" must be accepted");
assert!(accepts(&g, b"\"dog\""), "\"dog\" must be accepted");
assert!(rejects(&g, b"\"cot\""), "\"cot\" must be rejected");
assert!(rejects(&g, b"\"dig\""), "\"dig\" must be rejected");
}
#[test]
fn anyof_mixed_string_const_and_object() {
let g = compile_ok(
r#"{"anyOf":[{"const":"abc"},{"type":"object","properties":{"x":{"type":"integer"}},"required":["x"]}]}"#,
);
assert!(accepts(&g, b"\"abc\""), "const string must be accepted");
assert!(accepts(&g, b"{\"x\":1}"), "valid object must be accepted");
assert!(
rejects(&g, b"\"abd\""),
"non-member string must be rejected"
);
}
#[test]
fn string_enum_long_single_value_compiles() {
let val = "a".repeat(20_000);
let g = compile_ok(&format!(r#"{{"type":"string","enum":["{val}"]}}"#));
let mut input = vec![b'"'];
input.extend(std::iter::repeat_n(b'a', 20_000));
input.push(b'"');
assert!(accepts(&g, &input), "the long enum value must be accepted");
assert!(rejects(&g, b"\"a\""), "a shorter prefix must be rejected");
}
#[test]
fn string_enum_pathological_shared_prefix_fails_closed() {
let pfx = "a".repeat(MAX_TRIE_DEPTH + 50);
let schema = format!(r#"{{"enum":["{pfx}b","{pfx}c"]}}"#);
let v: Value = serde_json::from_str(&schema).unwrap();
let err = compile(&v).expect_err("an absurd shared prefix must be rejected, not crash");
assert!(
err.0.contains("depth"),
"error should name the depth limit, got: {err:?}"
);
}
#[test]
fn schema_error_display() {
let e = SchemaError("test".to_string());
assert!(e.to_string().contains("test"));
}
#[test]
fn unsupported_type_returns_error() {
let v = serde_json::json!({"type": "binary"});
assert!(compile_json_schema(&v).is_err());
}
#[test]
fn ref_not_found_returns_error() {
let v = serde_json::json!({"$ref": "#/$defs/Missing"});
assert!(compile(&v).is_err());
}
#[test]
fn defs_resolved() {
let schema = serde_json::json!({
"$defs": {
"Status": {"type": "string", "enum": ["ok", "err"]}
},
"type": "object",
"properties": {
"status": {"$ref": "#/$defs/Status"}
},
"required": ["status"]
});
let g = compile(&schema).unwrap();
assert!(accepts(&g, b"{\"status\":\"ok\"}"));
assert!(accepts(&g, b"{\"status\":\"err\"}"));
}
#[test]
fn object_both_required() {
let g = compile_ok(
r#"{
"type":"object",
"properties":{"r":{"type":"integer"},"s":{"type":"integer"}},
"required":["r","s"]
}"#,
);
assert!(
accepts(&g, b"{\"r\":1,\"s\":2}"),
"fully-present object must be accepted"
);
assert!(
rejects(&g, b"{\"r\":1}"),
"missing required s must be rejected"
);
assert!(
rejects(&g, b"{}"),
"empty object must be rejected when both required"
);
}
#[test]
fn object_single_optional_only() {
let g = compile_ok(
r#"{
"type":"object",
"properties":{"o":{"type":"integer"}}
}"#,
);
assert!(
accepts(&g, b"{}"),
"empty object must be accepted when property is optional"
);
assert!(
accepts(&g, b"{\"o\":1}"),
"object with optional property must be accepted"
);
assert!(
rejects(&g, b"{\"o\":1,}"),
"trailing comma must be rejected"
);
}
#[test]
fn object_required_plus_trailing_optional_issue_355() {
let g = compile_ok(
r#"{
"type":"object",
"properties":{"r":{"type":"integer"},"o":{"type":"integer"}},
"required":["r"]
}"#,
);
assert!(
accepts(&g, b"{\"r\":1}"),
"required-only object must be accepted (issue #355)"
);
assert!(
accepts(&g, b"{\"r\":1,\"o\":2}"),
"fully-present object must be accepted (issue #355)"
);
assert!(rejects(&g, b"{}"), "missing required r must be rejected");
assert!(
rejects(&g, b"{\"o\":2}"),
"object with only optional and no required must be rejected"
);
assert!(
rejects(&g, b"{\"r\":1,}"),
"trailing comma must be rejected"
);
assert!(
rejects(&g, b"{\"r\":\"bad\"}"),
"wrong value type for r must be rejected"
);
}
#[test]
fn object_two_trailing_optionals() {
let g = compile_ok(
r#"{
"type":"object",
"properties":{
"r":{"type":"integer"},
"o1":{"type":"integer"},
"o2":{"type":"integer"}
},
"required":["r"]
}"#,
);
assert!(accepts(&g, b"{\"r\":1}"), "required-only must be accepted");
assert!(
accepts(&g, b"{\"r\":1,\"o1\":2}"),
"r + o1 must be accepted"
);
assert!(
accepts(&g, b"{\"r\":1,\"o1\":2,\"o2\":3}"),
"all three properties must be accepted"
);
assert!(rejects(&g, b"{}"), "empty object must be rejected");
assert!(rejects(&g, b"{\"o1\":2}"), "missing r must be rejected");
assert!(
rejects(&g, b"{\"r\":1,\"o2\":3}"),
"#353: PDA over-rejects r+o2 (interleaved optional, skip o1)"
);
assert!(
rejects(&g, b"{\"r\":1,}"),
"trailing comma after r-only must reject"
);
assert!(
rejects(&g, b"{\"r\":1,\"o1\":2,}"),
"trailing comma after r+o1 must reject (no fallback to o2 skip alt)"
);
assert!(
rejects(&g, b"{\"r\":1,\"o1\":2,\"o2\":3,}"),
"trailing comma after all three must reject"
);
}
#[test]
fn object_required_first_reorder() {
let g = compile_ok(
r#"{
"type":"object",
"properties":{
"o1":{"type":"integer"},
"o2":{"type":"integer"},
"r":{"type":"integer"}
},
"required":["r"]
}"#,
);
assert!(
accepts(&g, b"{\"r\":1}"),
"required-only object must be accepted (the #355 fix)"
);
assert!(
accepts(&g, b"{\"r\":1,\"o1\":2,\"o2\":3}"),
"required-first canonical order must be accepted"
);
assert!(
rejects(&g, b"{\"o1\":2,\"o2\":3,\"r\":1}"),
"non-canonical (alphabetical) key order is rejected (required-first re-canonicalization)"
);
}
#[test]
fn object_no_over_acceptance() {
let g = compile_ok(
r#"{
"type":"object",
"properties":{"r":{"type":"integer"},"o":{"type":"integer"}},
"required":["r"]
}"#,
);
assert!(
rejects(&g, b"{\"r\":1,\"o\":\"bad\"}"),
"wrong type for optional must be rejected"
);
assert!(
rejects(&g, b"{\"r\":1,,\"o\":2}"),
"double comma must be rejected"
);
assert!(rejects(&g, b"{,\"r\":1}"), "leading comma must be rejected");
assert!(rejects(&g, b"\"r\":1"), "missing braces must be rejected");
assert!(
rejects(&g, b"{\"r\":1,\"o\":2,}"),
"trailing comma after optional must be rejected"
);
}
#[test]
fn object_fully_present_and_absent_both_correct() {
let g = compile_ok(
r#"{
"type":"object",
"properties":{"a":{"type":"string"},"b":{"type":"boolean"}},
"required":["a"]
}"#,
);
assert!(
accepts(&g, b"{\"a\":\"x\"}"),
"required-only string must be accepted"
);
assert!(
accepts(&g, b"{\"a\":\"x\",\"b\":true}"),
"both present must be accepted"
);
assert!(
accepts(&g, b"{\"a\":\"x\",\"b\":false}"),
"both present (false) must be accepted"
);
assert!(
rejects(&g, b"{\"b\":true}"),
"missing required a must be rejected"
);
assert!(rejects(&g, b"{}"), "empty must be rejected");
}
#[test]
fn object_two_optional_only_pda_boundary() {
let g = compile_ok(
r#"{"type":"object","properties":{"a":{"type":"integer"},"b":{"type":"integer"}}}"#,
);
assert!(
accepts(&g, b"{}"),
"empty object must be accepted when all optional"
);
assert!(
accepts(&g, b"{\"a\":1}"),
"first optional alone must be accepted"
);
assert!(
accepts(&g, b"{\"a\":1,\"b\":2}"),
"both present must be accepted"
);
assert!(
rejects(&g, b"{\"b\":2}"),
"PDA over-rejects skip-first optional (safe direction)"
);
assert!(
rejects(&g, b"{\"a\":1,}"),
"trailing comma after first optional must reject (no fallback to skip alt)"
);
assert!(
rejects(&g, b"{\"a\":1,\"b\":2,}"),
"trailing comma after last property must be rejected"
);
}
#[test]
fn object_three_props_trailing_comma_boundary() {
let g = compile_ok(
r#"{
"type":"object",
"properties":{
"r":{"type":"integer"},
"o1":{"type":"integer"},
"o2":{"type":"integer"}
},
"required":["r"]
}"#,
);
assert!(accepts(&g, b"{\"r\":1}"), "required-only must be accepted");
assert!(accepts(&g, b"{\"r\":1,\"o1\":2}"), "r+o1 must be accepted");
assert!(
accepts(&g, b"{\"r\":1,\"o1\":2,\"o2\":3}"),
"all three must be accepted"
);
assert!(
rejects(&g, b"{\"r\":1,}"),
"trailing comma after required-only must be rejected (inline-key fix)"
);
assert!(
rejects(&g, b"{\"r\":1,\"o1\":2,}"),
"trailing comma after r+o1 must reject (no fallback to o2 skip alt)"
);
assert!(
rejects(&g, b"{\"r\":1,\"o1\":2,\"o2\":3,}"),
"trailing comma after last property must be rejected"
);
}
#[test]
fn object_property_count_cap_rejects() {
let over = MAX_OBJECT_PROPERTIES + 1;
let mut props = String::new();
for i in 0..over {
if i > 0 {
props.push(',');
}
props.push_str(&format!("\"p{i}\":{{\"type\":\"integer\"}}"));
}
let schema_json = format!("{{\"type\":\"object\",\"properties\":{{{props}}}}}");
let v: Value = serde_json::from_str(&schema_json).unwrap();
let err = compile(&v).expect_err("over-cap object schema must be rejected");
assert!(
err.0.contains("exceeds the supported limit"),
"expected a property-count cap error, got: {}",
err.0
);
}
#[test]
fn object_under_property_cap_compiles() {
let mut props = String::new();
for i in 0..50 {
if i > 0 {
props.push(',');
}
props.push_str(&format!("\"p{i}\":{{\"type\":\"integer\"}}"));
}
let schema_json = format!("{{\"type\":\"object\",\"properties\":{{{props}}}}}");
let g = compile_ok(&schema_json);
assert!(
accepts(&g, b"{}"),
"empty object (all optional) must accept"
);
assert!(accepts(&g, b"{\"p0\":1}"), "single property must accept");
}
#[test]
fn string_literal_count_cap_typed_enum_rejects() {
let values: String = (0..=MAX_STRING_LITERALS)
.map(|i| format!("\"v{i}\""))
.collect::<Vec<_>>()
.join(",");
let schema_json = format!("{{\"type\":\"string\",\"enum\":[{values}]}}");
let v: Value = serde_json::from_str(&schema_json).unwrap();
let err = compile(&v).expect_err("over-cap string enum must be rejected");
assert!(
err.0.contains("exceeds the supported limit"),
"expected a literal-count cap error, got: {}",
err.0
);
}
#[test]
fn string_literal_count_cap_any_of_const_rejects() {
let branches: String = (0..=MAX_STRING_LITERALS)
.map(|i| format!("{{\"const\":\"c{i}\"}}"))
.collect::<Vec<_>>()
.join(",");
let schema_json = format!("{{\"anyOf\":[{branches}]}}");
let v: Value = serde_json::from_str(&schema_json).unwrap();
let err = compile(&v).expect_err("over-cap anyOf const set must be rejected");
assert!(
err.0.contains("exceeds the supported limit"),
"expected a literal-count cap error, got: {}",
err.0
);
}
#[test]
fn string_literal_byte_budget_cap_rejects() {
let long_val = "a".repeat(600 * 1024);
let values = format!("\"{long_val}\",\"{long_val}b\"");
let schema_json = format!("{{\"type\":\"string\",\"enum\":[{values}]}}");
let v: Value = serde_json::from_str(&schema_json).unwrap();
let err = compile(&v).expect_err("over-byte-budget string enum must be rejected");
assert!(
err.0.contains("exceeds the supported limit"),
"expected a byte-budget cap error, got: {}",
err.0
);
}
#[test]
fn string_literal_count_cap_any_of_duplicates_rejected_before_dedup() {
let branches: String = (0..=MAX_STRING_LITERALS)
.map(|_| r#"{"const":"dup"}"#.to_string())
.collect::<Vec<_>>()
.join(",");
let schema_json = format!("{{\"anyOf\":[{branches}]}}");
let v: Value = serde_json::from_str(&schema_json).unwrap();
let err = compile(&v)
.expect_err("anyOf with duplicate branches exceeding raw count cap must be rejected");
assert!(
err.0.contains("exceeds the supported limit"),
"expected a literal-count cap error, got: {}",
err.0
);
}
#[test]
fn string_literal_small_set_compiles() {
let schema = r#"{"type":"string","enum":["alpha","beta","gamma"]}"#;
let v: Value = serde_json::from_str(schema).unwrap();
let g = compile(&v).expect("small string enum must compile");
assert!(accepts(&g, b"\"alpha\""));
assert!(accepts(&g, b"\"beta\""));
assert!(rejects(&g, b"\"delta\""));
}
#[test]
fn string_enum_count_cap_early_guard_fires_before_alloc() {
let over = MAX_STRING_LITERALS + 1;
let values: String = (0..over)
.map(|i| format!("\"x{i:04}\""))
.collect::<Vec<_>>()
.join(",");
let schema_json = format!("{{\"type\":\"string\",\"enum\":[{values}]}}");
let v: Value = serde_json::from_str(&schema_json).unwrap();
let err = compile(&v)
.expect_err("string enum over MAX_STRING_LITERALS must be rejected before allocation");
assert!(
err.0.contains("string enum literal count"),
"early guard must fire with 'string enum literal count', got: {}",
err.0
);
}
#[test]
fn prefix_items_count_cap_rejects() {
let over = MAX_PREFIX_ITEMS + 1;
let items: String = (0..over)
.map(|_| r#"{"type":"string"}"#)
.collect::<Vec<_>>()
.join(",");
let schema_json = format!("{{\"type\":\"array\",\"prefixItems\":[{items}]}}");
let v: Value = serde_json::from_str(&schema_json).unwrap();
let err = compile(&v)
.expect_err("prefixItems over MAX_PREFIX_ITEMS with no maxItems must be rejected");
assert!(
err.0.contains("prefixItems length"),
"expected a prefixItems-length cap error, got: {}",
err.0
);
}
#[test]
fn prefix_items_small_tuple_compiles() {
let schema = r#"{"type":"array","prefixItems":[{"type":"integer"},{"type":"string"}]}"#;
let v: Value = serde_json::from_str(schema).unwrap();
let g = compile(&v).expect("small prefixItems tuple must compile");
assert!(
accepts(&g, b"[1,\"hi\"]"),
"a valid tuple value must be accepted"
);
}
#[test]
fn anyof_string_literal_count_cap_early_guard_fires_before_alloc() {
let branch_count = (MAX_STRING_LITERALS / 2) + 2;
let branches: String = (0..branch_count)
.map(|i| format!("{{\"enum\":[\"c{i}a\",\"c{i}b\"]}}"))
.collect::<Vec<_>>()
.join(",");
let schema_json = format!("{{\"anyOf\":[{branches}]}}");
let v: Value = serde_json::from_str(&schema_json).unwrap();
let err =
compile(&v).expect_err("anyOf branches summing past the literal cap must be rejected");
assert!(
err.0.contains("anyOf string literal count"),
"early per-extend guard must fire with 'anyOf string literal count', got: {}",
err.0
);
}
#[test]
fn anyof_single_branch_string_enum_count_cap_rejects() {
let over = MAX_STRING_LITERALS + 1;
let values: String = (0..over)
.map(|i| format!("\"v{i}\""))
.collect::<Vec<_>>()
.join(",");
let schema_json = format!("{{\"anyOf\":[{{\"type\":\"string\",\"enum\":[{values}]}}]}}");
let v: Value = serde_json::from_str(&schema_json).unwrap();
let err = compile(&v).expect_err("oversized single anyOf branch enum must be rejected");
assert!(
err.0.contains("anyOf string literal count"),
"expected the anyOf string-literal cap error, got: {}",
err.0
);
}
#[test]
fn anyof_single_branch_raw_cardinality_bypasses_filtered_count_guard() {
let over = MAX_STRING_LITERALS + 1;
let mut members: Vec<String> = (0..over).map(|i| i.to_string()).collect();
members.push("\"only-string\"".to_string());
let values = members.join(",");
let schema_json = format!("{{\"anyOf\":[{{\"type\":\"string\",\"enum\":[{values}]}}]}}");
let v: Value = serde_json::from_str(&schema_json).unwrap();
let err = compile(&v).expect_err(
"oversized raw enum cardinality must be rejected even with only 1 surviving string",
);
assert!(
err.0.contains("anyOf string literal count"),
"expected the anyOf string-literal cap error, got: {}",
err.0
);
}
#[test]
fn object_property_count_early_guard_fires_before_collect() {
let over = MAX_OBJECT_PROPERTIES + 1;
let mut props = String::new();
for i in 0..over {
if i > 0 {
props.push(',');
}
props.push_str(&format!("\"p{i}\":{{\"type\":\"integer\"}}"));
}
let schema_json = format!("{{\"type\":\"object\",\"properties\":{{{props}}}}}");
let v: Value = serde_json::from_str(&schema_json).unwrap();
let err = compile(&v).expect_err("over-cap object schema must be rejected");
assert!(
err.0.contains("object property count"),
"expected the pre-collect object-property-count cap error, got: {}",
err.0
);
}
#[test]
fn object_required_count_cap_rejects_before_collect() {
let over = MAX_OBJECT_PROPERTIES + 1;
let required_list: String = (0..over)
.map(|i| format!("\"r{i}\""))
.collect::<Vec<_>>()
.join(",");
let schema_json = format!(
"{{\"type\":\"object\",\"properties\":{{\"p\":{{\"type\":\"integer\"}}}},\"required\":[{required_list}]}}"
);
let v: Value = serde_json::from_str(&schema_json).unwrap();
let err = compile(&v).expect_err("over-cap required array must be rejected");
assert!(
err.0.contains("object required count"),
"expected the pre-collect required-count cap error, got: {}",
err.0
);
}
#[test]
fn string_const_plus_oversized_enum_rejected_by_raw_guard() {
let over = MAX_STRING_LITERALS + 1;
let values: String = (0..over)
.map(|i| format!("\"v{i}\""))
.collect::<Vec<_>>()
.join(",");
let schema_json = format!("{{\"type\":\"string\",\"const\":\"v0\",\"enum\":[{values}]}}");
let v: Value = serde_json::from_str(&schema_json).unwrap();
let err = compile(&v).expect_err(
"a const that narrows the enum to one survivor must not bypass the raw cardinality cap",
);
assert!(
err.0.contains("string enum literal count"),
"expected the string-enum literal-count cap error, got: {}",
err.0
);
}
#[test]
fn anyof_string_const_plus_oversized_enum_rejected() {
let over = MAX_STRING_LITERALS + 1;
let values: String = (0..over)
.map(|i| format!("\"v{i}\""))
.collect::<Vec<_>>()
.join(",");
let schema_json =
format!("{{\"anyOf\":[{{\"type\":\"string\",\"const\":\"v0\",\"enum\":[{values}]}}]}}");
let v: Value = serde_json::from_str(&schema_json).unwrap();
let err = compile(&v).expect_err(
"an anyOf branch's const-narrowed enum must not bypass the raw cardinality cap",
);
assert!(
err.0.contains("anyOf string literal count"),
"expected the anyOf string-literal cap error, got: {}",
err.0
);
}
#[test]
fn non_string_enum_cardinality_capped() {
let over = MAX_STRING_LITERALS + 1;
let values: String = (0..over)
.map(|i| i.to_string())
.collect::<Vec<_>>()
.join(",");
let schema_json = format!("{{\"enum\":[{values}]}}");
let v: Value = serde_json::from_str(&schema_json).unwrap();
let err = compile(&v).expect_err("a non-string enum over the cap must be rejected");
assert!(
err.0.contains("enum value count"),
"expected the enum-value-count cap error, got: {}",
err.0
);
}
#[test]
fn anyof_branch_count_capped() {
let over = MAX_ANYOF_BRANCHES + 1;
let branches: String = (0..over)
.map(|_| r#"{"type":"boolean"}"#.to_string())
.collect::<Vec<_>>()
.join(",");
let schema_json = format!("{{\"anyOf\":[{branches}]}}");
let v: Value = serde_json::from_str(&schema_json).unwrap();
let err = compile(&v).expect_err("anyOf branch count over the cap must be rejected");
assert!(
err.0.contains("anyOf/oneOf branch count"),
"expected the anyOf branch-count cap error, got: {}",
err.0
);
}
#[test]
fn mixed_enum_byte_budget_capped() {
let huge = "a".repeat(MAX_STRING_LITERAL_BYTES + 16);
let schema_json = format!("{{\"enum\":[\"{huge}\",1]}}");
let v: Value = serde_json::from_str(&schema_json).unwrap();
let err = compile(&v).expect_err("mixed enum over the byte budget must be rejected");
assert!(
err.0.contains("byte length"),
"expected the byte-length cap error, got: {}",
err.0
);
}
#[test]
fn mixed_enum_cumulative_bytes_capped() {
let chunk = "a".repeat(400 * 1024);
let members: String = (0..3)
.map(|i| format!(r#""{chunk}{i}""#))
.collect::<Vec<_>>()
.join(",");
let schema_json = format!("{{\"enum\":[{members},1]}}");
let v: Value = serde_json::from_str(&schema_json).unwrap();
let err = compile(&v)
.expect_err("mixed enum whose cumulative bytes exceed the budget must be rejected");
assert!(
err.0
.contains("enum value encoded byte length exceeds the supported limit"),
"expected the incremental enum byte-budget cap error, got: {}",
err.0
);
}
#[test]
fn anyof_const_cumulative_bytes_capped() {
let chunk = "a".repeat(400 * 1024);
let branches: String = (0..3)
.map(|i| format!(r#"{{"const":"{chunk}{i}"}}"#))
.collect::<Vec<_>>()
.join(",");
let schema_json = format!("{{\"anyOf\":[{branches}]}}");
let v: Value = serde_json::from_str(&schema_json).unwrap();
let err = compile(&v)
.expect_err("anyOf consts whose cumulative bytes exceed the budget must be rejected");
assert!(
err.0
.contains("string literal encoded byte length exceeds the supported limit"),
"expected the incremental anyOf byte-budget cap error, got: {}",
err.0
);
}
#[test]
fn const_literal_byte_capped() {
let huge = "a".repeat(MAX_STRING_LITERAL_BYTES + 1);
let schema_json = format!("{{\"const\":\"{huge}\"}}");
let v: Value = serde_json::from_str(&schema_json).unwrap();
let err = compile(&v).expect_err("a const literal over the byte cap must be rejected");
assert!(
err.0.contains("byte length"),
"expected the byte-length cap error, got: {}",
err.0
);
}
#[test]
fn object_key_byte_capped() {
let huge_key = "a".repeat(MAX_STRING_LITERAL_BYTES + 1);
let schema_json = format!(
"{{\"type\":\"object\",\"properties\":{{\"{huge_key}\":{{\"type\":\"integer\"}}}}}}"
);
let v: Value = serde_json::from_str(&schema_json).unwrap();
let err = compile(&v).expect_err("an object key over the byte cap must be rejected");
assert!(
err.0.contains("byte length"),
"expected the byte-length cap error, got: {}",
err.0
);
}
#[test]
fn anyof_const_single_byte_capped() {
let huge = "a".repeat(MAX_STRING_LITERAL_BYTES + 1);
let schema_json = format!("{{\"anyOf\":[{{\"const\":\"{huge}\"}}]}}");
let v: Value = serde_json::from_str(&schema_json).unwrap();
let err = compile(&v).expect_err("an anyOf const over the byte cap must be rejected");
assert!(
err.0.contains("byte length"),
"expected the byte-length cap error, got: {}",
err.0
);
}
#[test]
fn anyof_enum_member_byte_capped() {
let huge = "a".repeat(MAX_STRING_LITERAL_BYTES + 1);
let schema_json = format!("{{\"anyOf\":[{{\"enum\":[\"{huge}\"]}}]}}");
let v: Value = serde_json::from_str(&schema_json).unwrap();
let err = compile(&v).expect_err("an anyOf enum member over the byte cap must be rejected");
assert!(
err.0.contains("byte length"),
"expected the byte-length cap error, got: {}",
err.0
);
}
#[test]
fn schema_defs_name_byte_capped() {
let huge = "a".repeat(MAX_STRING_LITERAL_BYTES + 1);
let mut defs = serde_json::Map::new();
defs.insert(huge, serde_json::json!({"type": "integer"}));
let mut root = serde_json::Map::new();
root.insert("$defs".to_string(), Value::Object(defs));
let v = Value::Object(root);
let err = compile(&v).expect_err("a $defs name over the byte cap must be rejected");
assert!(
err.0.contains("byte length"),
"expected the byte-length cap error, got: {}",
err.0
);
}
#[test]
fn compile_json_schema_direct_defs_name_byte_capped() {
let huge = "a".repeat(MAX_STRING_LITERAL_BYTES + 1);
let mut defs = serde_json::Map::new();
defs.insert(huge, serde_json::json!({"type": "integer"}));
let mut root = serde_json::Map::new();
root.insert("$defs".to_string(), Value::Object(defs));
let v = Value::Object(root);
let err = compile_json_schema(&v)
.expect_err("a $defs name over the byte cap must be rejected at the direct entry");
assert!(
err.0.contains("byte length"),
"expected the byte-length cap error, got: {}",
err.0
);
}
#[test]
fn defs_names_cumulative_bytes_capped() {
let chunk = "a".repeat(400 * 1024);
let mut defs = serde_json::Map::new();
for i in 0..3 {
defs.insert(
format!("{chunk}{i}"),
serde_json::json!({"type": "integer"}),
);
}
let mut root = serde_json::Map::new();
root.insert("$defs".to_string(), Value::Object(defs));
let v = Value::Object(root);
let err =
compile(&v).expect_err("cumulative $defs name bytes over the budget must be rejected");
assert!(
err.0
.contains("schema definition name cumulative byte length"),
"expected the cumulative $defs-name cap error, got: {}",
err.0
);
}
#[test]
fn object_keys_cumulative_bytes_capped() {
let chunk = "a".repeat(400 * 1024);
let mut props = serde_json::Map::new();
for i in 0..3 {
props.insert(
format!("{chunk}{i}"),
serde_json::json!({"type": "integer"}),
);
}
let mut root = serde_json::Map::new();
root.insert("type".to_string(), Value::String("object".to_string()));
root.insert("properties".to_string(), Value::Object(props));
let v = Value::Object(root);
let err = compile(&v)
.expect_err("cumulative object property key bytes over the budget must be rejected");
assert!(
err.0.contains("object property key cumulative byte length"),
"expected the cumulative object-key cap error, got: {}",
err.0
);
}
}