1#ifdef NDEBUG
2#undef NDEBUG
3#endif
4
5#include "json-schema-to-grammar.h"
6
7#include "../src/llama-grammar.h"
8
9#include <nlohmann/json.hpp>
10
11#include <cassert>
12#include <fstream>
13#include <sstream>
14#include <regex>
15
16static std::string trim(const std::string & source) {
17 std::string s(source);
18 s.erase(pos: 0,n: s.find_first_not_of(s: " \n\r\t"));
19 s.erase(pos: s.find_last_not_of(s: " \n\r\t")+1);
20 return std::regex_replace(s: s, e: std::regex("(^|\n)[ \t]+"), fmt: "$1");
21}
22
23enum TestCaseStatus {
24 SUCCESS,
25 FAILURE
26};
27
28struct TestCase {
29 TestCaseStatus expected_status;
30 std::string name;
31 std::string schema;
32 std::string expected_grammar;
33
34 void _print_failure_header() const {
35 fprintf(stderr, format: "#\n# Test '%s' failed.\n#\n%s\n", name.c_str(), schema.c_str());
36 }
37 void verify(const std::string & actual_grammar) const {
38 if (trim(source: actual_grammar) != trim(source: expected_grammar)) {
39 _print_failure_header();
40 fprintf(stderr, format: "# EXPECTED:\n%s\n# ACTUAL:\n%s\n", expected_grammar.c_str(), actual_grammar.c_str());
41 assert(false);
42 }
43 }
44 void verify_expectation_parseable() const {
45 try {
46 llama_grammar_parser state;
47 state.parse(src: expected_grammar.c_str());
48 if (state.symbol_ids.find(x: "root") == state.symbol_ids.end()) {
49 throw std::runtime_error("Grammar failed to parse:\n" + expected_grammar);
50 }
51 } catch (const std::runtime_error & ex) {
52 _print_failure_header();
53 fprintf(stderr, format: "# GRAMMAR ERROR: %s\n", ex.what());
54 assert(false);
55 }
56 }
57 void verify_status(TestCaseStatus status) const {
58 if (status != expected_status) {
59 _print_failure_header();
60 fprintf(stderr, format: "# EXPECTED STATUS: %s\n", expected_status == SUCCESS ? "SUCCESS" : "FAILURE");
61 fprintf(stderr, format: "# ACTUAL STATUS: %s\n", status == SUCCESS ? "SUCCESS" : "FAILURE");
62 assert(false);
63 }
64 }
65};
66
67static void write(const std::string & file, const std::string & content) {
68 std::ofstream f;
69 f.open(s: file.c_str());
70 f << content.c_str();
71 f.close();
72}
73
74static std::string read(const std::string & file) {
75 std::ostringstream actuals;
76 actuals << std::ifstream(file.c_str()).rdbuf();
77 return actuals.str();
78}
79
80static void test_all(const std::string & lang, std::function<void(const TestCase &)> runner) {
81 fprintf(stderr, format: "#\n# Testing JSON schema conversion (%s)\n#\n", lang.c_str());
82 auto test = [&](const TestCase & tc) {
83 fprintf(stderr, format: "- %s%s\n", tc.name.c_str(), tc.expected_status == FAILURE ? " (failure expected)" : "");
84 runner(tc);
85 };
86
87 test({
88 .expected_status: SUCCESS,
89 .name: "min 0",
90 .schema: R"""({
91 "type": "integer",
92 "minimum": 0
93 })""",
94 .expected_grammar: R"""(
95 root ::= ([0] | [1-9] [0-9]{0,15}) space
96 space ::= | " " | "\n"{1,2} [ \t]{0,20}
97 )"""
98 });
99
100 test({
101 .expected_status: SUCCESS,
102 .name: "min 1",
103 .schema: R"""({
104 "type": "integer",
105 "minimum": 1
106 })""",
107 .expected_grammar: R"""(
108 root ::= ([1-9] [0-9]{0,15}) space
109 space ::= | " " | "\n"{1,2} [ \t]{0,20}
110 )"""
111 });
112
113 test({
114 .expected_status: SUCCESS,
115 .name: "min 3",
116 .schema: R"""({
117 "type": "integer",
118 "minimum": 3
119 })""",
120 .expected_grammar: R"""(
121 root ::= ([1-2] [0-9]{1,15} | [3-9] [0-9]{0,15}) space
122 space ::= | " " | "\n"{1,2} [ \t]{0,20}
123 )"""
124 });
125
126 test({
127 .expected_status: SUCCESS,
128 .name: "min 9",
129 .schema: R"""({
130 "type": "integer",
131 "minimum": 9
132 })""",
133 .expected_grammar: R"""(
134 root ::= ([1-8] [0-9]{1,15} | [9] [0-9]{0,15}) space
135 space ::= | " " | "\n"{1,2} [ \t]{0,20}
136 )"""
137 });
138
139 test({
140 .expected_status: SUCCESS,
141 .name: "min 10",
142 .schema: R"""({
143 "type": "integer",
144 "minimum": 10
145 })""",
146 .expected_grammar: R"""(
147 root ::= ([1] ([0-9]{1,15}) | [2-9] [0-9]{1,15}) space
148 space ::= | " " | "\n"{1,2} [ \t]{0,20}
149 )"""
150 });
151
152 test({
153 .expected_status: SUCCESS,
154 .name: "min 25",
155 .schema: R"""({
156 "type": "integer",
157 "minimum": 25
158 })""",
159 .expected_grammar: R"""(
160 root ::= ([1] [0-9]{2,15} | [2] ([0-4] [0-9]{1,14} | [5-9] [0-9]{0,14}) | [3-9] [0-9]{1,15}) space
161 space ::= | " " | "\n"{1,2} [ \t]{0,20}
162 )"""
163 });
164
165 test({
166 .expected_status: SUCCESS,
167 .name: "max 30",
168 .schema: R"""({
169 "type": "integer",
170 "maximum": 30
171 })""",
172 .expected_grammar: R"""(
173 root ::= ("-" [1-9] [0-9]{0,15} | [0-9] | ([1-2] [0-9] | [3] "0")) space
174 space ::= | " " | "\n"{1,2} [ \t]{0,20}
175 )"""
176 });
177
178 test({
179 .expected_status: SUCCESS,
180 .name: "min -5",
181 .schema: R"""({
182 "type": "integer",
183 "minimum": -5
184 })""",
185 .expected_grammar: R"""(
186 root ::= ("-" ([0-5]) | [0] | [1-9] [0-9]{0,15}) space
187 space ::= | " " | "\n"{1,2} [ \t]{0,20}
188 )"""
189 });
190
191 test({
192 .expected_status: SUCCESS,
193 .name: "min -123",
194 .schema: R"""({
195 "type": "integer",
196 "minimum": -123
197 })""",
198 .expected_grammar: R"""(
199 root ::= ("-" ([0-9] | ([1-8] [0-9] | [9] [0-9]) | "1" ([0-1] [0-9] | [2] [0-3])) | [0] | [1-9] [0-9]{0,15}) space
200 space ::= | " " | "\n"{1,2} [ \t]{0,20}
201 )"""
202 });
203
204 test({
205 .expected_status: SUCCESS,
206 .name: "max -5",
207 .schema: R"""({
208 "type": "integer",
209 "maximum": -5
210 })""",
211 .expected_grammar: R"""(
212 root ::= ("-" ([0-4] [0-9]{1,15} | [5-9] [0-9]{0,15})) space
213 space ::= | " " | "\n"{1,2} [ \t]{0,20}
214 )"""
215 });
216
217 test({
218 .expected_status: SUCCESS,
219 .name: "max 1",
220 .schema: R"""({
221 "type": "integer",
222 "maximum": 1
223 })""",
224 .expected_grammar: R"""(
225 root ::= ("-" [1-9] [0-9]{0,15} | [0-1]) space
226 space ::= | " " | "\n"{1,2} [ \t]{0,20}
227 )"""
228 });
229
230 test({
231 .expected_status: SUCCESS,
232 .name: "max 100",
233 .schema: R"""({
234 "type": "integer",
235 "maximum": 100
236 })""",
237 .expected_grammar: R"""(
238 root ::= ("-" [1-9] [0-9]{0,15} | [0-9] | ([1-8] [0-9] | [9] [0-9]) | "100") space
239 space ::= | " " | "\n"{1,2} [ \t]{0,20}
240 )"""
241 });
242
243 test({
244 .expected_status: SUCCESS,
245 .name: "min 0 max 23",
246 .schema: R"""({
247 "type": "integer",
248 "minimum": 0,
249 "maximum": 23
250 })""",
251 .expected_grammar: R"""(
252 root ::= ([0-9] | ([1] [0-9] | [2] [0-3])) space
253 space ::= | " " | "\n"{1,2} [ \t]{0,20}
254 )"""
255 });
256
257 test({
258 .expected_status: SUCCESS,
259 .name: "min 15 max 300",
260 .schema: R"""({
261 "type": "integer",
262 "minimum": 15,
263 "maximum": 300
264 })""",
265 .expected_grammar: R"""(
266 root ::= (([1] ([5-9]) | [2-9] [0-9]) | ([1-2] [0-9]{2} | [3] "00")) space
267 space ::= | " " | "\n"{1,2} [ \t]{0,20}
268 )"""
269 });
270
271 test({
272 .expected_status: SUCCESS,
273 .name: "min 5 max 30",
274 .schema: R"""({
275 "type": "integer",
276 "minimum": 5,
277 "maximum": 30
278 })""",
279 .expected_grammar: R"""(
280 root ::= ([5-9] | ([1-2] [0-9] | [3] "0")) space
281 space ::= | " " | "\n"{1,2} [ \t]{0,20}
282 )"""
283 });
284
285 test({
286 .expected_status: SUCCESS,
287 .name: "min -123 max 42",
288 .schema: R"""({
289 "type": "integer",
290 "minimum": -123,
291 "maximum": 42
292 })""",
293 .expected_grammar: R"""(
294 root ::= ("-" ([0-9] | ([1-8] [0-9] | [9] [0-9]) | "1" ([0-1] [0-9] | [2] [0-3])) | [0-9] | ([1-3] [0-9] | [4] [0-2])) space
295 space ::= | " " | "\n"{1,2} [ \t]{0,20}
296 )"""
297 });
298
299 test({
300 .expected_status: SUCCESS,
301 .name: "min -10 max 10",
302 .schema: R"""({
303 "type": "integer",
304 "minimum": -10,
305 "maximum": 10
306 })""",
307 .expected_grammar: R"""(
308 root ::= ("-" ([0-9] | "10") | [0-9] | "10") space
309 space ::= | " " | "\n"{1,2} [ \t]{0,20}
310 )"""
311 });
312
313 test({
314 .expected_status: FAILURE,
315 .name: "unknown type",
316 .schema: R"""({
317 "type": "kaboom"
318 })""",
319 .expected_grammar: ""
320 });
321
322 test({
323 .expected_status: FAILURE,
324 .name: "invalid type",
325 .schema: R"""({
326 "type": 123
327 })""",
328 .expected_grammar: ""
329 });
330
331 test({
332 .expected_status: SUCCESS,
333 .name: "empty schema (object)",
334 .schema: "{}",
335 .expected_grammar: R"""(
336 array ::= "[" space ( value ("," space value)* )? "]" space
337 boolean ::= ("true" | "false") space
338 char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})
339 decimal-part ::= [0-9]{1,16}
340 integral-part ::= [0] | [1-9] [0-9]{0,15}
341 null ::= "null" space
342 number ::= ("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)? space
343 object ::= "{" space ( string ":" space value ("," space string ":" space value)* )? "}" space
344 root ::= object
345 space ::= | " " | "\n"{1,2} [ \t]{0,20}
346 string ::= "\"" char* "\"" space
347 value ::= object | array | string | number | boolean | null
348 )"""
349 });
350
351 test({
352 .expected_status: SUCCESS,
353 .name: "exotic formats",
354 .schema: R"""({
355 "items": [
356 { "format": "date" },
357 { "format": "uuid" },
358 { "format": "time" },
359 { "format": "date-time" }
360 ]
361 })""",
362 .expected_grammar: R"""(
363 date ::= [0-9]{4} "-" ( "0" [1-9] | "1" [0-2] ) "-" ( "0" [1-9] | [1-2] [0-9] | "3" [0-1] )
364 date-string ::= "\"" date "\"" space
365 date-time ::= date "T" time
366 date-time-string ::= "\"" date-time "\"" space
367 root ::= "[" space tuple-0 "," space uuid "," space tuple-2 "," space tuple-3 "]" space
368 space ::= | " " | "\n"{1,2} [ \t]{0,20}
369 time ::= ([01] [0-9] | "2" [0-3]) ":" [0-5] [0-9] ":" [0-5] [0-9] ( "." [0-9]{3} )? ( "Z" | ( "+" | "-" ) ( [01] [0-9] | "2" [0-3] ) ":" [0-5] [0-9] )
370 time-string ::= "\"" time "\"" space
371 tuple-0 ::= date-string
372 tuple-2 ::= time-string
373 tuple-3 ::= date-time-string
374 uuid ::= "\"" [0-9a-fA-F]{8} "-" [0-9a-fA-F]{4} "-" [0-9a-fA-F]{4} "-" [0-9a-fA-F]{4} "-" [0-9a-fA-F]{12} "\"" space
375 )"""
376 });
377
378 test({
379 .expected_status: SUCCESS,
380 .name: "string",
381 .schema: R"""({
382 "type": "string"
383 })""",
384 .expected_grammar: R"""(
385 char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})
386 root ::= "\"" char* "\"" space
387 space ::= | " " | "\n"{1,2} [ \t]{0,20}
388 )"""
389 });
390
391 test({
392 .expected_status: SUCCESS,
393 .name: "string w/ min length 1",
394 .schema: R"""({
395 "type": "string",
396 "minLength": 1
397 })""",
398 .expected_grammar: R"""(
399 char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})
400 root ::= "\"" char+ "\"" space
401 space ::= | " " | "\n"{1,2} [ \t]{0,20}
402 )"""
403 });
404
405 test({
406 .expected_status: SUCCESS,
407 .name: "string w/ min length 3",
408 .schema: R"""({
409 "type": "string",
410 "minLength": 3
411 })""",
412 .expected_grammar: R"""(
413 char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})
414 root ::= "\"" char{3,} "\"" space
415 space ::= | " " | "\n"{1,2} [ \t]{0,20}
416 )"""
417 });
418
419 test({
420 .expected_status: SUCCESS,
421 .name: "string w/ max length",
422 .schema: R"""({
423 "type": "string",
424 "maxLength": 3
425 })""",
426 .expected_grammar: R"""(
427 char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})
428 root ::= "\"" char{0,3} "\"" space
429 space ::= | " " | "\n"{1,2} [ \t]{0,20}
430 )"""
431 });
432
433 test({
434 .expected_status: SUCCESS,
435 .name: "string w/ min & max length",
436 .schema: R"""({
437 "type": "string",
438 "minLength": 1,
439 "maxLength": 4
440 })""",
441 .expected_grammar: R"""(
442 char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})
443 root ::= "\"" char{1,4} "\"" space
444 space ::= | " " | "\n"{1,2} [ \t]{0,20}
445 )"""
446 });
447
448 test({
449 .expected_status: SUCCESS,
450 .name: "boolean",
451 .schema: R"""({
452 "type": "boolean"
453 })""",
454 .expected_grammar: R"""(
455 root ::= ("true" | "false") space
456 space ::= | " " | "\n"{1,2} [ \t]{0,20}
457 )"""
458 });
459
460 test({
461 .expected_status: SUCCESS,
462 .name: "integer",
463 .schema: R"""({
464 "type": "integer"
465 })""",
466 .expected_grammar: R"""(
467 integral-part ::= [0] | [1-9] [0-9]{0,15}
468 root ::= ("-"? integral-part) space
469 space ::= | " " | "\n"{1,2} [ \t]{0,20}
470 )"""
471 });
472
473 test({
474 .expected_status: SUCCESS,
475 .name: "string const",
476 .schema: R"""({
477 "const": "foo"
478 })""",
479 .expected_grammar: R"""(
480 root ::= "\"foo\"" space
481 space ::= | " " | "\n"{1,2} [ \t]{0,20}
482 )"""
483 });
484
485 test({
486 .expected_status: SUCCESS,
487 .name: "non-string const",
488 .schema: R"""({
489 "const": 123
490 })""",
491 .expected_grammar: R"""(
492 root ::= "123" space
493 space ::= | " " | "\n"{1,2} [ \t]{0,20}
494 )"""
495 });
496
497 test({
498 .expected_status: SUCCESS,
499 .name: "non-string enum",
500 .schema: R"""({
501 "enum": ["red", "amber", "green", null, 42, ["foo"]]
502 })""",
503 .expected_grammar: R"""(
504 root ::= ("\"red\"" | "\"amber\"" | "\"green\"" | "null" | "42" | "[\"foo\"]") space
505 space ::= | " " | "\n"{1,2} [ \t]{0,20}
506 )"""
507 });
508
509 test({
510 .expected_status: SUCCESS,
511 .name: "string array",
512 .schema: R"""({
513 "type": "array",
514 "prefixItems": { "type": "string" }
515 })""",
516 .expected_grammar: R"""(
517 char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})
518 root ::= "[" space (string ("," space string)*)? "]" space
519 space ::= | " " | "\n"{1,2} [ \t]{0,20}
520 string ::= "\"" char* "\"" space
521 )"""
522 });
523
524 test({
525 .expected_status: SUCCESS,
526 .name: "nullable string array",
527 .schema: R"""({
528 "type": ["array", "null"],
529 "prefixItems": { "type": "string" }
530 })""",
531 .expected_grammar: R"""(
532 alternative-0 ::= "[" space (string ("," space string)*)? "]" space
533 char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})
534 null ::= "null" space
535 root ::= alternative-0 | null
536 space ::= | " " | "\n"{1,2} [ \t]{0,20}
537 string ::= "\"" char* "\"" space
538 )"""
539 });
540
541 test({
542 .expected_status: SUCCESS,
543 .name: "tuple1",
544 .schema: R"""({
545 "prefixItems": [{ "type": "string" }]
546 })""",
547 .expected_grammar: R"""(
548 char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})
549 root ::= "[" space string "]" space
550 space ::= | " " | "\n"{1,2} [ \t]{0,20}
551 string ::= "\"" char* "\"" space
552 )"""
553 });
554
555 test({
556 .expected_status: SUCCESS,
557 .name: "tuple2",
558 .schema: R"""({
559 "prefixItems": [{ "type": "string" }, { "type": "number" }]
560 })""",
561 .expected_grammar: R"""(
562 char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})
563 decimal-part ::= [0-9]{1,16}
564 integral-part ::= [0] | [1-9] [0-9]{0,15}
565 number ::= ("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)? space
566 root ::= "[" space string "," space number "]" space
567 space ::= | " " | "\n"{1,2} [ \t]{0,20}
568 string ::= "\"" char* "\"" space
569 )"""
570 });
571
572 test({
573 .expected_status: SUCCESS,
574 .name: "number",
575 .schema: R"""({
576 "type": "number"
577 })""",
578 .expected_grammar: R"""(
579 decimal-part ::= [0-9]{1,16}
580 integral-part ::= [0] | [1-9] [0-9]{0,15}
581 root ::= ("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)? space
582 space ::= | " " | "\n"{1,2} [ \t]{0,20}
583 )"""
584 });
585
586 test({
587 .expected_status: SUCCESS,
588 .name: "minItems",
589 .schema: R"""({
590 "items": {
591 "type": "boolean"
592 },
593 "minItems": 2
594 })""",
595 .expected_grammar: R"""(
596 boolean ::= ("true" | "false") space
597 root ::= "[" space boolean ("," space boolean)+ "]" space
598 space ::= | " " | "\n"{1,2} [ \t]{0,20}
599 )"""
600 });
601
602 test({
603 .expected_status: SUCCESS,
604 .name: "maxItems 0",
605 .schema: R"""({
606 "items": {
607 "type": "boolean"
608 },
609 "maxItems": 0
610 })""",
611 .expected_grammar: R"""(
612 boolean ::= ("true" | "false") space
613 root ::= "[" space "]" space
614 space ::= | " " | "\n"{1,2} [ \t]{0,20}
615 )"""
616 });
617
618 test({
619 .expected_status: SUCCESS,
620 .name: "maxItems 1",
621 .schema: R"""({
622 "items": {
623 "type": "boolean"
624 },
625 "maxItems": 1
626 })""",
627 .expected_grammar: R"""(
628 boolean ::= ("true" | "false") space
629 root ::= "[" space boolean? "]" space
630 space ::= | " " | "\n"{1,2} [ \t]{0,20}
631 )"""
632 });
633
634 test({
635 .expected_status: SUCCESS,
636 .name: "maxItems 2",
637 .schema: R"""({
638 "items": {
639 "type": "boolean"
640 },
641 "maxItems": 2
642 })""",
643 .expected_grammar: R"""(
644 boolean ::= ("true" | "false") space
645 root ::= "[" space (boolean ("," space boolean)?)? "]" space
646 space ::= | " " | "\n"{1,2} [ \t]{0,20}
647 )"""
648 });
649
650 test({
651 .expected_status: SUCCESS,
652 .name: "min + maxItems",
653 .schema: R"""({
654 "items": {
655 "type": ["number", "integer"]
656 },
657 "minItems": 3,
658 "maxItems": 5
659 })""",
660 .expected_grammar: R"""(
661 decimal-part ::= [0-9]{1,16}
662 integer ::= ("-"? integral-part) space
663 integral-part ::= [0] | [1-9] [0-9]{0,15}
664 item ::= number | integer
665 number ::= ("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)? space
666 root ::= "[" space item ("," space item){2,4} "]" space
667 space ::= | " " | "\n"{1,2} [ \t]{0,20}
668 )"""
669 });
670
671 test({
672 .expected_status: SUCCESS,
673 .name: "min + max items with min + max values across zero",
674 .schema: R"""({
675 "items": {
676 "type": "integer",
677 "minimum": -12,
678 "maximum": 207
679 },
680 "minItems": 3,
681 "maxItems": 5
682 })""",
683 .expected_grammar: R"""(
684 item ::= ("-" ([0-9] | "1" [0-2]) | [0-9] | ([1-8] [0-9] | [9] [0-9]) | ([1] [0-9]{2} | [2] "0" [0-7])) space
685 root ::= "[" space item ("," space item){2,4} "]" space
686 space ::= | " " | "\n"{1,2} [ \t]{0,20}
687 )"""
688 });
689
690 test({
691 .expected_status: SUCCESS,
692 .name: "min + max items with min + max values",
693 .schema: R"""({
694 "items": {
695 "type": "integer",
696 "minimum": 12,
697 "maximum": 207
698 },
699 "minItems": 3,
700 "maxItems": 5
701 })""",
702 .expected_grammar: R"""(
703 item ::= (([1] ([2-9]) | [2-9] [0-9]) | ([1] [0-9]{2} | [2] "0" [0-7])) space
704 root ::= "[" space item ("," space item){2,4} "]" space
705 space ::= | " " | "\n"{1,2} [ \t]{0,20}
706 )"""
707 });
708
709 test({
710 .expected_status: SUCCESS,
711 .name: "simple regexp",
712 .schema: R"""({
713 "type": "string",
714 "pattern": "^abc?d*efg+(hij)?kl$"
715 })""",
716 .expected_grammar: R"""(
717 root ::= "\"" ("ab" "c"? "d"* "ef" "g"+ ("hij")? "kl") "\"" space
718 space ::= | " " | "\n"{1,2} [ \t]{0,20}
719 )"""
720 });
721
722 test({
723 .expected_status: SUCCESS,
724 .name: "regexp escapes",
725 .schema: R"""({
726 "type": "string",
727 "pattern": "^\\[\\]\\{\\}\\(\\)\\|\\+\\*\\?$"
728 })""",
729 .expected_grammar: R"""(
730 root ::= "\"" ("[]{}()|+*?") "\"" space
731 space ::= | " " | "\n"{1,2} [ \t]{0,20}
732 )"""
733 });
734
735 test({
736 .expected_status: SUCCESS,
737 .name: "regexp quote",
738 .schema: R"""({
739 "type": "string",
740 "pattern": "^\"$"
741 })""",
742 .expected_grammar: R"""(
743 root ::= "\"" ("\"") "\"" space
744 space ::= | " " | "\n"{1,2} [ \t]{0,20}
745 )"""
746 });
747
748 test({
749 .expected_status: SUCCESS,
750 .name: "regexp with top-level alternation",
751 .schema: R"""({
752 "type": "string",
753 "pattern": "^A|B|C|D$"
754 })""",
755 .expected_grammar: R"""(
756 root ::= "\"" ("A" | "B" | "C" | "D") "\"" space
757 space ::= | " " | "\n"{1,2} [ \t]{0,20}
758 )"""
759 });
760
761 test({
762 .expected_status: SUCCESS,
763 .name: "regexp",
764 .schema: R"""({
765 "type": "string",
766 "pattern": "^(\\([0-9]{1,3}\\))?[0-9]{3}-[0-9]{4} a{3,5}nd...$"
767 })""",
768 .expected_grammar: R"""(
769 dot ::= [^\x0A\x0D]
770 root ::= "\"" (("(" root-1{1,3} ")")? root-1{3,3} "-" root-1{4,4} " " "a"{3,5} "nd" dot dot dot) "\"" space
771 root-1 ::= [0-9]
772 space ::= | " " | "\n"{1,2} [ \t]{0,20}
773 )"""
774 });
775
776 test({
777 .expected_status: SUCCESS,
778 .name: "required props in original order",
779 .schema: R"""({
780 "type": "object",
781 "properties": {
782 "b": {"type": "string"},
783 "c": {"type": "string"},
784 "a": {"type": "string"}
785 },
786 "required": [
787 "a",
788 "b",
789 "c"
790 ],
791 "additionalProperties": false,
792 "definitions": {}
793 })""",
794 .expected_grammar: R"""(
795 a-kv ::= "\"a\"" space ":" space string
796 b-kv ::= "\"b\"" space ":" space string
797 c-kv ::= "\"c\"" space ":" space string
798 char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})
799 root ::= "{" space b-kv "," space c-kv "," space a-kv "}" space
800 space ::= | " " | "\n"{1,2} [ \t]{0,20}
801 string ::= "\"" char* "\"" space
802 )"""
803 });
804
805 test({
806 .expected_status: SUCCESS,
807 .name: "1 optional prop",
808 .schema: R"""({
809 "properties": {
810 "a": {
811 "type": "string"
812 }
813 },
814 "additionalProperties": false
815 })""",
816 .expected_grammar: R"""(
817 a-kv ::= "\"a\"" space ":" space string
818 char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})
819 root ::= "{" space (a-kv )? "}" space
820 space ::= | " " | "\n"{1,2} [ \t]{0,20}
821 string ::= "\"" char* "\"" space
822 )"""
823 });
824
825 test({
826 .expected_status: SUCCESS,
827 .name: "N optional props",
828 .schema: R"""({
829 "properties": {
830 "a": {"type": "string"},
831 "b": {"type": "string"},
832 "c": {"type": "string"}
833 },
834 "additionalProperties": false
835 })""",
836 .expected_grammar: R"""(
837 a-kv ::= "\"a\"" space ":" space string
838 a-rest ::= ( "," space b-kv )? b-rest
839 b-kv ::= "\"b\"" space ":" space string
840 b-rest ::= ( "," space c-kv )?
841 c-kv ::= "\"c\"" space ":" space string
842 char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})
843 root ::= "{" space (a-kv a-rest | b-kv b-rest | c-kv )? "}" space
844 space ::= | " " | "\n"{1,2} [ \t]{0,20}
845 string ::= "\"" char* "\"" space
846 )"""
847 });
848
849 test({
850 .expected_status: SUCCESS,
851 .name: "required + optional props each in original order",
852 .schema: R"""({
853 "properties": {
854 "b": {"type": "string"},
855 "a": {"type": "string"},
856 "d": {"type": "string"},
857 "c": {"type": "string"}
858 },
859 "required": ["a", "b"],
860 "additionalProperties": false
861 })""",
862 .expected_grammar: R"""(
863 a-kv ::= "\"a\"" space ":" space string
864 b-kv ::= "\"b\"" space ":" space string
865 c-kv ::= "\"c\"" space ":" space string
866 char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})
867 d-kv ::= "\"d\"" space ":" space string
868 d-rest ::= ( "," space c-kv )?
869 root ::= "{" space b-kv "," space a-kv ( "," space ( d-kv d-rest | c-kv ) )? "}" space
870 space ::= | " " | "\n"{1,2} [ \t]{0,20}
871 string ::= "\"" char* "\"" space
872 )"""
873 });
874
875 test({
876 .expected_status: SUCCESS,
877 .name: "additional props",
878 .schema: R"""({
879 "type": "object",
880 "additionalProperties": {"type": "array", "items": {"type": "number"}}
881 })""",
882 .expected_grammar: R"""(
883 additional-kv ::= string ":" space additional-value
884 additional-value ::= "[" space (number ("," space number)*)? "]" space
885 char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})
886 decimal-part ::= [0-9]{1,16}
887 integral-part ::= [0] | [1-9] [0-9]{0,15}
888 number ::= ("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)? space
889 root ::= "{" space (additional-kv ( "," space additional-kv )* )? "}" space
890 space ::= | " " | "\n"{1,2} [ \t]{0,20}
891 string ::= "\"" char* "\"" space
892 )"""
893 });
894
895 test({
896 .expected_status: SUCCESS,
897 .name: "additional props (true)",
898 .schema: R"""({
899 "type": "object",
900 "additionalProperties": true
901 })""",
902 .expected_grammar: R"""(
903 array ::= "[" space ( value ("," space value)* )? "]" space
904 boolean ::= ("true" | "false") space
905 char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})
906 decimal-part ::= [0-9]{1,16}
907 integral-part ::= [0] | [1-9] [0-9]{0,15}
908 null ::= "null" space
909 number ::= ("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)? space
910 object ::= "{" space ( string ":" space value ("," space string ":" space value)* )? "}" space
911 root ::= object
912 space ::= | " " | "\n"{1,2} [ \t]{0,20}
913 string ::= "\"" char* "\"" space
914 value ::= object | array | string | number | boolean | null
915 )"""
916 });
917
918 test({
919 .expected_status: SUCCESS,
920 .name: "additional props (implicit)",
921 .schema: R"""({
922 "type": "object"
923 })""",
924 .expected_grammar: R"""(
925 array ::= "[" space ( value ("," space value)* )? "]" space
926 boolean ::= ("true" | "false") space
927 char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})
928 decimal-part ::= [0-9]{1,16}
929 integral-part ::= [0] | [1-9] [0-9]{0,15}
930 null ::= "null" space
931 number ::= ("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)? space
932 object ::= "{" space ( string ":" space value ("," space string ":" space value)* )? "}" space
933 root ::= object
934 space ::= | " " | "\n"{1,2} [ \t]{0,20}
935 string ::= "\"" char* "\"" space
936 value ::= object | array | string | number | boolean | null
937 )"""
938 });
939
940 test({
941 .expected_status: SUCCESS,
942 .name: "empty w/o additional props",
943 .schema: R"""({
944 "type": "object",
945 "additionalProperties": false
946 })""",
947 .expected_grammar: R"""(
948 root ::= "{" space "}" space
949 space ::= | " " | "\n"{1,2} [ \t]{0,20}
950 )"""
951 });
952
953 test({
954 .expected_status: SUCCESS,
955 .name: "required + additional props",
956 .schema: R"""({
957 "type": "object",
958 "properties": {
959 "a": {"type": "number"}
960 },
961 "required": ["a"],
962 "additionalProperties": {"type": "string"}
963 })""",
964 .expected_grammar: R"""(
965 a-kv ::= "\"a\"" space ":" space number
966 additional-k ::= ["] ( [a] char+ | [^"a] char* )? ["] space
967 additional-kv ::= additional-k ":" space string
968 char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})
969 decimal-part ::= [0-9]{1,16}
970 integral-part ::= [0] | [1-9] [0-9]{0,15}
971 number ::= ("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)? space
972 root ::= "{" space a-kv ( "," space ( additional-kv ( "," space additional-kv )* ) )? "}" space
973 space ::= | " " | "\n"{1,2} [ \t]{0,20}
974 string ::= "\"" char* "\"" space
975 )"""
976 });
977
978 test({
979 .expected_status: SUCCESS,
980 .name: "optional + additional props",
981 .schema: R"""({
982 "type": "object",
983 "properties": {
984 "a": {"type": "number"}
985 },
986 "additionalProperties": {"type": "number"}
987 })""",
988 .expected_grammar: R"""(
989 a-kv ::= "\"a\"" space ":" space number
990 a-rest ::= ( "," space additional-kv )*
991 additional-k ::= ["] ( [a] char+ | [^"a] char* )? ["] space
992 additional-kv ::= additional-k ":" space number
993 char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})
994 decimal-part ::= [0-9]{1,16}
995 integral-part ::= [0] | [1-9] [0-9]{0,15}
996 number ::= ("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)? space
997 root ::= "{" space (a-kv a-rest | additional-kv ( "," space additional-kv )* )? "}" space
998 space ::= | " " | "\n"{1,2} [ \t]{0,20}
999 )"""
1000 });
1001
1002 test({
1003 .expected_status: SUCCESS,
1004 .name: "required + optional + additional props",
1005 .schema: R"""({
1006 "type": "object",
1007 "properties": {
1008 "and": {"type": "number"},
1009 "also": {"type": "number"}
1010 },
1011 "required": ["and"],
1012 "additionalProperties": {"type": "number"}
1013 })""",
1014 .expected_grammar: R"""(
1015 additional-k ::= ["] ( [a] ([l] ([s] ([o] char+ | [^"o] char*) | [^"s] char*) | [n] ([d] char+ | [^"d] char*) | [^"ln] char*) | [^"a] char* )? ["] space
1016 additional-kv ::= additional-k ":" space number
1017 also-kv ::= "\"also\"" space ":" space number
1018 also-rest ::= ( "," space additional-kv )*
1019 and-kv ::= "\"and\"" space ":" space number
1020 char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})
1021 decimal-part ::= [0-9]{1,16}
1022 integral-part ::= [0] | [1-9] [0-9]{0,15}
1023 number ::= ("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)? space
1024 root ::= "{" space and-kv ( "," space ( also-kv also-rest | additional-kv ( "," space additional-kv )* ) )? "}" space
1025 space ::= | " " | "\n"{1,2} [ \t]{0,20}
1026 )"""
1027 });
1028
1029 test({
1030 .expected_status: SUCCESS,
1031 .name: "optional props with empty name",
1032 .schema: R"""({
1033 "properties": {
1034 "": {"type": "integer"},
1035 "a": {"type": "integer"}
1036 },
1037 "additionalProperties": {"type": "integer"}
1038 })""",
1039 .expected_grammar: R"""(
1040 -kv ::= "\"\"" space ":" space root
1041 -rest ::= ( "," space a-kv )? a-rest
1042 a-kv ::= "\"a\"" space ":" space integer
1043 a-rest ::= ( "," space additional-kv )*
1044 additional-k ::= ["] ( [a] char+ | [^"a] char* ) ["] space
1045 additional-kv ::= additional-k ":" space integer
1046 char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})
1047 integer ::= ("-"? integral-part) space
1048 integral-part ::= [0] | [1-9] [0-9]{0,15}
1049 root ::= ("-"? integral-part) space
1050 root0 ::= "{" space (-kv -rest | a-kv a-rest | additional-kv ( "," space additional-kv )* )? "}" space
1051 space ::= | " " | "\n"{1,2} [ \t]{0,20}
1052 )"""
1053 });
1054
1055 test({
1056 .expected_status: SUCCESS,
1057 .name: "optional props with nested names",
1058 .schema: R"""({
1059 "properties": {
1060 "a": {"type": "integer"},
1061 "aa": {"type": "integer"}
1062 },
1063 "additionalProperties": {"type": "integer"}
1064 })""",
1065 .expected_grammar: R"""(
1066 a-kv ::= "\"a\"" space ":" space integer
1067 a-rest ::= ( "," space aa-kv )? aa-rest
1068 aa-kv ::= "\"aa\"" space ":" space integer
1069 aa-rest ::= ( "," space additional-kv )*
1070 additional-k ::= ["] ( [a] ([a] char+ | [^"a] char*) | [^"a] char* )? ["] space
1071 additional-kv ::= additional-k ":" space integer
1072 char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})
1073 integer ::= ("-"? integral-part) space
1074 integral-part ::= [0] | [1-9] [0-9]{0,15}
1075 root ::= "{" space (a-kv a-rest | aa-kv aa-rest | additional-kv ( "," space additional-kv )* )? "}" space
1076 space ::= | " " | "\n"{1,2} [ \t]{0,20}
1077 )"""
1078 });
1079
1080 test({
1081 .expected_status: SUCCESS,
1082 .name: "optional props with common prefix",
1083 .schema: R"""({
1084 "properties": {
1085 "ab": {"type": "integer"},
1086 "ac": {"type": "integer"}
1087 },
1088 "additionalProperties": {"type": "integer"}
1089 })""",
1090 .expected_grammar: R"""(
1091 ab-kv ::= "\"ab\"" space ":" space integer
1092 ab-rest ::= ( "," space ac-kv )? ac-rest
1093 ac-kv ::= "\"ac\"" space ":" space integer
1094 ac-rest ::= ( "," space additional-kv )*
1095 additional-k ::= ["] ( [a] ([b] char+ | [c] char+ | [^"bc] char*) | [^"a] char* )? ["] space
1096 additional-kv ::= additional-k ":" space integer
1097 char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})
1098 integer ::= ("-"? integral-part) space
1099 integral-part ::= [0] | [1-9] [0-9]{0,15}
1100 root ::= "{" space (ab-kv ab-rest | ac-kv ac-rest | additional-kv ( "," space additional-kv )* )? "}" space
1101 space ::= | " " | "\n"{1,2} [ \t]{0,20}
1102 )"""
1103 });
1104
1105 test({
1106 .expected_status: SUCCESS,
1107 .name: "top-level $ref",
1108 .schema: R"""({
1109 "$ref": "#/definitions/foo",
1110 "definitions": {
1111 "foo": {
1112 "type": "object",
1113 "properties": {
1114 "a": {
1115 "type": "string"
1116 }
1117 },
1118 "required": [
1119 "a"
1120 ],
1121 "additionalProperties": false
1122 }
1123 }
1124 })""",
1125 .expected_grammar: R"""(
1126 char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})
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 .expected_status: SUCCESS,
1137 .name: "anyOf",
1138 .schema: R"""({
1139 "anyOf": [
1140 {"$ref": "#/definitions/foo"},
1141 {"$ref": "#/definitions/bar"}
1142 ],
1143 "definitions": {
1144 "foo": {
1145 "properties": {"a": {"type": "number"}}
1146 },
1147 "bar": {
1148 "properties": {"b": {"type": "number"}}
1149 }
1150 },
1151 "type": "object"
1152 })""",
1153 .expected_grammar: R"""(
1154 alternative-0 ::= ref-definitions-foo
1155 alternative-1 ::= ref-definitions-bar
1156 decimal-part ::= [0-9]{1,16}
1157 integral-part ::= [0] | [1-9] [0-9]{0,15}
1158 number ::= ("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)? space
1159 ref-definitions-bar ::= "{" space (ref-definitions-bar-b-kv )? "}" space
1160 ref-definitions-bar-b-kv ::= "\"b\"" space ":" space number
1161 ref-definitions-foo ::= "{" space (ref-definitions-foo-a-kv )? "}" space
1162 ref-definitions-foo-a-kv ::= "\"a\"" space ":" space number
1163 root ::= alternative-0 | alternative-1
1164 space ::= | " " | "\n"{1,2} [ \t]{0,20}
1165 )"""
1166 });
1167
1168 test({
1169 .expected_status: SUCCESS,
1170 .name: "anyOf $ref",
1171 .schema: R"""({
1172 "properties": {
1173 "a": {
1174 "anyOf": [
1175 {"type": "string"},
1176 {"type": "number"}
1177 ]
1178 },
1179 "b": {
1180 "anyOf": [
1181 {"$ref": "#/properties/a/anyOf/0"},
1182 {"type": "boolean"}
1183 ]
1184 }
1185 },
1186 "type": "object"
1187 })""",
1188 .expected_grammar: R"""(
1189 a ::= string | number
1190 a-kv ::= "\"a\"" space ":" space a
1191 a-rest ::= ( "," space b-kv )?
1192 b ::= b-0 | boolean
1193 b-0 ::= string
1194 b-kv ::= "\"b\"" space ":" space b
1195 boolean ::= ("true" | "false") space
1196 char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})
1197 decimal-part ::= [0-9]{1,16}
1198 integral-part ::= [0] | [1-9] [0-9]{0,15}
1199 number ::= ("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)? space
1200 root ::= "{" space (a-kv a-rest | b-kv )? "}" space
1201 space ::= | " " | "\n"{1,2} [ \t]{0,20}
1202 string ::= "\"" char* "\"" space
1203 )"""
1204 });
1205
1206 test({
1207 .expected_status: SUCCESS,
1208 .name: "mix of allOf, anyOf and $ref (similar to https://json.schemastore.org/tsconfig.json)",
1209 .schema: R"""({
1210 "allOf": [
1211 {"$ref": "#/definitions/foo"},
1212 {"$ref": "#/definitions/bar"},
1213 {
1214 "anyOf": [
1215 {"$ref": "#/definitions/baz"},
1216 {"$ref": "#/definitions/bam"}
1217 ]
1218 }
1219 ],
1220 "definitions": {
1221 "foo": {
1222 "properties": {"a": {"type": "number"}}
1223 },
1224 "bar": {
1225 "properties": {"b": {"type": "number"}}
1226 },
1227 "bam": {
1228 "properties": {"c": {"type": "number"}}
1229 },
1230 "baz": {
1231 "properties": {"d": {"type": "number"}}
1232 }
1233 },
1234 "type": "object"
1235 })""",
1236 .expected_grammar: R"""(
1237 a-kv ::= "\"a\"" space ":" space number
1238 b-kv ::= "\"b\"" space ":" space number
1239 c-kv ::= "\"c\"" space ":" space number
1240 d-kv ::= "\"d\"" space ":" space number
1241 d-rest ::= ( "," space c-kv )?
1242 decimal-part ::= [0-9]{1,16}
1243 integral-part ::= [0] | [1-9] [0-9]{0,15}
1244 number ::= ("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)? space
1245 root ::= "{" space a-kv "," space b-kv ( "," space ( d-kv d-rest | c-kv ) )? "}" space
1246 space ::= | " " | "\n"{1,2} [ \t]{0,20}
1247 )"""
1248 });
1249
1250 test({
1251 .expected_status: SUCCESS,
1252 .name: "allOf with enum schema",
1253 .schema: R"""({
1254 "allOf": [
1255 {"$ref": "#/definitions/foo"}
1256 ],
1257 "definitions": {
1258 "foo": {
1259 "type": "string",
1260 "enum": ["a", "b"]
1261 }
1262 }
1263 })""",
1264 .expected_grammar: R"""(
1265 root ::= ("\"a\"" | "\"b\"") space
1266 space ::= | " " | "\n"{1,2} [ \t]{0,20}
1267 )"""
1268 });
1269
1270 test({
1271 .expected_status: SUCCESS,
1272 .name: "allOf with multiple enum schemas",
1273 .schema: R"""({
1274 "allOf": [
1275 {"$ref": "#/definitions/foo"},
1276 {"$ref": "#/definitions/bar"}
1277 ],
1278 "definitions": {
1279 "foo": {
1280 "type": "string",
1281 "enum": ["a", "b", "c"]
1282 },
1283 "bar": {
1284 "type": "string",
1285 "enum": ["b", "c", "d"]
1286 }
1287 }
1288 })""",
1289 .expected_grammar: R"""(
1290 root ::= ("\"b\"" | "\"c\"") space
1291 space ::= | " " | "\n"{1,2} [ \t]{0,20}
1292 )"""
1293 });
1294
1295 test({
1296 .expected_status: SUCCESS,
1297 .name: "conflicting names",
1298 .schema: R"""({
1299 "type": "object",
1300 "properties": {
1301 "number": {
1302 "type": "object",
1303 "properties": {
1304 "number": {
1305 "type": "object",
1306 "properties": {
1307 "root": {
1308 "type": "number"
1309 }
1310 },
1311 "required": [
1312 "root"
1313 ],
1314 "additionalProperties": false
1315 }
1316 },
1317 "required": [
1318 "number"
1319 ],
1320 "additionalProperties": false
1321 }
1322 },
1323 "required": [
1324 "number"
1325 ],
1326 "additionalProperties": false,
1327 "definitions": {}
1328 })""",
1329 .expected_grammar: R"""(
1330 decimal-part ::= [0-9]{1,16}
1331 integral-part ::= [0] | [1-9] [0-9]{0,15}
1332 number ::= ("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)? space
1333 number- ::= "{" space number-number-kv "}" space
1334 number-kv ::= "\"number\"" space ":" space number-
1335 number-number ::= "{" space number-number-root-kv "}" space
1336 number-number-kv ::= "\"number\"" space ":" space number-number
1337 number-number-root-kv ::= "\"root\"" space ":" space number
1338 root ::= "{" space number-kv "}" space
1339 space ::= | " " | "\n"{1,2} [ \t]{0,20}
1340 )"""
1341 });
1342}
1343
1344int main() {
1345 fprintf(stderr, format: "LLAMA_NODE_AVAILABLE = %s\n", getenv(name: "LLAMA_NODE_AVAILABLE") ? "true" : "false");
1346 fprintf(stderr, format: "LLAMA_PYTHON_AVAILABLE = %s\n", getenv(name: "LLAMA_PYTHON_AVAILABLE") ? "true" : "false");
1347
1348 test_all(lang: "C++", runner: [](const TestCase & tc) {
1349 try {
1350 tc.verify(actual_grammar: json_schema_to_grammar(schema: nlohmann::ordered_json::parse(i: tc.schema), force_gbnf: true));
1351 tc.verify_status(status: SUCCESS);
1352 } catch (const std::runtime_error & ex) {
1353 fprintf(stderr, format: "Error: %s\n", ex.what());
1354 tc.verify_status(status: FAILURE);
1355 }
1356 });
1357
1358 if (getenv(name: "LLAMA_SKIP_TESTS_SLOW_ON_EMULATOR")) {
1359 fprintf(stderr, format: "\033[33mWARNING: Skipping slow tests on emulator.\n\033[0m");
1360 } else {
1361 if (getenv(name: "LLAMA_PYTHON_AVAILABLE") || (std::system(command: "python -c \"import sys; exit(1) if sys.version_info < (3, 8) else print('Python version is sufficient')\"") == 0)) {
1362 test_all(lang: "Python", runner: [](const TestCase & tc) {
1363 write(file: "test-json-schema-input.tmp", content: tc.schema);
1364 tc.verify_status(status: std::system(
1365 command: "python ./examples/json_schema_to_grammar.py test-json-schema-input.tmp > test-grammar-output.tmp") == 0 ? SUCCESS : FAILURE);
1366 tc.verify(actual_grammar: read(file: "test-grammar-output.tmp"));
1367 });
1368 } else {
1369 fprintf(stderr, format: "\033[33mWARNING: Python not found (min version required is 3.8), skipping Python JSON schema -> grammar tests.\n\033[0m");
1370 }
1371
1372 if (getenv(name: "LLAMA_NODE_AVAILABLE") || (std::system(command: "node --version") == 0)) {
1373 test_all(lang: "JavaScript", runner: [](const TestCase & tc) {
1374 write(file: "test-json-schema-input.tmp", content: tc.schema);
1375 tc.verify_status(status: std::system(
1376 command: "node ./tests/run-json-schema-to-grammar.mjs test-json-schema-input.tmp > test-grammar-output.tmp") == 0 ? SUCCESS : FAILURE);
1377 tc.verify(actual_grammar: read(file: "test-grammar-output.tmp"));
1378 });
1379 } else {
1380 fprintf(stderr, format: "\033[33mWARNING: Node not found, skipping JavaScript JSON schema -> grammar tests.\n\033[0m");
1381 }
1382 }
1383
1384 test_all(lang: "Check Expectations Validity", runner: [](const TestCase & tc) {
1385 if (tc.expected_status == SUCCESS) {
1386 tc.verify_expectation_parseable();
1387 }
1388 });
1389}
1390