Skip to main content

ferrox_models/grammar/json_schema/
error.rs

1//! Every way this converter refuses a schema, naming what is missing.
2//!
3//! llama.cpp collects strings into `_errors` and throws one
4//! `std::invalid_argument` at the end. Worse, several keywords it does not
5//! implement are not errors at all: they fall through the `visit` chain
6//! and vanish. A `pattern` it cannot compile, a `minLength` on a schema
7//! with no explicit `"type": "string"`, `minItems` beside a tuple
8//! `items` -- each of those produces a grammar that *permits documents the
9//! schema forbids*, which is a wrong answer wearing the costume of a
10//! working one.
11//!
12//! Per `CLAUDE.md` ("return `Result` and name what is missing"), this port
13//! refuses instead. [`SchemaError::UnsupportedKeyword`] is raised for any
14//! keyword the branch that was chosen does not consume, so silence is not
15//! a reachable outcome.
16
17use crate::grammar::GrammarError;
18use std::fmt;
19
20/// What the JSON-schema converter refused, and why.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum SchemaError {
23    /// The schema text was not valid JSON.
24    NotJson(String),
25    /// A schema, or a subschema, was not a JSON object. JSON Schema's
26    /// boolean form (`true` / `false` as a whole schema) is not ported.
27    NotAnObject { at: String, kind: &'static str },
28    /// A keyword is present that the chosen branch does not act on.
29    /// Ignoring it would widen the grammar past what the schema allows.
30    UnsupportedKeyword {
31        keyword: String,
32        at: String,
33        why: &'static str,
34    },
35    /// A `pattern` this port cannot compile to GBNF. llama.cpp's
36    /// `_visit_pattern` handles a subset of ECMA-262; the cases named here
37    /// are the ones it mishandles rather than the ones it skips.
38    UnsupportedPattern { pattern: String, why: String },
39    /// `"format"` names something outside the six llama.cpp special-cases
40    /// (`date`, `time`, `date-time`, `uuid`, `uuid1`..`uuid5`).
41    UnsupportedFormat { format: String, at: String },
42    /// `"type"` is not a string, or names no JSON type.
43    UnknownType { at: String, found: String },
44    /// A `$ref` this port will not follow: a remote URL, or anything that
45    /// is not a `#/`-rooted JSON pointer into the same document.
46    UnsupportedRef { reference: String },
47    /// A `#/`-rooted `$ref` whose pointer does not land on anything.
48    RefNotFound { reference: String, token: String },
49    /// Two schemas compiled into ONE grammar ([`super::GrammarBuilder`])
50    /// spell the same `$ref` pointer and mean different subschemas. The
51    /// rule name is derived from the pointer, so honouring the first
52    /// would compile the second against the wrong definition.
53    RefCollision { pointer: String },
54    /// A keyword's value has the wrong JSON type for what it means.
55    BadValue {
56        keyword: String,
57        at: String,
58        why: String,
59    },
60    /// The top-level schema did not end up owning the rule named `root`,
61    /// so the grammar's entry point would be some subschema's rule. This
62    /// is reachable upstream (a property named `""` takes `root` first)
63    /// and silently produces a grammar for the wrong document.
64    RootDisplaced { got: String },
65    /// The converter emitted a grammar the engine in this repo cannot
66    /// parse. That is a bug in this module, never in the caller's schema.
67    Emitted(GrammarError),
68    /// An invariant of this module was violated -- a builtin table lookup
69    /// that cannot fail did. A bug here, not in the caller's schema.
70    Internal(&'static str),
71}
72
73impl fmt::Display for SchemaError {
74    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75        match self {
76            SchemaError::NotJson(msg) => write!(f, "JSON schema is not valid JSON: {msg}"),
77            SchemaError::NotAnObject { at, kind } => write!(
78                f,
79                "JSON schema at {at} is a {kind}, not an object; boolean schemas are not supported"
80            ),
81            SchemaError::UnsupportedKeyword { keyword, at, why } => write!(
82                f,
83                "JSON schema keyword {keyword:?} at {at} is not supported ({why}); it would be \
84                 ignored, and the grammar would then accept documents the schema rejects"
85            ),
86            SchemaError::UnsupportedPattern { pattern, why } => write!(
87                f,
88                "JSON schema \"pattern\" {pattern:?} cannot be compiled to a grammar: {why}"
89            ),
90            SchemaError::UnsupportedFormat { format, at } => write!(
91                f,
92                "JSON schema format {format:?} at {at} is not supported; only \"date\", \"time\", \
93                 \"date-time\" and \"uuid\" (or \"uuid1\"..\"uuid5\") have grammars"
94            ),
95            SchemaError::UnknownType { at, found } => {
96                write!(
97                    f,
98                    "JSON schema \"type\" at {at} is {found}, which names no JSON type"
99                )
100            }
101            SchemaError::UnsupportedRef { reference } => write!(
102                f,
103                "JSON schema $ref {reference:?} is not supported; only same-document refs of the \
104                 form \"#/...\" are resolved, and nothing is fetched over the network"
105            ),
106            SchemaError::RefNotFound { reference, token } => write!(
107                f,
108                "JSON schema $ref {reference:?} does not resolve: {token:?} is not in the document"
109            ),
110            SchemaError::RefCollision { pointer } => write!(
111                f,
112                "two schemas compiled into one grammar both define {pointer:?} and disagree about \
113                 what it is; rename one of them"
114            ),
115            SchemaError::BadValue { keyword, at, why } => {
116                write!(
117                    f,
118                    "JSON schema keyword {keyword:?} at {at} is invalid: {why}"
119                )
120            }
121            SchemaError::RootDisplaced { got } => write!(
122                f,
123                "the top-level schema compiled to rule {got:?} rather than \"root\", so the \
124                 grammar would start from a subschema; rename the colliding property"
125            ),
126            SchemaError::Internal(what) => {
127                write!(f, "internal error in the JSON schema converter: {what}")
128            }
129            SchemaError::Emitted(e) => write!(
130                f,
131                "the grammar generated from this schema does not parse, which is a bug in the \
132                 converter: {e}"
133            ),
134        }
135    }
136}
137
138impl std::error::Error for SchemaError {}
139
140impl From<GrammarError> for SchemaError {
141    fn from(e: GrammarError) -> Self {
142        SchemaError::Emitted(e)
143    }
144}
145
146/// The name of a subschema as it appears in an error, with the empty
147/// top-level name spelled out.
148pub(super) fn at(name: &str) -> String {
149    if name.is_empty() {
150        "the top level".to_string()
151    } else {
152        format!("{name:?}")
153    }
154}