Skip to content

Commit 9112541

Browse files
committed
grammar : support array references in json schema
1 parent 5d195f1 commit 9112541

File tree

4 files changed

+80
-18
lines changed

4 files changed

+80
-18
lines changed

common/json-schema-to-grammar.cpp

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -601,7 +601,11 @@ class SchemaConverter {
601601
}
602602

603603
std::string _resolve_ref(const std::string & ref) {
604-
std::string ref_name = ref.substr(ref.find_last_of('/') + 1);
604+
std::string ref_fragment = ref;
605+
if (ref.find('#') != std::string::npos) {
606+
ref_fragment = ref.substr(ref.find('#') + 1);
607+
}
608+
std::string ref_name = "ref" + std::regex_replace(ref_fragment, std::regex(R"([^a-zA-Z0-9-])"), "-");
605609
if (_rules.find(ref_name) == _rules.end() && _refs_being_resolved.find(ref) == _refs_being_resolved.end()) {
606610
_refs_being_resolved.insert(ref);
607611
json resolved = _refs[ref];
@@ -774,11 +778,24 @@ class SchemaConverter {
774778
std::vector<std::string> tokens = string_split(pointer, "/");
775779
for (size_t i = 1; i < tokens.size(); ++i) {
776780
std::string sel = tokens[i];
777-
if (target.is_null() || !target.contains(sel)) {
781+
if (target.is_object() && target.contains(sel)) {
782+
target = target[sel];
783+
} else if (target.is_array()) {
784+
size_t sel_index;
785+
try {
786+
sel_index = std::stoul(sel);
787+
} catch (const std::invalid_argument & e) {
788+
sel_index = target.size();
789+
}
790+
if (sel_index >= target.size()) {
791+
_errors.push_back("Error resolving ref " + ref + ": " + sel + " not in " + target.dump());
792+
return;
793+
}
794+
target = target[sel_index];
795+
} else {
778796
_errors.push_back("Error resolving ref " + ref + ": " + sel + " not in " + target.dump());
779797
return;
780798
}
781-
target = target[sel];
782799
}
783800
_refs[ref] = target;
784801
}

examples/json_schema_to_grammar.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -371,8 +371,17 @@ def visit(n: dict):
371371
raise ValueError(f'Unsupported ref {ref}')
372372

373373
for sel in ref.split('#')[-1].split('/')[1:]:
374-
assert target is not None and sel in target, f'Error resolving ref {ref}: {sel} not in {target}'
375-
target = target[sel]
374+
assert target is not None, f'Error resolving ref {ref}: {sel} not in {target}'
375+
if isinstance(target, list):
376+
try:
377+
sel_index = int(sel)
378+
except ValueError:
379+
raise ValueError(f'Error resolving ref {ref}: {sel} not in {target}')
380+
assert 0 <= sel_index < len(target), f'Error resolving ref {ref}: {sel} not in {target}'
381+
target = target[sel_index]
382+
else:
383+
assert sel in target, f'Error resolving ref {ref}: {sel} not in {target}'
384+
target = target[sel]
376385

377386
self._refs[ref] = target
378387
else:
@@ -547,7 +556,8 @@ def join_seq():
547556

548557

549558
def _resolve_ref(self, ref):
550-
ref_name = ref.split('/')[-1]
559+
ref_fragment = ref.split('#')[-1]
560+
ref_name = 'ref' + re.sub(r'[^a-zA-Z0-9-]', '-', ref_fragment)
551561
if ref_name not in self._rules and ref not in self._refs_being_resolved:
552562
self._refs_being_resolved.add(ref)
553563
resolved = self._refs[ref]

tests/test-json-schema-to-grammar.cpp

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1124,9 +1124,39 @@ static void test_all(const std::string & lang, std::function<void(const TestCase
11241124
})""",
11251125
R"""(
11261126
char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})
1127-
foo ::= "{" space foo-a-kv "}" space
1128-
foo-a-kv ::= "\"a\"" space ":" space string
1129-
root ::= foo
1127+
ref-definitions-foo ::= "{" space ref-definitions-foo-a-kv "}" space
1128+
ref-definitions-foo-a-kv ::= "\"a\"" space ":" space string
1129+
root ::= ref-definitions-foo
1130+
space ::= | " " | "\n"{1,2} [ \t]{0,20}
1131+
string ::= "\"" char* "\"" space
1132+
)"""
1133+
});
1134+
1135+
test({
1136+
SUCCESS,
1137+
"top-level $ref array item",
1138+
R"""({
1139+
"$ref": "#/definitions/0",
1140+
"definitions": [
1141+
{
1142+
"type": "object",
1143+
"properties": {
1144+
"a": {
1145+
"type": "string"
1146+
}
1147+
},
1148+
"required": [
1149+
"a"
1150+
],
1151+
"additionalProperties": false
1152+
}
1153+
]
1154+
})""",
1155+
R"""(
1156+
char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})
1157+
ref-definitions-0 ::= "{" space ref-definitions-0-a-kv "}" space
1158+
ref-definitions-0-a-kv ::= "\"a\"" space ":" space string
1159+
root ::= ref-definitions-0
11301160
space ::= | " " | "\n"{1,2} [ \t]{0,20}
11311161
string ::= "\"" char* "\"" space
11321162
)"""
@@ -1151,15 +1181,15 @@ static void test_all(const std::string & lang, std::function<void(const TestCase
11511181
"type": "object"
11521182
})""",
11531183
R"""(
1154-
alternative-0 ::= foo
1155-
alternative-1 ::= bar
1156-
bar ::= "{" space (bar-b-kv )? "}" space
1157-
bar-b-kv ::= "\"b\"" space ":" space number
1184+
alternative-0 ::= ref-definitions-foo
1185+
alternative-1 ::= ref-definitions-bar
11581186
decimal-part ::= [0-9]{1,16}
1159-
foo ::= "{" space (foo-a-kv )? "}" space
1160-
foo-a-kv ::= "\"a\"" space ":" space number
11611187
integral-part ::= [0] | [1-9] [0-9]{0,15}
11621188
number ::= ("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)? space
1189+
ref-definitions-bar ::= "{" space (ref-definitions-bar-b-kv )? "}" space
1190+
ref-definitions-bar-b-kv ::= "\"b\"" space ":" space number
1191+
ref-definitions-foo ::= "{" space (ref-definitions-foo-a-kv )? "}" space
1192+
ref-definitions-foo-a-kv ::= "\"a\"" space ":" space number
11631193
root ::= alternative-0 | alternative-1
11641194
space ::= | " " | "\n"{1,2} [ \t]{0,20}
11651195
)"""

tools/server/public_legacy/json-schema-to-grammar.mjs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -345,10 +345,14 @@ export class SchemaConverter {
345345

346346
const selectors = ref.split('#')[1].split('/').slice(1);
347347
for (const sel of selectors) {
348-
if (!target || !(sel in target)) {
348+
const selIndex = parseInt(sel, 10);
349+
if (target && sel in target) {
350+
target = target[sel];
351+
} else if (target && selIndex in target) {
352+
target = target[selIndex];
353+
} else {
349354
throw new Error(`Error resolving ref ${ref}: ${sel} not in ${JSON.stringify(target)}`);
350355
}
351-
target = target[sel];
352356
}
353357

354358
this._refs[ref] = target;
@@ -594,7 +598,8 @@ export class SchemaConverter {
594598
}
595599

596600
_resolveRef(ref) {
597-
let refName = ref.split('/').pop();
601+
let refFragment = ref.split('#').pop();
602+
let refName = 'ref' + refFragment.replace(/[^a-zA-Z0-9-]/, '-');
598603
if (!(refName in this._rules) && !this._refsBeingResolved.has(ref)) {
599604
this._refsBeingResolved.add(ref);
600605
const resolved = this._refs[ref];

0 commit comments

Comments
 (0)