mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-09-15 18:13:29 +02:00
1029 lines
41 KiB
C++
1029 lines
41 KiB
C++
#include "json-schema.h"
|
|
#include "common.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <map>
|
|
#include <set>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <unordered_set>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
class common_schema_parser {
|
|
const common_json & root_;
|
|
common_schema_document & doc_;
|
|
|
|
// the targets parsed here, moved into doc_ once the whole schema parsed
|
|
std::map<std::string, common_schema_ptr> refs_;
|
|
|
|
// 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() && refs_.find(ref) == refs_.end()) {
|
|
// reserve the key first, so that a cycle back to this $ref stops here
|
|
refs_[ref] = nullptr;
|
|
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" wins when both are present; as in the converter, a schema instead of an array is the item schema
|
|
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;
|
|
}
|
|
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:
|
|
common_schema_parser(const common_json & root, common_schema_document & doc) : root_(root), doc_(doc) {}
|
|
|
|
common_schema_ptr parse() {
|
|
auto node = parse_schema(root_, "#");
|
|
for (auto & entry : refs_) {
|
|
doc_.refs[entry.first] = std::move(entry.second);
|
|
}
|
|
for (auto * ref : pending_) {
|
|
ref->target = doc_.refs.at(ref->ref).get();
|
|
}
|
|
return node;
|
|
}
|
|
};
|
|
|
|
common_schema_document common_schema_parse(const common_json & schema) {
|
|
common_schema_document doc;
|
|
doc.root = common_schema_parser(schema, doc).parse();
|
|
return doc;
|
|
}
|
|
|
|
common_schema_ptr common_schema_parse(const common_json & schema, common_schema_document & doc) {
|
|
return common_schema_parser(schema, doc).parse();
|
|
}
|
|
|
|
class common_schema_optimizer {
|
|
common_schema_document & doc_;
|
|
|
|
// set when a $ref got replaced by the none its target became, the pass is then repeated
|
|
bool changed_ = false;
|
|
|
|
// the target pairs being intersected further up, a recursive schema would otherwise never bottom out
|
|
std::set<std::pair<const common_schema *, const common_schema *>> active_;
|
|
|
|
template <typename T>
|
|
static const T & as(const common_schema & node) {
|
|
return static_cast<const T &>(node);
|
|
}
|
|
|
|
template <typename T>
|
|
static T & as(common_schema & node) {
|
|
return static_cast<T &>(node);
|
|
}
|
|
|
|
static bool is(const common_schema & node, common_schema_kind kind) {
|
|
return node.kind() == kind;
|
|
}
|
|
|
|
static bool is(const common_schema_ptr & node, common_schema_kind kind) {
|
|
return node && node->kind() == kind;
|
|
}
|
|
|
|
static common_schema_ptr none() {
|
|
return std::make_unique<common_schema_none>();
|
|
}
|
|
|
|
// Follows a chain of $refs to a node of another kind.
|
|
// Gives nullptr for a target that is being rewritten right now, or a chain that loops back on itself.
|
|
const common_schema * resolve(const common_schema & node) const {
|
|
const common_schema * cur = &node;
|
|
for (size_t hops = 0; is(*cur, COMMON_SCHEMA_KIND_REF); hops++) {
|
|
if (hops > doc_.refs.size()) {
|
|
return nullptr;
|
|
}
|
|
auto it = doc_.refs.find(as<common_schema_ref>(*cur).ref);
|
|
if (it == doc_.refs.end() || !it->second) {
|
|
return nullptr;
|
|
}
|
|
cur = it->second.get();
|
|
}
|
|
return cur;
|
|
}
|
|
|
|
static std::vector<common_schema_ptr> clone_all(const std::vector<common_schema_ptr> & nodes) {
|
|
std::vector<common_schema_ptr> out;
|
|
for (const auto & node : nodes) {
|
|
out.push_back(clone(*node));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
static common_schema_ptr clone(const common_schema & node) {
|
|
switch (node.kind()) {
|
|
case COMMON_SCHEMA_KIND_ANY: return std::make_unique<common_schema_any>();
|
|
case COMMON_SCHEMA_KIND_NONE: return none();
|
|
case COMMON_SCHEMA_KIND_NULL: return std::make_unique<common_schema_null>();
|
|
case COMMON_SCHEMA_KIND_BOOLEAN: return std::make_unique<common_schema_boolean>();
|
|
case COMMON_SCHEMA_KIND_NUMBER: return std::make_unique<common_schema_number>();
|
|
case COMMON_SCHEMA_KIND_INTEGER: return std::make_unique<common_schema_integer>(as<common_schema_integer>(node));
|
|
case COMMON_SCHEMA_KIND_STRING: return std::make_unique<common_schema_string>(as<common_schema_string>(node));
|
|
case COMMON_SCHEMA_KIND_CONST: return std::make_unique<common_schema_const>(as<common_schema_const>(node).value);
|
|
case COMMON_SCHEMA_KIND_REF: {
|
|
const auto & ref = as<common_schema_ref>(node);
|
|
auto out = std::make_unique<common_schema_ref>(ref.ref);
|
|
out->target = ref.target;
|
|
return out;
|
|
}
|
|
case COMMON_SCHEMA_KIND_ENUM: {
|
|
auto out = std::make_unique<common_schema_enum>();
|
|
out->values = as<common_schema_enum>(node).values;
|
|
return out;
|
|
}
|
|
case COMMON_SCHEMA_KIND_ANY_OF: {
|
|
auto out = std::make_unique<common_schema_any_of>();
|
|
out->children = clone_all(as<common_schema_any_of>(node).children);
|
|
return out;
|
|
}
|
|
case COMMON_SCHEMA_KIND_ALL_OF: {
|
|
auto out = std::make_unique<common_schema_all_of>();
|
|
out->children = clone_all(as<common_schema_all_of>(node).children);
|
|
return out;
|
|
}
|
|
case COMMON_SCHEMA_KIND_ARRAY: {
|
|
const auto & arr = as<common_schema_array>(node);
|
|
auto out = std::make_unique<common_schema_array>();
|
|
out->items = clone(*arr.items);
|
|
out->min_items = arr.min_items;
|
|
out->max_items = arr.max_items;
|
|
return out;
|
|
}
|
|
case COMMON_SCHEMA_KIND_TUPLE: {
|
|
auto out = std::make_unique<common_schema_tuple>();
|
|
out->items = clone_all(as<common_schema_tuple>(node).items);
|
|
return out;
|
|
}
|
|
case COMMON_SCHEMA_KIND_OBJECT: {
|
|
const auto & obj = as<common_schema_object>(node);
|
|
auto out = std::make_unique<common_schema_object>();
|
|
for (const auto & prop : obj.properties) {
|
|
out->properties.push_back({prop.name, clone(*prop.schema), prop.required});
|
|
}
|
|
if (obj.additional_properties) {
|
|
out->additional_properties = clone(*obj.additional_properties);
|
|
}
|
|
return out;
|
|
}
|
|
}
|
|
return none();
|
|
}
|
|
|
|
static bool contains(const std::vector<common_json> & values, const common_json & value) {
|
|
return std::any_of(values.begin(), values.end(), [&](const common_json & v) { return v == value; });
|
|
}
|
|
|
|
// the values a const or enum accepts
|
|
static bool get_values(const common_schema & node, std::vector<common_json> & values) {
|
|
if (is(node, COMMON_SCHEMA_KIND_CONST)) {
|
|
values.push_back(as<common_schema_const>(node).value);
|
|
return true;
|
|
}
|
|
if (is(node, COMMON_SCHEMA_KIND_ENUM)) {
|
|
values = as<common_schema_enum>(node).values;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// a const for one value, none for no value at all
|
|
static common_schema_ptr make_values(std::vector<common_json> values) {
|
|
if (values.empty()) {
|
|
return none();
|
|
}
|
|
if (values.size() == 1) {
|
|
return std::make_unique<common_schema_const>(std::move(values[0]));
|
|
}
|
|
auto node = std::make_unique<common_schema_enum>();
|
|
node->values = std::move(values);
|
|
return node;
|
|
}
|
|
|
|
// The union of rewritten alternatives.
|
|
static common_schema_ptr make_any_of(std::vector<common_schema_ptr> children) {
|
|
std::vector<common_schema_ptr> flat;
|
|
for (auto & child : children) {
|
|
if (is(child, COMMON_SCHEMA_KIND_ANY_OF)) {
|
|
// a nested one is flat already
|
|
for (auto & c : as<common_schema_any_of>(*child).children) {
|
|
flat.push_back(std::move(c));
|
|
}
|
|
} else {
|
|
flat.push_back(std::move(child));
|
|
}
|
|
}
|
|
std::vector<common_schema_ptr> out;
|
|
for (auto & child : flat) {
|
|
if (is(child, COMMON_SCHEMA_KIND_ANY)) {
|
|
return std::make_unique<common_schema_any>();
|
|
}
|
|
if (!is(child, COMMON_SCHEMA_KIND_NONE)) {
|
|
out.push_back(std::move(child));
|
|
}
|
|
}
|
|
if (out.empty()) {
|
|
return none();
|
|
}
|
|
if (out.size() == 1) {
|
|
return std::move(out[0]);
|
|
}
|
|
auto node = std::make_unique<common_schema_any_of>();
|
|
node->children = std::move(out);
|
|
return node;
|
|
}
|
|
|
|
// An array with rewritten items.
|
|
static common_schema_ptr make_array(common_schema_ptr items, int min_items, int max_items) {
|
|
if ((max_items >= 0 && min_items > max_items) || (is(items, COMMON_SCHEMA_KIND_NONE) && min_items > 0)) {
|
|
return none();
|
|
}
|
|
if (max_items == 0 || is(items, COMMON_SCHEMA_KIND_NONE)) {
|
|
// only [] is left
|
|
return std::make_unique<common_schema_tuple>();
|
|
}
|
|
auto node = std::make_unique<common_schema_array>();
|
|
node->items = std::move(items);
|
|
node->min_items = min_items;
|
|
node->max_items = max_items;
|
|
return node;
|
|
}
|
|
|
|
// An object with rewritten property schemas.
|
|
static common_schema_ptr make_object(std::vector<common_schema_property> properties, common_schema_ptr additional) {
|
|
auto node = std::make_unique<common_schema_object>();
|
|
for (auto & prop : properties) {
|
|
if (is(prop.schema, COMMON_SCHEMA_KIND_NONE)) {
|
|
// it can never be present
|
|
if (prop.required) {
|
|
return none();
|
|
}
|
|
continue;
|
|
}
|
|
node->properties.push_back(std::move(prop));
|
|
}
|
|
if (additional && !is(additional, COMMON_SCHEMA_KIND_NONE)) {
|
|
node->additional_properties = std::move(additional);
|
|
}
|
|
return node;
|
|
}
|
|
|
|
std::vector<common_schema_ptr> rewrite_all(std::vector<common_schema_ptr> nodes) {
|
|
for (auto & node : nodes) {
|
|
node = rewrite(std::move(node));
|
|
}
|
|
return nodes;
|
|
}
|
|
|
|
// Rewrites a node bottom-up.
|
|
common_schema_ptr rewrite(common_schema_ptr node) {
|
|
switch (node->kind()) {
|
|
case COMMON_SCHEMA_KIND_ANY:
|
|
case COMMON_SCHEMA_KIND_NONE:
|
|
case COMMON_SCHEMA_KIND_CONST:
|
|
case COMMON_SCHEMA_KIND_NULL:
|
|
case COMMON_SCHEMA_KIND_BOOLEAN:
|
|
case COMMON_SCHEMA_KIND_NUMBER:
|
|
return node;
|
|
case COMMON_SCHEMA_KIND_REF: {
|
|
const common_schema * target = resolve(*node);
|
|
if (target && is(*target, COMMON_SCHEMA_KIND_NONE)) {
|
|
changed_ = true;
|
|
return none();
|
|
}
|
|
return node;
|
|
}
|
|
case COMMON_SCHEMA_KIND_ENUM: {
|
|
std::vector<common_json> values;
|
|
for (const auto & v : as<common_schema_enum>(*node).values) {
|
|
if (!contains(values, v)) {
|
|
values.push_back(v);
|
|
}
|
|
}
|
|
return make_values(std::move(values));
|
|
}
|
|
case COMMON_SCHEMA_KIND_INTEGER: {
|
|
const auto & i = as<common_schema_integer>(*node);
|
|
return i.minimum > i.maximum ? none() : std::move(node);
|
|
}
|
|
case COMMON_SCHEMA_KIND_STRING: {
|
|
const auto & s = as<common_schema_string>(*node);
|
|
return s.max_length >= 0 && s.min_length > s.max_length ? none() : std::move(node);
|
|
}
|
|
case COMMON_SCHEMA_KIND_ANY_OF:
|
|
return make_any_of(rewrite_all(std::move(as<common_schema_any_of>(*node).children)));
|
|
case COMMON_SCHEMA_KIND_ALL_OF:
|
|
return intersect_all(rewrite_all(std::move(as<common_schema_all_of>(*node).children)));
|
|
case COMMON_SCHEMA_KIND_ARRAY: {
|
|
auto & arr = as<common_schema_array>(*node);
|
|
return make_array(rewrite(std::move(arr.items)), arr.min_items, arr.max_items);
|
|
}
|
|
case COMMON_SCHEMA_KIND_TUPLE: {
|
|
auto & tup = as<common_schema_tuple>(*node);
|
|
tup.items = rewrite_all(std::move(tup.items));
|
|
for (const auto & item : tup.items) {
|
|
if (is(item, COMMON_SCHEMA_KIND_NONE)) {
|
|
return none();
|
|
}
|
|
}
|
|
return node;
|
|
}
|
|
case COMMON_SCHEMA_KIND_OBJECT: {
|
|
auto & obj = as<common_schema_object>(*node);
|
|
for (auto & prop : obj.properties) {
|
|
prop.schema = rewrite(std::move(prop.schema));
|
|
}
|
|
if (obj.additional_properties) {
|
|
obj.additional_properties = rewrite(std::move(obj.additional_properties));
|
|
}
|
|
return make_object(std::move(obj.properties), std::move(obj.additional_properties));
|
|
}
|
|
}
|
|
return node;
|
|
}
|
|
|
|
static common_schema_ptr make_all_of(std::vector<common_schema_ptr> children) {
|
|
if (children.size() == 1) {
|
|
return std::move(children[0]);
|
|
}
|
|
auto node = std::make_unique<common_schema_all_of>();
|
|
node->children = std::move(children);
|
|
return node;
|
|
}
|
|
|
|
// what is left when two nodes cannot be combined
|
|
static common_schema_ptr irreducible(const common_schema & a, const common_schema & b) {
|
|
std::vector<common_schema_ptr> children;
|
|
children.push_back(clone(a));
|
|
children.push_back(clone(b));
|
|
return make_all_of(std::move(children));
|
|
}
|
|
|
|
// The intersection of rewritten nodes, an allOf of those that could not be combined.
|
|
common_schema_ptr intersect_all(std::vector<common_schema_ptr> children) {
|
|
std::vector<common_schema_ptr> flat;
|
|
for (auto & child : children) {
|
|
if (is(child, COMMON_SCHEMA_KIND_ALL_OF)) {
|
|
for (auto & c : as<common_schema_all_of>(*child).children) {
|
|
flat.push_back(std::move(c));
|
|
}
|
|
} else {
|
|
flat.push_back(std::move(child));
|
|
}
|
|
}
|
|
|
|
std::vector<common_schema_ptr> residual;
|
|
for (auto & child : flat) {
|
|
bool merged = false;
|
|
for (auto & r : residual) {
|
|
auto cand = intersect(*r, *child);
|
|
if (is(cand, COMMON_SCHEMA_KIND_NONE)) {
|
|
return none();
|
|
}
|
|
if (!is(cand, COMMON_SCHEMA_KIND_ALL_OF)) {
|
|
r = std::move(cand);
|
|
merged = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!merged) {
|
|
residual.push_back(std::move(child));
|
|
}
|
|
}
|
|
if (residual.empty()) {
|
|
return std::make_unique<common_schema_any>();
|
|
}
|
|
return make_all_of(std::move(residual));
|
|
}
|
|
|
|
static const common_schema_property * find_property(const common_schema_object & obj, const std::string & name) {
|
|
for (const auto & prop : obj.properties) {
|
|
if (prop.name == name) {
|
|
return ∝
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
// Properties are merged the way json_schema_to_grammar() merges an allOf: a property the other
|
|
// side does not list survives, a side that is closed does not shut it out.
|
|
common_schema_ptr intersect_object(const common_schema_object & oa, const common_schema_object & ob) {
|
|
std::vector<common_schema_property> properties;
|
|
auto add = [&](const common_schema_object & x, const common_schema_object & y, bool skip_shared) {
|
|
for (const auto & px : x.properties) {
|
|
const auto * py = find_property(y, px.name);
|
|
if (py && skip_shared) {
|
|
continue;
|
|
}
|
|
common_schema_property prop;
|
|
prop.name = px.name;
|
|
prop.required = px.required || (py && py->required);
|
|
if (py) {
|
|
prop.schema = intersect(*px.schema, *py->schema);
|
|
} else if (y.additional_properties) {
|
|
prop.schema = intersect(*px.schema, *y.additional_properties);
|
|
} else {
|
|
prop.schema = clone(*px.schema);
|
|
}
|
|
properties.push_back(std::move(prop));
|
|
}
|
|
};
|
|
add(oa, ob, false);
|
|
add(ob, oa, true);
|
|
|
|
common_schema_ptr additional;
|
|
if (oa.additional_properties && ob.additional_properties) {
|
|
additional = intersect(*oa.additional_properties, *ob.additional_properties);
|
|
}
|
|
return make_object(std::move(properties), std::move(additional));
|
|
}
|
|
|
|
// The intersection of two rewritten nodes, an allOf of both where they cannot be combined.
|
|
common_schema_ptr intersect(const common_schema & a, const common_schema & b) {
|
|
if (is(a, COMMON_SCHEMA_KIND_ANY)) {
|
|
return clone(b);
|
|
}
|
|
if (is(b, COMMON_SCHEMA_KIND_ANY)) {
|
|
return clone(a);
|
|
}
|
|
if (is(a, COMMON_SCHEMA_KIND_NONE) || is(b, COMMON_SCHEMA_KIND_NONE)) {
|
|
return none();
|
|
}
|
|
if (is(a, COMMON_SCHEMA_KIND_REF) || is(b, COMMON_SCHEMA_KIND_REF)) {
|
|
if (is(a, COMMON_SCHEMA_KIND_REF) && is(b, COMMON_SCHEMA_KIND_REF) && as<common_schema_ref>(a).ref == as<common_schema_ref>(b).ref) {
|
|
return clone(a);
|
|
}
|
|
const common_schema * ta = resolve(a);
|
|
const common_schema * tb = resolve(b);
|
|
if (!ta || !tb) {
|
|
return irreducible(a, b);
|
|
}
|
|
auto key = std::make_pair(ta, tb);
|
|
if (!active_.insert(key).second) {
|
|
// the same pair is already being intersected further up, a recursive schema
|
|
return irreducible(a, b);
|
|
}
|
|
auto out = intersect(*ta, *tb);
|
|
active_.erase(key);
|
|
return out;
|
|
}
|
|
if (is(a, COMMON_SCHEMA_KIND_ANY_OF) || is(b, COMMON_SCHEMA_KIND_ANY_OF)) {
|
|
// (a1 | a2) & b = (a1 & b) | (a2 & b)
|
|
std::vector<common_schema_ptr> alts;
|
|
if (is(a, COMMON_SCHEMA_KIND_ANY_OF)) {
|
|
for (const auto & child : as<common_schema_any_of>(a).children) {
|
|
alts.push_back(intersect(*child, b));
|
|
}
|
|
} else {
|
|
for (const auto & child : as<common_schema_any_of>(b).children) {
|
|
alts.push_back(intersect(a, *child));
|
|
}
|
|
}
|
|
return make_any_of(std::move(alts));
|
|
}
|
|
if (is(a, COMMON_SCHEMA_KIND_ALL_OF) || is(b, COMMON_SCHEMA_KIND_ALL_OF)) {
|
|
std::vector<common_schema_ptr> parts;
|
|
for (const auto * node : {&a, &b}) {
|
|
if (is(*node, COMMON_SCHEMA_KIND_ALL_OF)) {
|
|
for (const auto & child : as<common_schema_all_of>(*node).children) {
|
|
parts.push_back(clone(*child));
|
|
}
|
|
} else {
|
|
parts.push_back(clone(*node));
|
|
}
|
|
}
|
|
return intersect_all(std::move(parts));
|
|
}
|
|
|
|
std::vector<common_json> va;
|
|
std::vector<common_json> vb;
|
|
bool values_a = get_values(a, va);
|
|
bool values_b = get_values(b, vb);
|
|
if (values_a && values_b) {
|
|
std::vector<common_json> common;
|
|
for (const auto & v : va) {
|
|
if (contains(vb, v)) {
|
|
common.push_back(v);
|
|
}
|
|
}
|
|
return make_values(std::move(common));
|
|
}
|
|
if (values_a || values_b) {
|
|
// whether a value fits a schema is not checked
|
|
return irreducible(a, b);
|
|
}
|
|
|
|
if (a.kind() != b.kind()) {
|
|
if (is(a, COMMON_SCHEMA_KIND_NUMBER) && is(b, COMMON_SCHEMA_KIND_INTEGER)) {
|
|
return clone(b);
|
|
}
|
|
if (is(a, COMMON_SCHEMA_KIND_INTEGER) && is(b, COMMON_SCHEMA_KIND_NUMBER)) {
|
|
return clone(a);
|
|
}
|
|
if ((is(a, COMMON_SCHEMA_KIND_ARRAY) && is(b, COMMON_SCHEMA_KIND_TUPLE)) || (is(a, COMMON_SCHEMA_KIND_TUPLE) && is(b, COMMON_SCHEMA_KIND_ARRAY))) {
|
|
return irreducible(a, b);
|
|
}
|
|
return none();
|
|
}
|
|
switch (a.kind()) {
|
|
case COMMON_SCHEMA_KIND_INTEGER: {
|
|
const auto & ia = as<common_schema_integer>(a);
|
|
const auto & ib = as<common_schema_integer>(b);
|
|
auto node = std::make_unique<common_schema_integer>();
|
|
node->minimum = std::max(ia.minimum, ib.minimum);
|
|
node->maximum = std::min(ia.maximum, ib.maximum);
|
|
return node->minimum > node->maximum ? none() : std::move(node);
|
|
}
|
|
case COMMON_SCHEMA_KIND_STRING: {
|
|
const auto & sa = as<common_schema_string>(a);
|
|
const auto & sb = as<common_schema_string>(b);
|
|
if (!sa.pattern.empty() && !sb.pattern.empty() && sa.pattern != sb.pattern) {
|
|
return irreducible(a, b);
|
|
}
|
|
if (sa.format != COMMON_SCHEMA_FORMAT_NONE && sb.format != COMMON_SCHEMA_FORMAT_NONE && sa.format != sb.format) {
|
|
return none();
|
|
}
|
|
auto node = std::make_unique<common_schema_string>();
|
|
node->pattern = sa.pattern.empty() ? sb.pattern : sa.pattern;
|
|
node->format = sa.format == COMMON_SCHEMA_FORMAT_NONE ? sb.format : sa.format;
|
|
node->min_length = std::max(sa.min_length, sb.min_length);
|
|
node->max_length = sa.max_length < 0 ? sb.max_length : sb.max_length < 0 ? sa.max_length : std::min(sa.max_length, sb.max_length);
|
|
return node->max_length >= 0 && node->min_length > node->max_length ? none() : std::move(node);
|
|
}
|
|
case COMMON_SCHEMA_KIND_ARRAY: {
|
|
const auto & aa = as<common_schema_array>(a);
|
|
const auto & ab = as<common_schema_array>(b);
|
|
int min_items = std::max(aa.min_items, ab.min_items);
|
|
int max_items = aa.max_items < 0 ? ab.max_items : ab.max_items < 0 ? aa.max_items : std::min(aa.max_items, ab.max_items);
|
|
return make_array(intersect(*aa.items, *ab.items), min_items, max_items);
|
|
}
|
|
case COMMON_SCHEMA_KIND_TUPLE: {
|
|
const auto & ta = as<common_schema_tuple>(a);
|
|
const auto & tb = as<common_schema_tuple>(b);
|
|
if (ta.items.size() != tb.items.size()) {
|
|
return none();
|
|
}
|
|
auto node = std::make_unique<common_schema_tuple>();
|
|
for (size_t i = 0; i < ta.items.size(); i++) {
|
|
auto item = intersect(*ta.items[i], *tb.items[i]);
|
|
if (is(item, COMMON_SCHEMA_KIND_NONE)) {
|
|
return none();
|
|
}
|
|
node->items.push_back(std::move(item));
|
|
}
|
|
return node;
|
|
}
|
|
case COMMON_SCHEMA_KIND_OBJECT:
|
|
return intersect_object(as<common_schema_object>(a), as<common_schema_object>(b));
|
|
default:
|
|
// null, boolean and number have no fields to differ in
|
|
return clone(a);
|
|
}
|
|
}
|
|
|
|
// Moves the refs the node reaches into live, and points each ref node at its target there.
|
|
void link(common_schema & node, std::map<std::string, common_schema_ptr> & live) {
|
|
switch (node.kind()) {
|
|
case COMMON_SCHEMA_KIND_REF: {
|
|
auto & ref = as<common_schema_ref>(node);
|
|
auto it = live.find(ref.ref);
|
|
if (it == live.end()) {
|
|
it = live.emplace(ref.ref, std::move(doc_.refs.at(ref.ref))).first;
|
|
link(*it->second, live);
|
|
}
|
|
ref.target = it->second.get();
|
|
return;
|
|
}
|
|
case COMMON_SCHEMA_KIND_ANY_OF:
|
|
for (auto & child : as<common_schema_any_of>(node).children) {
|
|
link(*child, live);
|
|
}
|
|
return;
|
|
case COMMON_SCHEMA_KIND_ALL_OF:
|
|
for (auto & child : as<common_schema_all_of>(node).children) {
|
|
link(*child, live);
|
|
}
|
|
return;
|
|
case COMMON_SCHEMA_KIND_ARRAY:
|
|
link(*as<common_schema_array>(node).items, live);
|
|
return;
|
|
case COMMON_SCHEMA_KIND_TUPLE:
|
|
for (auto & item : as<common_schema_tuple>(node).items) {
|
|
link(*item, live);
|
|
}
|
|
return;
|
|
case COMMON_SCHEMA_KIND_OBJECT: {
|
|
auto & obj = as<common_schema_object>(node);
|
|
for (auto & prop : obj.properties) {
|
|
link(*prop.schema, live);
|
|
}
|
|
if (obj.additional_properties) {
|
|
link(*obj.additional_properties, live);
|
|
}
|
|
return;
|
|
}
|
|
default:
|
|
return;
|
|
}
|
|
}
|
|
|
|
public:
|
|
explicit common_schema_optimizer(common_schema_document & doc) : doc_(doc) {}
|
|
|
|
void run() {
|
|
// a $ref whose target became none is none too, which the next pass can then prune in its parent
|
|
do {
|
|
changed_ = false;
|
|
for (auto & entry : doc_.refs) {
|
|
entry.second = rewrite(std::move(entry.second));
|
|
}
|
|
doc_.root = rewrite(std::move(doc_.root));
|
|
} while (changed_);
|
|
|
|
// only the refs the root still reaches are kept, and every ref node gets its new target
|
|
std::map<std::string, common_schema_ptr> live;
|
|
link(*doc_.root, live);
|
|
doc_.refs = std::move(live);
|
|
}
|
|
};
|
|
|
|
void common_schema_optimize(common_schema_document & doc) {
|
|
common_schema_optimizer(doc).run();
|
|
}
|
|
|
|
static common_schema_kind kind_of(const common_json & value) {
|
|
if (value.is_null()) {
|
|
return COMMON_SCHEMA_KIND_NULL;
|
|
}
|
|
if (value.is_boolean()) {
|
|
return COMMON_SCHEMA_KIND_BOOLEAN;
|
|
}
|
|
if (value.is_number_integer()) {
|
|
return COMMON_SCHEMA_KIND_INTEGER;
|
|
}
|
|
if (value.is_number()) {
|
|
return COMMON_SCHEMA_KIND_NUMBER;
|
|
}
|
|
if (value.is_string()) {
|
|
return COMMON_SCHEMA_KIND_STRING;
|
|
}
|
|
if (value.is_array()) {
|
|
return COMMON_SCHEMA_KIND_ARRAY;
|
|
}
|
|
return COMMON_SCHEMA_KIND_OBJECT;
|
|
}
|
|
|
|
static common_schema_kinds resolve_kinds(const common_schema & s, std::unordered_set<const common_schema *> & visited) {
|
|
switch (s.kind()) {
|
|
case COMMON_SCHEMA_KIND_ANY:
|
|
return common_schema_kinds::all();
|
|
case COMMON_SCHEMA_KIND_NONE:
|
|
return {};
|
|
case COMMON_SCHEMA_KIND_NUMBER:
|
|
return { COMMON_SCHEMA_KIND_NUMBER, COMMON_SCHEMA_KIND_INTEGER };
|
|
case COMMON_SCHEMA_KIND_TUPLE:
|
|
return { COMMON_SCHEMA_KIND_ARRAY };
|
|
case COMMON_SCHEMA_KIND_NULL:
|
|
case COMMON_SCHEMA_KIND_BOOLEAN:
|
|
case COMMON_SCHEMA_KIND_INTEGER:
|
|
case COMMON_SCHEMA_KIND_STRING:
|
|
case COMMON_SCHEMA_KIND_ARRAY:
|
|
case COMMON_SCHEMA_KIND_OBJECT:
|
|
return { s.kind() };
|
|
case COMMON_SCHEMA_KIND_CONST:
|
|
return { kind_of(static_cast<const common_schema_const &>(s).value) };
|
|
case COMMON_SCHEMA_KIND_ENUM: {
|
|
common_schema_kinds kinds;
|
|
for (const auto & value : static_cast<const common_schema_enum &>(s).values) {
|
|
kinds.add(kind_of(value));
|
|
}
|
|
return kinds;
|
|
}
|
|
case COMMON_SCHEMA_KIND_REF: {
|
|
const auto * target = static_cast<const common_schema_ref &>(s).target;
|
|
if (!target || !visited.insert(target).second) {
|
|
// a cycle contributes no kind, to be safe
|
|
return {};
|
|
}
|
|
auto kinds = resolve_kinds(*target, visited);
|
|
visited.erase(target);
|
|
return kinds;
|
|
}
|
|
case COMMON_SCHEMA_KIND_ANY_OF: {
|
|
common_schema_kinds kinds;
|
|
for (const auto & child : static_cast<const common_schema_any_of &>(s).children) {
|
|
kinds |= resolve_kinds(*child, visited);
|
|
}
|
|
return kinds;
|
|
}
|
|
case COMMON_SCHEMA_KIND_ALL_OF: {
|
|
auto kinds = common_schema_kinds::all();
|
|
for (const auto & child : static_cast<const common_schema_all_of &>(s).children) {
|
|
kinds &= resolve_kinds(*child, visited);
|
|
}
|
|
return kinds;
|
|
}
|
|
}
|
|
return {};
|
|
}
|
|
|
|
common_schema_kinds common_schema_resolve_kinds(const common_schema & schema) {
|
|
std::unordered_set<const common_schema *> visited;
|
|
return resolve_kinds(schema, visited);
|
|
}
|