#ifdef NDEBUG #undef NDEBUG #endif #include "json-schema-to-grammar.h" #include "../src/llama-grammar.h" #include "json.h" #include #include static std::string trim(const std::string & source) { std::string s(source); s.erase(0,s.find_first_not_of(" \n\r\t")); s.erase(s.find_last_not_of(" \n\r\t")+1); return std::regex_replace(s, std::regex("(^|\n)[ \t]+"), "$1"); } enum TestCaseStatus { SUCCESS, FAILURE }; struct TestCase { TestCaseStatus expected_status; std::string name; std::string schema; std::string expected_grammar; void _print_failure_header() const { fprintf(stderr, "#\n# Test '%s' failed.\n#\n%s\n", name.c_str(), schema.c_str()); } void verify(const std::string & actual_grammar) const { if (trim(actual_grammar) != trim(expected_grammar)) { _print_failure_header(); fprintf(stderr, "# EXPECTED:\n%s\n# ACTUAL:\n%s\n", expected_grammar.c_str(), actual_grammar.c_str()); assert(false); } } void verify_expectation_parseable() const { try { llama_grammar_parser state; state.parse(expected_grammar.c_str()); if (state.symbol_ids.find("root") == state.symbol_ids.end()) { throw std::runtime_error("Grammar failed to parse:\n" + expected_grammar); } } catch (const std::runtime_error & ex) { _print_failure_header(); fprintf(stderr, "# GRAMMAR ERROR: %s\n", ex.what()); assert(false); } } void verify_status(TestCaseStatus status) const { if (status != expected_status) { _print_failure_header(); fprintf(stderr, "# EXPECTED STATUS: %s\n", expected_status == SUCCESS ? "SUCCESS" : "FAILURE"); fprintf(stderr, "# ACTUAL STATUS: %s\n", status == SUCCESS ? "SUCCESS" : "FAILURE"); assert(false); } } }; static void test_all(const std::string & title, std::function runner) { fprintf(stderr, "#\n# %s\n#\n", title.c_str()); auto test = [&](const TestCase & tc) { fprintf(stderr, "- %s%s\n", tc.name.c_str(), tc.expected_status == FAILURE ? " (failure expected)" : ""); runner(tc); }; test({ SUCCESS, "min 0", R"""({ "type": "integer", "minimum": 0 })""", R"""( root ::= ([0] | [1-9] [0-9]{0,15}) space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "min 1", R"""({ "type": "integer", "minimum": 1 })""", R"""( root ::= ([1-9] [0-9]{0,15}) space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "min 3", R"""({ "type": "integer", "minimum": 3 })""", R"""( root ::= ([1-2] [0-9]{1,15} | [3-9] [0-9]{0,15}) space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "min 9", R"""({ "type": "integer", "minimum": 9 })""", R"""( root ::= ([1-8] [0-9]{1,15} | [9] [0-9]{0,15}) space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "min 10", R"""({ "type": "integer", "minimum": 10 })""", R"""( root ::= ([1] ([0-9]{1,15}) | [2-9] [0-9]{1,15}) space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "min 25", R"""({ "type": "integer", "minimum": 25 })""", R"""( 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 ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "max 30", R"""({ "type": "integer", "maximum": 30 })""", R"""( root ::= ("-" [1-9] [0-9]{0,15} | [0-9] | ([1-2] [0-9] | [3] "0")) space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "min -5", R"""({ "type": "integer", "minimum": -5 })""", R"""( root ::= ("-" ([0-5]) | [0] | [1-9] [0-9]{0,15}) space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "min -123", R"""({ "type": "integer", "minimum": -123 })""", R"""( 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 ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "max -5", R"""({ "type": "integer", "maximum": -5 })""", R"""( root ::= ("-" ([0-4] [0-9]{1,15} | [5-9] [0-9]{0,15})) space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "max 1", R"""({ "type": "integer", "maximum": 1 })""", R"""( root ::= ("-" [1-9] [0-9]{0,15} | [0-1]) space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "max 100", R"""({ "type": "integer", "maximum": 100 })""", R"""( root ::= ("-" [1-9] [0-9]{0,15} | [0-9] | ([1-8] [0-9] | [9] [0-9]) | "100") space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "min 0 max 23", R"""({ "type": "integer", "minimum": 0, "maximum": 23 })""", R"""( root ::= ([0-9] | ([1] [0-9] | [2] [0-3])) space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "min 15 max 300", R"""({ "type": "integer", "minimum": 15, "maximum": 300 })""", R"""( root ::= (([1] ([5-9]) | [2-9] [0-9]) | ([1-2] [0-9]{2} | [3] "00")) space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "min 5 max 30", R"""({ "type": "integer", "minimum": 5, "maximum": 30 })""", R"""( root ::= ([5-9] | ([1-2] [0-9] | [3] "0")) space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "min -123 max 42", R"""({ "type": "integer", "minimum": -123, "maximum": 42 })""", R"""( 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 ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "min -10 max 10", R"""({ "type": "integer", "minimum": -10, "maximum": 10 })""", R"""( root ::= ("-" ([0-9] | "10") | [0-9] | "10") space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ FAILURE, "unknown type", R"""({ "type": "kaboom" })""", "" }); test({ FAILURE, "invalid type", R"""({ "type": 123 })""", "" }); test({ SUCCESS, "empty schema (any value)", "{}", R"""( array ::= "[" space ( value ("," space value)* )? space "]" boolean ::= ("true" | "false") char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4}) decimal-part ::= [0-9]{1,16} integral-part ::= [0] | [1-9] [0-9]{0,15} null ::= "null" number ::= ("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)? object ::= "{" space ( string ":" space value ("," space string ":" space value)* )? space "}" root ::= value space ::= | " " | "\n"{1,2} [ \t]{0,20} string ::= "\"" char* "\"" value ::= object | array | string | number | boolean | null )""" }); test({ SUCCESS, "exotic formats", R"""({ "items": [ { "format": "date" }, { "format": "uuid" }, { "format": "time" }, { "format": "date-time" } ] })""", R"""( date ::= [0-9]{4} "-" ( "0" [1-9] | "1" [0-2] ) "-" ( "0" [1-9] | [1-2] [0-9] | "3" [0-1] ) date-string ::= "\"" date "\"" date-time ::= date "T" time date-time-string ::= "\"" date-time "\"" root ::= "[" space tuple-0 "," space uuid "," space tuple-2 "," space tuple-3 space "]" space ::= | " " | "\n"{1,2} [ \t]{0,20} 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] ) time-string ::= "\"" time "\"" tuple-0 ::= date-string tuple-2 ::= time-string tuple-3 ::= date-time-string 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} "\"" )""" }); test({ SUCCESS, "string", R"""({ "type": "string" })""", R"""( char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4}) root ::= "\"" char* "\"" space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "string w/ min length 1", R"""({ "type": "string", "minLength": 1 })""", R"""( char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4}) root ::= "\"" char+ "\"" space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "string w/ min length 3", R"""({ "type": "string", "minLength": 3 })""", R"""( char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4}) root ::= "\"" char{3,} "\"" space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "string w/ max length", R"""({ "type": "string", "maxLength": 3 })""", R"""( char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4}) root ::= "\"" char{0,3} "\"" space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "string w/ min & max length", R"""({ "type": "string", "minLength": 1, "maxLength": 4 })""", R"""( char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4}) root ::= "\"" char{1,4} "\"" space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "boolean", R"""({ "type": "boolean" })""", R"""( root ::= ("true" | "false") space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "integer", R"""({ "type": "integer" })""", R"""( integral-part ::= [0] | [1-9] [0-9]{0,15} root ::= ("-"? integral-part) space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "string const", R"""({ "const": "foo" })""", R"""( root ::= "\"foo\"" space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "non-string const", R"""({ "const": 123 })""", R"""( root ::= "123" space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "non-string enum", R"""({ "enum": ["red", "amber", "green", null, 42, ["foo"]] })""", R"""( root ::= ("\"red\"" | "\"amber\"" | "\"green\"" | "null" | "42" | "[\"foo\"]") space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "string array", R"""({ "type": "array", "prefixItems": { "type": "string" } })""", R"""( char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4}) root ::= "[" space (string ("," space string)*)? space "]" space ::= | " " | "\n"{1,2} [ \t]{0,20} string ::= "\"" char* "\"" )""" }); test({ SUCCESS, "nullable string array", R"""({ "type": ["array", "null"], "prefixItems": { "type": "string" } })""", R"""( alternative-0 ::= "[" space (string ("," space string)*)? space "]" char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4}) null ::= "null" root ::= alternative-0 | null space ::= | " " | "\n"{1,2} [ \t]{0,20} string ::= "\"" char* "\"" )""" }); test({ SUCCESS, "tuple1", R"""({ "prefixItems": [{ "type": "string" }] })""", R"""( char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4}) root ::= "[" space string space "]" space ::= | " " | "\n"{1,2} [ \t]{0,20} string ::= "\"" char* "\"" )""" }); test({ SUCCESS, "tuple2", R"""({ "prefixItems": [{ "type": "string" }, { "type": "number" }] })""", R"""( char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4}) decimal-part ::= [0-9]{1,16} integral-part ::= [0] | [1-9] [0-9]{0,15} number ::= ("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)? root ::= "[" space string "," space number space "]" space ::= | " " | "\n"{1,2} [ \t]{0,20} string ::= "\"" char* "\"" )""" }); // items {} constrains nothing, the same as no items at all test({ SUCCESS, "array with empty items", R"""({ "type": "array", "items": {} })""", R"""( array ::= "[" space ( value ("," space value)* )? space "]" boolean ::= ("true" | "false") char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4}) decimal-part ::= [0-9]{1,16} integral-part ::= [0] | [1-9] [0-9]{0,15} null ::= "null" number ::= ("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)? object ::= "{" space ( string ":" space value ("," space string ":" space value)* )? space "}" root ::= "[" space ( value ("," space value)* )? space "]" space ::= | " " | "\n"{1,2} [ \t]{0,20} string ::= "\"" char* "\"" value ::= object | array | string | number | boolean | null )""" }); test({ SUCCESS, "array with empty items and prefixItems", R"""({ "type": "array", "items": {}, "prefixItems": { "type": "string" } })""", R"""( array ::= "[" space ( value ("," space value)* )? space "]" boolean ::= ("true" | "false") char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4}) decimal-part ::= [0-9]{1,16} integral-part ::= [0] | [1-9] [0-9]{0,15} null ::= "null" number ::= ("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)? object ::= "{" space ( string ":" space value ("," space string ":" space value)* )? space "}" root ::= "[" space ( value ("," space value)* )? space "]" space ::= | " " | "\n"{1,2} [ \t]{0,20} string ::= "\"" char* "\"" value ::= object | array | string | number | boolean | null )""" }); test({ SUCCESS, "number", R"""({ "type": "number" })""", R"""( decimal-part ::= [0-9]{1,16} integral-part ::= [0] | [1-9] [0-9]{0,15} root ::= ("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)? space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "minItems", R"""({ "items": { "type": "boolean" }, "minItems": 2 })""", R"""( boolean ::= ("true" | "false") root ::= "[" space boolean ("," space boolean)+ space "]" space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "maxItems 0", R"""({ "items": { "type": "boolean" }, "maxItems": 0 })""", R"""( boolean ::= ("true" | "false") root ::= "[" space space "]" space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "maxItems 1", R"""({ "items": { "type": "boolean" }, "maxItems": 1 })""", R"""( boolean ::= ("true" | "false") root ::= "[" space boolean? space "]" space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "maxItems 2", R"""({ "items": { "type": "boolean" }, "maxItems": 2 })""", R"""( boolean ::= ("true" | "false") root ::= "[" space (boolean ("," space boolean)?)? space "]" space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "min + maxItems", R"""({ "items": { "type": ["number", "integer"] }, "minItems": 3, "maxItems": 5 })""", R"""( decimal-part ::= [0-9]{1,16} integer ::= ("-"? integral-part) integral-part ::= [0] | [1-9] [0-9]{0,15} item ::= number | integer number ::= ("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)? root ::= "[" space item ("," space item){2,4} space "]" space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "min + max items with min + max values across zero", R"""({ "items": { "type": "integer", "minimum": -12, "maximum": 207 }, "minItems": 3, "maxItems": 5 })""", R"""( item ::= ("-" ([0-9] | "1" [0-2]) | [0-9] | ([1-8] [0-9] | [9] [0-9]) | ([1] [0-9]{2} | [2] "0" [0-7])) root ::= "[" space item ("," space item){2,4} space "]" space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "min + max items with min + max values", R"""({ "items": { "type": "integer", "minimum": 12, "maximum": 207 }, "minItems": 3, "maxItems": 5 })""", R"""( item ::= (([1] ([2-9]) | [2-9] [0-9]) | ([1] [0-9]{2} | [2] "0" [0-7])) root ::= "[" space item ("," space item){2,4} space "]" space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "simple regexp", R"""({ "type": "string", "pattern": "^abc?d*efg+(hij)?kl$" })""", R"""( root ::= "\"" ("ab" "c"? "d"* "ef" "g"+ ("hij")? "kl") "\"" space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "regexp escapes", R"""({ "type": "string", "pattern": "^\\[\\]\\{\\}\\(\\)\\|\\+\\*\\?$" })""", R"""( root ::= "\"" ("[]{}()|+*?") "\"" space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "regexp quote", R"""({ "type": "string", "pattern": "^\"$" })""", R"""( root ::= "\"" ("\"") "\"" space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "regexp with top-level alternation", R"""({ "type": "string", "pattern": "^A|B|C|D$" })""", R"""( root ::= "\"" ("A" | "B" | "C" | "D") "\"" space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "regexp", R"""({ "type": "string", "pattern": "^(\\([0-9]{1,3}\\))?[0-9]{3}-[0-9]{4} a{3,5}nd...$" })""", R"""( dot ::= [^\x0A\x0D] root ::= "\"" (("(" root-1{1,3} ")")? root-1{3,3} "-" root-1{4,4} " " "a"{3,5} "nd" dot dot dot) "\"" root-1 ::= [0-9] space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "required props in original order", R"""({ "type": "object", "properties": { "b": {"type": "string"}, "c": {"type": "string"}, "a": {"type": "string"} }, "required": [ "a", "b", "c" ], "additionalProperties": false, "definitions": {} })""", R"""( a-kv ::= "\"a\"" space ":" space string b-kv ::= "\"b\"" space ":" space string c-kv ::= "\"c\"" space ":" space string char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4}) root ::= "{" space b-kv "," space c-kv "," space a-kv space "}" space ::= | " " | "\n"{1,2} [ \t]{0,20} string ::= "\"" char* "\"" )""" }); test({ SUCCESS, "1 optional prop", R"""({ "properties": { "a": { "type": "string" } }, "additionalProperties": false })""", R"""( a-kv ::= "\"a\"" space ":" space string char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4}) root ::= "{" space (a-kv )? space "}" space ::= | " " | "\n"{1,2} [ \t]{0,20} string ::= "\"" char* "\"" )""" }); test({ SUCCESS, "N optional props", R"""({ "properties": { "a": {"type": "string"}, "b": {"type": "string"}, "c": {"type": "string"} }, "additionalProperties": false })""", R"""( a-kv ::= "\"a\"" space ":" space string a-rest ::= ( "," space b-kv )? b-rest b-kv ::= "\"b\"" space ":" space string b-rest ::= ( "," space c-kv )? c-kv ::= "\"c\"" space ":" space string char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4}) root ::= "{" space (a-kv a-rest | b-kv b-rest | c-kv )? space "}" space ::= | " " | "\n"{1,2} [ \t]{0,20} string ::= "\"" char* "\"" )""" }); test({ SUCCESS, "required + optional props each in original order", R"""({ "properties": { "b": {"type": "string"}, "a": {"type": "string"}, "d": {"type": "string"}, "c": {"type": "string"} }, "required": ["a", "b"], "additionalProperties": false })""", R"""( a-kv ::= "\"a\"" space ":" space string b-kv ::= "\"b\"" space ":" space string c-kv ::= "\"c\"" space ":" space string char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4}) d-kv ::= "\"d\"" space ":" space string d-rest ::= ( "," space c-kv )? root ::= "{" space b-kv "," space a-kv ( "," space ( d-kv d-rest | c-kv ) )? space "}" space ::= | " " | "\n"{1,2} [ \t]{0,20} string ::= "\"" char* "\"" )""" }); test({ SUCCESS, "additional props", R"""({ "type": "object", "additionalProperties": {"type": "array", "items": {"type": "number"}} })""", R"""( additional-kv ::= string ":" space additional-value additional-value ::= "[" space (number ("," space number)*)? space "]" char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4}) decimal-part ::= [0-9]{1,16} integral-part ::= [0] | [1-9] [0-9]{0,15} number ::= ("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)? root ::= "{" space (additional-kv ( "," space additional-kv )* )? space "}" space ::= | " " | "\n"{1,2} [ \t]{0,20} string ::= "\"" char* "\"" )""" }); test({ SUCCESS, "additional props (true)", R"""({ "type": "object", "additionalProperties": true })""", R"""( array ::= "[" space ( value ("," space value)* )? space "]" boolean ::= ("true" | "false") char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4}) decimal-part ::= [0-9]{1,16} integral-part ::= [0] | [1-9] [0-9]{0,15} null ::= "null" number ::= ("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)? object ::= "{" space ( string ":" space value ("," space string ":" space value)* )? space "}" root ::= object space ::= | " " | "\n"{1,2} [ \t]{0,20} string ::= "\"" char* "\"" value ::= object | array | string | number | boolean | null )""" }); test({ SUCCESS, "additional props (implicit)", R"""({ "type": "object" })""", R"""( array ::= "[" space ( value ("," space value)* )? space "]" boolean ::= ("true" | "false") char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4}) decimal-part ::= [0-9]{1,16} integral-part ::= [0] | [1-9] [0-9]{0,15} null ::= "null" number ::= ("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)? object ::= "{" space ( string ":" space value ("," space string ":" space value)* )? space "}" root ::= object space ::= | " " | "\n"{1,2} [ \t]{0,20} string ::= "\"" char* "\"" value ::= object | array | string | number | boolean | null )""" }); test({ SUCCESS, "empty w/o additional props", R"""({ "type": "object", "additionalProperties": false })""", R"""( root ::= "{" space "}" space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "required + additional props", R"""({ "type": "object", "properties": { "a": {"type": "number"} }, "required": ["a"], "additionalProperties": {"type": "string"} })""", R"""( a-kv ::= "\"a\"" space ":" space number additional-k ::= ["] ( [a] char+ | [^"a] char* )? ["] additional-kv ::= additional-k ":" space string char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4}) decimal-part ::= [0-9]{1,16} integral-part ::= [0] | [1-9] [0-9]{0,15} number ::= ("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)? root ::= "{" space a-kv ( "," space ( additional-kv ( "," space additional-kv )* ) )? space "}" space ::= | " " | "\n"{1,2} [ \t]{0,20} string ::= "\"" char* "\"" )""" }); test({ SUCCESS, "optional + additional props", R"""({ "type": "object", "properties": { "a": {"type": "number"} }, "additionalProperties": {"type": "number"} })""", R"""( a-kv ::= "\"a\"" space ":" space number a-rest ::= ( "," space additional-kv )* additional-k ::= ["] ( [a] char+ | [^"a] char* )? ["] additional-kv ::= additional-k ":" space number char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4}) decimal-part ::= [0-9]{1,16} integral-part ::= [0] | [1-9] [0-9]{0,15} number ::= ("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)? root ::= "{" space (a-kv a-rest | additional-kv ( "," space additional-kv )* )? space "}" space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "required + optional + additional props", R"""({ "type": "object", "properties": { "and": {"type": "number"}, "also": {"type": "number"} }, "required": ["and"], "additionalProperties": {"type": "number"} })""", R"""( additional-k ::= ["] ( [a] ([l] ([s] ([o] char+ | [^"o] char*) | [^"s] char*) | [n] ([d] char+ | [^"d] char*) | [^"ln] char*) | [^"a] char* )? ["] additional-kv ::= additional-k ":" space number also-kv ::= "\"also\"" space ":" space number also-rest ::= ( "," space additional-kv )* and-kv ::= "\"and\"" space ":" space number char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4}) decimal-part ::= [0-9]{1,16} integral-part ::= [0] | [1-9] [0-9]{0,15} number ::= ("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)? root ::= "{" space and-kv ( "," space ( also-kv also-rest | additional-kv ( "," space additional-kv )* ) )? space "}" space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "optional props with empty name", R"""({ "properties": { "": {"type": "integer"}, "a": {"type": "integer"} }, "additionalProperties": {"type": "integer"} })""", R"""( -kv ::= "\"\"" space ":" space root -rest ::= ( "," space a-kv )? a-rest a-kv ::= "\"a\"" space ":" space integer a-rest ::= ( "," space additional-kv )* additional-k ::= ["] ( [a] char+ | [^"a] char* ) ["] additional-kv ::= additional-k ":" space integer char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4}) integer ::= ("-"? integral-part) integral-part ::= [0] | [1-9] [0-9]{0,15} root ::= ("-"? integral-part) root0 ::= "{" space (-kv -rest | a-kv a-rest | additional-kv ( "," space additional-kv )* )? space "}" space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "optional props with nested names", R"""({ "properties": { "a": {"type": "integer"}, "aa": {"type": "integer"} }, "additionalProperties": {"type": "integer"} })""", R"""( a-kv ::= "\"a\"" space ":" space integer a-rest ::= ( "," space aa-kv )? aa-rest aa-kv ::= "\"aa\"" space ":" space integer aa-rest ::= ( "," space additional-kv )* additional-k ::= ["] ( [a] ([a] char+ | [^"a] char*) | [^"a] char* )? ["] additional-kv ::= additional-k ":" space integer char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4}) integer ::= ("-"? integral-part) integral-part ::= [0] | [1-9] [0-9]{0,15} root ::= "{" space (a-kv a-rest | aa-kv aa-rest | additional-kv ( "," space additional-kv )* )? space "}" space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "optional props with common prefix", R"""({ "properties": { "ab": {"type": "integer"}, "ac": {"type": "integer"} }, "additionalProperties": {"type": "integer"} })""", R"""( ab-kv ::= "\"ab\"" space ":" space integer ab-rest ::= ( "," space ac-kv )? ac-rest ac-kv ::= "\"ac\"" space ":" space integer ac-rest ::= ( "," space additional-kv )* additional-k ::= ["] ( [a] ([b] char+ | [c] char+ | [^"bc] char*) | [^"a] char* )? ["] additional-kv ::= additional-k ":" space integer char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4}) integer ::= ("-"? integral-part) integral-part ::= [0] | [1-9] [0-9]{0,15} root ::= "{" space (ab-kv ab-rest | ac-kv ac-rest | additional-kv ( "," space additional-kv )* )? space "}" space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "top-level $ref", R"""({ "$ref": "#/definitions/foo", "definitions": { "foo": { "type": "object", "properties": { "a": { "type": "string" } }, "required": [ "a" ], "additionalProperties": false } } })""", R"""( char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4}) ref-definitions-foo ::= "{" space ref-definitions-foo-a-kv space "}" ref-definitions-foo-a-kv ::= "\"a\"" space ":" space string root ::= ref-definitions-foo space ::= | " " | "\n"{1,2} [ \t]{0,20} string ::= "\"" char* "\"" )""" }); test({ SUCCESS, "anyOf", R"""({ "anyOf": [ {"$ref": "#/definitions/foo"}, {"$ref": "#/definitions/bar"} ], "definitions": { "foo": { "properties": {"a": {"type": "number"}} }, "bar": { "properties": {"b": {"type": "number"}} } }, "type": "object" })""", R"""( alternative-0 ::= ref-definitions-foo alternative-1 ::= ref-definitions-bar decimal-part ::= [0-9]{1,16} integral-part ::= [0] | [1-9] [0-9]{0,15} number ::= ("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)? ref-definitions-bar ::= "{" space (ref-definitions-bar-b-kv )? space "}" ref-definitions-bar-b-kv ::= "\"b\"" space ":" space number ref-definitions-foo ::= "{" space (ref-definitions-foo-a-kv )? space "}" ref-definitions-foo-a-kv ::= "\"a\"" space ":" space number root ::= alternative-0 | alternative-1 space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "anyOf $ref", R"""({ "properties": { "a": { "anyOf": [ {"type": "string"}, {"type": "number"} ] }, "b": { "anyOf": [ {"$ref": "#/properties/a/anyOf/0"}, {"type": "boolean"} ] } }, "type": "object" })""", R"""( a ::= string | number a-kv ::= "\"a\"" space ":" space a a-rest ::= ( "," space b-kv )? b ::= b-0 | boolean b-0 ::= string b-kv ::= "\"b\"" space ":" space b boolean ::= ("true" | "false") char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4}) decimal-part ::= [0-9]{1,16} integral-part ::= [0] | [1-9] [0-9]{0,15} number ::= ("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)? root ::= "{" space (a-kv a-rest | b-kv )? space "}" space ::= | " " | "\n"{1,2} [ \t]{0,20} string ::= "\"" char* "\"" )""" }); test({ SUCCESS, "mix of allOf, anyOf and $ref (similar to https://json.schemastore.org/tsconfig.json)", R"""({ "allOf": [ {"$ref": "#/definitions/foo"}, {"$ref": "#/definitions/bar"}, { "anyOf": [ {"$ref": "#/definitions/baz"}, {"$ref": "#/definitions/bam"} ] } ], "definitions": { "foo": { "properties": {"a": {"type": "number"}} }, "bar": { "properties": {"b": {"type": "number"}} }, "bam": { "properties": {"c": {"type": "number"}} }, "baz": { "properties": {"d": {"type": "number"}} } }, "type": "object" })""", R"""( a-kv ::= "\"a\"" space ":" space number b-kv ::= "\"b\"" space ":" space number c-kv ::= "\"c\"" space ":" space number d-kv ::= "\"d\"" space ":" space number d-rest ::= ( "," space c-kv )? decimal-part ::= [0-9]{1,16} integral-part ::= [0] | [1-9] [0-9]{0,15} number ::= ("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)? root ::= "{" space a-kv "," space b-kv ( "," space ( d-kv d-rest | c-kv ) )? space "}" space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "allOf with enum schema", R"""({ "allOf": [ {"$ref": "#/definitions/foo"} ], "definitions": { "foo": { "type": "string", "enum": ["a", "b"] } } })""", R"""( root ::= ("\"a\"" | "\"b\"") space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "allOf with multiple enum schemas", R"""({ "allOf": [ {"$ref": "#/definitions/foo"}, {"$ref": "#/definitions/bar"} ], "definitions": { "foo": { "type": "string", "enum": ["a", "b", "c"] }, "bar": { "type": "string", "enum": ["b", "c", "d"] } } })""", R"""( root ::= ("\"b\"" | "\"c\"") space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "conflicting names", R"""({ "type": "object", "properties": { "number": { "type": "object", "properties": { "number": { "type": "object", "properties": { "root": { "type": "number" } }, "required": [ "root" ], "additionalProperties": false } }, "required": [ "number" ], "additionalProperties": false } }, "required": [ "number" ], "additionalProperties": false, "definitions": {} })""", R"""( decimal-part ::= [0-9]{1,16} integral-part ::= [0] | [1-9] [0-9]{0,15} number ::= ("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)? number- ::= "{" space number-number-kv space "}" number-kv ::= "\"number\"" space ":" space number- number-number ::= "{" space number-number-root-kv space "}" number-number-kv ::= "\"number\"" space ":" space number-number number-number-root-kv ::= "\"root\"" space ":" space number root ::= "{" space number-kv space "}" space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "description only (no type) treated as unconstrained", R"""({"description": "The 0-based index of the last line to be retrieved (inclusive). If None, read until the end of the file."})""", R"""( array ::= "[" space ( value ("," space value)* )? space "]" boolean ::= ("true" | "false") char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4}) decimal-part ::= [0-9]{1,16} integral-part ::= [0] | [1-9] [0-9]{0,15} null ::= "null" number ::= ("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)? object ::= "{" space ( string ":" space value ("," space string ":" space value)* )? space "}" root ::= value space ::= | " " | "\n"{1,2} [ \t]{0,20} string ::= "\"" char* "\"" value ::= object | array | string | number | boolean | null )""" }); test({ SUCCESS, "literal string with escapes", R"""({ "properties": { "code": { "const": " \r \n \" \\ ", "description": "Generated code", "title": "Code", "type": "string" } }, "required": [ "code" ], "title": "DecoderResponse", "type": "object" })""", R"""( code ::= "\" \\r \\n \\\" \\\\ \"" code-kv ::= "\"code\"" space ":" space code root ::= "{" space code-kv space "}" space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "regexp with non-capturing group", R"""({ "type": "string", "pattern": "^(?:foo|bar)baz$" })""", R"""( root ::= "\"" (("foo" | "bar") "baz") "\"" space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "regexp with nested non-capturing groups", R"""({ "type": "string", "pattern": "^(?:(?:ab)+c)?d$" })""", R"""( root ::= "\"" ((("ab")+ "c")? "d") "\"" space ::= | " " | "\n"{1,2} [ \t]{0,20} )""" }); test({ SUCCESS, "unanchored regexp", R"""({ "type": "string", "pattern": "[0-9]+" })""", R"""( char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4}) root ::= string space ::= | " " | "\n"{1,2} [ \t]{0,20} string ::= "\"" char* "\"" )""" }); // the rules of the partial conversion (here "root-0") must not leak into the grammar test({ SUCCESS, "regexp with unsupported shorthand", R"""({ "type": "string", "pattern": "^[0-9]{3}\\w$" })""", R"""( char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4}) root ::= string space ::= | " " | "\n"{1,2} [ \t]{0,20} string ::= "\"" char* "\"" )""" }); // a regexp that is invalid under any flavor is still an error test({ FAILURE, "regexp with unbalanced parentheses", R"""({ "type": "string", "pattern": "^(a$" })""", "" }); // only the property with the bad pattern degrades test({ SUCCESS, "unsupported regexp in a property", R"""({ "type": "object", "properties": { "a": { "type": "string", "pattern": "^[a-z\\-]+$" } }, "required": ["a"], "additionalProperties": false })""", R"""( a ::= string a-kv ::= "\"a\"" space ":" space a char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4}) root ::= "{" space a-kv space "}" space ::= | " " | "\n"{1,2} [ \t]{0,20} string ::= "\"" char* "\"" )""" }); } int main() { test_all("JSON schema conversion", [](const TestCase & tc) { try { tc.verify(json_schema_to_grammar(common_json::parse(tc.schema), true)); tc.verify_status(SUCCESS); } catch (const std::invalid_argument & ex) { fprintf(stderr, "Error: %s\n", ex.what()); tc.verify_status(FAILURE); } }); // a document parsed up front gives the same grammar as the JSON, recursion included { fprintf(stderr, "- parsed document\n"); auto schema = common_json::parse(R"""({ "$ref": "#/$defs/node", "$defs": { "node": { "type": "object", "properties": {"next": {"$ref": "#/$defs/node"}, "leaf": {}}, "additionalProperties": false } } })"""); assert(json_schema_to_grammar(common_chat_schema_from_json(schema)) == json_schema_to_grammar(schema, true)); } // a property node carries its $ref target, so its grammar names the ref rule { fprintf(stderr, "- sub-schema $ref\n"); auto parameters = common_json::parse(R"""({ "type": "object", "properties": {"item": {"$ref": "#/$defs/item"}}, "$defs": { "item": { "type": "object", "properties": {"a": {"type": "string"}}, "required": ["a"], "additionalProperties": false } } })"""); TestCase tc { SUCCESS, "sub-schema $ref", "", R"""( char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4}) ref-defs-item ::= "{" space ref-defs-item-a-kv space "}" ref-defs-item-a-kv ::= "\"a\"" space ":" space string root ::= ref-defs-item space ::= | " " | "\n"{1,2} [ \t]{0,20} string ::= "\"" char* "\"" )""", }; auto doc = common_chat_schema_from_json(parameters); tc.verify(build_grammar([&](const common_grammar_builder & builder) { const auto & item = static_cast(*doc.root).properties.at(0); builder.add_schema("root", *item.schema); })); } test_all("Check the expectations parse", [](const TestCase & tc) { if (tc.expected_status == SUCCESS) { tc.verify_expectation_parseable(); } }); }