diff --git a/examples/json-schema-to-grammar.py b/examples/json-schema-to-grammar.py index cdbee7d39..2a7c78afe 100755 --- a/examples/json-schema-to-grammar.py +++ b/examples/json-schema-to-grammar.py @@ -35,6 +35,8 @@ DATE_RULES = { 'date-time-string': '"\\"" date-time "\\"" space', } +RESERVED_NAMES = set(["root", *PRIMITIVE_RULES.keys(), *DATE_RULES.keys()]) + INVALID_RULE_CHARS_RE = re.compile(r'[^a-zA-Z0-9-]+') GRAMMAR_LITERAL_ESCAPE_RE = re.compile(r'[\r\n"]') GRAMMAR_RANGE_LITERAL_ESCAPE_RE = re.compile(r'[\r\n"\]\-\\]') @@ -300,7 +302,7 @@ class SchemaConverter: def visit(self, schema, name): schema_type = schema.get('type') schema_format = schema.get('format') - rule_name = name or 'root' + rule_name = name + '-' if name in RESERVED_NAMES else name or 'root' if (ref := schema.get('$ref')) is not None: return self._add_rule(rule_name, self._resolve_ref(ref)) diff --git a/examples/server/json-schema-to-grammar.cpp b/examples/server/json-schema-to-grammar.cpp index 054f49a5b..059ffd7ed 100644 --- a/examples/server/json-schema-to-grammar.cpp +++ b/examples/server/json-schema-to-grammar.cpp @@ -43,6 +43,16 @@ unordered_map DATE_RULES = { {"date-time-string", "\"\\\"\" date-time \"\\\"\" space"} }; +static bool is_reserved_name(const string& name) { + static std::unordered_set RESERVED_NAMES; + if (RESERVED_NAMES.empty()) { + RESERVED_NAMES.insert("root"); + for (const auto &p : PRIMITIVE_RULES) RESERVED_NAMES.insert(p.first); + for (const auto &p : DATE_RULES) RESERVED_NAMES.insert(p.first); + } + return RESERVED_NAMES.find(name) != RESERVED_NAMES.end(); +} + regex INVALID_RULE_CHARS_RE("[^a-zA-Z0-9-]+"); regex GRAMMAR_LITERAL_ESCAPE_RE("[\r\n\"]"); regex GRAMMAR_RANGE_LITERAL_ESCAPE_RE("[\r\n\"\\]\\-\\\\]"); @@ -523,7 +533,7 @@ public: string visit(const json& schema, const string& name) { json schema_type = schema.contains("type") ? schema["type"] : json(); string schema_format = schema.contains("format") ? schema["format"].get() : ""; - string rule_name = name.empty() ? "root" : name; + string rule_name = is_reserved_name(name) ? name + "-" : name.empty() ? "root" : name; if (schema.contains("$ref")) { return _add_rule(rule_name, _resolve_ref(schema["$ref"])); diff --git a/examples/server/public/json-schema-to-grammar.mjs b/examples/server/public/json-schema-to-grammar.mjs index 862a9079f..604680488 100644 --- a/examples/server/public/json-schema-to-grammar.mjs +++ b/examples/server/public/json-schema-to-grammar.mjs @@ -26,6 +26,8 @@ const DATE_RULES = { 'date-time-string': '"\\"" date-time "\\"" space', }; +const RESERVED_NAMES = {'root': true, ...PRIMITIVE_RULES, ...DATE_RULES}; + const INVALID_RULE_CHARS_RE = /[^\dA-Za-z-]+/g; const GRAMMAR_LITERAL_ESCAPE_RE = /[\n\r"]/g; const GRAMMAR_RANGE_LITERAL_ESCAPE_RE = /[\n\r"\]\-\\]/g; @@ -326,7 +328,7 @@ export class SchemaConverter { visit(schema, name) { const schemaType = schema.type; const schemaFormat = schema.format; - const ruleName = name || 'root'; + const ruleName = name in RESERVED_NAMES ? name + '-' : name == '' ? 'root' : name; const ref = schema.$ref; if (ref !== undefined) { diff --git a/tests/test-json-schema-to-grammar.cpp b/tests/test-json-schema-to-grammar.cpp index 8d0f015b5..9e2fa9521 100755 --- a/tests/test-json-schema-to-grammar.cpp +++ b/tests/test-json-schema-to-grammar.cpp @@ -259,10 +259,61 @@ int main() { )""" }); + run_all({ + "object w/ required props", + R"""({ + "type": "object", + "properties": { + "a": { + "type": "string" + }, + "b": { + "type": "string" + } + }, + "required": [ + "a", + "b" + ], + "additionalProperties": false, + "definitions": {} + })""", + R"""( + a-kv ::= "\"a\"" space ":" space string + b-kv ::= "\"b\"" space ":" space string + root ::= "{" space a-kv "," space b-kv "}" space + space ::= " "? + string ::= "\"" ( + [^"\\] | + "\\" (["\\/bfnrt] | "u" [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F]) + )* "\"" space + )""" + }); + + run_all({ + "1 optional", + R"""({ + "properties": { + "a": { + "type": "string" + } + }, + "additionalProperties": false + })""", + R"""( + a-kv ::= "\"a\"" space ":" space string + root ::= "{" space (a-kv )? "}" space + space ::= " "? + string ::= "\"" ( + [^"\\] | + "\\" (["\\/bfnrt] | "u" [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F]) + )* "\"" space + )""" + }); + run_all({ "optionals", R"""({ - "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { "a": { @@ -333,7 +384,6 @@ int main() { run_all({ "top-level $ref", R"""({ - "$schema": "http://json-schema.org/draft-07/schema#", "$ref": "#/definitions/MyType", "definitions": { "MyType": { @@ -362,4 +412,48 @@ int main() { )""" }); + run_all({ + "conflicting names", + R"""({ + "type": "object", + "properties": { + "number": { + "type": "object", + "properties": { + "number": { + "type": "object", + "properties": { + "root": { + "type": "number" + } + }, + "required": [ + "root" + ], + "additionalProperties": false + } + }, + "required": [ + "number" + ], + "additionalProperties": false + } + }, + "required": [ + "number" + ], + "additionalProperties": false, + "definitions": {} + })""", + R"""( + number ::= ("-"? ([0-9] | [1-9] [0-9]*)) ("." [0-9]+)? ([eE] [-+]? [0-9]+)? space + number- ::= "{" space number-number-kv "}" space + number-kv ::= "\"number\"" space ":" space number- + number-number ::= "{" space number-number-root-kv "}" space + number-number-kv ::= "\"number\"" space ":" space number-number + number-number-root-kv ::= "\"root\"" space ":" space number + root ::= "{" space number-kv "}" space + space ::= " "? + )""" + }); }