mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-09-17 20:31:47 +02:00
common : implement common_schema types
This commit is contained in:
@@ -84,6 +84,8 @@ add_library(${TARGET}
|
||||
imatrix-loader.cpp
|
||||
imatrix-loader.h
|
||||
json-schema-to-grammar.cpp
|
||||
json-schema.cpp
|
||||
json-schema.h
|
||||
json.cpp
|
||||
json.h
|
||||
llguidance.cpp
|
||||
|
||||
@@ -0,0 +1,338 @@
|
||||
#include "json-schema.h"
|
||||
#include "common.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
class common_schema_parser {
|
||||
const common_json & root_;
|
||||
common_schema_document doc_;
|
||||
|
||||
// ref nodes get their target once every $ref is parsed, a cycle would otherwise need it too early
|
||||
std::vector<common_schema_ref *> pending_;
|
||||
|
||||
[[noreturn]] static void fail(const std::string & path, const std::string & msg) {
|
||||
throw std::runtime_error("JSON schema error at " + path + ": " + msg);
|
||||
}
|
||||
|
||||
static int get_count(const common_json & schema, const std::string & key, const std::string & path, int def) {
|
||||
if (!schema.contains(key)) {
|
||||
return def;
|
||||
}
|
||||
const common_json & value = schema.at(key);
|
||||
if (!value.is_number_integer() || value.get<int>() < 0) {
|
||||
fail(path, key + " must be a non-negative integer");
|
||||
}
|
||||
return value.get<int>();
|
||||
}
|
||||
|
||||
// a fractional bound is rounded inwards, towards the integers it still admits
|
||||
static int64_t get_bound(const common_json & schema, const std::string & key, const std::string & path, bool round_up) {
|
||||
const common_json & value = schema.at(key);
|
||||
if (value.is_number_integer()) {
|
||||
return value.get<int64_t>();
|
||||
}
|
||||
if (!value.is_number()) {
|
||||
fail(path, key + " must be a number");
|
||||
}
|
||||
double d = value.get<double>();
|
||||
return (int64_t) (round_up ? std::ceil(d) : std::floor(d));
|
||||
}
|
||||
|
||||
static common_schema_format get_format(const common_json & schema, const std::string & path) {
|
||||
if (!schema.contains("format")) {
|
||||
return COMMON_SCHEMA_FORMAT_NONE;
|
||||
}
|
||||
const common_json & value = schema.at("format");
|
||||
if (!value.is_string()) {
|
||||
fail(path, "format must be a string");
|
||||
}
|
||||
std::string format = value.get<std::string>();
|
||||
if (format == "date") {
|
||||
return COMMON_SCHEMA_FORMAT_DATE;
|
||||
}
|
||||
if (format == "time") {
|
||||
return COMMON_SCHEMA_FORMAT_TIME;
|
||||
}
|
||||
if (format == "date-time") {
|
||||
return COMMON_SCHEMA_FORMAT_DATE_TIME;
|
||||
}
|
||||
if (format == "uuid" || (format.size() == 5 && format.compare(0, 4, "uuid") == 0 && format[4] >= '1' && format[4] <= '5')) {
|
||||
return COMMON_SCHEMA_FORMAT_UUID;
|
||||
}
|
||||
// any other format is a plain string
|
||||
return COMMON_SCHEMA_FORMAT_NONE;
|
||||
}
|
||||
|
||||
const common_json & resolve_ref(const std::string & ref, const std::string & path) {
|
||||
const common_json * target = &root_;
|
||||
auto tokens = string_split(ref.substr(1), "/");
|
||||
for (size_t i = 1; i < tokens.size(); i++) {
|
||||
const std::string & sel = tokens[i];
|
||||
if (target->is_object() && target->contains(sel)) {
|
||||
target = &target->at(sel);
|
||||
} else if (target->is_array()) {
|
||||
size_t idx;
|
||||
try {
|
||||
idx = std::stoull(sel);
|
||||
} catch (const std::logic_error &) {
|
||||
idx = target->size();
|
||||
}
|
||||
if (idx >= target->size()) {
|
||||
fail(path, "cannot resolve $ref " + ref + ", " + sel + " is out of range");
|
||||
}
|
||||
target = &target->at(idx);
|
||||
} else {
|
||||
fail(path, "cannot resolve $ref " + ref + ", " + sel + " not found");
|
||||
}
|
||||
}
|
||||
return *target;
|
||||
}
|
||||
|
||||
common_schema_ptr parse_ref(const common_json & value, const std::string & path) {
|
||||
if (!value.is_string()) {
|
||||
fail(path, "$ref must be a string");
|
||||
}
|
||||
std::string ref = value.get<std::string>();
|
||||
if (ref.compare(0, 2, "#/") != 0) {
|
||||
fail(path, "unsupported $ref " + ref + ", only references into the same document are supported");
|
||||
}
|
||||
if (doc_.refs.find(ref) == doc_.refs.end()) {
|
||||
// reserve the key first, so that a cycle back to this $ref stops here
|
||||
doc_.refs[ref] = nullptr;
|
||||
doc_.refs[ref] = parse_schema(resolve_ref(ref, path), ref);
|
||||
}
|
||||
auto node = std::make_unique<common_schema_ref>(ref);
|
||||
pending_.push_back(node.get());
|
||||
return node;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
common_schema_ptr parse_alternatives(const common_json & alts, const std::string & path) {
|
||||
if (!alts.is_array()) {
|
||||
fail(path, "must be an array of schemas");
|
||||
}
|
||||
if (alts.empty()) {
|
||||
fail(path, "must not be empty");
|
||||
}
|
||||
auto node = std::make_unique<T>();
|
||||
size_t i = 0;
|
||||
for (const auto & alt : alts) {
|
||||
node->children.push_back(parse_schema(alt, path + "/" + std::to_string(i++)));
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
common_schema_ptr parse_object(const common_json & schema, const std::string & path) {
|
||||
auto node = std::make_unique<common_schema_object>();
|
||||
|
||||
std::unordered_set<std::string> required;
|
||||
if (schema.contains("required") && schema.at("required").is_array()) {
|
||||
for (const auto & name : schema.at("required")) {
|
||||
if (name.is_string()) {
|
||||
required.insert(name.get<std::string>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (schema.contains("properties")) {
|
||||
const common_json & properties = schema.at("properties");
|
||||
if (!properties.is_object()) {
|
||||
fail(path, "properties must be an object");
|
||||
}
|
||||
for (const auto & [name, prop] : properties.items()) {
|
||||
node->properties.push_back({name, parse_schema(prop, path + "/properties/" + name), required.count(name) > 0});
|
||||
}
|
||||
}
|
||||
|
||||
if (schema.contains("additionalProperties")) {
|
||||
const common_json & additional = schema.at("additionalProperties");
|
||||
if (additional.is_boolean()) {
|
||||
if (additional.get<bool>()) {
|
||||
node->additional_properties = std::make_unique<common_schema_any>();
|
||||
}
|
||||
} else if (additional.is_object()) {
|
||||
node->additional_properties = parse_schema(additional, path + "/additionalProperties");
|
||||
} else {
|
||||
fail(path, "additionalProperties must be a boolean or a schema");
|
||||
}
|
||||
} else if (!schema.contains("properties")) {
|
||||
// {"type": "object"} on its own accepts any object
|
||||
node->additional_properties = std::make_unique<common_schema_any>();
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
common_schema_ptr parse_array(const common_json & schema, const std::string & path) {
|
||||
auto node = std::make_unique<common_schema_array>();
|
||||
if (schema.contains("items") || schema.contains("prefixItems")) {
|
||||
// "items" given as an array is the older spelling of "prefixItems", and wins when both are present
|
||||
const std::string key = schema.contains("items") ? "items" : "prefixItems";
|
||||
const common_json & items = schema.at(key);
|
||||
if (items.is_array()) {
|
||||
auto tuple = std::make_unique<common_schema_tuple>();
|
||||
size_t i = 0;
|
||||
for (const auto & item : items) {
|
||||
tuple->items.push_back(parse_schema(item, path + "/" + key + "/" + std::to_string(i++)));
|
||||
}
|
||||
return tuple;
|
||||
}
|
||||
if (key == "prefixItems") {
|
||||
fail(path, "prefixItems must be an array");
|
||||
}
|
||||
node->items = parse_schema(items, path + "/" + key);
|
||||
} else {
|
||||
node->items = std::make_unique<common_schema_any>();
|
||||
}
|
||||
node->min_items = get_count(schema, "minItems", path, 0);
|
||||
node->max_items = get_count(schema, "maxItems", path, -1);
|
||||
return node;
|
||||
}
|
||||
|
||||
common_schema_ptr parse_string(const common_json & schema, const std::string & path) {
|
||||
auto node = std::make_unique<common_schema_string>();
|
||||
if (schema.contains("pattern")) {
|
||||
const common_json & pattern = schema.at("pattern");
|
||||
if (!pattern.is_string()) {
|
||||
fail(path, "pattern must be a string");
|
||||
}
|
||||
node->pattern = pattern.get<std::string>();
|
||||
}
|
||||
node->format = get_format(schema, path);
|
||||
node->min_length = get_count(schema, "minLength", path, 0);
|
||||
node->max_length = get_count(schema, "maxLength", path, -1);
|
||||
return node;
|
||||
}
|
||||
|
||||
common_schema_ptr parse_integer(const common_json & schema, const std::string & path) {
|
||||
auto node = std::make_unique<common_schema_integer>();
|
||||
if (schema.contains("minimum")) {
|
||||
node->minimum = get_bound(schema, "minimum", path, /* round_up */ true);
|
||||
} else if (schema.contains("exclusiveMinimum")) {
|
||||
node->minimum = get_bound(schema, "exclusiveMinimum", path, /* round_up */ false) + 1;
|
||||
}
|
||||
if (schema.contains("maximum")) {
|
||||
node->maximum = get_bound(schema, "maximum", path, /* round_up */ false);
|
||||
} else if (schema.contains("exclusiveMaximum")) {
|
||||
node->maximum = get_bound(schema, "exclusiveMaximum", path, /* round_up */ true) - 1;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
common_schema_ptr parse_schema(const common_json & schema, const std::string & path) {
|
||||
if (!schema.is_object()) {
|
||||
fail(path, "schema must be an object");
|
||||
}
|
||||
if (schema.contains("$ref")) {
|
||||
return parse_ref(schema.at("$ref"), path);
|
||||
}
|
||||
if (schema.contains("oneOf") || schema.contains("anyOf")) {
|
||||
const std::string key = schema.contains("oneOf") ? "oneOf" : "anyOf";
|
||||
return parse_alternatives<common_schema_any_of>(schema.at(key), path + "/" + key);
|
||||
}
|
||||
|
||||
common_json type;
|
||||
if (schema.contains("type")) {
|
||||
type = schema.at("type");
|
||||
}
|
||||
if (type.is_array()) {
|
||||
// {"type": ["a", "b"], ...} is {"anyOf": [{"type": "a", ...}, {"type": "b", ...}]}
|
||||
if (type.empty()) {
|
||||
fail(path, "type must not be empty");
|
||||
}
|
||||
auto node = std::make_unique<common_schema_any_of>();
|
||||
size_t i = 0;
|
||||
for (const auto & t : type) {
|
||||
common_json alt = schema;
|
||||
alt["type"] = t;
|
||||
node->children.push_back(parse_schema(alt, path + "/type/" + std::to_string(i++)));
|
||||
}
|
||||
return node;
|
||||
}
|
||||
if (schema.contains("const")) {
|
||||
return std::make_unique<common_schema_const>(schema.at("const"));
|
||||
}
|
||||
if (schema.contains("enum")) {
|
||||
const common_json & values = schema.at("enum");
|
||||
if (!values.is_array() || values.empty()) {
|
||||
fail(path, "enum must be a non-empty array");
|
||||
}
|
||||
auto node = std::make_unique<common_schema_enum>();
|
||||
for (const auto & value : values) {
|
||||
node->values.push_back(value);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
if (!type.is_null() && !type.is_string()) {
|
||||
fail(path, "type must be a string or an array of strings");
|
||||
}
|
||||
|
||||
const std::string type_name = type.is_string() ? type.get<std::string>() : "";
|
||||
const bool has_properties = schema.contains("properties") ||
|
||||
(schema.contains("additionalProperties") && schema.at("additionalProperties") != true);
|
||||
|
||||
if (type_name.empty()) {
|
||||
// without a type the structural keywords decide, in the same order as the converter
|
||||
if (has_properties) {
|
||||
return parse_object(schema, path);
|
||||
}
|
||||
if (schema.contains("allOf")) {
|
||||
return parse_alternatives<common_schema_all_of>(schema.at("allOf"), path + "/allOf");
|
||||
}
|
||||
if (schema.contains("items") || schema.contains("prefixItems")) {
|
||||
return parse_array(schema, path);
|
||||
}
|
||||
if (schema.contains("pattern") || get_format(schema, path) != COMMON_SCHEMA_FORMAT_NONE) {
|
||||
return parse_string(schema, path);
|
||||
}
|
||||
return std::make_unique<common_schema_any>();
|
||||
}
|
||||
if (type_name == "object") {
|
||||
if (!has_properties && schema.contains("allOf")) {
|
||||
return parse_alternatives<common_schema_all_of>(schema.at("allOf"), path + "/allOf");
|
||||
}
|
||||
return parse_object(schema, path);
|
||||
}
|
||||
if (type_name == "string") {
|
||||
if (schema.contains("allOf")) {
|
||||
return parse_alternatives<common_schema_all_of>(schema.at("allOf"), path + "/allOf");
|
||||
}
|
||||
return parse_string(schema, path);
|
||||
}
|
||||
if (type_name == "array") {
|
||||
return parse_array(schema, path);
|
||||
}
|
||||
if (type_name == "integer") {
|
||||
return parse_integer(schema, path);
|
||||
}
|
||||
if (type_name == "number") {
|
||||
return std::make_unique<common_schema_number>();
|
||||
}
|
||||
if (type_name == "boolean") {
|
||||
return std::make_unique<common_schema_boolean>();
|
||||
}
|
||||
if (type_name == "null") {
|
||||
return std::make_unique<common_schema_null>();
|
||||
}
|
||||
fail(path, "unrecognized type " + type_name);
|
||||
}
|
||||
|
||||
public:
|
||||
explicit common_schema_parser(const common_json & root) : root_(root) {}
|
||||
|
||||
common_schema_document parse() {
|
||||
doc_.root = parse_schema(root_, "#");
|
||||
for (auto * node : pending_) {
|
||||
node->target = doc_.refs.at(node->ref).get();
|
||||
}
|
||||
return std::move(doc_);
|
||||
}
|
||||
};
|
||||
|
||||
common_schema_document common_schema_parse(const common_json & schema) {
|
||||
return common_schema_parser(schema).parse();
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
#pragma once
|
||||
|
||||
#include "json.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// JSON schema IR, covering the subset that json_schema_to_grammar() can convert.
|
||||
// A $ref becomes a common_schema_ref whose target is owned by the common_schema_document, so a recursive schema stays finite.
|
||||
|
||||
enum common_schema_kind {
|
||||
COMMON_SCHEMA_KIND_ANY,
|
||||
COMMON_SCHEMA_KIND_REF,
|
||||
COMMON_SCHEMA_KIND_ANY_OF,
|
||||
COMMON_SCHEMA_KIND_ALL_OF,
|
||||
COMMON_SCHEMA_KIND_CONST,
|
||||
COMMON_SCHEMA_KIND_ENUM,
|
||||
COMMON_SCHEMA_KIND_NULL,
|
||||
COMMON_SCHEMA_KIND_BOOLEAN,
|
||||
COMMON_SCHEMA_KIND_NUMBER,
|
||||
COMMON_SCHEMA_KIND_INTEGER,
|
||||
COMMON_SCHEMA_KIND_STRING,
|
||||
COMMON_SCHEMA_KIND_ARRAY,
|
||||
COMMON_SCHEMA_KIND_TUPLE,
|
||||
COMMON_SCHEMA_KIND_OBJECT,
|
||||
};
|
||||
|
||||
enum common_schema_format {
|
||||
COMMON_SCHEMA_FORMAT_NONE,
|
||||
COMMON_SCHEMA_FORMAT_UUID, // uuid, uuid1 .. uuid5
|
||||
COMMON_SCHEMA_FORMAT_DATE,
|
||||
COMMON_SCHEMA_FORMAT_TIME,
|
||||
COMMON_SCHEMA_FORMAT_DATE_TIME,
|
||||
};
|
||||
|
||||
// Base class for all nodes, the concrete ones are the common_schema_* structs below
|
||||
struct common_schema {
|
||||
virtual ~common_schema() = default;
|
||||
virtual common_schema_kind kind() const = 0;
|
||||
};
|
||||
|
||||
using common_schema_ptr = std::unique_ptr<common_schema>;
|
||||
|
||||
// {} or a schema with no recognized keywords: any JSON value
|
||||
struct common_schema_any : common_schema {
|
||||
common_schema_kind kind() const override { return COMMON_SCHEMA_KIND_ANY; }
|
||||
};
|
||||
|
||||
// {"$ref": "#/..."}, only references into the same document are supported
|
||||
struct common_schema_ref : common_schema {
|
||||
std::string ref;
|
||||
const common_schema * target = nullptr; // owned by common_schema_document::refs
|
||||
|
||||
explicit common_schema_ref(std::string ref) : ref(std::move(ref)) {}
|
||||
|
||||
common_schema_kind kind() const override { return COMMON_SCHEMA_KIND_REF; }
|
||||
};
|
||||
|
||||
// oneOf / anyOf, or a "type" array expanded to one alternative per type
|
||||
struct common_schema_any_of : common_schema {
|
||||
std::vector<common_schema_ptr> children;
|
||||
|
||||
common_schema_kind kind() const override { return COMMON_SCHEMA_KIND_ANY_OF; }
|
||||
};
|
||||
|
||||
struct common_schema_all_of : common_schema {
|
||||
std::vector<common_schema_ptr> children;
|
||||
|
||||
common_schema_kind kind() const override { return COMMON_SCHEMA_KIND_ALL_OF; }
|
||||
};
|
||||
|
||||
struct common_schema_const : common_schema {
|
||||
common_json value;
|
||||
|
||||
explicit common_schema_const(common_json value) : value(std::move(value)) {}
|
||||
|
||||
common_schema_kind kind() const override { return COMMON_SCHEMA_KIND_CONST; }
|
||||
};
|
||||
|
||||
struct common_schema_enum : common_schema {
|
||||
std::vector<common_json> values;
|
||||
|
||||
common_schema_kind kind() const override { return COMMON_SCHEMA_KIND_ENUM; }
|
||||
};
|
||||
|
||||
struct common_schema_null : common_schema {
|
||||
common_schema_kind kind() const override { return COMMON_SCHEMA_KIND_NULL; }
|
||||
};
|
||||
|
||||
struct common_schema_boolean : common_schema {
|
||||
common_schema_kind kind() const override { return COMMON_SCHEMA_KIND_BOOLEAN; }
|
||||
};
|
||||
|
||||
struct common_schema_number : common_schema {
|
||||
common_schema_kind kind() const override { return COMMON_SCHEMA_KIND_NUMBER; }
|
||||
};
|
||||
|
||||
// bounds are inclusive, exclusiveMinimum / exclusiveMaximum are folded in
|
||||
struct common_schema_integer : common_schema {
|
||||
int64_t minimum = INT64_MIN; // INT64_MIN for unbounded
|
||||
int64_t maximum = INT64_MAX; // INT64_MAX for unbounded
|
||||
|
||||
common_schema_kind kind() const override { return COMMON_SCHEMA_KIND_INTEGER; }
|
||||
};
|
||||
|
||||
struct common_schema_string : common_schema {
|
||||
std::string pattern; // empty when absent
|
||||
common_schema_format format = COMMON_SCHEMA_FORMAT_NONE;
|
||||
int min_length = 0;
|
||||
int max_length = -1; // -1 for unbounded
|
||||
|
||||
common_schema_kind kind() const override { return COMMON_SCHEMA_KIND_STRING; }
|
||||
};
|
||||
|
||||
struct common_schema_array : common_schema {
|
||||
common_schema_ptr items; // a common_schema_any when "items" is absent
|
||||
int min_items = 0;
|
||||
int max_items = -1; // -1 for unbounded
|
||||
|
||||
common_schema_kind kind() const override { return COMMON_SCHEMA_KIND_ARRAY; }
|
||||
};
|
||||
|
||||
// "prefixItems", or "items" given as an array: one schema per position
|
||||
struct common_schema_tuple : common_schema {
|
||||
std::vector<common_schema_ptr> items;
|
||||
|
||||
common_schema_kind kind() const override { return COMMON_SCHEMA_KIND_TUPLE; }
|
||||
};
|
||||
|
||||
struct common_schema_property {
|
||||
std::string name;
|
||||
common_schema_ptr schema;
|
||||
bool required = false;
|
||||
};
|
||||
|
||||
struct common_schema_object : common_schema {
|
||||
std::vector<common_schema_property> properties; // in schema order
|
||||
common_schema_ptr additional_properties; // null when not allowed
|
||||
|
||||
common_schema_kind kind() const override { return COMMON_SCHEMA_KIND_OBJECT; }
|
||||
};
|
||||
|
||||
// A parsed schema: its root, and the target of every $ref it reaches keyed by the $ref string
|
||||
struct common_schema_document {
|
||||
common_schema_ptr root;
|
||||
std::map<std::string, common_schema_ptr> refs;
|
||||
};
|
||||
|
||||
// Parses a JSON schema into a document.
|
||||
// Throws std::runtime_error when the schema falls outside the supported subset.
|
||||
common_schema_document common_schema_parse(const common_json & schema);
|
||||
@@ -262,6 +262,7 @@ endif()
|
||||
llama_build_and_test(test-chat-peg-parser.cpp peg-parser/simple-tokenize.cpp)
|
||||
llama_build_and_test(test-jinja.cpp)
|
||||
llama_test(test-jinja NAME test-jinja-py ARGS -py LABEL python)
|
||||
llama_build_and_test(test-json-schema.cpp)
|
||||
llama_build_and_test(test-chat-auto-parser.cpp WORKING_DIRECTORY ${PROJECT_SOURCE_DIR})
|
||||
llama_build_and_test(test-chat-template.cpp)
|
||||
# debug tool for chat template differential analysis (not registered as a test, run it manually)
|
||||
|
||||
@@ -0,0 +1,624 @@
|
||||
#include "json-schema.h"
|
||||
#include "json.h"
|
||||
#include "testing.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
static common_schema_document parse(const std::string & schema) {
|
||||
return common_schema_parse(common_json::parse(schema));
|
||||
}
|
||||
|
||||
// the node as T, aborting the current test when it is some other kind
|
||||
template <typename T>
|
||||
static const T & as(testing & t, const common_schema * node, const std::string & what) {
|
||||
const T * typed = dynamic_cast<const T *>(node);
|
||||
if (!t.assert_true(what + " has the expected kind", typed != nullptr)) {
|
||||
throw std::runtime_error(what + " has the wrong kind");
|
||||
}
|
||||
return *typed;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
static const T & root(testing & t, const common_schema_document & doc) {
|
||||
return as<T>(t, doc.root.get(), "root");
|
||||
}
|
||||
|
||||
static void assert_error(testing & t, const std::string & schema, const std::string & needle) {
|
||||
try {
|
||||
parse(schema);
|
||||
t.assert_true(schema + " is rejected", false);
|
||||
} catch (const std::runtime_error & e) {
|
||||
std::string what = e.what();
|
||||
t.assert_true(schema + " -> " + what, what.find(needle) != std::string::npos);
|
||||
}
|
||||
}
|
||||
|
||||
static void test_any(testing & t) {
|
||||
t.test("empty schema", [](testing & t) {
|
||||
auto doc = parse("{}");
|
||||
root<common_schema_any>(t, doc);
|
||||
t.assert_true("no refs", doc.refs.empty());
|
||||
});
|
||||
|
||||
t.test("unrecognized keywords only", [](testing & t) {
|
||||
auto doc = parse(R"({"description": "x", "title": "y", "default": 1})");
|
||||
root<common_schema_any>(t, doc);
|
||||
});
|
||||
|
||||
t.test("unknown format without a type", [](testing & t) {
|
||||
auto doc = parse(R"({"format": "email"})");
|
||||
root<common_schema_any>(t, doc);
|
||||
});
|
||||
|
||||
t.test("length keywords do not imply a string", [](testing & t) {
|
||||
auto doc = parse(R"({"minLength": 3, "maxLength": 5})");
|
||||
root<common_schema_any>(t, doc);
|
||||
});
|
||||
|
||||
t.test("additionalProperties true alone", [](testing & t) {
|
||||
auto doc = parse(R"({"additionalProperties": true})");
|
||||
root<common_schema_any>(t, doc);
|
||||
});
|
||||
}
|
||||
|
||||
static void test_primitives(testing & t) {
|
||||
t.test("null", [](testing & t) {
|
||||
auto doc = parse(R"({"type": "null"})");
|
||||
root<common_schema_null>(t, doc);
|
||||
});
|
||||
|
||||
t.test("boolean", [](testing & t) {
|
||||
auto doc = parse(R"({"type": "boolean"})");
|
||||
root<common_schema_boolean>(t, doc);
|
||||
});
|
||||
|
||||
t.test("number", [](testing & t) {
|
||||
auto doc = parse(R"({"type": "number"})");
|
||||
root<common_schema_number>(t, doc);
|
||||
});
|
||||
|
||||
t.test("number ignores bounds", [](testing & t) {
|
||||
auto doc = parse(R"({"type": "number", "minimum": 1, "maximum": 2})");
|
||||
root<common_schema_number>(t, doc);
|
||||
});
|
||||
}
|
||||
|
||||
static void test_integer(testing & t) {
|
||||
t.test("unbounded", [](testing & t) {
|
||||
auto doc = parse(R"({"type": "integer"})");
|
||||
const auto & i = root<common_schema_integer>(t, doc);
|
||||
t.assert_equal("minimum", INT64_MIN, i.minimum);
|
||||
t.assert_equal("maximum", INT64_MAX, i.maximum);
|
||||
});
|
||||
|
||||
t.test("inclusive bounds", [](testing & t) {
|
||||
auto doc = parse(R"({"type": "integer", "minimum": -5, "maximum": 10})");
|
||||
const auto & i = root<common_schema_integer>(t, doc);
|
||||
t.assert_equal("minimum", -5, i.minimum);
|
||||
t.assert_equal("maximum", 10, i.maximum);
|
||||
});
|
||||
|
||||
t.test("exclusive bounds are folded", [](testing & t) {
|
||||
auto doc = parse(R"({"type": "integer", "exclusiveMinimum": 0, "exclusiveMaximum": 10})");
|
||||
const auto & i = root<common_schema_integer>(t, doc);
|
||||
t.assert_equal("minimum", 1, i.minimum);
|
||||
t.assert_equal("maximum", 9, i.maximum);
|
||||
});
|
||||
|
||||
t.test("inclusive bound wins over exclusive", [](testing & t) {
|
||||
auto doc = parse(R"({"type": "integer", "minimum": 3, "exclusiveMinimum": 0, "maximum": 7, "exclusiveMaximum": 100})");
|
||||
const auto & i = root<common_schema_integer>(t, doc);
|
||||
t.assert_equal("minimum", 3, i.minimum);
|
||||
t.assert_equal("maximum", 7, i.maximum);
|
||||
});
|
||||
|
||||
t.test("fractional bounds round inwards", [](testing & t) {
|
||||
auto doc = parse(R"({"type": "integer", "minimum": 1.5, "maximum": 9.5})");
|
||||
const auto & i = root<common_schema_integer>(t, doc);
|
||||
t.assert_equal("minimum", 2, i.minimum);
|
||||
t.assert_equal("maximum", 9, i.maximum);
|
||||
});
|
||||
|
||||
t.test("fractional exclusive bounds round inwards", [](testing & t) {
|
||||
auto doc = parse(R"({"type": "integer", "exclusiveMinimum": 1.5, "exclusiveMaximum": 9.5})");
|
||||
const auto & i = root<common_schema_integer>(t, doc);
|
||||
t.assert_equal("minimum", 2, i.minimum);
|
||||
t.assert_equal("maximum", 9, i.maximum);
|
||||
});
|
||||
|
||||
t.test("integral floats as exclusive bounds", [](testing & t) {
|
||||
auto doc = parse(R"({"type": "integer", "exclusiveMinimum": 2.0, "exclusiveMaximum": 9.0})");
|
||||
const auto & i = root<common_schema_integer>(t, doc);
|
||||
t.assert_equal("minimum", 3, i.minimum);
|
||||
t.assert_equal("maximum", 8, i.maximum);
|
||||
});
|
||||
}
|
||||
|
||||
static void test_string(testing & t) {
|
||||
t.test("defaults", [](testing & t) {
|
||||
auto doc = parse(R"({"type": "string"})");
|
||||
const auto & s = root<common_schema_string>(t, doc);
|
||||
t.assert_equal("pattern", "", s.pattern);
|
||||
t.assert_equal("format", COMMON_SCHEMA_FORMAT_NONE, s.format);
|
||||
t.assert_equal("min_length", 0, s.min_length);
|
||||
t.assert_equal("max_length", -1, s.max_length);
|
||||
});
|
||||
|
||||
t.test("all keywords are kept", [](testing & t) {
|
||||
auto doc = parse(R"({"type": "string", "pattern": "^[a-z]+$", "format": "date", "minLength": 2, "maxLength": 8})");
|
||||
const auto & s = root<common_schema_string>(t, doc);
|
||||
t.assert_equal("pattern", "^[a-z]+$", s.pattern);
|
||||
t.assert_equal("format", COMMON_SCHEMA_FORMAT_DATE, s.format);
|
||||
t.assert_equal("min_length", 2, s.min_length);
|
||||
t.assert_equal("max_length", 8, s.max_length);
|
||||
});
|
||||
|
||||
t.test("formats", [](testing & t) {
|
||||
auto expect = [&](const char * format, common_schema_format expected) {
|
||||
auto doc = parse(std::string(R"({"type": "string", "format": ")") + format + "\"}");
|
||||
t.assert_equal(format, expected, root<common_schema_string>(t, doc).format);
|
||||
};
|
||||
expect("date", COMMON_SCHEMA_FORMAT_DATE);
|
||||
expect("time", COMMON_SCHEMA_FORMAT_TIME);
|
||||
expect("date-time", COMMON_SCHEMA_FORMAT_DATE_TIME);
|
||||
expect("uuid", COMMON_SCHEMA_FORMAT_UUID);
|
||||
expect("uuid1", COMMON_SCHEMA_FORMAT_UUID);
|
||||
expect("uuid5", COMMON_SCHEMA_FORMAT_UUID);
|
||||
expect("uuid6", COMMON_SCHEMA_FORMAT_NONE);
|
||||
expect("email", COMMON_SCHEMA_FORMAT_NONE);
|
||||
expect("uri", COMMON_SCHEMA_FORMAT_NONE);
|
||||
});
|
||||
|
||||
t.test("pattern implies a string", [](testing & t) {
|
||||
auto doc = parse(R"({"pattern": "^a$"})");
|
||||
t.assert_equal("pattern", "^a$", root<common_schema_string>(t, doc).pattern);
|
||||
});
|
||||
|
||||
t.test("known format implies a string", [](testing & t) {
|
||||
auto doc = parse(R"({"format": "uuid"})");
|
||||
t.assert_equal("format", COMMON_SCHEMA_FORMAT_UUID, root<common_schema_string>(t, doc).format);
|
||||
});
|
||||
}
|
||||
|
||||
static void test_array(testing & t) {
|
||||
t.test("items with bounds", [](testing & t) {
|
||||
auto doc = parse(R"({"type": "array", "items": {"type": "integer"}, "minItems": 1, "maxItems": 3})");
|
||||
const auto & a = root<common_schema_array>(t, doc);
|
||||
as<common_schema_integer>(t, a.items.get(), "items");
|
||||
t.assert_equal("min_items", 1, a.min_items);
|
||||
t.assert_equal("max_items", 3, a.max_items);
|
||||
});
|
||||
|
||||
t.test("no items", [](testing & t) {
|
||||
auto doc = parse(R"({"type": "array"})");
|
||||
const auto & a = root<common_schema_array>(t, doc);
|
||||
as<common_schema_any>(t, a.items.get(), "items");
|
||||
t.assert_equal("min_items", 0, a.min_items);
|
||||
t.assert_equal("max_items", -1, a.max_items);
|
||||
});
|
||||
|
||||
t.test("bounds without items", [](testing & t) {
|
||||
auto doc = parse(R"({"type": "array", "minItems": 2})");
|
||||
const auto & a = root<common_schema_array>(t, doc);
|
||||
as<common_schema_any>(t, a.items.get(), "items");
|
||||
t.assert_equal("min_items", 2, a.min_items);
|
||||
});
|
||||
|
||||
t.test("items imply an array", [](testing & t) {
|
||||
auto doc = parse(R"({"items": {"type": "string"}})");
|
||||
const auto & a = root<common_schema_array>(t, doc);
|
||||
as<common_schema_string>(t, a.items.get(), "items");
|
||||
});
|
||||
}
|
||||
|
||||
static void test_tuple(testing & t) {
|
||||
t.test("prefixItems", [](testing & t) {
|
||||
auto doc = parse(R"({"prefixItems": [{"type": "string"}, {"type": "number"}]})");
|
||||
const auto & tup = root<common_schema_tuple>(t, doc);
|
||||
t.assert_equal("size", (size_t) 2, tup.items.size());
|
||||
as<common_schema_string>(t, tup.items[0].get(), "items[0]");
|
||||
as<common_schema_number>(t, tup.items[1].get(), "items[1]");
|
||||
});
|
||||
|
||||
t.test("items as an array", [](testing & t) {
|
||||
auto doc = parse(R"({"type": "array", "items": [{"type": "boolean"}]})");
|
||||
const auto & tup = root<common_schema_tuple>(t, doc);
|
||||
t.assert_equal("size", (size_t) 1, tup.items.size());
|
||||
as<common_schema_boolean>(t, tup.items[0].get(), "items[0]");
|
||||
});
|
||||
|
||||
t.test("items array wins over prefixItems", [](testing & t) {
|
||||
auto doc = parse(R"({"items": [{"type": "null"}], "prefixItems": [{"type": "string"}, {"type": "string"}]})");
|
||||
const auto & tup = root<common_schema_tuple>(t, doc);
|
||||
t.assert_equal("size", (size_t) 1, tup.items.size());
|
||||
as<common_schema_null>(t, tup.items[0].get(), "items[0]");
|
||||
});
|
||||
|
||||
t.test("empty", [](testing & t) {
|
||||
auto doc = parse(R"({"prefixItems": []})");
|
||||
t.assert_true("no items", root<common_schema_tuple>(t, doc).items.empty());
|
||||
});
|
||||
}
|
||||
|
||||
static void test_object(testing & t) {
|
||||
t.test("type alone accepts any object", [](testing & t) {
|
||||
auto doc = parse(R"({"type": "object"})");
|
||||
const auto & o = root<common_schema_object>(t, doc);
|
||||
t.assert_true("no properties", o.properties.empty());
|
||||
as<common_schema_any>(t, o.additional_properties.get(), "additional_properties");
|
||||
});
|
||||
|
||||
t.test("properties", [](testing & t) {
|
||||
auto doc = parse(R"({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"b": {"type": "string"},
|
||||
"a": {"type": "integer"},
|
||||
"c": {"type": "boolean"}
|
||||
},
|
||||
"required": ["a", "c"]
|
||||
})");
|
||||
const auto & o = root<common_schema_object>(t, doc);
|
||||
t.assert_equal("size", (size_t) 3, o.properties.size());
|
||||
t.assert_equal("order", "b", o.properties[0].name);
|
||||
t.assert_equal("order", "a", o.properties[1].name);
|
||||
t.assert_equal("order", "c", o.properties[2].name);
|
||||
t.assert_true("b optional", !o.properties[0].required);
|
||||
t.assert_true("a required", o.properties[1].required);
|
||||
t.assert_true("c required", o.properties[2].required);
|
||||
as<common_schema_string>(t, o.properties[0].schema.get(), "b");
|
||||
as<common_schema_integer>(t, o.properties[1].schema.get(), "a");
|
||||
as<common_schema_boolean>(t, o.properties[2].schema.get(), "c");
|
||||
t.assert_true("closed", o.additional_properties == nullptr);
|
||||
});
|
||||
|
||||
t.test("properties imply an object", [](testing & t) {
|
||||
auto doc = parse(R"({"properties": {"a": {}}})");
|
||||
const auto & o = root<common_schema_object>(t, doc);
|
||||
t.assert_equal("size", (size_t) 1, o.properties.size());
|
||||
t.assert_true("closed", o.additional_properties == nullptr);
|
||||
});
|
||||
|
||||
t.test("required entries that are not properties are ignored", [](testing & t) {
|
||||
auto doc = parse(R"({"properties": {"a": {}}, "required": ["a", "zzz", 1]})");
|
||||
const auto & o = root<common_schema_object>(t, doc);
|
||||
t.assert_equal("size", (size_t) 1, o.properties.size());
|
||||
t.assert_true("a required", o.properties[0].required);
|
||||
});
|
||||
|
||||
t.test("required that is not an array is ignored", [](testing & t) {
|
||||
auto doc = parse(R"({"properties": {"a": {}}, "required": "a"})");
|
||||
t.assert_true("a optional", !root<common_schema_object>(t, doc).properties[0].required);
|
||||
});
|
||||
|
||||
t.test("additionalProperties false", [](testing & t) {
|
||||
auto doc = parse(R"({"type": "object", "additionalProperties": false})");
|
||||
t.assert_true("closed", root<common_schema_object>(t, doc).additional_properties == nullptr);
|
||||
});
|
||||
|
||||
t.test("additionalProperties false implies an object", [](testing & t) {
|
||||
auto doc = parse(R"({"additionalProperties": false})");
|
||||
const auto & o = root<common_schema_object>(t, doc);
|
||||
t.assert_true("no properties", o.properties.empty());
|
||||
t.assert_true("closed", o.additional_properties == nullptr);
|
||||
});
|
||||
|
||||
t.test("additionalProperties true with properties", [](testing & t) {
|
||||
auto doc = parse(R"({"properties": {"a": {}}, "additionalProperties": true})");
|
||||
const auto & o = root<common_schema_object>(t, doc);
|
||||
t.assert_equal("size", (size_t) 1, o.properties.size());
|
||||
as<common_schema_any>(t, o.additional_properties.get(), "additional_properties");
|
||||
});
|
||||
|
||||
t.test("additionalProperties schema", [](testing & t) {
|
||||
auto doc = parse(R"({"additionalProperties": {"type": "integer", "minimum": 0}})");
|
||||
const auto & o = root<common_schema_object>(t, doc);
|
||||
t.assert_true("no properties", o.properties.empty());
|
||||
const auto & v = as<common_schema_integer>(t, o.additional_properties.get(), "additional_properties");
|
||||
t.assert_equal("minimum", 0, v.minimum);
|
||||
});
|
||||
|
||||
t.test("nested", [](testing & t) {
|
||||
auto doc = parse(R"({"properties": {"inner": {"properties": {"leaf": {"type": "null"}}, "required": ["leaf"]}}})");
|
||||
const auto & o = root<common_schema_object>(t, doc);
|
||||
const auto & inner = as<common_schema_object>(t, o.properties[0].schema.get(), "inner");
|
||||
t.assert_equal("leaf name", "leaf", inner.properties[0].name);
|
||||
t.assert_true("leaf required", inner.properties[0].required);
|
||||
as<common_schema_null>(t, inner.properties[0].schema.get(), "leaf");
|
||||
});
|
||||
}
|
||||
|
||||
static void test_const_enum(testing & t) {
|
||||
t.test("const", [](testing & t) {
|
||||
auto doc = parse(R"({"const": "x"})");
|
||||
t.assert_equal("value", "\"x\"", root<common_schema_const>(t, doc).value.dump());
|
||||
});
|
||||
|
||||
t.test("const object", [](testing & t) {
|
||||
auto doc = parse(R"({"const": {"a": [1, null]}})");
|
||||
t.assert_equal("value", R"({"a":[1,null]})", root<common_schema_const>(t, doc).value.dump());
|
||||
});
|
||||
|
||||
t.test("enum", [](testing & t) {
|
||||
auto doc = parse(R"({"enum": ["a", 1, null, true]})");
|
||||
const auto & e = root<common_schema_enum>(t, doc);
|
||||
t.assert_equal("size", (size_t) 4, e.values.size());
|
||||
t.assert_equal("values[0]", "\"a\"", e.values[0].dump());
|
||||
t.assert_equal("values[1]", "1", e.values[1].dump());
|
||||
t.assert_equal("values[2]", "null", e.values[2].dump());
|
||||
t.assert_equal("values[3]", "true", e.values[3].dump());
|
||||
});
|
||||
|
||||
t.test("const and enum win over type", [](testing & t) {
|
||||
auto doc_const = parse(R"({"type": "string", "const": "x"})");
|
||||
root<common_schema_const>(t, doc_const);
|
||||
auto doc_enum = parse(R"({"type": "integer", "enum": [1, 2]})");
|
||||
root<common_schema_enum>(t, doc_enum);
|
||||
});
|
||||
|
||||
t.test("const wins over enum", [](testing & t) {
|
||||
auto doc = parse(R"({"const": "x", "enum": ["y"]})");
|
||||
t.assert_equal("value", "\"x\"", root<common_schema_const>(t, doc).value.dump());
|
||||
});
|
||||
}
|
||||
|
||||
static void test_any_of(testing & t) {
|
||||
t.test("anyOf", [](testing & t) {
|
||||
auto doc = parse(R"({"anyOf": [{"type": "string"}, {"type": "number"}]})");
|
||||
const auto & u = root<common_schema_any_of>(t, doc);
|
||||
t.assert_equal("size", (size_t) 2, u.children.size());
|
||||
as<common_schema_string>(t, u.children[0].get(), "children[0]");
|
||||
as<common_schema_number>(t, u.children[1].get(), "children[1]");
|
||||
});
|
||||
|
||||
t.test("oneOf", [](testing & t) {
|
||||
auto doc = parse(R"({"oneOf": [{"type": "null"}]})");
|
||||
const auto & u = root<common_schema_any_of>(t, doc);
|
||||
t.assert_equal("size", (size_t) 1, u.children.size());
|
||||
as<common_schema_null>(t, u.children[0].get(), "children[0]");
|
||||
});
|
||||
|
||||
t.test("oneOf wins over anyOf and type", [](testing & t) {
|
||||
auto doc = parse(R"({"type": "string", "oneOf": [{"type": "null"}], "anyOf": [{"type": "number"}, {"type": "boolean"}]})");
|
||||
const auto & u = root<common_schema_any_of>(t, doc);
|
||||
t.assert_equal("size", (size_t) 1, u.children.size());
|
||||
as<common_schema_null>(t, u.children[0].get(), "children[0]");
|
||||
});
|
||||
|
||||
t.test("type array expands with sibling keywords", [](testing & t) {
|
||||
auto doc = parse(R"({"type": ["string", "null", "integer"], "minLength": 2, "minimum": 5})");
|
||||
const auto & u = root<common_schema_any_of>(t, doc);
|
||||
t.assert_equal("size", (size_t) 3, u.children.size());
|
||||
t.assert_equal("min_length", 2, as<common_schema_string>(t, u.children[0].get(), "children[0]").min_length);
|
||||
as<common_schema_null>(t, u.children[1].get(), "children[1]");
|
||||
t.assert_equal("minimum", 5, as<common_schema_integer>(t, u.children[2].get(), "children[2]").minimum);
|
||||
});
|
||||
|
||||
t.test("type array with structural keywords", [](testing & t) {
|
||||
auto doc = parse(R"({"type": ["object", "array"], "properties": {"a": {}}, "items": {"type": "null"}})");
|
||||
const auto & u = root<common_schema_any_of>(t, doc);
|
||||
t.assert_equal("size", (size_t) 2, u.children.size());
|
||||
t.assert_equal("properties", (size_t) 1, as<common_schema_object>(t, u.children[0].get(), "children[0]").properties.size());
|
||||
as<common_schema_null>(t, as<common_schema_array>(t, u.children[1].get(), "children[1]").items.get(), "items");
|
||||
});
|
||||
}
|
||||
|
||||
static void test_all_of(testing & t) {
|
||||
t.test("without a type", [](testing & t) {
|
||||
auto doc = parse(R"({"allOf": [{"properties": {"a": {}}}, {"properties": {"b": {}}}]})");
|
||||
const auto & all = root<common_schema_all_of>(t, doc);
|
||||
t.assert_equal("size", (size_t) 2, all.children.size());
|
||||
as<common_schema_object>(t, all.children[0].get(), "children[0]");
|
||||
as<common_schema_object>(t, all.children[1].get(), "children[1]");
|
||||
});
|
||||
|
||||
t.test("type string", [](testing & t) {
|
||||
auto doc = parse(R"({"type": "string", "allOf": [{"enum": ["a", "b"]}, {"enum": ["b", "c"]}]})");
|
||||
const auto & all = root<common_schema_all_of>(t, doc);
|
||||
t.assert_equal("size", (size_t) 2, all.children.size());
|
||||
as<common_schema_enum>(t, all.children[0].get(), "children[0]");
|
||||
});
|
||||
|
||||
t.test("type object without properties", [](testing & t) {
|
||||
auto doc = parse(R"({"type": "object", "allOf": [{"properties": {"a": {}}}]})");
|
||||
root<common_schema_all_of>(t, doc);
|
||||
});
|
||||
|
||||
t.test("properties win over allOf", [](testing & t) {
|
||||
auto doc = parse(R"({"type": "object", "properties": {"a": {}}, "allOf": [{"properties": {"b": {}}}]})");
|
||||
t.assert_equal("size", (size_t) 1, root<common_schema_object>(t, doc).properties.size());
|
||||
});
|
||||
|
||||
t.test("nested anyOf component", [](testing & t) {
|
||||
auto doc = parse(R"({"allOf": [{"anyOf": [{"properties": {"a": {}}}, {"properties": {"b": {}}}]}]})");
|
||||
const auto & all = root<common_schema_all_of>(t, doc);
|
||||
const auto & any = as<common_schema_any_of>(t, all.children[0].get(), "children[0]");
|
||||
t.assert_equal("size", (size_t) 2, any.children.size());
|
||||
});
|
||||
|
||||
t.test("other types ignore allOf", [](testing & t) {
|
||||
auto doc = parse(R"({"type": "integer", "allOf": [{"minimum": 1}]})");
|
||||
root<common_schema_integer>(t, doc);
|
||||
});
|
||||
}
|
||||
|
||||
static void test_ref(testing & t) {
|
||||
t.test("target is owned by the document", [](testing & t) {
|
||||
auto doc = parse(R"({"$ref": "#/$defs/t", "$defs": {"t": {"type": "boolean"}}})");
|
||||
const auto & r = root<common_schema_ref>(t, doc);
|
||||
t.assert_equal("ref", "#/$defs/t", r.ref);
|
||||
t.assert_equal("refs", (size_t) 1, doc.refs.size());
|
||||
t.assert_true("target", r.target != nullptr && r.target == doc.refs.at("#/$defs/t").get());
|
||||
as<common_schema_boolean>(t, r.target, "target");
|
||||
});
|
||||
|
||||
t.test("sibling keywords are ignored", [](testing & t) {
|
||||
auto doc = parse(R"({"$ref": "#/$defs/t", "type": "string", "$defs": {"t": {"type": "null"}}})");
|
||||
as<common_schema_null>(t, root<common_schema_ref>(t, doc).target, "target");
|
||||
});
|
||||
|
||||
t.test("definitions", [](testing & t) {
|
||||
auto doc = parse(R"({"properties": {"a": {"$ref": "#/definitions/t"}}, "definitions": {"t": {"type": "number"}}})");
|
||||
const auto & o = root<common_schema_object>(t, doc);
|
||||
const auto & r = as<common_schema_ref>(t, o.properties[0].schema.get(), "a");
|
||||
as<common_schema_number>(t, r.target, "target");
|
||||
});
|
||||
|
||||
t.test("same ref shares one target", [](testing & t) {
|
||||
auto doc = parse(R"({"properties": {"a": {"$ref": "#/$defs/t"}, "b": {"$ref": "#/$defs/t"}}, "$defs": {"t": {"type": "boolean"}}})");
|
||||
const auto & o = root<common_schema_object>(t, doc);
|
||||
const auto & ra = as<common_schema_ref>(t, o.properties[0].schema.get(), "a");
|
||||
const auto & rb = as<common_schema_ref>(t, o.properties[1].schema.get(), "b");
|
||||
t.assert_true("shared", ra.target != nullptr && ra.target == rb.target);
|
||||
t.assert_equal("refs", (size_t) 1, doc.refs.size());
|
||||
});
|
||||
|
||||
t.test("recursive", [](testing & t) {
|
||||
auto doc = parse(R"({
|
||||
"$ref": "#/$defs/node",
|
||||
"$defs": {
|
||||
"node": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"value": {"type": "number"},
|
||||
"next": {"$ref": "#/$defs/node"}
|
||||
},
|
||||
"required": ["value"]
|
||||
}
|
||||
}
|
||||
})");
|
||||
const auto & r = root<common_schema_ref>(t, doc);
|
||||
const auto & node = as<common_schema_object>(t, r.target, "node");
|
||||
t.assert_equal("properties", (size_t) 2, node.properties.size());
|
||||
const auto & next = as<common_schema_ref>(t, node.properties[1].schema.get(), "next");
|
||||
t.assert_true("cycle", next.target == r.target);
|
||||
t.assert_equal("refs", (size_t) 1, doc.refs.size());
|
||||
});
|
||||
|
||||
t.test("mutually recursive", [](testing & t) {
|
||||
auto doc = parse(R"({
|
||||
"$ref": "#/$defs/a",
|
||||
"$defs": {
|
||||
"a": {"properties": {"b": {"$ref": "#/$defs/b"}}},
|
||||
"b": {"properties": {"a": {"$ref": "#/$defs/a"}}}
|
||||
}
|
||||
})");
|
||||
const auto & ra = root<common_schema_ref>(t, doc);
|
||||
const auto & a = as<common_schema_object>(t, ra.target, "a");
|
||||
const auto & rb = as<common_schema_ref>(t, a.properties[0].schema.get(), "a.b");
|
||||
const auto & b = as<common_schema_object>(t, rb.target, "b");
|
||||
const auto & back = as<common_schema_ref>(t, b.properties[0].schema.get(), "b.a");
|
||||
t.assert_true("cycle", back.target == ra.target);
|
||||
t.assert_equal("refs", (size_t) 2, doc.refs.size());
|
||||
});
|
||||
|
||||
t.test("pointer through an array", [](testing & t) {
|
||||
auto doc = parse(R"({"oneOf": [{"type": "null"}, {"$ref": "#/oneOf/0"}]})");
|
||||
const auto & u = root<common_schema_any_of>(t, doc);
|
||||
const auto & r = as<common_schema_ref>(t, u.children[1].get(), "children[1]");
|
||||
as<common_schema_null>(t, r.target, "target");
|
||||
});
|
||||
|
||||
t.test("targets survive moving the document", [](testing & t) {
|
||||
auto parsed = parse(R"({"items": {"$ref": "#/$defs/t"}, "$defs": {"t": {"type": "null"}}})");
|
||||
common_schema_document doc = std::move(parsed);
|
||||
const auto & a = root<common_schema_array>(t, doc);
|
||||
const auto & r = as<common_schema_ref>(t, a.items.get(), "items");
|
||||
t.assert_true("target", r.target == doc.refs.at("#/$defs/t").get());
|
||||
as<common_schema_null>(t, r.target, "target");
|
||||
});
|
||||
}
|
||||
|
||||
static void test_errors(testing & t) {
|
||||
t.test("not a schema", [](testing & t) {
|
||||
assert_error(t, R"([])", "#: schema must be an object");
|
||||
assert_error(t, R"("string")", "#: schema must be an object");
|
||||
});
|
||||
|
||||
t.test("type", [](testing & t) {
|
||||
assert_error(t, R"({"type": "char"})", "#: unrecognized type char");
|
||||
assert_error(t, R"({"type": 5})", "#: type must be a string or an array of strings");
|
||||
assert_error(t, R"({"type": []})", "#: type must not be empty");
|
||||
assert_error(t, R"({"type": ["string", "bad"]})", "#/type/1: unrecognized type bad");
|
||||
});
|
||||
|
||||
t.test("ref", [](testing & t) {
|
||||
assert_error(t, R"({"$ref": 5})", "#: $ref must be a string");
|
||||
assert_error(t, R"({"$ref": "https://example.com/x.json"})", "#: unsupported $ref https://example.com/x.json");
|
||||
assert_error(t, R"({"$ref": "#"})", "#: unsupported $ref #");
|
||||
assert_error(t, R"({"$ref": "#/$defs/missing"})", "#: cannot resolve $ref #/$defs/missing, $defs not found");
|
||||
assert_error(t, R"({"$defs": {}, "$ref": "#/$defs/missing"})", "#: cannot resolve $ref #/$defs/missing, missing not found");
|
||||
assert_error(t, R"({"oneOf": [{}], "$ref": "#/oneOf/1"})", "#: cannot resolve $ref #/oneOf/1, 1 is out of range");
|
||||
assert_error(t, R"({"oneOf": [{}], "$ref": "#/oneOf/x"})", "#: cannot resolve $ref #/oneOf/x, x is out of range");
|
||||
assert_error(t, R"({"$defs": {"a": {"$ref": "#/$defs/a/nope"}}, "$ref": "#/$defs/a"})", "#/$defs/a: cannot resolve $ref #/$defs/a/nope, nope not found");
|
||||
});
|
||||
|
||||
t.test("alternatives", [](testing & t) {
|
||||
assert_error(t, R"({"oneOf": []})", "#/oneOf: must not be empty");
|
||||
assert_error(t, R"({"anyOf": {}})", "#/anyOf: must be an array of schemas");
|
||||
assert_error(t, R"({"allOf": []})", "#/allOf: must not be empty");
|
||||
assert_error(t, R"({"anyOf": [{"type": "string"}, {"items": {"type": "x"}}]})", "#/anyOf/1/items: unrecognized type x");
|
||||
});
|
||||
|
||||
t.test("enum", [](testing & t) {
|
||||
assert_error(t, R"({"enum": []})", "#: enum must be a non-empty array");
|
||||
assert_error(t, R"({"enum": "a"})", "#: enum must be a non-empty array");
|
||||
});
|
||||
|
||||
t.test("string", [](testing & t) {
|
||||
assert_error(t, R"({"type": "string", "pattern": 5})", "#: pattern must be a string");
|
||||
assert_error(t, R"({"type": "string", "format": 5})", "#: format must be a string");
|
||||
assert_error(t, R"({"type": "string", "minLength": -1})", "#: minLength must be a non-negative integer");
|
||||
assert_error(t, R"({"type": "string", "maxLength": "5"})", "#: maxLength must be a non-negative integer");
|
||||
});
|
||||
|
||||
t.test("integer", [](testing & t) {
|
||||
assert_error(t, R"({"type": "integer", "minimum": "1"})", "#: minimum must be a number");
|
||||
assert_error(t, R"({"type": "integer", "exclusiveMaximum": null})", "#: exclusiveMaximum must be a number");
|
||||
});
|
||||
|
||||
t.test("array", [](testing & t) {
|
||||
assert_error(t, R"({"type": "array", "maxItems": 1.5})", "#: maxItems must be a non-negative integer");
|
||||
assert_error(t, R"({"type": "array", "items": {"type": "x"}})", "#/items: unrecognized type x");
|
||||
assert_error(t, R"({"prefixItems": {}})", "#: prefixItems must be an array");
|
||||
assert_error(t, R"({"prefixItems": [{"type": "x"}]})", "#/prefixItems/0: unrecognized type x");
|
||||
});
|
||||
|
||||
t.test("object", [](testing & t) {
|
||||
assert_error(t, R"({"properties": []})", "#: properties must be an object");
|
||||
assert_error(t, R"({"properties": {"a": {"type": "nope"}}})", "#/properties/a: unrecognized type nope");
|
||||
assert_error(t, R"({"additionalProperties": null})", "#: additionalProperties must be a boolean or a schema");
|
||||
assert_error(t, R"({"additionalProperties": {"type": "x"}})", "#/additionalProperties: unrecognized type x");
|
||||
});
|
||||
}
|
||||
|
||||
int main(int argc, char * argv[]) {
|
||||
testing t(std::cout);
|
||||
if (argc >= 2) {
|
||||
t.set_filter(argv[1]);
|
||||
}
|
||||
|
||||
const char * verbose = getenv("LLAMA_TEST_VERBOSE");
|
||||
if (verbose) {
|
||||
t.verbose = std::string(verbose) == "1";
|
||||
}
|
||||
|
||||
t.test("any", test_any);
|
||||
t.test("primitives", test_primitives);
|
||||
t.test("integer", test_integer);
|
||||
t.test("string", test_string);
|
||||
t.test("array", test_array);
|
||||
t.test("tuple", test_tuple);
|
||||
t.test("object", test_object);
|
||||
t.test("const and enum", test_const_enum);
|
||||
t.test("any_of", test_any_of);
|
||||
t.test("all_of", test_all_of);
|
||||
t.test("ref", test_ref);
|
||||
t.test("errors", test_errors);
|
||||
|
||||
return t.summary();
|
||||
}
|
||||
Reference in New Issue
Block a user