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(0,s.find_first_not_of(" \n\r\t"));
  19    s.erase(s.find_last_not_of(" \n\r\t")+1);
  20    return std::regex_replace(s, std::regex("(^|\n)[ \t]+"), "$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, "#\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(actual_grammar) != trim(expected_grammar)) {
  39        _print_failure_header();
  40        fprintf(stderr, "# 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(expected_grammar.c_str());
  48            if (state.symbol_ids.find("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, "# 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, "# EXPECTED STATUS: %s\n", expected_status == SUCCESS ? "SUCCESS" : "FAILURE");
  61            fprintf(stderr, "# 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(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, "#\n# Testing JSON schema conversion (%s)\n#\n", lang.c_str());
  82    auto test = [&](const TestCase & tc) {
  83        fprintf(stderr, "- %s%s\n", tc.name.c_str(), tc.expected_status == FAILURE ? " (failure expected)" : "");
  84        runner(tc);
  85    };
  86
  87    test({
  88        SUCCESS,
  89        "min 0",
  90        R"""({
  91            "type": "integer",
  92            "minimum": 0
  93        })""",
  94        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        SUCCESS,
 102        "min 1",
 103        R"""({
 104            "type": "integer",
 105            "minimum": 1
 106        })""",
 107        R"""(
 108            root ::= ([1-9] [0-9]{0,15}) space
 109            space ::= | " " | "\n"{1,2} [ \t]{0,20}
 110        )"""
 111    });
 112
 113    test({
 114        SUCCESS,
 115        "min 3",
 116        R"""({
 117            "type": "integer",
 118            "minimum": 3
 119        })""",
 120        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        SUCCESS,
 128        "min 9",
 129        R"""({
 130            "type": "integer",
 131            "minimum": 9
 132        })""",
 133        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        SUCCESS,
 141        "min 10",
 142        R"""({
 143            "type": "integer",
 144            "minimum": 10
 145        })""",
 146        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        SUCCESS,
 154        "min 25",
 155        R"""({
 156            "type": "integer",
 157            "minimum": 25
 158        })""",
 159        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        SUCCESS,
 167        "max 30",
 168        R"""({
 169            "type": "integer",
 170            "maximum": 30
 171        })""",
 172        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        SUCCESS,
 180        "min -5",
 181        R"""({
 182            "type": "integer",
 183            "minimum": -5
 184        })""",
 185        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        SUCCESS,
 193        "min -123",
 194        R"""({
 195            "type": "integer",
 196            "minimum": -123
 197        })""",
 198        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        SUCCESS,
 206        "max -5",
 207        R"""({
 208            "type": "integer",
 209            "maximum": -5
 210        })""",
 211        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        SUCCESS,
 219        "max 1",
 220        R"""({
 221            "type": "integer",
 222            "maximum": 1
 223        })""",
 224        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        SUCCESS,
 232        "max 100",
 233        R"""({
 234            "type": "integer",
 235            "maximum": 100
 236        })""",
 237        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        SUCCESS,
 245        "min 0 max 23",
 246        R"""({
 247            "type": "integer",
 248            "minimum": 0,
 249            "maximum": 23
 250        })""",
 251        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        SUCCESS,
 259        "min 15 max 300",
 260        R"""({
 261            "type": "integer",
 262            "minimum": 15,
 263            "maximum": 300
 264        })""",
 265        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        SUCCESS,
 273        "min 5 max 30",
 274        R"""({
 275            "type": "integer",
 276            "minimum": 5,
 277            "maximum": 30
 278        })""",
 279        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        SUCCESS,
 287        "min -123 max 42",
 288        R"""({
 289            "type": "integer",
 290            "minimum": -123,
 291            "maximum": 42
 292        })""",
 293        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        SUCCESS,
 301        "min -10 max 10",
 302        R"""({
 303            "type": "integer",
 304            "minimum": -10,
 305            "maximum": 10
 306        })""",
 307        R"""(
 308            root ::= ("-" ([0-9] | "10") | [0-9] | "10") space
 309            space ::= | " " | "\n"{1,2} [ \t]{0,20}
 310        )"""
 311    });
 312
 313    test({
 314        FAILURE,
 315        "unknown type",
 316        R"""({
 317            "type": "kaboom"
 318        })""",
 319        ""
 320    });
 321
 322    test({
 323        FAILURE,
 324        "invalid type",
 325        R"""({
 326            "type": 123
 327        })""",
 328        ""
 329    });
 330
 331    test({
 332        SUCCESS,
 333        "empty schema (object)",
 334        "{}",
 335        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        SUCCESS,
 353        "exotic formats",
 354        R"""({
 355            "items": [
 356                { "format": "date" },
 357                { "format": "uuid" },
 358                { "format": "time" },
 359                { "format": "date-time" }
 360            ]
 361        })""",
 362        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        SUCCESS,
 380        "string",
 381        R"""({
 382            "type": "string"
 383        })""",
 384        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        SUCCESS,
 393        "string w/ min length 1",
 394        R"""({
 395            "type": "string",
 396            "minLength": 1
 397        })""",
 398        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        SUCCESS,
 407        "string w/ min length 3",
 408        R"""({
 409            "type": "string",
 410            "minLength": 3
 411        })""",
 412        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        SUCCESS,
 421        "string w/ max length",
 422        R"""({
 423            "type": "string",
 424            "maxLength": 3
 425        })""",
 426        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        SUCCESS,
 435        "string w/ min & max length",
 436        R"""({
 437            "type": "string",
 438            "minLength": 1,
 439            "maxLength": 4
 440        })""",
 441        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        SUCCESS,
 450        "boolean",
 451        R"""({
 452            "type": "boolean"
 453        })""",
 454        R"""(
 455            root ::= ("true" | "false") space
 456            space ::= | " " | "\n"{1,2} [ \t]{0,20}
 457        )"""
 458    });
 459
 460    test({
 461        SUCCESS,
 462        "integer",
 463        R"""({
 464            "type": "integer"
 465        })""",
 466        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        SUCCESS,
 475        "string const",
 476        R"""({
 477            "const": "foo"
 478        })""",
 479        R"""(
 480            root ::= "\"foo\"" space
 481            space ::= | " " | "\n"{1,2} [ \t]{0,20}
 482        )"""
 483    });
 484
 485    test({
 486        SUCCESS,
 487        "non-string const",
 488        R"""({
 489            "const": 123
 490        })""",
 491        R"""(
 492            root ::= "123" space
 493            space ::= | " " | "\n"{1,2} [ \t]{0,20}
 494        )"""
 495    });
 496
 497    test({
 498        SUCCESS,
 499        "non-string enum",
 500        R"""({
 501            "enum": ["red", "amber", "green", null, 42, ["foo"]]
 502        })""",
 503        R"""(
 504            root ::= ("\"red\"" | "\"amber\"" | "\"green\"" | "null" | "42" | "[\"foo\"]") space
 505            space ::= | " " | "\n"{1,2} [ \t]{0,20}
 506        )"""
 507    });
 508
 509    test({
 510        SUCCESS,
 511        "string array",
 512        R"""({
 513            "type": "array",
 514            "prefixItems": { "type": "string" }
 515        })""",
 516        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        SUCCESS,
 526        "nullable string array",
 527        R"""({
 528            "type": ["array", "null"],
 529            "prefixItems": { "type": "string" }
 530        })""",
 531        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        SUCCESS,
 543        "tuple1",
 544        R"""({
 545            "prefixItems": [{ "type": "string" }]
 546        })""",
 547        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        SUCCESS,
 557        "tuple2",
 558        R"""({
 559            "prefixItems": [{ "type": "string" }, { "type": "number" }]
 560        })""",
 561        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        SUCCESS,
 574        "number",
 575        R"""({
 576            "type": "number"
 577        })""",
 578        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        SUCCESS,
 588        "minItems",
 589        R"""({
 590            "items": {
 591                "type": "boolean"
 592            },
 593            "minItems": 2
 594        })""",
 595        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        SUCCESS,
 604        "maxItems 0",
 605        R"""({
 606            "items": {
 607                "type": "boolean"
 608            },
 609            "maxItems": 0
 610        })""",
 611        R"""(
 612            boolean ::= ("true" | "false") space
 613            root ::= "[" space  "]" space
 614            space ::= | " " | "\n"{1,2} [ \t]{0,20}
 615        )"""
 616    });
 617
 618    test({
 619        SUCCESS,
 620        "maxItems 1",
 621        R"""({
 622            "items": {
 623                "type": "boolean"
 624            },
 625            "maxItems": 1
 626        })""",
 627        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        SUCCESS,
 636        "maxItems 2",
 637        R"""({
 638            "items": {
 639                "type": "boolean"
 640            },
 641            "maxItems": 2
 642        })""",
 643        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        SUCCESS,
 652        "min + maxItems",
 653        R"""({
 654            "items": {
 655                "type": ["number", "integer"]
 656            },
 657            "minItems": 3,
 658            "maxItems": 5
 659        })""",
 660        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        SUCCESS,
 673        "min + max items with min + max values across zero",
 674        R"""({
 675            "items": {
 676                "type": "integer",
 677                "minimum": -12,
 678                "maximum": 207
 679            },
 680            "minItems": 3,
 681            "maxItems": 5
 682        })""",
 683        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        SUCCESS,
 692        "min + max items with min + max values",
 693        R"""({
 694            "items": {
 695                "type": "integer",
 696                "minimum": 12,
 697                "maximum": 207
 698            },
 699            "minItems": 3,
 700            "maxItems": 5
 701        })""",
 702        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        SUCCESS,
 711        "simple regexp",
 712        R"""({
 713            "type": "string",
 714            "pattern": "^abc?d*efg+(hij)?kl$"
 715        })""",
 716        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        SUCCESS,
 724        "regexp escapes",
 725        R"""({
 726            "type": "string",
 727            "pattern": "^\\[\\]\\{\\}\\(\\)\\|\\+\\*\\?$"
 728        })""",
 729        R"""(
 730            root ::= "\"" ("[]{}()|+*?") "\"" space
 731            space ::= | " " | "\n"{1,2} [ \t]{0,20}
 732        )"""
 733    });
 734
 735    test({
 736        SUCCESS,
 737        "regexp quote",
 738        R"""({
 739            "type": "string",
 740            "pattern": "^\"$"
 741        })""",
 742        R"""(
 743            root ::= "\"" ("\"") "\"" space
 744            space ::= | " " | "\n"{1,2} [ \t]{0,20}
 745        )"""
 746    });
 747
 748    test({
 749        SUCCESS,
 750        "regexp with top-level alternation",
 751        R"""({
 752            "type": "string",
 753            "pattern": "^A|B|C|D$"
 754        })""",
 755        R"""(
 756            root ::= "\"" ("A" | "B" | "C" | "D") "\"" space
 757            space ::= | " " | "\n"{1,2} [ \t]{0,20}
 758        )"""
 759    });
 760
 761    test({
 762        SUCCESS,
 763        "regexp",
 764        R"""({
 765            "type": "string",
 766            "pattern": "^(\\([0-9]{1,3}\\))?[0-9]{3}-[0-9]{4} a{3,5}nd...$"
 767        })""",
 768        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        SUCCESS,
 778        "required props in original order",
 779        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        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        SUCCESS,
 807        "1 optional prop",
 808        R"""({
 809            "properties": {
 810                "a": {
 811                "type": "string"
 812                }
 813            },
 814            "additionalProperties": false
 815        })""",
 816        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        SUCCESS,
 827        "N optional props",
 828        R"""({
 829            "properties": {
 830                "a": {"type": "string"},
 831                "b": {"type": "string"},
 832                "c": {"type": "string"}
 833            },
 834            "additionalProperties": false
 835        })""",
 836        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        SUCCESS,
 851        "required + optional props each in original order",
 852        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        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        SUCCESS,
 877        "additional props",
 878        R"""({
 879            "type": "object",
 880            "additionalProperties": {"type": "array", "items": {"type": "number"}}
 881        })""",
 882        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        SUCCESS,
 897        "additional props (true)",
 898        R"""({
 899            "type": "object",
 900            "additionalProperties": true
 901        })""",
 902        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        SUCCESS,
 920        "additional props (implicit)",
 921        R"""({
 922            "type": "object"
 923        })""",
 924        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        SUCCESS,
 942        "empty w/o additional props",
 943        R"""({
 944            "type": "object",
 945            "additionalProperties": false
 946        })""",
 947        R"""(
 948            root ::= "{" space  "}" space
 949            space ::= | " " | "\n"{1,2} [ \t]{0,20}
 950        )"""
 951    });
 952
 953    test({
 954        SUCCESS,
 955        "required + additional props",
 956        R"""({
 957            "type": "object",
 958            "properties": {
 959                "a": {"type": "number"}
 960            },
 961            "required": ["a"],
 962            "additionalProperties": {"type": "string"}
 963        })""",
 964        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        SUCCESS,
 980        "optional + additional props",
 981        R"""({
 982            "type": "object",
 983            "properties": {
 984                "a": {"type": "number"}
 985            },
 986            "additionalProperties": {"type": "number"}
 987        })""",
 988        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        SUCCESS,
1004        "required + optional + additional props",
1005        R"""({
1006            "type": "object",
1007            "properties": {
1008                "and": {"type": "number"},
1009                "also": {"type": "number"}
1010            },
1011            "required": ["and"],
1012            "additionalProperties": {"type": "number"}
1013        })""",
1014        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        SUCCESS,
1031        "optional props with empty name",
1032        R"""({
1033            "properties": {
1034                "": {"type": "integer"},
1035                "a": {"type": "integer"}
1036            },
1037            "additionalProperties": {"type": "integer"}
1038        })""",
1039        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        SUCCESS,
1057        "optional props with nested names",
1058        R"""({
1059            "properties": {
1060                "a": {"type": "integer"},
1061                "aa": {"type": "integer"}
1062            },
1063            "additionalProperties": {"type": "integer"}
1064        })""",
1065        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        SUCCESS,
1082        "optional props with common prefix",
1083        R"""({
1084            "properties": {
1085                "ab": {"type": "integer"},
1086                "ac": {"type": "integer"}
1087            },
1088            "additionalProperties": {"type": "integer"}
1089        })""",
1090        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        SUCCESS,
1107        "top-level $ref",
1108        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        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        SUCCESS,
1137        "anyOf",
1138        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        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        SUCCESS,
1170        "anyOf $ref",
1171        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        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        SUCCESS,
1208        "mix of allOf, anyOf and $ref (similar to https://json.schemastore.org/tsconfig.json)",
1209        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        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        SUCCESS,
1252        "allOf with enum schema",
1253        R"""({
1254            "allOf": [
1255                {"$ref": "#/definitions/foo"}
1256            ],
1257            "definitions": {
1258                "foo": {
1259                    "type": "string",
1260                    "enum": ["a", "b"]
1261                }
1262            }
1263        })""",
1264        R"""(
1265            root ::= ("\"a\"" | "\"b\"") space
1266            space ::= | " " | "\n"{1,2} [ \t]{0,20}
1267        )"""
1268    });
1269
1270    test({
1271        SUCCESS,
1272        "allOf with multiple enum schemas",
1273        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        R"""(
1290            root ::= ("\"b\"" | "\"c\"") space
1291            space ::= | " " | "\n"{1,2} [ \t]{0,20}
1292        )"""
1293    });
1294
1295    test({
1296        SUCCESS,
1297        "conflicting names",
1298        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        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    test({
1344        SUCCESS,
1345        "literal string with escapes",
1346        R"""({
1347            "properties": {
1348                "code": {
1349                    "const": " \r \n \" \\ ",
1350                    "description": "Generated code",
1351                    "title": "Code",
1352                    "type": "string"
1353                }
1354            },
1355            "required": [
1356                "code"
1357            ],
1358            "title": "DecoderResponse",
1359            "type": "object"
1360        })""",
1361        R"""(
1362            code ::= "\" \\r \\n \\\" \\\\ \"" space
1363            code-kv ::= "\"code\"" space ":" space code
1364            root ::= "{" space code-kv "}" space
1365            space ::= | " " | "\n"{1,2} [ \t]{0,20}
1366        )"""
1367    });
1368}
1369
1370static void test_resolves_to_string() {
1371    fprintf(stderr, "#\n# Testing resolves_to_string\n#\n");
1372
1373    auto test = [](const std::string & name, const std::string & schema_str, bool expected) {
1374        fprintf(stderr, "- %s\n", name.c_str());
1375        common_schema_info info;
1376        auto schema = nlohmann::ordered_json::parse(schema_str);
1377        info.resolve_refs(schema);
1378        bool result = info.resolves_to_string(schema);
1379        if (result != expected) {
1380            fprintf(stderr, "#\n# Test '%s' failed.\n#\n", name.c_str());
1381            fprintf(stderr, "Schema: %s\n", schema_str.c_str());
1382            fprintf(stderr, "Expected: %s, Got: %s\n", expected ? "true" : "false", result ? "true" : "false");
1383            assert(false);
1384        }
1385    };
1386
1387    // Basic type checks
1388    test("type string", R"({"type": "string"})", true);
1389    test("type integer", R"({"type": "integer"})", false);
1390    test("type number", R"({"type": "number"})", false);
1391    test("type boolean", R"({"type": "boolean"})", false);
1392    test("type object", R"({"type": "object"})", false);
1393    test("type array", R"({"type": "array"})", false);
1394
1395    // Type array (nullable string)
1396    test("type array with string", R"({"type": ["string", "null"]})", true);
1397    test("type array without string", R"({"type": ["integer", "null"]})", false);
1398
1399    // String-specific keywords
1400    test("minLength implies string", R"({"minLength": 1})", true);
1401    test("maxLength implies string", R"({"maxLength": 10})", true);
1402    test("pattern implies string", R"({"pattern": "^[a-z]+$"})", true);
1403
1404    // Format
1405    test("format date", R"({"format": "date"})", true);
1406    test("format uuid", R"({"format": "uuid"})", true);
1407    test("format email", R"({"format": "email"})", true);
1408
1409    // Const
1410    test("const string", R"({"const": "hello"})", true);
1411    test("const number", R"({"const": 123})", false);
1412
1413    // Enum
1414    test("enum with strings", R"({"enum": ["a", "b", "c"]})", true);
1415    test("enum with numbers", R"({"enum": [1, 2, 3]})", false);
1416    test("enum mixed with string", R"({"enum": [1, "a", null]})", true);
1417
1418    // anyOf
1419    test("anyOf with string", R"({"anyOf": [{"type": "string"}, {"type": "integer"}]})", true);
1420    test("anyOf without string", R"({"anyOf": [{"type": "integer"}, {"type": "boolean"}]})", false);
1421
1422    // oneOf
1423    test("oneOf with string", R"({"oneOf": [{"type": "string"}, {"type": "number"}]})", true);
1424    test("oneOf without string", R"({"oneOf": [{"type": "object"}, {"type": "array"}]})", false);
1425
1426    // allOf - all must be strings
1427    test("allOf all strings", R"({"allOf": [{"type": "string"}, {"minLength": 1}]})", true);
1428    test("allOf mixed types", R"({"allOf": [{"type": "string"}, {"type": "integer"}]})", false);
1429
1430    // $ref
1431    test("$ref to string",
1432        R"({"$ref": "#/$defs/str", "$defs": {"str": {"type": "string"}}})", true);
1433    test("$ref to integer",
1434        R"({"$ref": "#/$defs/num", "$defs": {"num": {"type": "integer"}}})", false);
1435
1436    // Nested
1437    test("nested anyOf with string",
1438        R"({"anyOf": [{"anyOf": [{"type": "integer"}, {"type": "string"}]}, {"type": "boolean"}]})", true);
1439
1440    fprintf(stderr, "All resolves_to_string tests passed!\n");
1441}
1442
1443int main() {
1444    fprintf(stderr, "LLAMA_NODE_AVAILABLE = %s\n", getenv("LLAMA_NODE_AVAILABLE") ? "true" : "false");
1445    fprintf(stderr, "LLAMA_PYTHON_AVAILABLE = %s\n", getenv("LLAMA_PYTHON_AVAILABLE") ? "true" : "false");
1446
1447    test_resolves_to_string();
1448
1449    test_all("C++", [](const TestCase & tc) {
1450        try {
1451            tc.verify(json_schema_to_grammar(nlohmann::ordered_json::parse(tc.schema), true));
1452            tc.verify_status(SUCCESS);
1453        } catch (const std::invalid_argument & ex) {
1454            fprintf(stderr, "Error: %s\n", ex.what());
1455            tc.verify_status(FAILURE);
1456        }
1457    });
1458
1459    if (getenv("LLAMA_SKIP_TESTS_SLOW_ON_EMULATOR")) {
1460        fprintf(stderr, "\033[33mWARNING: Skipping slow tests on emulator.\n\033[0m");
1461    } else {
1462        if (getenv("LLAMA_PYTHON_AVAILABLE") || (std::system("python -c \"import sys; exit(1) if sys.version_info < (3, 8) else print('Python version is sufficient')\"") == 0)) {
1463            test_all("Python", [](const TestCase & tc) {
1464                write("test-json-schema-input.tmp", tc.schema);
1465                tc.verify_status(std::system(
1466                    "python ./examples/json_schema_to_grammar.py test-json-schema-input.tmp > test-grammar-output.tmp") == 0 ? SUCCESS : FAILURE);
1467                tc.verify(read("test-grammar-output.tmp"));
1468            });
1469        } else {
1470            fprintf(stderr, "\033[33mWARNING: Python not found (min version required is 3.8), skipping Python JSON schema -> grammar tests.\n\033[0m");
1471        }
1472
1473        if (getenv("LLAMA_NODE_AVAILABLE") || (std::system("node --version") == 0)) {
1474            test_all("JavaScript", [](const TestCase & tc) {
1475                write("test-json-schema-input.tmp", tc.schema);
1476                tc.verify_status(std::system(
1477                    "node ./tests/run-json-schema-to-grammar.mjs test-json-schema-input.tmp > test-grammar-output.tmp") == 0 ? SUCCESS : FAILURE);
1478                tc.verify(read("test-grammar-output.tmp"));
1479            });
1480        } else {
1481            fprintf(stderr, "\033[33mWARNING: Node not found, skipping JavaScript JSON schema -> grammar tests.\n\033[0m");
1482        }
1483    }
1484
1485    test_all("Check Expectations Validity", [](const TestCase & tc) {
1486        if (tc.expected_status == SUCCESS) {
1487            tc.verify_expectation_parseable();
1488        }
1489    });
1490}