char-fork/llama.cpp

git clone https://git.t4t.associates/char-fork/llama.cpp

Aldehir Rojascommon : implement common_schema internal representation for JSON schemas (#28736)acecd5603

char/aliceai-t5-moe
24.3 KiB513 linesraw
1#include "json-schema.h"
2#include "json.h"
3#include "testing.h"
4
5#include <cstdlib>
6#include <iostream>
7#include <stdexcept>
8#include <string>
9#include <utility>
10
11static common_chat_schema_document parse(const std::string & schema) {
12    return common_chat_schema_from_json(common_json::parse(schema));
13}
14
15// the node as T, aborting the current test when it is some other kind
16template <typename T>
17static const T & as(testing & t, const common_chat_schema * node, const char * what) {
18    const T * typed = dynamic_cast<const T *>(node);
19    if (!t.assert_true(std::string(what) + " has the expected kind", typed != nullptr)) {
20        throw std::runtime_error(std::string(what) + " has the wrong kind");
21    }
22    return *typed;
23}
24
25template <typename T>
26static const T & root(testing & t, const common_chat_schema_document & doc) {
27    return as<T>(t, doc.root.get(), "root");
28}
29
30static void assert_error(testing & t, const std::string & schema, const std::string & needle) {
31    try {
32        parse(schema);
33        t.assert_true(schema + " is rejected", false);
34    } catch (const std::runtime_error & e) {
35        std::string what = e.what();
36        t.assert_true(schema + " -> " + what, what.find(needle) != std::string::npos);
37    }
38}
39
40static void test_any(testing & t) {
41    t.test("empty schema", [](testing & t) {
42        auto doc = parse("{}");
43        root<common_chat_schema_any>(t, doc);
44        t.assert_true("no refs", doc.refs.empty());
45    });
46
47    t.test("keywords that do not imply a type", [](testing & t) {
48        auto doc = parse(R"({"description": "x", "format": "email", "additionalProperties": true})");
49        root<common_chat_schema_any>(t, doc);
50    });
51}
52
53static void test_primitives(testing & t) {
54    t.test("null, boolean, number", [](testing & t) {
55        auto doc_null = parse(R"({"type": "null"})");
56        root<common_chat_schema_null>(t, doc_null);
57        auto doc_bool = parse(R"({"type": "boolean"})");
58        root<common_chat_schema_boolean>(t, doc_bool);
59        auto doc_num = parse(R"({"type": "number", "minimum": 1, "maximum": 2})");
60        root<common_chat_schema_number>(t, doc_num);
61    });
62}
63
64static void test_integer(testing & t) {
65    t.test("unbounded", [](testing & t) {
66        auto doc = parse(R"({"type": "integer"})");
67        const auto & i = root<common_chat_schema_integer>(t, doc);
68        t.assert_equal("minimum", INT64_MIN, i.minimum);
69        t.assert_equal("maximum", INT64_MAX, i.maximum);
70    });
71
72    t.test("inclusive bounds", [](testing & t) {
73        auto doc = parse(R"({"type": "integer", "minimum": -5, "maximum": 10})");
74        const auto & i = root<common_chat_schema_integer>(t, doc);
75        t.assert_equal("minimum", -5, i.minimum);
76        t.assert_equal("maximum", 10, i.maximum);
77    });
78
79    t.test("exclusive bounds are folded", [](testing & t) {
80        auto doc = parse(R"({"type": "integer", "exclusiveMinimum": 0, "exclusiveMaximum": 10})");
81        const auto & i = root<common_chat_schema_integer>(t, doc);
82        t.assert_equal("minimum", 1, i.minimum);
83        t.assert_equal("maximum", 9, i.maximum);
84    });
85
86    t.test("fractional bounds round inwards", [](testing & t) {
87        auto doc = parse(R"({"type": "integer", "minimum": 1.5, "exclusiveMaximum": 9.5})");
88        const auto & i = root<common_chat_schema_integer>(t, doc);
89        t.assert_equal("minimum", 2, i.minimum);
90        t.assert_equal("maximum", 9, i.maximum);
91    });
92}
93
94static void test_string(testing & t) {
95    t.test("defaults", [](testing & t) {
96        auto doc = parse(R"({"type": "string"})");
97        const auto & s = root<common_chat_schema_string>(t, doc);
98        t.assert_equal("pattern", "", s.pattern);
99        t.assert_equal("format", common_chat_schema::FORMAT_NONE, s.format);
100        t.assert_equal("min_length", 0, s.min_length);
101        t.assert_equal("max_length", -1, s.max_length);
102    });
103
104    t.test("all keywords are kept", [](testing & t) {
105        auto doc = parse(R"({"type": "string", "pattern": "^[a-z]+$", "format": "date", "minLength": 2, "maxLength": 8})");
106        const auto & s = root<common_chat_schema_string>(t, doc);
107        t.assert_equal("pattern", "^[a-z]+$", s.pattern);
108        t.assert_equal("format", common_chat_schema::FORMAT_DATE, s.format);
109        t.assert_equal("min_length", 2, s.min_length);
110        t.assert_equal("max_length", 8, s.max_length);
111    });
112
113    t.test("formats", [](testing & t) {
114        auto expect = [&](const char * format, common_chat_schema::string_format expected) {
115            auto doc = parse(std::string(R"({"type": "string", "format": ")") + format + "\"}");
116            t.assert_equal(format, expected, root<common_chat_schema_string>(t, doc).format);
117        };
118        expect("time",      common_chat_schema::FORMAT_TIME);
119        expect("date-time", common_chat_schema::FORMAT_DATE_TIME);
120        expect("uuid",      common_chat_schema::FORMAT_UUID);
121        expect("uuid5",     common_chat_schema::FORMAT_UUID);
122        expect("email",     common_chat_schema::FORMAT_NONE);
123    });
124
125    t.test("pattern, length and known format imply a string", [](testing & t) {
126        auto doc_pattern = parse(R"({"pattern": "^a$"})");
127        t.assert_equal("pattern", "^a$", root<common_chat_schema_string>(t, doc_pattern).pattern);
128        auto doc_length = parse(R"({"minLength": 1, "maxLength": 3})");
129        t.assert_equal("min_length", 1, root<common_chat_schema_string>(t, doc_length).min_length);
130        t.assert_equal("max_length", 3, root<common_chat_schema_string>(t, doc_length).max_length);
131        auto doc_format = parse(R"({"format": "uuid"})");
132        t.assert_equal("format", common_chat_schema::FORMAT_UUID, root<common_chat_schema_string>(t, doc_format).format);
133    });
134}
135
136static void test_array(testing & t) {
137    t.test("items with bounds", [](testing & t) {
138        auto doc = parse(R"({"type": "array", "items": {"type": "integer"}, "minItems": 1, "maxItems": 3})");
139        const auto & a = root<common_chat_schema_array>(t, doc);
140        as<common_chat_schema_integer>(t, a.items.get(), "items");
141        t.assert_equal("min_items", 1, a.min_items);
142        t.assert_equal("max_items", 3, a.max_items);
143    });
144
145    t.test("no items", [](testing & t) {
146        auto doc = parse(R"({"type": "array"})");
147        const auto & a = root<common_chat_schema_array>(t, doc);
148        as<common_chat_schema_any>(t, a.items.get(), "items");
149        t.assert_equal("min_items", 0, a.min_items);
150        t.assert_equal("max_items", -1, a.max_items);
151    });
152
153    t.test("items imply an array", [](testing & t) {
154        auto doc = parse(R"({"items": {"type": "string"}})");
155        const auto & a = root<common_chat_schema_array>(t, doc);
156        as<common_chat_schema_string>(t, a.items.get(), "items");
157    });
158}
159
160static void test_tuple(testing & t) {
161    t.test("prefixItems", [](testing & t) {
162        auto doc = parse(R"({"prefixItems": [{"type": "string"}, {"type": "number"}]})");
163        const auto & tup = root<common_chat_schema_tuple>(t, doc);
164        t.assert_equal("size", (size_t) 2, tup.items.size());
165        as<common_chat_schema_string>(t, tup.items[0].get(), "items[0]");
166        as<common_chat_schema_number>(t, tup.items[1].get(), "items[1]");
167    });
168
169    t.test("items as an array", [](testing & t) {
170        auto doc = parse(R"({"type": "array", "items": [{"type": "boolean"}]})");
171        const auto & tup = root<common_chat_schema_tuple>(t, doc);
172        t.assert_equal("size", (size_t) 1, tup.items.size());
173        as<common_chat_schema_boolean>(t, tup.items[0].get(), "items[0]");
174    });
175}
176
177static void test_object(testing & t) {
178    t.test("type alone accepts any object", [](testing & t) {
179        auto doc = parse(R"({"type": "object"})");
180        const auto & o = root<common_chat_schema_object>(t, doc);
181        t.assert_true("no properties", o.properties.empty());
182        as<common_chat_schema_any>(t, o.additional_properties.get(), "additional_properties");
183    });
184
185    t.test("properties", [](testing & t) {
186        auto doc = parse(R"({
187            "type": "object",
188            "properties": {
189                "b": {"type": "string"},
190                "a": {"type": "integer"},
191                "c": {"type": "boolean"}
192            },
193            "required": ["a", "c"]
194        })");
195        const auto & o = root<common_chat_schema_object>(t, doc);
196        t.assert_equal("size", (size_t) 3, o.properties.size());
197        t.assert_equal("order", "b", o.properties[0].name);
198        t.assert_equal("order", "a", o.properties[1].name);
199        t.assert_equal("order", "c", o.properties[2].name);
200        t.assert_true("b optional", !o.properties[0].required);
201        t.assert_true("a required", o.properties[1].required);
202        t.assert_true("c required", o.properties[2].required);
203        as<common_chat_schema_string>(t, o.properties[0].schema.get(), "b");
204        as<common_chat_schema_integer>(t, o.properties[1].schema.get(), "a");
205        as<common_chat_schema_boolean>(t, o.properties[2].schema.get(), "c");
206        t.assert_true("closed", o.additional_properties == nullptr);
207    });
208
209    t.test("unknown required entries are ignored", [](testing & t) {
210        auto doc = parse(R"({"properties": {"a": {}}, "required": ["a", "zzz", 1]})");
211        const auto & o = root<common_chat_schema_object>(t, doc);
212        t.assert_equal("size", (size_t) 1, o.properties.size());
213        t.assert_true("a required", o.properties[0].required);
214    });
215
216    t.test("additionalProperties false implies an object", [](testing & t) {
217        auto doc = parse(R"({"additionalProperties": false})");
218        const auto & o = root<common_chat_schema_object>(t, doc);
219        t.assert_true("no properties", o.properties.empty());
220        t.assert_true("closed", o.additional_properties == nullptr);
221    });
222
223    t.test("additionalProperties schema", [](testing & t) {
224        auto doc = parse(R"({"properties": {"a": {}}, "additionalProperties": {"type": "integer", "minimum": 0}})");
225        const auto & o = root<common_chat_schema_object>(t, doc);
226        t.assert_equal("size", (size_t) 1, o.properties.size());
227        const auto & v = as<common_chat_schema_integer>(t, o.additional_properties.get(), "additional_properties");
228        t.assert_equal("minimum", 0, v.minimum);
229    });
230
231    t.test("nested", [](testing & t) {
232        auto doc = parse(R"({"properties": {"inner": {"properties": {"leaf": {"type": "null"}}, "required": ["leaf"]}}})");
233        const auto & o = root<common_chat_schema_object>(t, doc);
234        const auto & inner = as<common_chat_schema_object>(t, o.properties[0].schema.get(), "inner");
235        t.assert_equal("leaf name", "leaf", inner.properties[0].name);
236        t.assert_true("leaf required", inner.properties[0].required);
237        as<common_chat_schema_null>(t, inner.properties[0].schema.get(), "leaf");
238    });
239}
240
241static void test_const_enum(testing & t) {
242    t.test("const", [](testing & t) {
243        auto doc = parse(R"({"const": {"a": [1, null]}})");
244        t.assert_equal("value", R"({"a":[1,null]})", root<common_chat_schema_const>(t, doc).value.dump());
245    });
246
247    t.test("enum", [](testing & t) {
248        auto doc = parse(R"({"enum": ["a", 1, null, true]})");
249        const auto & e = root<common_chat_schema_enum>(t, doc);
250        t.assert_equal("size", (size_t) 4, e.values.size());
251        t.assert_equal("values[0]", "\"a\"", e.values[0].dump());
252        t.assert_equal("values[1]", "1", e.values[1].dump());
253        t.assert_equal("values[2]", "null", e.values[2].dump());
254        t.assert_equal("values[3]", "true", e.values[3].dump());
255    });
256
257    t.test("const wins over enum, enum wins over type", [](testing & t) {
258        auto doc_enum = parse(R"({"type": "integer", "enum": [1, 2]})");
259        root<common_chat_schema_enum>(t, doc_enum);
260        auto doc_const = parse(R"({"type": "string", "const": "x", "enum": ["y"]})");
261        t.assert_equal("value", "\"x\"", root<common_chat_schema_const>(t, doc_const).value.dump());
262    });
263}
264
265static void test_any_of(testing & t) {
266    t.test("anyOf and oneOf", [](testing & t) {
267        auto doc_any = parse(R"({"anyOf": [{"type": "string"}, {"type": "number"}]})");
268        const auto & u = root<common_chat_schema_any_of>(t, doc_any);
269        t.assert_equal("size", (size_t) 2, u.children.size());
270        as<common_chat_schema_string>(t, u.children[0].get(), "children[0]");
271        as<common_chat_schema_number>(t, u.children[1].get(), "children[1]");
272
273        auto doc_one = parse(R"({"oneOf": [{"type": "null"}]})");
274        const auto & o = root<common_chat_schema_any_of>(t, doc_one);
275        t.assert_equal("size", (size_t) 1, o.children.size());
276        as<common_chat_schema_null>(t, o.children[0].get(), "children[0]");
277    });
278
279    t.test("oneOf wins over anyOf and type", [](testing & t) {
280        auto doc = parse(R"({"type": "string", "oneOf": [{"type": "null"}], "anyOf": [{"type": "number"}, {"type": "boolean"}]})");
281        const auto & u = root<common_chat_schema_any_of>(t, doc);
282        t.assert_equal("size", (size_t) 1, u.children.size());
283        as<common_chat_schema_null>(t, u.children[0].get(), "children[0]");
284    });
285
286    t.test("type array expands with sibling keywords", [](testing & t) {
287        auto doc = parse(R"({"type": ["string", "null", "integer"], "minLength": 2, "minimum": 5})");
288        const auto & u = root<common_chat_schema_any_of>(t, doc);
289        t.assert_equal("size", (size_t) 3, u.children.size());
290        t.assert_equal("min_length", 2, as<common_chat_schema_string>(t, u.children[0].get(), "children[0]").min_length);
291        as<common_chat_schema_null>(t, u.children[1].get(), "children[1]");
292        t.assert_equal("minimum", 5, as<common_chat_schema_integer>(t, u.children[2].get(), "children[2]").minimum);
293    });
294}
295
296static void test_all_of(testing & t) {
297    t.test("components", [](testing & t) {
298        auto doc = parse(R"({"allOf": [{"properties": {"a": {}}}, {"anyOf": [{"properties": {"b": {}}}, {"type": "null"}]}]})");
299        const auto & all = root<common_chat_schema_all_of>(t, doc);
300        t.assert_equal("size", (size_t) 2, all.children.size());
301        as<common_chat_schema_object>(t, all.children[0].get(), "children[0]");
302        as<common_chat_schema_any_of>(t, all.children[1].get(), "children[1]");
303
304        auto doc_typed = parse(R"({"type": "object", "allOf": [{"properties": {"a": {}}}]})");
305        root<common_chat_schema_all_of>(t, doc_typed);
306    });
307
308    t.test("properties win over allOf", [](testing & t) {
309        auto doc = parse(R"({"type": "object", "properties": {"a": {}}, "allOf": [{"properties": {"b": {}}}]})");
310        t.assert_equal("size", (size_t) 1, root<common_chat_schema_object>(t, doc).properties.size());
311    });
312
313    t.test("other types ignore allOf", [](testing & t) {
314        auto doc = parse(R"({"type": "integer", "allOf": [{"minimum": 1}]})");
315        root<common_chat_schema_integer>(t, doc);
316    });
317}
318
319static void test_ref(testing & t) {
320    t.test("target is owned by the document", [](testing & t) {
321        auto doc = parse(R"({"$ref": "#/$defs/t", "type": "string", "$defs": {"t": {"type": "boolean"}}})");
322        const auto & r = root<common_chat_schema_ref>(t, doc);
323        t.assert_equal("ref", "#/$defs/t", r.ref);
324        t.assert_equal("refs", (size_t) 1, doc.refs.size());
325        t.assert_true("target", r.target != nullptr && r.target == doc.refs.at("#/$defs/t").get());
326        as<common_chat_schema_boolean>(t, r.target, "target");
327    });
328
329    t.test("definitions", [](testing & t) {
330        auto doc = parse(R"({"properties": {"a": {"$ref": "#/definitions/t"}}, "definitions": {"t": {"type": "number"}}})");
331        const auto & o = root<common_chat_schema_object>(t, doc);
332        const auto & r = as<common_chat_schema_ref>(t, o.properties[0].schema.get(), "a");
333        as<common_chat_schema_number>(t, r.target, "target");
334    });
335
336    t.test("recursive", [](testing & t) {
337        auto doc = parse(R"({
338            "$ref": "#/$defs/node",
339            "$defs": {
340                "node": {
341                    "type": "object",
342                    "properties": {
343                        "value": {"type": "number"},
344                        "next": {"$ref": "#/$defs/node"}
345                    },
346                    "required": ["value"]
347                }
348            }
349        })");
350        const auto & r = root<common_chat_schema_ref>(t, doc);
351        const auto & node = as<common_chat_schema_object>(t, r.target, "node");
352        t.assert_equal("properties", (size_t) 2, node.properties.size());
353        const auto & next = as<common_chat_schema_ref>(t, node.properties[1].schema.get(), "next");
354        t.assert_true("cycle", next.target == r.target);
355        t.assert_equal("refs", (size_t) 1, doc.refs.size());
356    });
357
358    t.test("pointer through an array", [](testing & t) {
359        auto doc = parse(R"({"oneOf": [{"type": "null"}, {"$ref": "#/oneOf/0"}]})");
360        const auto & u = root<common_chat_schema_any_of>(t, doc);
361        const auto & r = as<common_chat_schema_ref>(t, u.children[1].get(), "children[1]");
362        as<common_chat_schema_null>(t, r.target, "target");
363    });
364
365    t.test("targets survive moving the document", [](testing & t) {
366        auto parsed = parse(R"({"items": {"$ref": "#/$defs/t"}, "$defs": {"t": {"type": "null"}}})");
367        common_chat_schema_document doc = std::move(parsed);
368        const auto & a = root<common_chat_schema_array>(t, doc);
369        const auto & r = as<common_chat_schema_ref>(t, a.items.get(), "items");
370        t.assert_true("target", r.target == doc.refs.at("#/$defs/t").get());
371        as<common_chat_schema_null>(t, r.target, "target");
372    });
373}
374
375static void test_may_be_string(testing & t) {
376    auto check = [](testing & t, const std::string & schema, bool expected) {
377        t.assert_equal(schema, expected, parse(schema).root->may_be_string());
378    };
379
380    t.test("leaves", [&](testing & t) {
381        check(t, R"({"type": "string"})", true);
382        check(t, R"({"type": "integer"})", false);
383        check(t, R"({"minLength": 1})", true);
384        check(t, R"({"pattern": "^[a-z]+$"})", true);
385        check(t, R"({"const": "hello"})", true);
386        check(t, R"({"const": 123})", false);
387        check(t, R"({"enum": [1, "a", null]})", true);
388        check(t, R"({"enum": [1, 2, 3]})", false);
389    });
390
391    t.test("composites", [&](testing & t) {
392        check(t, R"({"type": ["integer", "string"]})", true);
393        check(t, R"({"anyOf": [{"type": "integer"}, {"type": "boolean"}]})", false);
394        check(t, R"({"allOf": [{"type": "string"}, {"minLength": 1}]})", true);
395        check(t, R"({"allOf": [{"type": "string"}, {"type": "integer"}]})", false);
396        check(t, R"({"allOf": [{"minLength": 1}, {"maxLength": 2}]})", true);
397    });
398
399    t.test("ref", [&](testing & t) {
400        check(t, R"({"$ref": "#/$defs/n", "$defs": {"n": {"anyOf": [{"$ref": "#/$defs/n"}, {"type": "string"}]}}})", true);
401        check(t, R"({"$ref": "#/$defs/n", "$defs": {"n": {"$ref": "#/$defs/n"}}})", false);
402        check(t, R"({"anyOf": [{"$ref": "#/$defs/a"}, {"$ref": "#/$defs/b"}], "$defs": {"a": {"allOf": [{"$ref": "#/$defs/b"}, {"type": "integer"}]}, "b": {"type": "string"}}})", true);
403    });
404}
405
406// e.g. {number, integer}, in type order
407static std::string dump(const common_chat_schema::type_set & types) {
408    static const common_chat_schema::value_type order[] = { common_chat_schema::TYPE_NULL,   common_chat_schema::TYPE_BOOLEAN, common_chat_schema::TYPE_NUMBER,
409                                                            common_chat_schema::TYPE_INTEGER, common_chat_schema::TYPE_STRING,  common_chat_schema::TYPE_ARRAY,
410                                                            common_chat_schema::TYPE_OBJECT };
411    std::string out;
412    for (auto type : order) {
413        if (types.has(type)) {
414            out += (out.empty() ? "" : ", ") + std::string(common_chat_schema::type_name(type));
415        }
416    }
417    return "{" + out + "}";
418}
419
420static void test_value_types(testing & t) {
421    auto check = [](testing & t, const std::string & schema, const common_chat_schema::type_set & expected) {
422        t.assert_equal(schema, dump(expected), dump(parse(schema).root->value_types()));
423    };
424
425    t.test("leaves", [&](testing & t) {
426        check(t, R"({"type": "string"})", { common_chat_schema::TYPE_STRING });
427        check(t, R"({"type": "number"})", { common_chat_schema::TYPE_NUMBER, common_chat_schema::TYPE_INTEGER });
428        check(t, R"({"description": "x"})", common_chat_schema::type_set::all());
429        check(t, R"({"properties": {"a": {"type": "string"}}})", { common_chat_schema::TYPE_OBJECT });
430        check(t, R"({"items": {"type": "string"}})", { common_chat_schema::TYPE_ARRAY });
431        check(t, R"({"const": 1.5})", { common_chat_schema::TYPE_NUMBER });
432        check(t, R"({"enum": [1, "a", null]})", { common_chat_schema::TYPE_INTEGER, common_chat_schema::TYPE_STRING, common_chat_schema::TYPE_NULL });
433    });
434
435    t.test("any_of is the union, all_of is the intersection", [&](testing & t) {
436        check(t, R"({"type": ["string", "null"]})", { common_chat_schema::TYPE_STRING, common_chat_schema::TYPE_NULL });
437        check(t, R"({"allOf": [{"type": ["string", "number"]}, {"type": ["number", "object"]}]})", { common_chat_schema::TYPE_NUMBER, common_chat_schema::TYPE_INTEGER });
438        check(t, R"({"allOf": [{"type": "string"}, {"type": "integer"}]})", {});
439    });
440
441    t.test("ref", [&](testing & t) {
442        check(t, R"({"$ref": "#/$defs/n", "$defs": {"n": {"anyOf": [{"$ref": "#/$defs/n"}, {"type": "string"}]}}})",
443              { common_chat_schema::TYPE_STRING });
444    });
445}
446
447static void test_errors(testing & t) {
448    t.test("not a schema", [](testing & t) {
449        assert_error(t, R"([])", "#: schema must be an object");
450    });
451
452    t.test("type", [](testing & t) {
453        assert_error(t, R"({"type": 5})", "#: type must be a string or an array of strings");
454        assert_error(t, R"({"type": []})", "#: type must not be empty");
455        assert_error(t, R"({"type": ["string", "bad"]})", "#/type/1: unrecognized type bad");
456    });
457
458    t.test("ref", [](testing & t) {
459        assert_error(t, R"({"$ref": 5})", "#: $ref must be a string");
460        assert_error(t, R"({"$ref": "https://example.com/x.json"})", "#: unsupported $ref https://example.com/x.json");
461        assert_error(t, R"({"$ref": ""})", "#: unsupported $ref ,");
462        assert_error(t, R"({"$ref": "#"})", "#: unsupported $ref #,");
463        assert_error(t, R"({"$defs": {}, "$ref": "#/$defs/missing"})", "#: cannot resolve $ref #/$defs/missing, missing not found");
464        assert_error(t, R"({"oneOf": [{}], "$ref": "#/oneOf/1"})", "#: cannot resolve $ref #/oneOf/1, 1 is out of range");
465        assert_error(t, R"({"$defs": {"a": {"$ref": "#/$defs/a/nope"}}, "$ref": "#/$defs/a"})", "#/$defs/a: cannot resolve $ref #/$defs/a/nope, nope not found");
466    });
467
468    t.test("alternatives", [](testing & t) {
469        assert_error(t, R"({"oneOf": []})", "#/oneOf: must not be empty");
470        assert_error(t, R"({"anyOf": {}})", "#/anyOf: must be an array of schemas");
471        assert_error(t, R"({"anyOf": [{"type": "string"}, {"items": {"type": "x"}}]})", "#/anyOf/1/items: unrecognized type x");
472    });
473
474    t.test("keywords", [](testing & t) {
475        assert_error(t, R"({"enum": []})", "#: enum must be a non-empty array");
476        assert_error(t, R"({"type": "string", "pattern": 5})", "#: pattern must be a string");
477        assert_error(t, R"({"type": "string", "minLength": -1})", "#: minLength must be a non-negative integer");
478        assert_error(t, R"({"type": "integer", "minimum": "1"})", "#: minimum must be a number");
479        assert_error(t, R"({"type": "array", "maxItems": 1.5})", "#: maxItems must be a non-negative integer");
480        assert_error(t, R"({"properties": []})", "#: properties must be an object");
481        assert_error(t, R"({"properties": {"a": {"type": "nope"}}})", "#/properties/a: unrecognized type nope");
482        assert_error(t, R"({"additionalProperties": null})", "#: additionalProperties must be a boolean or a schema");
483    });
484}
485
486int main(int argc, char * argv[]) {
487    testing t(std::cout);
488    if (argc >= 2) {
489        t.set_filter(argv[1]);
490    }
491
492    const char * verbose = getenv("LLAMA_TEST_VERBOSE");
493    if (verbose) {
494        t.verbose = std::string(verbose) == "1";
495    }
496
497    t.test("any", test_any);
498    t.test("primitives", test_primitives);
499    t.test("integer", test_integer);
500    t.test("string", test_string);
501    t.test("array", test_array);
502    t.test("tuple", test_tuple);
503    t.test("object", test_object);
504    t.test("const and enum", test_const_enum);
505    t.test("any_of", test_any_of);
506    t.test("all_of", test_all_of);
507    t.test("ref", test_ref);
508    t.test("may_be_string", test_may_be_string);
509    t.test("value_types", test_value_types);
510    t.test("errors", test_errors);
511
512    return t.summary();
513}