use std::collections::{BTreeMap, HashMap, HashSet};
use serde_json::Value;
const SPACE_RULE: &str = r#"| " " | "\n"{1,2} [ \t]{0,20}"#;
fn primitive(name: &str) -> Option<(&'static str, &'static str, &'static [&'static str])> {
match name {
"boolean" => Some(("boolean", r#"("true" | "false") space"#, &[])),
"decimal-part" => Some(("decimal-part", r#"[0-9]{1,16}"#, &[])),
"integral-part" => Some(("integral-part", r#"[0] | [1-9] [0-9]{0,15}"#, &[])),
"number" => Some((
"number",
r#"("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)? space"#,
&["integral-part", "decimal-part"],
)),
"integer" => Some((
"integer",
r#"("-"? integral-part) space"#,
&["integral-part"],
)),
"value" => Some((
"value",
r#"object | array | string | number | boolean | null"#,
&["object", "array", "string", "number", "boolean", "null"],
)),
"object" => Some((
"object",
r#"{ space ( string ":" space value ("," space string ":" space value)* )? } space"#,
&["string", "value"],
)),
"array" => Some((
"array",
r#""[" space ( value ("," space value)* )? "]" space"#,
&["value"],
)),
"char" => Some((
"char",
r#"[^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})"#,
&[],
)),
"string" => Some(("string", r#""\"" char* "\"" space"#, &["char"])),
"null" => Some(("null", r#""null" space"#, &[])),
_ => None,
}
}
fn primitive_exact(name: &str) -> Option<(&'static str, &'static str, &'static [&'static str])> {
match name {
"boolean" => Some(("boolean", r#"("true" | "false") space"#, &[])),
"decimal-part" => Some(("decimal-part", r#"[0-9]{1,16}"#, &[])),
"integral-part" => Some(("integral-part", r#"[0] | [1-9] [0-9]{0,15}"#, &[])),
"number" => Some((
"number",
r#"("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)? space"#,
&["integral-part", "decimal-part"],
)),
"integer" => Some((
"integer",
r#"("-"? integral-part) space"#,
&["integral-part"],
)),
"value" => Some((
"value",
r#"object | array | string | number | boolean | null"#,
&["object", "array", "string", "number", "boolean", "null"],
)),
"object" => Some((
"object",
r#""{" space ( string ":" space value ("," space string ":" space value)* )? "}" space"#,
&["string", "value"],
)),
"array" => Some((
"array",
r#""[" space ( value ("," space value)* )? "]" space"#,
&["value"],
)),
"char" => Some((
"char",
r#"[^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})"#,
&[],
)),
"string" => Some(("string", r#""\"" char* "\"" space"#, &["char"])),
"null" => Some(("null", r#""null" space"#, &[])),
_ => None,
}
}
#[allow(dead_code)]
const _UNUSED_PRIMITIVE: fn(&str) -> Option<(&'static str, &'static str, &'static [&'static str])> =
primitive;
pub fn format_literal(literal: &str) -> String {
let mut out = String::with_capacity(literal.len() + 2);
out.push('"');
for c in literal.chars() {
match c {
'\r' => out.push_str("\\r"),
'\n' => out.push_str("\\n"),
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
_ => out.push(c),
}
}
out.push('"');
out
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SchemaError {
TooManyRequiredKeys {
fn_name: String,
count: usize,
max: usize,
},
Generic {
path: String,
message: String,
},
}
impl std::fmt::Display for SchemaError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SchemaError::TooManyRequiredKeys {
fn_name,
count,
max,
} => write!(
f,
"json-schema-to-grammar error at {}: object has {} required \
properties; ADR-005 grammar enforcement supports at most {} \
required properties per object (CFG-for-permutation is \
provably exponential per Moshier & Rounds ACL 1987). \
Reduce required properties or split the schema.",
if fn_name.is_empty() { "root" } else { fn_name },
count,
max,
),
SchemaError::Generic { path, message } => {
write!(f, "json-schema-to-grammar error at {}: {}", path, message)
}
}
}
}
impl std::error::Error for SchemaError {}
pub fn schema_to_gbnf(schema: &Value) -> Result<String, SchemaError> {
let mut conv = Converter {
rules: BTreeMap::new(),
added_primitives: HashSet::new(),
};
let root_body = conv.visit(schema, "")?;
conv.rules.insert("root".to_string(), root_body);
conv.rules
.entry("space".to_string())
.or_insert_with(|| SPACE_RULE.to_string());
let mut out = String::new();
if let Some(body) = conv.rules.get("root") {
out.push_str(&format!("root ::= {}\n", body));
}
for (name, body) in &conv.rules {
if name == "root" {
continue;
}
out.push_str(&format!("{} ::= {}\n", name, body));
}
Ok(out)
}
struct Converter {
rules: BTreeMap<String, String>,
added_primitives: HashSet<&'static str>,
}
impl Converter {
fn add_primitive(&mut self, name: &'static str) {
if self.added_primitives.contains(name) {
return;
}
self.added_primitives.insert(name);
let (_, body, deps) = primitive_exact(name).expect("unknown primitive");
self.rules.insert(name.to_string(), body.to_string());
for dep in deps {
self.add_primitive(dep);
}
}
fn visit(&mut self, schema: &Value, path: &str) -> Result<String, SchemaError> {
let obj = match schema.as_object() {
Some(o) => o,
None => {
return Err(SchemaError::Generic {
path: path.to_string(),
message: "schema must be a JSON object".into(),
});
}
};
if let Some(Value::Array(values)) = obj.get("enum") {
if values.is_empty() {
return Err(SchemaError::Generic {
path: path.to_string(),
message: "enum cannot be empty".into(),
});
}
let mut alts: Vec<String> = Vec::with_capacity(values.len());
for v in values {
match v {
Value::String(s) => {
let quoted_value =
serde_json::to_string(s).map_err(|e| SchemaError::Generic {
path: path.to_string(),
message: format!("enum serialize: {e}"),
})?;
alts.push(format_literal("ed_value));
}
Value::Number(_) | Value::Bool(_) | Value::Null => {
let text = serde_json::to_string(v).map_err(|e| SchemaError::Generic {
path: path.to_string(),
message: format!("enum serialize: {e}"),
})?;
alts.push(format_literal(&text));
}
Value::Array(_) | Value::Object(_) => {
return Err(SchemaError::Generic {
path: format!("{}/enum", path),
message: "enum values must be scalars (string/number/bool/null)".into(),
});
}
}
}
self.rules
.entry("space".to_string())
.or_insert_with(|| SPACE_RULE.to_string());
return Ok(format!("({}) space", alts.join(" | ")));
}
let type_val = obj.get("type");
let type_str = match type_val {
None => {
self.add_primitive("value");
return Ok("value".into());
}
Some(Value::String(s)) => s.clone(),
Some(Value::Array(types)) => {
let mut alts: Vec<String> = Vec::with_capacity(types.len());
for (i, t) in types.iter().enumerate() {
let tstr = t.as_str().ok_or_else(|| SchemaError::Generic {
path: format!("{}/type/{}", path, i),
message: "type array entries must be strings".into(),
})?;
let mut stub = serde_json::Map::new();
stub.insert("type".into(), Value::String(tstr.into()));
let body =
self.visit(&Value::Object(stub), &format!("{}/type[{}]", path, i))?;
alts.push(body);
}
return Ok(alts.join(" | "));
}
Some(other) => {
return Err(SchemaError::Generic {
path: format!("{}/type", path),
message: format!("type must be a string or array of strings, got {:?}", other),
});
}
};
match type_str.as_str() {
"string" => {
self.add_primitive("string");
Ok("string".into())
}
"number" => {
self.add_primitive("number");
Ok("number".into())
}
"integer" => {
self.add_primitive("integer");
Ok("integer".into())
}
"boolean" => {
self.add_primitive("boolean");
Ok("boolean".into())
}
"null" => {
self.add_primitive("null");
Ok("null".into())
}
"object" => self.visit_object(obj, path),
"array" => self.visit_array(obj, path),
other => Err(SchemaError::Generic {
path: format!("{}/type", path),
message: format!("unsupported type '{}'", other),
}),
}
}
fn visit_object(
&mut self,
obj: &serde_json::Map<String, Value>,
path: &str,
) -> Result<String, SchemaError> {
self.add_primitive("string");
self.add_primitive("value");
self.rules
.entry("space".to_string())
.or_insert_with(|| SPACE_RULE.to_string());
let properties = obj
.get("properties")
.and_then(|v| v.as_object())
.cloned()
.unwrap_or_default();
let required_list: HashSet<String> = obj
.get("required")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
let additional_props = obj.get("additionalProperties");
let additional_closed = matches!(additional_props, Some(Value::Bool(false)));
if properties.is_empty() {
if additional_closed {
return Ok(r#""{" space "}" space"#.into());
}
self.add_primitive("object");
return Ok("object".into());
}
let mut all_keys: Vec<&String> = properties.keys().collect();
all_keys.sort();
let slug = path_slug(path);
let mut kv_rule_name: HashMap<String, String> = HashMap::new();
let mut required_keys: Vec<String> = Vec::new();
let mut optional_keys: Vec<String> = Vec::new();
for k in &all_keys {
let v = &properties[*k];
let vbody = self.visit(v, &format!("{}/properties/{}", path, k))?;
let val_rule = format!("{}-{}", slug, sanitize_rule_name(k));
self.rules.insert(val_rule.clone(), vbody);
let quoted_key = format_literal(&format!("\"{}\"", k));
let kv_body = format!("{} \":\" space {}", quoted_key, val_rule);
let kv_name = format!("{}-{}-kv", slug, sanitize_rule_name(k));
self.rules.insert(kv_name.clone(), kv_body);
kv_rule_name.insert((*k).clone(), kv_name);
if required_list.contains(*k) {
required_keys.push((*k).clone());
} else {
optional_keys.push((*k).clone());
}
}
if required_keys.is_empty() && optional_keys.is_empty() {
return Ok(r#""{" space "}" space"#.into());
}
let n_total = required_keys.len() + optional_keys.len();
if n_total > 32 {
return Err(SchemaError::Generic {
path: path.to_string(),
message: format!(
"object schema has {} properties (required={} + optional={}); \
max supported for any-position grammar is 32",
n_total,
required_keys.len(),
optional_keys.len(),
),
});
}
const ANY_ORDER_MAX_REQUIRED: usize = 8;
if !additional_closed {
let extra_kv_name = format!("{}-extra-kv", slug);
self.rules
.entry(extra_kv_name)
.or_insert_with(|| "string \":\" space value".to_string());
}
let inner = if required_keys.is_empty() {
let mut entries: Vec<(String, bool)> = optional_keys
.iter()
.map(|k| (kv_rule_name[k].clone(), false))
.collect();
if !additional_closed {
let extra_kv_name = format!("{}-extra-kv", slug);
entries.push((extra_kv_name, true));
}
if entries.is_empty() {
return Ok(r#""{" space "}" space"#.into());
}
let chain = self.build_optional_chain(&slug, &entries);
format!("( {} )?", chain)
} else if required_keys.len() <= ANY_ORDER_MAX_REQUIRED {
let n_req = required_keys.len(); let req_full: u32 = u32::MAX >> (32 - n_req);
let opt_full: u32 = if optional_keys.is_empty() {
0
} else {
let n_opt = optional_keys.len(); u32::MAX >> (32 - n_opt)
};
self.build_unified_inner(
&slug,
req_full,
opt_full,
&required_keys,
&optional_keys,
&kv_rule_name,
!additional_closed,
)
} else {
return Err(SchemaError::TooManyRequiredKeys {
fn_name: path.to_string(),
count: required_keys.len(),
max: ANY_ORDER_MAX_REQUIRED,
});
};
Ok(format!(r#""{{" space {} "}}" space"#, inner))
}
fn build_unified_inner(
&mut self,
slug: &str,
req_remaining: u32,
opt_remaining: u32,
required_keys: &[String],
optional_keys: &[String],
kv_rule_name: &HashMap<String, String>,
allow_extra_kv: bool,
) -> String {
let rule_name = format!("{}-up-r{:08x}-o{:08x}", slug, req_remaining, opt_remaining);
if self.rules.contains_key(&rule_name) {
return rule_name;
}
self.rules.insert(rule_name.clone(), String::new());
let mut alts: Vec<String> = Vec::new();
for (i, k) in required_keys.iter().enumerate() {
if req_remaining & (1u32 << i) == 0 {
continue; }
let kv = kv_rule_name[k].clone();
let new_req = req_remaining & !(1u32 << i);
if new_req == 0 {
let opt_suffix = self.build_optional_suffix_masked(
slug,
opt_remaining,
optional_keys,
kv_rule_name,
allow_extra_kv,
);
let alt = if opt_suffix.is_empty() {
kv.clone()
} else {
format!("{} {}", kv, opt_suffix)
};
alts.push(alt);
} else {
let next = self.build_unified_inner(
slug,
new_req,
opt_remaining,
required_keys,
optional_keys,
kv_rule_name,
allow_extra_kv,
);
alts.push(format!("{} \",\" space {}", kv, next));
}
}
for (j, o) in optional_keys.iter().enumerate() {
if opt_remaining & (1u32 << j) == 0 {
continue; }
let kv = kv_rule_name[o].clone();
let new_opt = opt_remaining & !(1u32 << j);
let next = self.build_unified_inner(
slug,
req_remaining,
new_opt,
required_keys,
optional_keys,
kv_rule_name,
allow_extra_kv,
);
alts.push(format!("{} \",\" space {}", kv, next));
}
if allow_extra_kv {
let extra_kv_name = format!("{}-extra-kv", slug);
alts.push(format!("{} \",\" space {}", extra_kv_name, rule_name));
}
let body = alts.join(" | ");
self.rules.insert(rule_name.clone(), body);
rule_name
}
fn build_optional_suffix_masked(
&mut self,
slug: &str,
opt_mask: u32,
optional_keys: &[String],
kv_rule_name: &HashMap<String, String>,
allow_extra_kv: bool,
) -> String {
let mut entries: Vec<(String, bool)> = Vec::new();
for (j, o) in optional_keys.iter().enumerate() {
if opt_mask & (1u32 << j) != 0 {
entries.push((kv_rule_name[o].clone(), false));
}
}
if allow_extra_kv {
let extra_kv_name = format!("{}-extra-kv", slug);
entries.push((extra_kv_name, true));
}
if entries.is_empty() {
return String::new();
}
let chain = self.build_optional_chain(slug, &entries);
format!("( \",\" space {} )?", chain)
}
fn build_optional_chain(&mut self, slug: &str, entries: &[(String, bool)]) -> String {
let mut names: Vec<&str> = entries.iter().map(|(n, _)| n.as_str()).collect();
names.sort_unstable();
let fp = sanitize_rule_name(&names.join("-"));
let rule_name = format!("{}-opt-{}", slug, fp);
if self.rules.contains_key(&rule_name) {
return rule_name;
}
self.rules.insert(rule_name.clone(), String::new());
let mut alts: Vec<String> = Vec::new();
for (i, (kv, is_wildcard)) in entries.iter().enumerate() {
let keep_self = *is_wildcard; let remaining: Vec<(String, bool)> = entries
.iter()
.enumerate()
.filter(|(j, _)| *j != i || keep_self)
.map(|(_, e)| e.clone())
.collect();
let alt = if remaining.is_empty() {
kv.clone()
} else {
let rest = self.build_optional_chain(slug, &remaining);
format!("{} ( \",\" space {} )?", kv, rest)
};
alts.push(alt);
}
let body = alts.join(" | ");
self.rules.insert(rule_name.clone(), body);
rule_name
}
fn visit_array(
&mut self,
obj: &serde_json::Map<String, Value>,
path: &str,
) -> Result<String, SchemaError> {
self.rules
.entry("space".to_string())
.or_insert_with(|| SPACE_RULE.to_string());
let item_schema = obj.get("items");
let item_rule = match item_schema {
None => {
self.add_primitive("value");
"value".to_string()
}
Some(Value::Object(_)) => {
self.visit(item_schema.unwrap(), &format!("{}/items", path))?
}
Some(Value::Array(_)) => {
return Err(SchemaError::Generic {
path: format!("{}/items", path),
message: "tuple-form arrays (items: [...]) not yet supported".into(),
});
}
_ => {
return Err(SchemaError::Generic {
path: format!("{}/items", path),
message: "items must be an object schema".into(),
});
}
};
Ok(format!(
r#""[" space ( {0} ("," space {0})* )? "]" space"#,
item_rule
))
}
}
fn sanitize_rule_name(raw: &str) -> String {
let mut out = String::with_capacity(raw.len());
for c in raw.chars() {
if c.is_ascii_alphanumeric() || c == '-' {
out.push(c);
} else {
out.push('-');
}
}
if out.is_empty() {
out.push('x');
}
out
}
fn path_slug(path: &str) -> String {
if path.is_empty() {
return "root".into();
}
sanitize_rule_name(path.trim_start_matches('/'))
}
#[cfg(test)]
mod tests {
use super::super::parser::parse;
use super::super::sampler::GrammarRuntime;
use super::*;
fn compile(schema_json: &str) -> String {
let schema: Value = serde_json::from_str(schema_json).unwrap();
schema_to_gbnf(&schema).unwrap_or_else(|e| panic!("schema_to_gbnf: {:?}", e))
}
fn runtime(schema_json: &str) -> GrammarRuntime {
let gbnf = compile(schema_json);
let g = parse(&gbnf).unwrap_or_else(|e| panic!("parse gbnf:\n{}\nerror: {}", gbnf, e));
let rid = g.rule_id("root").unwrap();
GrammarRuntime::new(g, rid).unwrap()
}
#[test]
fn primitive_boolean_schema_accepts_true_and_false() {
let mut rt_true = runtime(r#"{"type":"boolean"}"#);
assert!(rt_true.accept_bytes(b"true"));
assert!(rt_true.is_accepted());
let mut rt_false = runtime(r#"{"type":"boolean"}"#);
assert!(rt_false.accept_bytes(b"false"));
assert!(rt_false.is_accepted());
let mut rt_bad = runtime(r#"{"type":"boolean"}"#);
let ok = rt_bad.accept_bytes(b"maybe");
assert!(!(ok && rt_bad.is_accepted()));
}
#[test]
fn primitive_integer_schema_accepts_numbers() {
for num in &["0", "1", "-42", "12345"] {
let mut rt = runtime(r#"{"type":"integer"}"#);
assert!(rt.accept_bytes(num.as_bytes()), "accept {:?}", num);
assert!(rt.is_accepted(), "is_accepted for {:?}", num);
}
for bad in &["1.5", "abc", ""] {
let mut rt = runtime(r#"{"type":"integer"}"#);
let ok = rt.accept_bytes(bad.as_bytes());
assert!(!(ok && rt.is_accepted()), "reject {:?}", bad);
}
}
#[test]
fn primitive_number_schema_accepts_decimals() {
for num in &["0", "1.5", "-42.0", "3.14", "2e10", "-1.5E-3"] {
let mut rt = runtime(r#"{"type":"number"}"#);
assert!(rt.accept_bytes(num.as_bytes()), "accept {:?}", num);
assert!(rt.is_accepted(), "is_accepted for {:?}", num);
}
}
#[test]
fn primitive_string_schema_accepts_quoted() {
let mut rt = runtime(r#"{"type":"string"}"#);
assert!(rt.accept_bytes(b"\"hello\""));
assert!(rt.is_accepted());
let mut rt2 = runtime(r#"{"type":"string"}"#);
let ok = rt2.accept_bytes(b"unquoted");
assert!(!(ok && rt2.is_accepted()));
}
#[test]
fn primitive_null_schema_accepts_null_keyword() {
let mut rt = runtime(r#"{"type":"null"}"#);
assert!(rt.accept_bytes(b"null"));
assert!(rt.is_accepted());
}
#[test]
fn enum_string_values() {
let schema = r#"{"enum":["red","green","blue"]}"#;
for good in &["\"red\"", "\"green\"", "\"blue\""] {
let mut rt = runtime(schema);
assert!(rt.accept_bytes(good.as_bytes()), "accept {}", good);
assert!(rt.is_accepted(), "is_accepted {}", good);
}
for bad in &["\"yellow\"", "red", "\"\""] {
let mut rt = runtime(schema);
let ok = rt.accept_bytes(bad.as_bytes());
assert!(!(ok && rt.is_accepted()), "reject {}", bad);
}
}
#[test]
fn empty_schema_accepts_any_json_value() {
let schema = r#"{}"#;
for good in &["42", "\"hi\"", "true", "null", "[]", "{}", "[1,2,3]"] {
let mut rt = runtime(schema);
assert!(rt.accept_bytes(good.as_bytes()), "accept {}", good);
assert!(rt.is_accepted(), "is_accepted {}", good);
}
}
#[test]
fn object_with_single_required_property() {
let schema = r#"{
"type": "object",
"properties": {"name": {"type": "string"}},
"required": ["name"]
}"#;
let mut rt = runtime(schema);
assert!(rt.accept_bytes(b"{\"name\":\"Alice\"}"));
assert!(rt.is_accepted());
let mut rt2 = runtime(schema);
let ok = rt2.accept_bytes(b"{}");
assert!(!(ok && rt2.is_accepted()));
}
#[test]
fn object_with_multiple_required_properties() {
let schema = r#"{
"type": "object",
"properties": {"name": {"type": "string"}, "age": {"type": "integer"}},
"required": ["name", "age"]
}"#;
let mut rt = runtime(schema);
assert!(
rt.accept_bytes(b"{\"age\":30,\"name\":\"Bob\"}"),
"age-first rejected"
);
assert!(rt.is_accepted());
let mut rt2 = runtime(schema);
assert!(
rt2.accept_bytes(b"{\"name\":\"Bob\",\"age\":30}"),
"name-first rejected"
);
assert!(rt2.is_accepted());
}
#[test]
fn object_with_optional_property() {
let schema = r#"{
"type": "object",
"properties": {"name": {"type": "string"}, "nickname": {"type": "string"}},
"required": ["name"]
}"#;
let mut rt = runtime(schema);
assert!(rt.accept_bytes(b"{\"name\":\"Carol\",\"nickname\":\"Carrie\"}"));
assert!(rt.is_accepted());
let mut rt2 = runtime(schema);
assert!(rt2.accept_bytes(b"{\"name\":\"Carol\"}"));
assert!(rt2.is_accepted());
}
#[test]
fn array_of_integers() {
let schema = r#"{"type":"array","items":{"type":"integer"}}"#;
for good in &["[]", "[1]", "[1,2,3]", "[-5,0,42]"] {
let mut rt = runtime(schema);
assert!(rt.accept_bytes(good.as_bytes()), "accept {}", good);
assert!(rt.is_accepted(), "is_accepted {}", good);
}
let mut rt_bad = runtime(schema);
let ok = rt_bad.accept_bytes(b"[1,\"x\"]");
assert!(!(ok && rt_bad.is_accepted()));
}
#[test]
fn array_without_items_accepts_any_values() {
let schema = r#"{"type":"array"}"#;
let mut rt = runtime(schema);
assert!(rt.accept_bytes(b"[1,\"x\",true,null]"));
assert!(rt.is_accepted());
}
#[test]
fn union_type_string_or_null() {
let schema = r#"{"type":["string","null"]}"#;
let mut rt_s = runtime(schema);
assert!(rt_s.accept_bytes(b"\"hi\""));
assert!(rt_s.is_accepted());
let mut rt_n = runtime(schema);
assert!(rt_n.accept_bytes(b"null"));
assert!(rt_n.is_accepted());
let mut rt_bad = runtime(schema);
let ok = rt_bad.accept_bytes(b"42");
assert!(!(ok && rt_bad.is_accepted()));
}
#[test]
fn nested_object_with_array() {
let schema = r#"{
"type": "object",
"properties": {
"name": {"type": "string"},
"arguments": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}
},
"required": ["name", "arguments"]
}"#;
let mut rt = runtime(schema);
assert!(rt.accept_bytes(b"{\"arguments\":{\"city\":\"NYC\"},\"name\":\"get_weather\"}"));
assert!(rt.is_accepted());
let mut rt2 = runtime(schema);
assert!(rt2.accept_bytes(b"{\"name\":\"get_weather\",\"arguments\":{\"city\":\"NYC\"}}"));
assert!(rt2.is_accepted());
}
#[test]
fn unsupported_type_rejected_at_compile_time() {
let schema: Value = serde_json::from_str(r#"{"type":"notathing"}"#).unwrap();
let err = schema_to_gbnf(&schema).unwrap_err();
assert!(err.to_string().contains("unsupported type"));
}
#[test]
fn pattern_not_yet_supported_but_compiles_when_ignored() {
let schema = r#"{"type":"string","pattern":"^[a-z]+$"}"#;
let mut rt = runtime(schema);
assert!(rt.accept_bytes(b"\"ABC123\""));
assert!(rt.is_accepted());
}
#[test]
fn enum_non_string_value_accepted() {
let schema = r#"{"enum":[42, true, null]}"#;
for good in &["42", "true", "null"] {
let mut rt = runtime(schema);
assert!(rt.accept_bytes(good.as_bytes()), "accept {}", good);
assert!(rt.is_accepted(), "is_accepted {}", good);
}
}
#[test]
fn compiled_grammar_has_root_rule() {
let out = compile(r#"{"type":"boolean"}"#);
assert!(out.starts_with("root ::="), "output:\n{}", out);
}
#[test]
fn function_call_with_single_string_argument() {
let schema = r#"{
"type": "object",
"properties": {
"city": {"type": "string"}
},
"required": ["city"],
"additionalProperties": false
}"#;
let mut rt = runtime(schema);
assert!(
rt.accept_bytes(br#"{"city":"London"}"#),
"rejected valid function-call payload"
);
assert!(rt.is_accepted(), "runtime not accepted at end");
let mut rt = runtime(schema);
let ok = rt.accept_bytes(br#"{}"#);
assert!(
!(ok && rt.is_accepted()),
"accepted empty object missing required city"
);
}
#[test]
fn function_call_with_nested_object_argument() {
let schema = r#"{
"type": "object",
"properties": {
"query": {"type": "string"},
"filters": {
"type": "object",
"properties": {
"min_price": {"type": "number"},
"max_price": {"type": "number"}
},
"required": ["min_price", "max_price"]
}
},
"required": ["query", "filters"]
}"#;
let mut rt = runtime(schema);
let payload = br#"{"filters":{"max_price":2000,"min_price":500},"query":"laptops"}"#;
assert!(rt.accept_bytes(payload), "rejected nested (filters-first)");
assert!(rt.is_accepted());
let mut rt2 = runtime(schema);
let payload2 = br#"{"query":"laptops","filters":{"max_price":2000,"min_price":500}}"#;
assert!(rt2.accept_bytes(payload2), "rejected nested (query-first)");
assert!(rt2.is_accepted());
let mut rt = runtime(schema);
let missing = br#"{"filters":{"max_price":2000,"min_price":500}}"#;
let ok = rt.accept_bytes(missing);
assert!(
!(ok && rt.is_accepted()),
"accepted object missing required 'query' (iter 74 regression)"
);
}
#[test]
fn function_call_with_enum_argument() {
let schema = r#"{
"type": "object",
"properties": {
"city": {"type": "string"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
},
"required": ["city", "unit"]
}"#;
let mut rt = runtime(schema);
assert!(
rt.accept_bytes(br#"{"city":"London","unit":"celsius"}"#),
"rejected enum value (city-first)"
);
assert!(rt.is_accepted());
let mut rt = runtime(schema);
assert!(
rt.accept_bytes(br#"{"unit":"celsius","city":"London"}"#),
"rejected enum value (unit-first)"
);
assert!(rt.is_accepted());
let mut rt = runtime(schema);
let ok = rt.accept_bytes(br#"{"city":"London","unit":"kelvin"}"#);
assert!(
!(ok && rt.is_accepted()),
"accepted 'kelvin' not in [celsius, fahrenheit]"
);
let mut rt = runtime(schema);
let ok = rt.accept_bytes(br#"{"city":"London"}"#);
assert!(
!(ok && rt.is_accepted()),
"accepted object missing required 'unit'"
);
}
#[test]
fn function_call_with_array_arguments_field() {
let schema = r#"{
"type": "object",
"properties": {
"url": {"type": "string"},
"tags": {
"type": "array",
"items": {"type": "string"}
}
},
"required": ["url", "tags"]
}"#;
let mut rt = runtime(schema);
assert!(rt.accept_bytes(br#"{"tags":["news","tech"],"url":"https://example.com"}"#));
assert!(rt.is_accepted());
let mut rt = runtime(schema);
assert!(rt.accept_bytes(br#"{"url":"https://example.com","tags":["news","tech"]}"#));
assert!(rt.is_accepted());
let mut rt = runtime(schema);
assert!(rt.accept_bytes(br#"{"tags":[],"url":"https://example.com"}"#));
assert!(rt.is_accepted());
let mut rt = runtime(schema);
let ok = rt.accept_bytes(br#"{"url":"https://example.com"}"#);
assert!(
!(ok && rt.is_accepted()),
"accepted object missing required 'tags'"
);
}
#[test]
fn object_keys_accepted_in_any_order_three_required() {
let schema = r#"{
"type": "object",
"properties": {
"a": {"type": "integer"},
"b": {"type": "integer"},
"c": {"type": "integer"}
},
"required": ["a", "b", "c"]
}"#;
let perms: &[&[u8]] = &[
br#"{"a":1,"b":2,"c":3}"#,
br#"{"a":1,"c":3,"b":2}"#,
br#"{"b":2,"a":1,"c":3}"#,
br#"{"b":2,"c":3,"a":1}"#,
br#"{"c":3,"a":1,"b":2}"#,
br#"{"c":3,"b":2,"a":1}"#,
];
for perm in perms {
let mut rt = runtime(schema);
assert!(
rt.accept_bytes(perm),
"rejected permutation: {}",
std::str::from_utf8(perm).unwrap()
);
assert!(
rt.is_accepted(),
"not accepted after: {}",
std::str::from_utf8(perm).unwrap()
);
}
let mut rt = runtime(schema);
let ok = rt.accept_bytes(br#"{"a":1,"b":2}"#);
assert!(
!(ok && rt.is_accepted()),
"accepted object missing required 'c'"
);
let mut rt = runtime(schema);
let ok = rt.accept_bytes(br#"{"b":2,"c":3}"#);
assert!(
!(ok && rt.is_accepted()),
"accepted object missing required 'a'"
);
}
#[test]
fn additional_properties_false_rejects_extra_keys() {
let schema = r#"{
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"}
},
"required": ["name", "age"],
"additionalProperties": false
}"#;
let mut rt = runtime(schema);
assert!(rt.accept_bytes(br#"{"name":"Alice","age":30}"#));
assert!(rt.is_accepted());
let mut rt = runtime(schema);
assert!(rt.accept_bytes(br#"{"age":30,"name":"Alice"}"#));
assert!(rt.is_accepted());
let mut rt = runtime(schema);
let ok = rt.accept_bytes(br#"{"name":"Alice","age":30,"extra":"xxx"}"#);
assert!(
!(ok && rt.is_accepted()),
"accepted extra key when additionalProperties:false"
);
}
#[test]
fn additional_properties_true_accepts_extra_keys() {
let schema = r#"{
"type": "object",
"properties": {
"name": {"type": "string"}
},
"required": ["name"],
"additionalProperties": true
}"#;
let mut rt = runtime(schema);
assert!(rt.accept_bytes(br#"{"name":"Alice"}"#));
assert!(rt.is_accepted());
let mut rt = runtime(schema);
assert!(
rt.accept_bytes(br#"{"name":"Alice","extra":"xxx"}"#),
"rejected extra key when additionalProperties:true"
);
assert!(rt.is_accepted());
}
#[test]
fn additional_properties_unset_accepts_extra_keys() {
let schema = r#"{
"type": "object",
"properties": {
"name": {"type": "string"}
},
"required": ["name"]
}"#;
let mut rt = runtime(schema);
assert!(rt.accept_bytes(br#"{"name":"Alice"}"#));
assert!(rt.is_accepted());
let mut rt = runtime(schema);
assert!(
rt.accept_bytes(br#"{"name":"Alice","extra":"xxx"}"#),
"rejected extra key when additionalProperties unset (must be permissive)"
);
assert!(rt.is_accepted());
}
#[test]
fn prereq_optional_key_before_required() {
let schema = r#"{
"type": "object",
"properties": {
"name": {"type": "string"},
"title": {"type": "string"}
},
"required": ["name"]
}"#;
let mut rt = runtime(schema);
assert!(
rt.accept_bytes(br#"{"title":"Dr","name":"Alice"}"#),
"optional key before required key was rejected"
);
assert!(
rt.is_accepted(),
"not accepted after optional-before-required"
);
}
#[test]
fn prereq_extras_surrounding_required_key() {
let schema = r#"{
"type": "object",
"properties": {
"id": {"type": "integer"}
},
"required": ["id"]
}"#;
let mut rt = runtime(schema);
assert!(
rt.accept_bytes(br#"{"before":"x","id":1,"after":"y"}"#),
"extras-then-required-then-extras was rejected (additionalProperties unset)"
);
assert!(rt.is_accepted());
}
#[test]
fn prereq_only_extra_keys_no_required() {
let schema = r#"{
"type": "object",
"properties": {}
}"#;
let mut rt = runtime(schema);
assert!(
rt.accept_bytes(br#"{"anything":"goes"}"#),
"no-required-keys object with only extra keys was rejected"
);
assert!(rt.is_accepted());
}
#[test]
fn prereq_extras_before_and_after_two_required() {
let schema = r#"{
"type": "object",
"properties": {
"a": {"type": "integer"},
"b": {"type": "integer"}
},
"required": ["a", "b"]
}"#;
let mut rt = runtime(schema);
assert!(
rt.accept_bytes(br#"{"z":0,"a":1,"y":0,"b":2,"x":0}"#),
"extras interspersed between two required keys was rejected"
);
assert!(rt.is_accepted());
}
#[test]
fn prereq_multiple_extras_then_required() {
let schema = r#"{
"type": "object",
"properties": {
"name": {"type": "string"}
},
"required": ["name"]
}"#;
let mut rt = runtime(schema);
assert!(
rt.accept_bytes(br#"{"x":"v1","y":"v2","name":"Alice"}"#),
"multiple extras before required key was rejected"
);
assert!(rt.is_accepted());
}
#[test]
fn extras_before_required_additional_properties_false_rejects() {
let schema = r#"{
"type": "object",
"properties": {
"name": {"type": "string"}
},
"required": ["name"],
"additionalProperties": false
}"#;
let mut rt = runtime(schema);
let ok = rt.accept_bytes(br#"{"extra":"x","name":"Alice"}"#);
assert!(
!(ok && rt.is_accepted()),
"accepted extra key before required when additionalProperties:false"
);
let mut rt = runtime(schema);
assert!(rt.accept_bytes(br#"{"name":"Alice"}"#));
assert!(rt.is_accepted());
}
#[test]
fn duplicate_optional_key_rejected() {
let schema = r#"{
"type": "object",
"properties": {
"name": {"type": "string"},
"title": {"type": "string"}
},
"required": ["name"],
"additionalProperties": false
}"#;
let mut rt = runtime(schema);
let ok = rt.accept_bytes(br#"{"name":"Alice","title":"Dr","title":"Prof"}"#);
assert!(
!(ok && rt.is_accepted()),
"accepted duplicate optional key 'title'"
);
let mut rt = runtime(schema);
assert!(rt.accept_bytes(br#"{"name":"Alice","title":"Dr"}"#));
assert!(rt.is_accepted());
}
#[test]
fn large_schema_33_properties_returns_error() {
let mut props = serde_json::Map::new();
let mut required = Vec::new();
for i in 0..33usize {
let key = format!("prop{:02}", i);
props.insert(key.clone(), serde_json::json!({"type": "string"}));
required.push(serde_json::Value::String(key));
}
let schema = serde_json::Value::Object({
let mut m = serde_json::Map::new();
m.insert("type".into(), serde_json::json!("object"));
m.insert("properties".into(), serde_json::Value::Object(props));
m.insert("required".into(), serde_json::Value::Array(required));
m
});
let err = schema_to_gbnf(&schema).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("33") || msg.contains("max supported"),
"expected error mentioning property count or 'max supported'; got: {:?}",
msg
);
}
#[test]
fn large_schema_32_properties_compiles_ok() {
let mut props = serde_json::Map::new();
let mut required = Vec::new();
for i in 0..4usize {
let key = format!("req{:02}", i);
props.insert(key.clone(), serde_json::json!({"type": "string"}));
required.push(serde_json::Value::String(key));
}
for i in 0..4usize {
let key = format!("opt{:02}", i);
props.insert(key.clone(), serde_json::json!({"type": "string"}));
}
let schema = serde_json::Value::Object({
let mut m = serde_json::Map::new();
m.insert("type".into(), serde_json::json!("object"));
m.insert("properties".into(), serde_json::Value::Object(props));
m.insert("required".into(), serde_json::Value::Array(required));
m
});
let result = schema_to_gbnf(&schema);
assert!(
result.is_ok(),
"4-required + 4-optional schema failed to compile: {:?}",
result.err()
);
}
#[test]
fn nine_required_keys_returns_too_many_required_keys() {
let mut props = serde_json::Map::new();
let mut required = Vec::new();
for i in 0..9usize {
let key = format!("k{}", i);
props.insert(key.clone(), serde_json::json!({"type": "string"}));
required.push(serde_json::Value::String(key));
}
let schema = serde_json::Value::Object({
let mut m = serde_json::Map::new();
m.insert("type".into(), serde_json::json!("object"));
m.insert("properties".into(), serde_json::Value::Object(props));
m.insert("required".into(), serde_json::Value::Array(required));
m
});
let err = schema_to_gbnf(&schema).unwrap_err();
match &err {
SchemaError::TooManyRequiredKeys { count, max, .. } => {
assert_eq!(*count, 9, "variant must carry count=9");
assert_eq!(*max, 8_usize, "variant must carry max=8");
}
other => panic!("expected TooManyRequiredKeys variant; got {:?}", other),
}
let msg = err.to_string();
assert!(
msg.contains("9") && msg.contains("8"),
"expected error mentioning count=9 and limit=8; got: {:?}",
msg
);
assert!(
msg.contains("Moshier") || msg.contains("ADR-005"),
"expected operator-actionable citation; got: {:?}",
msg
);
assert!(
msg.contains("Reduce") || msg.contains("split"),
"expected actionable instruction; got: {:?}",
msg
);
}
#[test]
fn eight_required_keys_compiles_ok() {
let mut props = serde_json::Map::new();
let mut required = Vec::new();
for i in 0..8usize {
let key = format!("k{}", i);
props.insert(key.clone(), serde_json::json!({"type": "integer"}));
required.push(serde_json::Value::String(key));
}
let schema = serde_json::Value::Object({
let mut m = serde_json::Map::new();
m.insert("type".into(), serde_json::json!("object"));
m.insert("properties".into(), serde_json::Value::Object(props));
m.insert("required".into(), serde_json::Value::Array(required));
m
});
let result = schema_to_gbnf(&schema);
assert!(
result.is_ok(),
"8 required keys should compile (is the supported max); got: {:?}",
result.err()
);
let gbnf = result.unwrap();
let g = super::super::parser::parse(&gbnf).unwrap_or_else(|e| panic!("parse gbnf: {}", e));
let rid = g.rule_id("root").unwrap();
let mut rt = GrammarRuntime::new(g, rid).unwrap();
let reversed = br#"{"k7":7,"k6":6,"k5":5,"k4":4,"k3":3,"k2":2,"k1":1,"k0":0}"#;
assert!(
rt.accept_bytes(reversed),
"8-key schema rejected reversed-order input (any-position not enforced)"
);
assert!(
rt.is_accepted(),
"8-key schema not accepted after reversed input"
);
}
#[test]
fn additional_properties_permissive_accepts_multiple_extras() {
let schema = r#"{
"type": "object",
"properties": {
"name": {"type": "string"}
},
"required": ["name"]
}"#;
let mut rt = runtime(schema);
assert!(
rt.accept_bytes(br#"{"name":"Alice","x1":"v1","x2":"v2","x3":"v3"}"#),
"three trailing extra keys were rejected (additionalProperties permissive)"
);
assert!(rt.is_accepted());
let mut rt = runtime(schema);
assert!(
rt.accept_bytes(br#"{"before":"b","name":"Alice","after1":"a1","after2":"a2"}"#),
"extra keys surrounding required key were rejected"
);
assert!(rt.is_accepted());
let schema_no_req = r#"{
"type": "object",
"properties": {
"opt": {"type": "string"}
}
}"#;
let mut rt = runtime(schema_no_req);
assert!(
rt.accept_bytes(br#"{"opt":"v","extra1":"e1","extra2":"e2"}"#),
"multiple extras in no-required-keys object were rejected"
);
assert!(rt.is_accepted());
}
#[test]
fn schema_error_generic_variant_displays_correctly() {
let schema: Value = serde_json::from_str(r#"{"type":"notathing"}"#).unwrap();
let err = schema_to_gbnf(&schema).unwrap_err();
assert!(
matches!(&err, SchemaError::Generic { message, .. } if message.contains("unsupported type")),
"unsupported-type error must be SchemaError::Generic; got {:?}",
err
);
let s = err.to_string();
assert!(
s.contains("json-schema-to-grammar error"),
"Display must contain prefix: {}",
s
);
assert!(
s.contains("unsupported type"),
"Display must contain message: {}",
s
);
}
#[test]
fn too_many_required_keys_variant_carries_fn_name_and_count() {
let mut props = serde_json::Map::new();
let mut required = Vec::new();
for i in 0..9usize {
let k = format!("field{}", i);
props.insert(k.clone(), serde_json::json!({"type": "string"}));
required.push(serde_json::Value::String(k));
}
let schema = serde_json::json!({
"type": "object",
"properties": props,
"required": required
});
let err = schema_to_gbnf(&schema).unwrap_err();
match &err {
SchemaError::TooManyRequiredKeys {
fn_name,
count,
max,
} => {
let _ = fn_name; assert_eq!(*count, 9, "TooManyRequiredKeys must carry count=9");
assert_eq!(*max, 8, "TooManyRequiredKeys must carry max=8");
}
other => panic!("expected SchemaError::TooManyRequiredKeys; got {:?}", other),
}
let s = err.to_string();
assert!(s.contains("9"), "Display must mention count: {}", s);
assert!(s.contains("8"), "Display must mention cap: {}", s);
}
}