mirror of
https://github.com/ultimatepp/ultimatepp.git
synced 2026-06-30 06:12:22 -06:00
318 lines
7.4 KiB
C++
318 lines
7.4 KiB
C++
#include "JsonValidator.h"
|
|
|
|
#include <plugin/pcre/Pcre.h>
|
|
|
|
void JsonSchemaChecker::InvalidSchema()
|
|
{
|
|
Error("invalid schema");
|
|
}
|
|
|
|
void JsonSchemaChecker::Error(const String& error)
|
|
{
|
|
if(logics)
|
|
throw FailedBranch();
|
|
else
|
|
WhenError(error);
|
|
}
|
|
|
|
void JsonSchemaChecker::ArrayFilters(Value schema, Value data, int depth)
|
|
{
|
|
int i = 0;
|
|
Value pfx = schema["prefixItems"];
|
|
for(; i < min(pfx.GetCount(), data.GetCount()); i++) {
|
|
Path __(this, "prefixItems");
|
|
schema_path << i;
|
|
data_path << i;
|
|
Check(pfx[i], data[i], depth + 1);
|
|
}
|
|
Value items = schema["items"];
|
|
if(!items.IsVoid()) {
|
|
if(items.Is<bool>()) {
|
|
if(!(bool)items && i < data.GetCount())
|
|
Error("additional array items are not allowed");
|
|
}
|
|
else {
|
|
if(!IsValueMap(items))
|
|
InvalidSchema();
|
|
if(items.GetCount())
|
|
for(; i < data.GetCount(); i++) {
|
|
Path __(this, "items");
|
|
data_path << i;
|
|
Check(items, data[i], depth + 1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void JsonSchemaChecker::ObjectFilters(Value schema, Value data, int depth)
|
|
{
|
|
ValueMap properties = schema["properties"];
|
|
ValueMap patterns = schema["patternProperties"];
|
|
if(properties.GetCount() || patterns.GetCount()) {
|
|
if(!data.Is<ValueMap>())
|
|
Error("object required");
|
|
ValueMap object = data;
|
|
Index<String> ids;
|
|
Value additionalProperties = schema["additionalProperties"];
|
|
if(!(IsVoid(additionalProperties) || additionalProperties.Is<ValueMap>() || additionalProperties.Is<bool>()))
|
|
InvalidSchema();
|
|
|
|
bool noAdditionalProperties = additionalProperties.Is<bool>() ? !(bool)additionalProperties : false;
|
|
Array<RegExp> regexp;
|
|
for(int i = 0; i < patterns.GetCount(); i++)
|
|
regexp.Create(~patterns.GetKey(i));
|
|
for(int i = 0; i < object.GetCount(); i++) {
|
|
String id = ~object.GetKey(i);
|
|
Value val = object.GetValue(i);
|
|
bool found = false;
|
|
for(int j = 0; j < properties.GetCount(); j++)
|
|
if(properties.GetKey(j) == id) {
|
|
Path __(this, "properties");
|
|
data_path << id;
|
|
schema_path << id;
|
|
Check(properties.GetValue(j), val, depth + 1);
|
|
found = true;
|
|
break;
|
|
}
|
|
|
|
if(!found)
|
|
for(int j = 0; j < patterns.GetCount(); j++)
|
|
if(regexp[j].Match(id)) {
|
|
Path __(this, "patternProperties");
|
|
data_path << id;
|
|
schema_path << ~patterns.GetKey(i);
|
|
Check(patterns.GetValue(j), val, depth + 1);
|
|
found = true;
|
|
break;
|
|
}
|
|
|
|
if(!found) {
|
|
if(noAdditionalProperties)
|
|
Error("unknown property " + id);
|
|
if(additionalProperties.Is<ValueMap>()) {
|
|
Path __(this, "additionalProperties");
|
|
data_path << id;
|
|
Check(additionalProperties, val, depth + 1);
|
|
}
|
|
}
|
|
ids.FindAdd(id);
|
|
}
|
|
for(Value v : schema["required"]) {
|
|
String id = ~v;
|
|
if(ids.Find(id) < 0)
|
|
Error("required property " + id);
|
|
}
|
|
ValueMap m = schema["dependentRequired"];
|
|
for(int i = 0; i < m.GetCount(); i++) {
|
|
String id = ~m.GetKey(i);
|
|
if(ids.Find(id) >= 0) {
|
|
for(Value v : m.GetValue(i))
|
|
if(ids.Find(~v) < 0)
|
|
Error(String() << "required dependent property " << v << ", depends on " << id);
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void JsonSchemaChecker::NumberFilters(Value schema, Value data)
|
|
{
|
|
double v = data;
|
|
double m = schema["multipleOf"];
|
|
if(!IsNull(m) && abs(roundf(v / m) * m - v) > abs(v) * 1e-14)
|
|
Error("number must be multiple of " + AsString(m));
|
|
m = schema["minimum"];
|
|
if(!IsNull(m) && v < m)
|
|
Error("number must be >= " + AsString(m));
|
|
m = schema["exclusiveMinimum"];
|
|
if(!IsNull(m) && v <= m)
|
|
Error("number must be > " + AsString(m));
|
|
m = schema["maximum"];
|
|
if(!IsNull(m) && v > m)
|
|
Error("number must be <= " + AsString(m));
|
|
m = schema["exclusiveMaximum"];
|
|
if(!IsNull(m) && v >= m)
|
|
Error("number must be < " + AsString(m));
|
|
}
|
|
|
|
void JsonSchemaChecker::StringFilters(Value schema, Value data)
|
|
{
|
|
WString s = data;
|
|
int m = schema["minLength"];
|
|
if(!IsNull(m) && s.GetLength() < m)
|
|
Error("string must have at least " + AsString(m) + " characters");
|
|
m = schema["maxLength"];
|
|
if(!IsNull(m) && s.GetLength() > m)
|
|
Error("string must have at most " + AsString(m) + " characters");
|
|
String p = schema["pattern"];
|
|
if(!IsNull(p)) {
|
|
RegExp exp(p);
|
|
if(!exp.Match(~data))
|
|
Error("failed regexp pattern " + p);
|
|
}
|
|
}
|
|
|
|
void JsonSchemaChecker::Check(Value schema, Value data, int depth)
|
|
{
|
|
Value allOf = schema["allOf"];
|
|
if(!allOf.IsVoid()) {
|
|
if(!allOf.Is<ValueArray>())
|
|
InvalidSchema();
|
|
for(int i = 0; i < allOf.GetCount(); i++) {
|
|
Path __(this, "allOf");
|
|
schema_path << i;
|
|
Check0(allOf[i], data, depth + 1);
|
|
}
|
|
}
|
|
logics++;
|
|
auto countOf = [&](Value m, int max, int def) {
|
|
if(m.IsVoid())
|
|
return def;
|
|
if(!m.Is<ValueArray>())
|
|
InvalidSchema();
|
|
int count = 0;
|
|
for(int i = 0; i < m.GetCount() && count < max; i++)
|
|
try {
|
|
Check0(m[i], data, depth + 1);
|
|
count++;
|
|
}
|
|
catch(FailedBranch) {}
|
|
return count;
|
|
};
|
|
if(countOf(schema["anyOf"], 1, 1) == 0) {
|
|
Path __(this, "anyOf");
|
|
Error("anyOf failed");
|
|
}
|
|
if(countOf(schema["oneOf"], 2, 1) != 1) {
|
|
Path __(this, "oneOf");
|
|
Error("oneOf failed");
|
|
}
|
|
|
|
Value Not = schema["not"];
|
|
if(!Not.IsVoid()) {
|
|
if(!Not.Is<ValueMap>())
|
|
InvalidSchema();
|
|
bool ok = true;
|
|
try {
|
|
Path __(this, "not");
|
|
Check(Not, data, depth + 1);
|
|
ok = false;
|
|
}
|
|
catch(FailedBranch) {}
|
|
if(!ok)
|
|
Error("'not' failed");
|
|
}
|
|
logics--;
|
|
|
|
String ref = schema["$ref"];
|
|
if(ref.GetCount()) {
|
|
String ptr;
|
|
int q = ref.Find('#');
|
|
if(q >= 0) {
|
|
ptr = ref.Mid(q + 1);
|
|
ref.Trim(q);
|
|
}
|
|
Value ref_schema = refs.Get(ref, Value());
|
|
if(ref_schema.IsVoid()) {
|
|
ref_schema = Ref(ref);
|
|
refs.Add(ref, ref_schema);
|
|
}
|
|
if(ptr.GetCount())
|
|
ref_schema = DereferenceJSONPointer(ref_schema, ptr);
|
|
Path __(this, "$ref:" + ref);
|
|
Check0(ref_schema, data, depth);
|
|
}
|
|
Check0(schema, data, depth);
|
|
}
|
|
|
|
void JsonSchemaChecker::Check0(Value schema, Value data, int depth)
|
|
{
|
|
if(depth++ > 200) {
|
|
Error("invalid schema - depth is too high");
|
|
return;
|
|
}
|
|
Value m = schema["enum"];
|
|
if(m.Is<ValueArray>()) {
|
|
bool ok = false;
|
|
for(Value v : m)
|
|
if(IsNull(v) && IsNumber(data) && IsNull(data) || v == data) {
|
|
ok = true;
|
|
break;
|
|
}
|
|
if(!ok)
|
|
Error("enum error");
|
|
}
|
|
|
|
Value c = schema["const"];
|
|
if(!c.IsVoid() && !(IsNumber(c) && IsNull(c) ? IsNumber(data) && IsNull(data) : data == c))
|
|
Error("constant mismatch, required: " + ~c);
|
|
|
|
Value type = schema["type"];
|
|
bool type_ok = type.IsVoid();
|
|
auto CheckType = [&](const char *t) {
|
|
if(type_ok)
|
|
return;
|
|
if(type == t) {
|
|
type_ok = true;
|
|
return;
|
|
}
|
|
for(Value v : type) {
|
|
if(v == t) {
|
|
type_ok = true;
|
|
return;
|
|
}
|
|
}
|
|
};
|
|
|
|
if(IsString(data)) {
|
|
CheckType("string");
|
|
StringFilters(schema, data);
|
|
}
|
|
else
|
|
if(IsNumber(data) && IsNull(data) && !IsVoid(data)) // ParseJSON produces int Null
|
|
CheckType("null");
|
|
else
|
|
if(IsNumber(data) && !data.Is<bool>()) {
|
|
if(FitsInInt64(data) && (int64)data == data)
|
|
CheckType("integer");
|
|
CheckType("number");
|
|
NumberFilters(schema, data);
|
|
}
|
|
else
|
|
if(data.Is<ValueMap>()) {
|
|
CheckType("object");
|
|
ObjectFilters(schema, data, depth);
|
|
}
|
|
else
|
|
if(data.Is<ValueArray>()) {
|
|
CheckType("array");
|
|
ArrayFilters(schema, data, depth);
|
|
}
|
|
else
|
|
if(data.Is<bool>())
|
|
CheckType("boolean");
|
|
|
|
if(!type_ok) {
|
|
String s;
|
|
if(IsValueArray(type))
|
|
for(Value v : type)
|
|
MergeWith(s, " or ", ~v);
|
|
else
|
|
s = ~type;
|
|
Error("type mismatch, required " + s);
|
|
}
|
|
|
|
depth--;
|
|
}
|
|
|
|
void JsonSchemaChecker::Validate(Value schema, Value data)
|
|
{
|
|
schema_path.Clear();
|
|
data_path.Clear();
|
|
logics = 0;
|
|
refs.GetAdd(Null) = schema;
|
|
try {
|
|
Check(schema, data, 0);
|
|
}
|
|
catch(ValueTypeError e) { InvalidSchema(); }
|
|
}
|