|
#ifdef NDEBUG |
|
#undef NDEBUG |
|
#endif |
|
|
|
#include "unicode.h" |
|
#include "llama-grammar.h" |
|
#include "json-schema-to-grammar.h" |
|
|
|
#include <cassert> |
|
#include <string> |
|
#include <vector> |
|
|
|
using json = nlohmann::ordered_json; |
|
|
|
static llama_grammar * build_grammar(const std::string & grammar_str) { |
|
return llama_grammar_init_impl(nullptr, grammar_str.c_str(), "root"); |
|
} |
|
|
|
static bool test_build_grammar_fails(const std::string & grammar_str) { |
|
fprintf(stderr, "⚫ Testing failure for grammar: %s\n", grammar_str.c_str()); |
|
bool grammar_fails = false; |
|
llama_grammar * grammar = build_grammar(grammar_str); |
|
if (grammar != nullptr) { |
|
fprintf(stderr, " ❌ Expected build failure, but succeeded\n"); |
|
} else { |
|
grammar_fails = true; |
|
fprintf(stdout, " ✅︎\n"); |
|
} |
|
return grammar_fails; |
|
} |
|
|
|
static bool match_string(const std::string & input, llama_grammar * grammar) { |
|
const auto cpts = unicode_cpts_from_utf8(input); |
|
|
|
const llama_grammar_rules & rules = llama_grammar_get_rules (grammar); |
|
llama_grammar_stacks & stacks_cur = llama_grammar_get_stacks(grammar); |
|
|
|
for (const auto & cpt : cpts) { |
|
const llama_grammar_stacks stacks_prev = llama_grammar_get_stacks(grammar); |
|
|
|
llama_grammar_accept(rules, stacks_prev, cpt, stacks_cur); |
|
|
|
if (stacks_cur.empty()) { |
|
|
|
return false; |
|
} |
|
} |
|
|
|
for (const auto & stack : stacks_cur) { |
|
if (stack.empty()) { |
|
|
|
return true; |
|
} |
|
} |
|
|
|
return false; |
|
} |
|
|
|
static void test(const std::string & test_desc, const std::string & grammar_str, const std::vector<std::string> & passing_strings, const std::vector<std::string> & failing_strings) { |
|
fprintf(stderr, "⚫ Testing %s\n%s\n", test_desc.c_str(), grammar_str.c_str()); |
|
fflush(stderr); |
|
|
|
auto * grammar = build_grammar(grammar_str); |
|
|
|
|
|
const llama_grammar_stacks stacks_org = llama_grammar_get_stacks(grammar); |
|
|
|
llama_grammar_stacks & stacks_cur = llama_grammar_get_stacks(grammar); |
|
|
|
fprintf(stderr, " 🔵 Valid strings:\n"); |
|
|
|
|
|
for (const auto & test_string : passing_strings) { |
|
fprintf(stderr, " \"%s\" ", test_string.c_str()); |
|
fflush(stderr); |
|
|
|
bool matched = match_string(test_string, grammar); |
|
|
|
if (!matched) { |
|
fprintf(stderr, "❌ (failed to match)\n"); |
|
|
|
|
|
|
|
FILE* grammar_file = fopen("test-grammar-integration.grammar.gbnf", "w"); |
|
if (grammar_file) { |
|
fprintf(grammar_file, "%s", grammar_str.c_str()); |
|
fclose(grammar_file); |
|
} |
|
|
|
|
|
FILE* string_file = fopen("test-grammar-integration.string.txt", "w"); |
|
if (string_file) { |
|
fprintf(string_file, "%s", test_string.c_str()); |
|
fclose(string_file); |
|
} |
|
|
|
fprintf(stderr, "\n NOTE: Debug grammar file generated. To analyze this failure in detail, run the following command: ./llama-gbnf-validator test-grammar-integration.grammar.gbnf test-grammar-integration.string.txt\n\n"); |
|
} else { |
|
fprintf(stdout, "✅︎\n"); |
|
} |
|
|
|
assert(matched); |
|
|
|
|
|
stacks_cur = stacks_org; |
|
} |
|
|
|
fprintf(stderr, " 🟠 Invalid strings:\n"); |
|
|
|
|
|
for (const auto & test_string : failing_strings) { |
|
fprintf(stderr, " \"%s\" ", test_string.c_str()); |
|
fflush(stderr); |
|
|
|
bool matched = match_string(test_string, grammar); |
|
|
|
if (matched) { |
|
fprintf(stderr, "❌ (incorrectly matched)\n"); |
|
} else { |
|
fprintf(stdout, "✅︎\n"); |
|
} |
|
assert(!matched); |
|
|
|
|
|
stacks_cur = stacks_org; |
|
} |
|
|
|
|
|
llama_grammar_free_impl(grammar); |
|
} |
|
static void test_grammar(const std::string & test_desc, const std::string & grammar_str, const std::vector<std::string> & passing_strings, const std::vector<std::string> & failing_strings) { |
|
test(test_desc + ". Grammar: " + grammar_str, grammar_str, passing_strings, failing_strings); |
|
} |
|
static void test_schema(const std::string & test_desc, const std::string & schema_str, const std::vector<std::string> & passing_strings, const std::vector<std::string> & failing_strings) { |
|
test(test_desc + ". Schema: " + schema_str, json_schema_to_grammar(json::parse(schema_str)), passing_strings, failing_strings); |
|
} |
|
|
|
static void test_simple_grammar() { |
|
test_schema( |
|
"min 0", |
|
R"""({ |
|
"type": "integer", |
|
"minimum": 0 |
|
})""", |
|
|
|
{ |
|
"0", |
|
"10", |
|
"12", |
|
"10000", |
|
}, |
|
|
|
{ |
|
"-1", |
|
"-10", |
|
"-10000", |
|
"-100000000000000000000000000000000", |
|
"100000000000000000000000000000000", |
|
"00", |
|
"01", |
|
"-0", |
|
} |
|
); |
|
test_schema( |
|
"min 2", |
|
|
|
R"""({ |
|
"type": "integer", |
|
"minimum": 2 |
|
})""", |
|
|
|
{ |
|
"2", |
|
"3", |
|
"4", |
|
"10", |
|
"20", |
|
"1234567890000000", |
|
}, |
|
|
|
{ |
|
"0", |
|
"1", |
|
"-1", |
|
"-100", |
|
"0", |
|
"1", |
|
"01", |
|
"02", |
|
"12345678900000000", |
|
} |
|
); |
|
test_schema( |
|
"min 456", |
|
R"""({ |
|
"type": "integer", |
|
"minimum": 456 |
|
})""", |
|
|
|
{ |
|
"456", |
|
"4560", |
|
"457", |
|
"460", |
|
"500", |
|
}, |
|
|
|
{ |
|
"455", |
|
"356", |
|
"50", |
|
"050", |
|
"-1", |
|
"-456", |
|
} |
|
); |
|
test_schema( |
|
"min -123", |
|
R"""({ |
|
"type": "integer", |
|
"minimum": -123 |
|
})""", |
|
|
|
{ |
|
"-123", |
|
"-122", |
|
"-11", |
|
"-1", |
|
"0", |
|
"1", |
|
"123", |
|
"1234", |
|
"2345", |
|
}, |
|
|
|
{ |
|
"-1234", |
|
"-124", |
|
} |
|
); |
|
|
|
test_schema( |
|
"max 9999", |
|
|
|
R"""({ |
|
"type": "integer", |
|
"maximum": 9999 |
|
})""", |
|
|
|
{ |
|
"-99999", |
|
"0", |
|
"9999", |
|
}, |
|
|
|
{ |
|
"10000", |
|
"99991", |
|
} |
|
); |
|
test_schema( |
|
"max -9999", |
|
|
|
R"""({ |
|
"type": "integer", |
|
"maximum": -9999 |
|
})""", |
|
|
|
{ |
|
"-10000", |
|
"-9999", |
|
}, |
|
|
|
{ |
|
"-9998", |
|
"0", |
|
"9999", |
|
} |
|
); |
|
test_schema( |
|
"min 5 max 30", |
|
|
|
R"""({ |
|
"type": "integer", |
|
"minimum": 5, |
|
"maximum": 30 |
|
})""", |
|
|
|
{ |
|
"5", |
|
"10", |
|
"30", |
|
}, |
|
|
|
{ |
|
"05", |
|
"4", |
|
"-1", |
|
"31", |
|
"123", |
|
"0123", |
|
} |
|
); |
|
test_schema( |
|
"min -1 max 1", |
|
R"""({ |
|
"type": "integer", |
|
"minimum": -1, |
|
"maximum": 1 |
|
})""", |
|
|
|
{ |
|
"-1", |
|
"0", |
|
"1", |
|
}, |
|
|
|
{ |
|
"-11", |
|
"-10", |
|
"-2", |
|
"2", |
|
"10", |
|
"11", |
|
} |
|
); |
|
test_schema( |
|
"min -123 max 42", |
|
R"""({ |
|
"type": "integer", |
|
"minimum": -123, |
|
"maximum": 42 |
|
})""", |
|
|
|
{ |
|
"-123", |
|
"-122", |
|
"-13", |
|
"-11", |
|
"-2", |
|
"-1", |
|
"0", |
|
"1", |
|
"5", |
|
"10", |
|
"39", |
|
"40", |
|
"42", |
|
}, |
|
|
|
{ |
|
"-0123", |
|
"-124", |
|
"-1123", |
|
"-200", |
|
"43", |
|
"123", |
|
"0123", |
|
} |
|
); |
|
test_schema( |
|
"exclusive min / max", |
|
|
|
R"""({ |
|
"type": "integer", |
|
"exclusiveMinimum": 0, |
|
"exclusiveMaximum": 10000 |
|
})""", |
|
|
|
{ |
|
"1", |
|
"9999", |
|
}, |
|
|
|
{ |
|
"0", |
|
"01", |
|
"10000", |
|
"99999", |
|
} |
|
); |
|
|
|
|
|
test_grammar( |
|
"simple grammar", |
|
R"""( |
|
root ::= expr |
|
expr ::= term ("+" term)* |
|
term ::= number |
|
number ::= [0-9]+)""", |
|
|
|
{ |
|
"42", |
|
"1+2+3+4+5", |
|
"123+456", |
|
}, |
|
|
|
{ |
|
"+", |
|
"/ 3", |
|
"1+2+3+4+5+", |
|
"12a45", |
|
} |
|
); |
|
} |
|
|
|
static void test_complex_grammar() { |
|
|
|
test_grammar( |
|
"medium complexity grammar", |
|
|
|
R"""( |
|
root ::= expression |
|
expression ::= term ws (("+"|"-") ws term)* |
|
term ::= factor ws (("*"|"/") ws factor)* |
|
factor ::= number | variable | "(" expression ")" | function-call |
|
number ::= [0-9]+ |
|
variable ::= [a-zA-Z_][a-zA-Z0-9_]* |
|
function-call ::= variable ws "(" (expression ("," ws expression)*)? ")" |
|
ws ::= [ \t\n\r]?)""", |
|
|
|
{ |
|
"42", |
|
"1*2*3*4*5", |
|
"x", |
|
"x+10", |
|
"x1+y2", |
|
"(a+b)*(c-d)", |
|
"func()", |
|
"func(x,y+2)", |
|
"a*(b+c)-d/e", |
|
"f(g(x),h(y,z))", |
|
"x + 10", |
|
"x1 + y2", |
|
"(a + b) * (c - d)", |
|
"func()", |
|
"func(x, y + 2)", |
|
"a * (b + c) - d / e", |
|
"f(g(x), h(y, z))", |
|
"123+456", |
|
"123*456*789-123/456+789*123", |
|
"123+456*789-123/456+789*123-456/789+123*456-789/123+456*789-123/456+789*123-456" |
|
}, |
|
|
|
{ |
|
"+", |
|
"/ 3x", |
|
"x + + y", |
|
"a * / b", |
|
"func(,)", |
|
"func(x y)", |
|
"(a + b", |
|
"x + y)", |
|
"a + b * (c - d", |
|
"42 +", |
|
"x +", |
|
"x + 10 +", |
|
"(a + b) * (c - d", |
|
"func(", |
|
"func(x, y + 2", |
|
"a * (b + c) - d /", |
|
"f(g(x), h(y, z)", |
|
"123+456*789-123/456+789*123-456/789+123*456-789/123+456*789-123/456+789*123-456/", |
|
} |
|
); |
|
} |
|
|
|
static void test_special_chars() { |
|
|
|
test_grammar( |
|
"special characters", |
|
|
|
R"""( |
|
root ::= ... "abc" ... |
|
)""", |
|
|
|
{ |
|
"abcabcabc", |
|
"aaaabcccc", |
|
|
|
"🔵🟠✅abc❌🟠🔵" |
|
}, |
|
|
|
{ |
|
"aaabcccc", |
|
"aaaaabcccc", |
|
"aaaabccc", |
|
"aaaabccccc", |
|
"🔵🟠✅❌abc❌✅🟠🔵", |
|
"🔵🟠abc🟠🔵" |
|
} |
|
); |
|
} |
|
|
|
static void test_quantifiers() { |
|
|
|
|
|
test_grammar( |
|
"* quantifier", |
|
|
|
R"""(root ::= "a"*)""", |
|
|
|
{ |
|
"", |
|
"a", |
|
"aaaaa", |
|
"aaaaaaaaaaaaaaaaaa", |
|
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" |
|
}, |
|
|
|
{ |
|
"b", |
|
"ab", |
|
"aab", |
|
"ba", |
|
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab" |
|
} |
|
); |
|
test_grammar( |
|
"+ quantifier", |
|
|
|
R"""(root ::= "a"+)""", |
|
|
|
{ |
|
"a", |
|
"aaaaa", |
|
"aaaaaaaaaaaaaaaaaa", |
|
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" |
|
}, |
|
|
|
{ |
|
"", |
|
"b", |
|
"ab", |
|
"aab", |
|
"ba", |
|
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab" |
|
} |
|
); |
|
test_grammar( |
|
"? quantifier", |
|
|
|
R"""(root ::= "a"?)""", |
|
|
|
{ |
|
"", |
|
"a" |
|
}, |
|
|
|
{ |
|
"b", |
|
"ab", |
|
"aa", |
|
"ba", |
|
} |
|
); |
|
test_grammar( |
|
"mixed quantifiers", |
|
|
|
R"""( |
|
root ::= cons+ vowel* cons? (vowel cons)* |
|
vowel ::= [aeiouy] |
|
cons ::= [bcdfghjklmnpqrstvwxyz] |
|
)""", |
|
|
|
{ |
|
"yes", |
|
"no", |
|
"noyes", |
|
"crwth", |
|
"four", |
|
"bryyyy", |
|
}, |
|
|
|
{ |
|
"yess", |
|
"yesno", |
|
"forty", |
|
"catyyy", |
|
} |
|
); |
|
test_grammar( |
|
"simple exact repetition", |
|
|
|
R"""( |
|
root ::= [ab]{4} |
|
)""", |
|
|
|
{ |
|
"aaaa", |
|
"bbbb", |
|
"abab", |
|
}, |
|
|
|
{ |
|
"a", |
|
"b", |
|
"aaaaa", |
|
} |
|
); |
|
test_grammar( |
|
"simple min repetition", |
|
|
|
R"""( |
|
root ::= [ab]{4,} |
|
)""", |
|
|
|
{ |
|
"aaaa", |
|
"aaaaab", |
|
"bbbb", |
|
"ababab", |
|
}, |
|
|
|
{ |
|
"", |
|
"aba", |
|
} |
|
); |
|
test_grammar( |
|
"simple max repetition", |
|
|
|
R"""( |
|
root ::= [ab]{0,4} |
|
)""", |
|
|
|
{ |
|
"", |
|
"a", |
|
"aa", |
|
"aaa", |
|
"aaab", |
|
}, |
|
|
|
{ |
|
"aaaaa", |
|
} |
|
); |
|
test_grammar( |
|
"min / max repetition", |
|
|
|
R"""( |
|
root ::= ("0x" [A-F0-9]{2} " "?){3,5} |
|
)""", |
|
|
|
{ |
|
"0xFF 0x12 0xAB", |
|
"0xFF 0x12 0xAB 0x00 0x00", |
|
}, |
|
|
|
{ |
|
"", |
|
"0xFF", |
|
"0xFF 0x12", |
|
"0xFF 0x12 0xAB 0x00 0x00 0x00", |
|
} |
|
); |
|
} |
|
|
|
static void test_failure_missing_root() { |
|
fprintf(stderr, "⚫ Testing missing root node:\n"); |
|
|
|
const std::string grammar_str = R"""( |
|
rot ::= expr |
|
expr ::= term ("+" term)* |
|
term ::= number |
|
number ::= [0-9]+)"""; |
|
|
|
llama_grammar_parser parsed_grammar; |
|
parsed_grammar.parse(grammar_str.c_str()); |
|
|
|
|
|
assert(!parsed_grammar.rules.empty()); |
|
|
|
|
|
assert(parsed_grammar.symbol_ids.find("root") == parsed_grammar.symbol_ids.end()); |
|
fprintf(stderr, " ✅︎ Passed\n"); |
|
} |
|
|
|
static void test_failure_missing_reference() { |
|
fprintf(stderr, "⚫ Testing missing reference node:\n"); |
|
|
|
|
|
const std::string grammar_str = |
|
R"""(root ::= expr |
|
expr ::= term ("+" term)* |
|
term ::= numero |
|
number ::= [0-9]+)"""; |
|
|
|
fprintf(stderr, " Expected error: "); |
|
|
|
llama_grammar_parser parsed_grammar; |
|
parsed_grammar.parse(grammar_str.c_str()); |
|
|
|
|
|
assert(parsed_grammar.rules.empty()); |
|
|
|
fprintf(stderr, " End of expected error.\n"); |
|
fprintf(stderr, " ✅︎ Passed\n"); |
|
} |
|
|
|
static void test_failure_left_recursion() { |
|
fprintf(stderr, "⚫ Testing left recursion detection:\n"); |
|
|
|
|
|
const std::string simple_str = R"""(root ::= "a" | root "a")"""; |
|
assert(test_build_grammar_fails(simple_str)); |
|
|
|
|
|
const std::string medium_str = R"""( |
|
root ::= asdf |
|
asdf ::= "a" | asdf "a" |
|
)"""; |
|
assert(test_build_grammar_fails(medium_str)); |
|
|
|
|
|
const std::string hard_str = R"""( |
|
root ::= asdf |
|
asdf ::= "a" | foo "b" |
|
foo ::= "c" | asdf "d" | "e")"""; |
|
assert(test_build_grammar_fails(hard_str)); |
|
|
|
|
|
const std::string hardest_str = R"""( |
|
root ::= asdf |
|
asdf ::= "a" | foo "b" |
|
foo ::= "c" | empty asdf "d" | "e" |
|
empty ::= "blah" | )"""; |
|
assert(test_build_grammar_fails(hardest_str)); |
|
|
|
fprintf(stderr, " ✅︎ Passed\n"); |
|
} |
|
|
|
static void test_json_schema() { |
|
|
|
|
|
|
|
|
|
test_schema( |
|
"empty schema (object)", |
|
|
|
R"""( |
|
{} |
|
)""", |
|
|
|
{ |
|
R"""({})""", |
|
R"""({"foo": "bar"})""", |
|
}, |
|
|
|
{ |
|
"", |
|
"[]", |
|
"null", |
|
R"""("")""", |
|
"true", |
|
} |
|
); |
|
|
|
test_schema( |
|
"exotic formats (list)", |
|
|
|
R"""({ |
|
"items": [ |
|
{ "format": "date" }, |
|
{ "format": "uuid" }, |
|
{ "format": "time" }, |
|
{ "format": "date-time" } |
|
] |
|
})""", |
|
|
|
{ |
|
|
|
|
|
R"""(["2012-04-23", "12345678-1234-1234-1234-1234567890ab", "18:25:43.511Z", "2012-04-23T18:25:43.511Z"])""", |
|
|
|
|
|
}, |
|
|
|
{ |
|
R"""(["foo", "bar"])""", |
|
R"""(["12345678-1234-1234-1234-1234567890ab"])""", |
|
} |
|
); |
|
|
|
test_schema( |
|
"string", |
|
|
|
R"""({ |
|
"type": "string" |
|
})""", |
|
|
|
{ |
|
R"""("foo")""", |
|
R"""("bar")""", |
|
R"""("")""", |
|
}, |
|
|
|
{ |
|
R"""({})""", |
|
R"""("foo": "bar")""", |
|
} |
|
); |
|
|
|
test_schema( |
|
"string w/ min length 1", |
|
|
|
R"""({ |
|
"type": "string", |
|
"minLength": 1 |
|
})""", |
|
|
|
{ |
|
R"""("foo")""", |
|
R"""("bar")""", |
|
}, |
|
|
|
{ |
|
R"""("")""", |
|
R"""({})""", |
|
R"""("foo": "bar")""", |
|
} |
|
); |
|
|
|
test_schema( |
|
"string w/ min length 3", |
|
|
|
R"""({ |
|
"type": "string", |
|
"minLength": 3 |
|
})""", |
|
|
|
{ |
|
R"""("foo")""", |
|
R"""("bar")""", |
|
R"""("foobar")""", |
|
}, |
|
|
|
{ |
|
R"""("")""", |
|
R"""("f")""", |
|
R"""("fo")""", |
|
} |
|
); |
|
|
|
test_schema( |
|
"string w/ max length", |
|
|
|
R"""({ |
|
"type": "string", |
|
"maxLength": 3 |
|
})""", |
|
|
|
{ |
|
R"""("foo")""", |
|
R"""("bar")""", |
|
R"""("")""", |
|
R"""("f")""", |
|
R"""("fo")""", |
|
}, |
|
|
|
{ |
|
R"""("foobar")""", |
|
} |
|
); |
|
|
|
test_schema( |
|
"string w/ min & max length", |
|
|
|
R"""({ |
|
"type": "string", |
|
"minLength": 1, |
|
"maxLength": 4 |
|
})""", |
|
|
|
{ |
|
R"""("foo")""", |
|
R"""("bar")""", |
|
R"""("f")""", |
|
R"""("barf")""", |
|
}, |
|
|
|
{ |
|
R"""("")""", |
|
R"""("barfo")""", |
|
R"""("foobar")""", |
|
} |
|
); |
|
|
|
test_schema( |
|
"boolean", |
|
|
|
R"""({ |
|
"type": "boolean" |
|
})""", |
|
|
|
{ |
|
"true", |
|
"false", |
|
}, |
|
|
|
{ |
|
R"""("")""", |
|
R"""("true")""", |
|
R"""(True)""", |
|
R"""(FALSE)""", |
|
} |
|
); |
|
|
|
test_schema( |
|
"integer", |
|
|
|
R"""({ |
|
"type": "integer" |
|
})""", |
|
|
|
{ |
|
R"""(0)""", |
|
R"""(12345)""", |
|
R"""(1234567890123456)""", |
|
}, |
|
|
|
{ |
|
R"""()""", |
|
R"""(01)""", |
|
R"""(007)""", |
|
R"""(12345678901234567 )""", |
|
} |
|
); |
|
|
|
test_schema( |
|
"string const", |
|
|
|
R"""({ |
|
"const": "foo" |
|
})""", |
|
|
|
{ |
|
R"""("foo")""", |
|
}, |
|
|
|
{ |
|
R"""(foo)""", |
|
R"""("bar")""", |
|
} |
|
); |
|
|
|
test_schema( |
|
"non-string const", |
|
|
|
R"""({ |
|
"const": true |
|
})""", |
|
|
|
{ |
|
R"""(true)""", |
|
}, |
|
|
|
{ |
|
R"""()""", |
|
R"""(foo)""", |
|
R"""("true")""", |
|
} |
|
); |
|
|
|
test_schema( |
|
"non-string const", |
|
|
|
R"""({ |
|
"enum": ["red", "amber", "green", null, 42, ["foo"]] |
|
})""", |
|
|
|
{ |
|
R"""("red")""", |
|
R"""(null)""", |
|
R"""(42)""", |
|
R"""(["foo"])""", |
|
}, |
|
|
|
{ |
|
R"""()""", |
|
R"""(420)""", |
|
R"""(true)""", |
|
R"""(foo)""", |
|
} |
|
); |
|
|
|
test_schema( |
|
"simple pattern", |
|
|
|
R"""({ |
|
"pattern": "^[a-zA-Z0-9_-]*$" |
|
})""", |
|
|
|
{ |
|
R"""("")""", |
|
R"""("He_llo-12")""", |
|
}, |
|
|
|
{ |
|
R"""("!")""", |
|
R"""("Hello World")""", |
|
} |
|
); |
|
|
|
test_schema( |
|
"pattern with escapes", |
|
|
|
R"""({ |
|
"pattern": "^a\\^\\$\\.\\[\\]\\(\\)\\|\\{\\}\\*\\+\\?b$" |
|
})""", |
|
|
|
{ |
|
R"""("a^$.[]()|{}*+?b")""", |
|
}, |
|
|
|
{ |
|
R"""("ab")""", |
|
} |
|
); |
|
|
|
test_schema( |
|
"", |
|
|
|
R"""( |
|
{ |
|
"type": ["array", "null"], |
|
"items": { "type": "string" } |
|
} |
|
)""", |
|
|
|
{ |
|
"null", |
|
"[]", |
|
"[\"123\"]", |
|
"[\"foo\", \"bar\"]", |
|
}, |
|
|
|
{ |
|
"", |
|
"[123]", |
|
"\"foo\"", |
|
"[\"foo\", 42]", |
|
} |
|
); |
|
|
|
test_schema( |
|
"min+max items", |
|
|
|
R"""({ |
|
"items": { |
|
"type": ["number", "integer"] |
|
}, |
|
"minItems": 3, |
|
"maxItems": 5 |
|
})""", |
|
|
|
{ |
|
R"""([1, 2, 3])""", |
|
R"""([1, 2, 3, 4])""", |
|
R"""([1, 2, 3, 4, 5])""", |
|
}, |
|
|
|
{ |
|
R"""([1, 2])""", |
|
R"""([1, 2, 3, 4, 5, 6])""", |
|
R"""(1)""", |
|
} |
|
); |
|
|
|
|
|
test_schema( |
|
"object properties", |
|
|
|
R"""({ |
|
"type": "object", |
|
"properties": { |
|
"number": { "type": "number" }, |
|
"street_name": { "type": "string" }, |
|
"street_type": { "enum": ["Street", "Avenue", "Boulevard"] } |
|
} |
|
})""", |
|
|
|
{ |
|
R"""({ "number": 1600, "street_name": "Pennsylvania", "street_type":"Avenue"})""", |
|
|
|
R"""({ "street_name": "Pennsylvania" })""", |
|
R"""({ "number": 1600, "street_name": "Pennsylvania" })""", |
|
|
|
R"""({})""", |
|
R"""({ "number": 1600, "street_name": "Pennsylvania", "street_type": "Avenue" })""", |
|
}, |
|
|
|
{ |
|
|
|
R"""({ "number": "1600", "street_name": "Pennsylvania", "street_type":"Avenue"})""", |
|
|
|
R"""({ "street_name": "Pennsylvania", "number": 1600 })""", |
|
|
|
R"""({ "number": "1600", "street_name": "Pennsylvania", "street_type":"Avenue"})""", |
|
|
|
R"""({ "number": 1600, "street_name": "Pennsylvania", "street_type":"Avenue", "direction":"NW"})""", |
|
|
|
} |
|
); |
|
|
|
test_schema( |
|
"additional properties can't override other properties", |
|
R"""({ |
|
"properties": { |
|
"a": {"type": "integer"}, |
|
"b": {"type": "integer"} |
|
}, |
|
"additionalProperties": true |
|
})""", |
|
|
|
{ |
|
R"""({"a": 42})""", |
|
R"""({"c": ""})""", |
|
R"""({"a": 42, "c": ""})""", |
|
R"""({"a_": ""})""", |
|
}, |
|
|
|
{ |
|
R"""()""", |
|
R"""({"a": ""})""", |
|
R"""({"a": "", "b": ""})""", |
|
} |
|
); |
|
|
|
|
|
test_schema( |
|
"object properties, additionalProperties: true", |
|
|
|
R"""({ |
|
"type": "object", |
|
"properties": { |
|
"number": { "type": "number" }, |
|
"street_name": { "type": "string" }, |
|
"street_type": { "enum": ["Street", "Avenue", "Boulevard"] } |
|
}, |
|
"additionalProperties": true |
|
})""", |
|
|
|
{ |
|
|
|
R"""({})""", |
|
R"""({"number":1600,"street_name":"Pennsylvania","street_type":"Avenue"})""", |
|
|
|
R"""({ "street_name": "Pennsylvania" })""", |
|
R"""({ "number": 1600, "street_name": "Pennsylvania" })""", |
|
|
|
R"""({ "number": 1600, "street_name": "Pennsylvania", "street_type":"Avenue", "direction":"NW"})""", |
|
R"""({ "number": 1600, "street_name": "Pennsylvania", "street_type": "Avenue" })""", |
|
}, |
|
|
|
{ |
|
|
|
R"""({ "number": "1600", "street_name": "Pennsylvania", "street_type":"Avenue"})""", |
|
|
|
R"""({ "street_name": "Pennsylvania", "number": 1600, "street_type":"Avenue"})""", |
|
} |
|
); |
|
|
|
|
|
test_schema( |
|
"required + optional props each in original order", |
|
|
|
R"""({ |
|
"type": "object", |
|
"properties": { |
|
"number": { "type": "number" }, |
|
"street_name": { "type": "string" }, |
|
"street_type": { "enum": ["Street", "Avenue", "Boulevard"] } |
|
}, |
|
"additionalProperties": false |
|
})""", |
|
|
|
{ |
|
R"""({ "street_name": "Pennsylvania" })""", |
|
R"""({ "number": 1600, "street_type":"Avenue"})""", |
|
R"""({ "number": 1600, "street_name": "Pennsylvania" })""", |
|
R"""({ "number": 1600, "street_name": "Pennsylvania", "street_type":"Avenue"})""", |
|
|
|
R"""({ "number": 1600, "street_name": "Pennsylvania", "street_type": "Avenue" })""", |
|
}, |
|
|
|
{ |
|
|
|
R"""({ "street_type": "Avenue", "number": 1600 })""", |
|
|
|
R"""({ "number": 1600, "street_name": "Pennsylvania", "street_type": "Avenue", "direction": "NW" })""", |
|
} |
|
); |
|
|
|
test_schema( |
|
"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"""({"b": "foo", "a": "bar"})""", |
|
R"""({"b":"foo","a":"bar","d":"qux"})""", |
|
R"""({"b":"foo", "a":"bar", "d":"qux", "c":"baz"})""", |
|
}, |
|
|
|
{ |
|
R"""({"a": "foo", "b": "bar"})""", |
|
R"""({"b": "bar"})""", |
|
R"""({"a": "foo", "c": "baz"})""", |
|
R"""({"a":"foo", "b":"bar", "c":"baz", "d":"qux"})""", |
|
} |
|
); |
|
|
|
|
|
test_schema( |
|
"required props", |
|
|
|
R"""({ |
|
"$schema": "https://json-schema.org/draft/2020-12/schema", |
|
"$id": "https://example.com/product.schema.json", |
|
"title": "Product", |
|
"description": "A product from Acme's catalog", |
|
"type": "object", |
|
"properties": { |
|
"productId": { |
|
"description": "The unique identifier for a product", |
|
"type": "integer" |
|
}, |
|
"productName": { |
|
"description": "Name of the product", |
|
"type": "string" |
|
}, |
|
"price": { |
|
"description": "The price of the product", |
|
"type": "number", |
|
"exclusiveMinimum": 0 |
|
}, |
|
"tags": { |
|
"description": "Tags for the product", |
|
"type": "array", |
|
"items": { |
|
"type": "string" |
|
}, |
|
"minItems": 1, |
|
"uniqueItems": true |
|
}, |
|
"dimensions": { |
|
"type": "object", |
|
"properties": { |
|
"length": { |
|
"type": "number" |
|
}, |
|
"width": { |
|
"type": "number" |
|
}, |
|
"height": { |
|
"type": "number" |
|
} |
|
}, |
|
"required": [ "length", "width", "height" ] |
|
} |
|
}, |
|
"required": [ "productId", "productName", "price" ] |
|
})""", |
|
|
|
{ |
|
R"""({"productId": 1, "productName": "A green door", "price": 12.50})""", |
|
R"""({"productId": 1, "productName": "A green door", "price": 12.50, "tags": ["home", "green"]})""", |
|
R"""({"productId": 1, "productName": "A green door", "price": 12.50, "tags": ["home", "green"], "dimensions": {"length": 785, "width": 250.5, "height": -0.359}})""", |
|
}, |
|
|
|
{ |
|
R"""({})""", |
|
R"""({"productName": "A green door", "price": 12.50, "productId": 1})""", |
|
|
|
|
|
|
|
R"""({"productId": 1, "productName": "A green door"})""", |
|
R"""({"productName": "A green door", "price": 12.50})""", |
|
R"""({"productId": 1, "productName": "A green door", "price": 12.50, "tags": []})""", |
|
R"""({"productId": 1, "productName": "A green door", "price": 12.50, "dimensions": {"length": 785, "width": 250.5, "height": -0.359}, "tags": ["home", "green"]})""", |
|
|
|
|
|
} |
|
); |
|
} |
|
|
|
int main() { |
|
fprintf(stdout, "Running grammar integration tests...\n"); |
|
test_simple_grammar(); |
|
test_complex_grammar(); |
|
test_special_chars(); |
|
test_quantifiers(); |
|
test_failure_missing_root(); |
|
test_failure_missing_reference(); |
|
test_failure_left_recursion(); |
|
test_json_schema(); |
|
fprintf(stdout, "All tests passed.\n"); |
|
return 0; |
|
} |
|
|