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;
#[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> {
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();
if let Some(defs_map) = doc_root.get("$defs").and_then(Value::as_object) {
for (k, v) in defs_map {
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.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
}
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)
{
let mut all_alts: Vec<Alt> = Vec::new();
for sub in any_of {
let sub_alts = self.compile_schema(sub, _path)?;
all_alts.extend(sub_alts);
}
return Ok(all_alts);
}
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 compile_object(&mut self, schema: &'a Value) -> Result<Vec<Alt>, SchemaError> {
let properties = schema
.get("properties")
.and_then(Value::as_object)
.map(|m| m.iter().collect::<Vec<_>>());
let required: Vec<&str> = schema
.get("required")
.and_then(Value::as_array)
.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 main_alt: Alt = vec![Symbol::Terminal(b'{')];
main_alt.push(Symbol::NonTerminal(ws_id));
let mut first = true;
for (key, val_schema) in &props {
let key_str: &str = key.as_str();
let is_req = required.contains(&key_str);
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
};
if !first {
main_alt.push(Symbol::NonTerminal(ws_id));
main_alt.push(Symbol::Terminal(b','));
main_alt.push(Symbol::NonTerminal(ws_id));
}
if is_req {
main_alt.push(Symbol::NonTerminal(pair_id));
} else {
let opt_name = format!("opt_{pair_rule_name}");
let opt_id = if let Some(id) = self.builder.rule_id(&opt_name) {
id
} else {
let id = self.builder.reserve(&opt_name);
let opt_alts = vec![
vec![Symbol::NonTerminal(pair_id)],
vec![], ];
self.builder.set_alts(id, opt_alts);
id
};
main_alt.push(Symbol::NonTerminal(opt_id));
}
first = false;
}
main_alt.push(Symbol::NonTerminal(ws_id));
main_alt.push(Symbol::Terminal(b'}'));
Ok(vec![main_alt])
}
}
}
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 {
if 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 {
if 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 let Some(m) = max_items {
if 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) {
let str_values: Vec<&str> = values.iter().filter_map(|v| v.as_str()).collect();
if !str_values.is_empty() {
let choices_name = format!("str_enum_{}", self.enum_counter);
self.enum_counter += 1;
let choices_id = self.builder.reserve(&choices_name);
let mut choice_alts: Vec<Alt> = 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];
choice_alts.push(inner.bytes().map(Symbol::Terminal).collect());
}
self.builder.set_alts(choices_id, choice_alts);
return Ok(vec![vec![
Symbol::Terminal(b'"'),
Symbol::NonTerminal(choices_id),
Symbol::Terminal(b'"'),
]]);
}
}
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 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 compile_enum(values: &[Value]) -> Result<Vec<Alt>, SchemaError> {
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)?])
}
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}")))?;
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}")))?;
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 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\"}"));
}
}