From 5674f2a2e91ac657b2f0362fbc6c163a0f22fbe3 Mon Sep 17 00:00:00 2001 From: Danny Robson Date: Wed, 18 Mar 2015 16:08:18 +1100 Subject: [PATCH] json: add basic json-schema draft 4 support --- Makefile.am | 6 +- configure.ac | 1 + json/schema.cpp | 383 ++++++++++++ json/schema.hpp | 33 + test/json-schema.test.in | 38 ++ test/json/schema/empty.schema | 1 + test/json/schema/empty_0001_pass.json | 1 + test/json/schema/empty_0002_pass.json | 1 + test/json/schema/string_length.schema | 7 + test/json/schema/string_length_0001_pass.json | 1 + test/json/schema/string_length_0002_pass.json | 1 + test/json/schema/string_length_0003_fail.json | 1 + test/json/schema/string_length_0004_fail.json | 1 + test/json/schema/type_any.schema | 1 + test/json/schema/type_any_0001_pass.json | 1 + test/json/schema/type_any_0002_pass.json | 1 + test/json/schema/type_any_0003_pass.json | 1 + test/json/schema/type_any_0004_pass.json | 1 + test/json/schema/type_any_0005_pass.json | 1 + test/json/schema/type_any_0006_pass.json | 1 + test/json/schema/type_number.schema | 1 + test/json/schema/type_number_0001_pass.json | 1 + test/json/schema/type_number_0002_pass.json | 1 + test/json/schema/type_number_0003_fail.json | 1 + test/json/schema/type_object.schema | 1 + test/json/schema/type_object_0001_pass.json | 1 + test/json/schema/type_object_0002_fail.json | 1 + tools/json-schema.cpp | 577 +----------------- 28 files changed, 497 insertions(+), 569 deletions(-) create mode 100644 json/schema.cpp create mode 100644 json/schema.hpp create mode 100644 test/json-schema.test.in create mode 100644 test/json/schema/empty.schema create mode 100644 test/json/schema/empty_0001_pass.json create mode 100644 test/json/schema/empty_0002_pass.json create mode 100644 test/json/schema/string_length.schema create mode 100644 test/json/schema/string_length_0001_pass.json create mode 100644 test/json/schema/string_length_0002_pass.json create mode 100644 test/json/schema/string_length_0003_fail.json create mode 100644 test/json/schema/string_length_0004_fail.json create mode 100644 test/json/schema/type_any.schema create mode 100644 test/json/schema/type_any_0001_pass.json create mode 100644 test/json/schema/type_any_0002_pass.json create mode 100644 test/json/schema/type_any_0003_pass.json create mode 100644 test/json/schema/type_any_0004_pass.json create mode 100644 test/json/schema/type_any_0005_pass.json create mode 100644 test/json/schema/type_any_0006_pass.json create mode 100644 test/json/schema/type_number.schema create mode 100644 test/json/schema/type_number_0001_pass.json create mode 100644 test/json/schema/type_number_0002_pass.json create mode 100644 test/json/schema/type_number_0003_fail.json create mode 100644 test/json/schema/type_object.schema create mode 100644 test/json/schema/type_object_0001_pass.json create mode 100644 test/json/schema/type_object_0002_fail.json diff --git a/Makefile.am b/Makefile.am index 886924f8..0ec9976d 100644 --- a/Makefile.am +++ b/Makefile.am @@ -85,6 +85,8 @@ UTIL_FILES = \ json/except.hpp \ json/flat.cpp \ json/flat.hpp \ + json/schema.cpp \ + json/schema.hpp \ json/tree.cpp \ json/tree.hpp \ lerp.cpp \ @@ -283,6 +285,6 @@ test_hton_LDFLAGS = -lws2_32 endif TEST_LOG_DRIVER = env AM_TAP_AWK='$(AWK)' $(SHELL) $(top_srcdir)/build-aux/tap-driver.sh -TESTS = $(top_builddir)/test/static.test $(top_builddir)/test/json.test +TESTS = $(top_builddir)/test/static.test $(top_builddir)/test/json.test $(top_builddir)/test/json-schema.test check_PROGRAMS = $(TEST_BIN) -EXTRA_DIST += test/static.test test/json.test test/json +EXTRA_DIST += test/static.test test/json.test test/json test/json-schema diff --git a/configure.ac b/configure.ac index 9fcd43f8..1d4742c1 100644 --- a/configure.ac +++ b/configure.ac @@ -109,5 +109,6 @@ AC_CONFIG_FILES([ test/Makefile ]) AC_CONFIG_FILES([test/json.test], [chmod a+x test/json.test]) +AC_CONFIG_FILES([test/json-schema.test], [chmod a+x test/json-schema.test]) AC_CONFIG_FILES([test/static.test], [chmod a+x test/static.test]) AC_OUTPUT diff --git a/json/schema.cpp b/json/schema.cpp new file mode 100644 index 00000000..14ff19fd --- /dev/null +++ b/json/schema.cpp @@ -0,0 +1,383 @@ +/* + * This file is part of libgim. + * + * libgim is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * libgim is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License + * along with libgim. If not, see . + * + * Copyright 2015 Danny Robson + */ + +#include "schema.hpp" + +#include "debug.hpp" +#include "maths.hpp" +#include "./tree.hpp" +#include "./except.hpp" + +#include + + +/////////////////////////////////////////////////////////////////////////////// +static bool validate (json::tree::node&, const json::tree::object&); + + +/////////////////////////////////////////////////////////////////////////////// +static bool +validate (json::tree::object &node, + const json::tree::object &schema) +{ + bool success = true; + + auto properties = schema.find ("properties"); + auto additional = schema.find ("additionalProperties"); + auto pattern = schema.find ("patternProperties"); + + if (properties != schema.cend ()) { + for (const auto &kv: properties->second->as_object ()) { + auto p = node.find (kv.first); + if (p != node.cend ()) + success = success && validate (*p->second, kv.second->as_object ()); + else { + try { + node.insert (kv.first, (*kv.second)["default"].clone ()); + success = success && validate (node[kv.first], kv.second->as_object ()); + } catch (const json::key_error&) + { ; } + } + } + } + + if (pattern != schema.cend ()) + not_implemented (); + + if (additional != schema.cend ()) + not_implemented (); + + if (schema.has ("dependencies")) + not_implemented (); + + // properties must be checked after the 'properties' check has a chance to + // create the defaulted entries. + auto maxProperties = schema.find ("maxProperties"); + if (maxProperties != schema.cend ()) + success = success && node.size () <= maxProperties->second->as_uint (); + + auto minProperties = schema.find ("minProperties"); + if (minProperties != schema.cend ()) + success = success && node.size () >= minProperties->second->as_uint (); + + auto required = schema.find ("required"); + if (required != schema.cend ()) + for (const auto &i: required->second->as_array ()) + success = success && node.has (i.as_string ()); + + return success; +} + + +//----------------------------------------------------------------------------- +static bool +validate (json::tree::array &node, + const json::tree::object &schema) +{ + bool success = true; + + // attempt to match the item and additionalItem schemas + auto items = schema.find ("items"); + auto additional = schema.find ("additionalItems"); + + if (items != schema.cend ()) { + // items is an object, test all elements with it as a schema + if (items->second->is_object ()) { + for (auto &i: node) + success = success && validate (i, items->second->as_object ()); + // items is a list of schemas, test n-elements with it as a schema + } else if (items->second->is_array ()) { + const auto &itemArray = items->second->as_array (); + + size_t i = 0; + for (; i < itemArray.size () && i < node.size (); ++i) + success = success && validate (node[i], itemArray[i].as_object ()); + + // we've exhausted the schema list, use the additional schema + if (i == itemArray.size ()) { + if (additional->second->is_boolean ()) { + success = success && additional->second->as_boolean (); + } else if (additional->second->is_object ()) { + for ( ; i < node.size (); ++i) + success = success && validate (node[i], additional->second->as_object ()); + } else { + success = false; + } + } + } + } + + auto maxItems = schema.find ("maxItems"); + if (maxItems != schema.cend ()) + success = success && node.size () <= maxItems->second->as_uint (); + + auto minItems = schema.find ("minItems"); + if (minItems != schema.cend ()) + success = success && node.size () >= minItems->second->as_uint (); + + // check all element are unique + // XXX: uses a naive n^2 brute force search on equality because it's 2am + // and I don't want to write a type aware comparator for the sort. + auto unique = schema.find ("uniqueItems"); + if (unique != schema.cend () && unique->second->as_boolean ()) { + for (size_t a = 0; a < node.size (); ++a) + for (size_t b = a + 1; b < node.size (); ++b) + if (node[a] == node[b]) { + success = false; + goto notunique; + } +notunique: ; + } + + return success; +} + + +//----------------------------------------------------------------------------- +static bool +validate (json::tree::string &node, + const json::tree::object &schema) +{ + const auto &val = node.native (); + + // check length is less than a maximum + auto maxLength = schema.find ("maxLength"); + if (maxLength != schema.cend ()) { + auto cmp = maxLength->second->as_number ().native (); + if (!is_integer (cmp)) + return false; + + if (val.size () > cmp) + return false; + } + + // check length is greater than a maximum + auto minLength = schema.find ("minLength"); + if (minLength != schema.cend ()) { + auto cmp = minLength->second->as_number ().native (); + if (!is_integer (cmp)) + return false; + + if (val.size () < cmp) + return false; + } + + // check the string conforms to a regex + // Note: this uses the c++11 regex engine which slightly differs from ECMA 262 + auto pattern = schema.find ("pattern"); + if (pattern != schema.cend ()) { + std::regex r (pattern->second->as_string ().native (), + std::regex_constants::ECMAScript); + if (!std::regex_search (val, r)) + return false; + } + + return true; +} + + +//----------------------------------------------------------------------------- +static bool +validate (json::tree::number &node, + const json::tree::object &schema) +{ + const auto &val = node.native (); + + // check strictly positive integer multiple + auto mult = schema.find ("multipleOf"); + if (mult != schema.cend ()) { + auto div = mult->second->as_number ().native (); + + if (val <= 0 || almost_equal (val, div)) + return false; + } + + // check maximum holds. exclusive requires max condition. + auto max = schema.find ("maximum"); + auto exclusiveMax = schema.find ("exclusiveMaximum"); + if (max != schema.cend ()) { + auto cmp = max->second->as_number ().native (); + + if (exclusiveMax->second->as_boolean () ? (val <= cmp) : (val < cmp)) + return false; + } else { + if (exclusiveMax != schema.cend ()) + return false; + } + + // check minimum holds. exclusive requires min condition + auto min = schema.find ("minimum"); + auto exclusiveMin = schema.find ("exclusiveMinimum"); + if (min != schema.cend ()) { + auto cmp = min->second->as_number ().native (); + + if (exclusiveMin->second->as_boolean () ? val >= cmp : val > cmp) + return false; + } else { + if (exclusiveMin != schema.cend ()) + return false; + } + + return true; +} + + +//----------------------------------------------------------------------------- +static bool +validate (json::tree::boolean&, + const json::tree::object&) +{ + return true; +} + + +//----------------------------------------------------------------------------- +static bool +validate (json::tree::null&, + const json::tree::object&) +{ + return true; +} + + +//----------------------------------------------------------------------------- +static std::string +to_string (json::tree::type_t t) +{ + switch (t) { + case json::tree::OBJECT: return "object"; + case json::tree::ARRAY: return "array"; + case json::tree::STRING: return "string"; + case json::tree::NUMBER: return "number"; + case json::tree::BOOLEAN: return "boolean"; + case json::tree::NONE: return "null"; + } + + unreachable (); +} + + +//----------------------------------------------------------------------------- +static bool +validate (json::tree::node &node, + const json::tree::object &schema) +{ + // check the value is in the prescribed list + auto enumPos = schema.find ("enum"); + if (enumPos != schema.cend ()) { + auto pos = std::find (enumPos->second->as_array ().cbegin (), + enumPos->second->as_array ().cend (), + node); + if (pos == enumPos->second->as_array ().cend ()) + return false; + } + + auto type = schema.find ("type"); + if (type != schema.cend ()) { + if (type->second->is_string ()) { + auto a = type->second->as_string (); + auto b = to_string (node.type ()); + + if (a != b) { + std::cerr << a << " != " << b << '\n'; + return false; + } + } else if (type->second->is_array ()) { + auto pos = std::find_if (type->second->as_array ().begin (), + type->second->as_array ().end (), + [&] (const auto &i) { return i.as_string () == to_string (node.type ()); }); + if (pos == type->second->as_array ().end ()) + return false; + } else + return false; + } + + auto allOf = schema.find ("allOf"); + if (allOf != schema.cend ()) { + for (const auto &i: allOf->second->as_array ()) + if (!validate (node, i.as_object ())) + return false; + } + + auto anyOf = schema.find ("anyOf"); + if (anyOf != schema.cend ()) { + bool success = false; + for (const auto &i: anyOf->second->as_array ()) { + success = validate (node, i.as_object ()); + if (success) + break; + } + + if (!success) + return false; + } + + auto oneOf = schema.find ("oneOf"); + if (oneOf != schema.cend ()) { + unsigned count = 0; + + for (const auto &i: oneOf->second->as_array ()) { + if (validate (node, i.as_object ())) + count++; + if (count > 1) + return false; + } + + if (count != 1) + return false; + } + + auto notSchema = schema.find ("not"); + if (notSchema != schema.cend ()) { + for (const auto &i: notSchema->second->as_array ()) + if (validate (node, i.as_object ())) + return false; + } + + switch (node.type ()) { + case json::tree::OBJECT: return validate (node.as_object (), schema); + case json::tree::ARRAY: return validate (node.as_array (), schema); + case json::tree::STRING: return validate (node.as_string (), schema); + case json::tree::NUMBER: return validate (node.as_number (), schema); + case json::tree::BOOLEAN: return validate (node.as_boolean (), schema); + case json::tree::NONE: return validate (node.as_null (), schema); + } + + unreachable (); + return false; +} + + +//----------------------------------------------------------------------------- +bool +json::schema::validate (json::tree::node &data, + const json::tree::object &schema) +{ + auto title = schema.find ("title"); + if (title != schema.cend ()) + if (!title->second->is_string ()) + return false; + + auto description = schema.find ("description"); + if (description != schema.cend ()) + if (!description->second->is_string ()) + return false; + + return ::validate (data, schema.as_object ()); +} diff --git a/json/schema.hpp b/json/schema.hpp new file mode 100644 index 00000000..c1df3c31 --- /dev/null +++ b/json/schema.hpp @@ -0,0 +1,33 @@ +/* + * This file is part of libgim. + * + * libgim is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * libgim is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License + * along with libgim. If not, see . + * + * Copyright 2015 Danny Robson + */ + +#ifndef __UTIL_JSON_SCHEMA_HPP +#define __UTIL_JSON_SCHEMA_HPP + +#include "./fwd.hpp" + +#include + +namespace json { namespace schema { + bool + validate (json::tree::node &data, + const json::tree::object &schema); +} } + +#endif diff --git a/test/json-schema.test.in b/test/json-schema.test.in new file mode 100644 index 00000000..6e8ffd86 --- /dev/null +++ b/test/json-schema.test.in @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 + +import glob +import os.path +import subprocess +import re + +SDIR = "@abs_top_srcdir@" +BDIR = "@abs_top_builddir@" + +TOOL = os.path.join(BDIR, "tools/json-schema") + +TEST_EXTRACT = re.compile("(.*?)_(\d{4})_(pass|fail).json") + +SCHEMA_DIR = os.path.join(SDIR, "test/json/schema") +SCHEMAS = glob.iglob(os.path.join(SCHEMA_DIR, "*.schema")) + +EXPECTED = { + "pass": 0, + "fail": 1 +} + + +print("1..%s" % len(glob.glob(os.path.join(SCHEMA_DIR, "*.json")))) + +for schema in SCHEMAS: + (name, _) = os.path.splitext(os.path.basename(schema)) + test_glob = name + "_*.json" + + for test in glob.iglob(os.path.join(SCHEMA_DIR, test_glob)): + command = [TOOL, schema, test] + (name, seq, success) = TEST_EXTRACT.match(test).groups() + res = subprocess.call(command, stdout=subprocess.DEVNULL,stderr=subprocess.STDOUT) + + if res != EXPECTED[success]: + print('not ok -', os.path.basename(test), '#', ' '.join(command)) + else: + print('ok -', os.path.basename(test)) diff --git a/test/json/schema/empty.schema b/test/json/schema/empty.schema new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/test/json/schema/empty.schema @@ -0,0 +1 @@ +{} diff --git a/test/json/schema/empty_0001_pass.json b/test/json/schema/empty_0001_pass.json new file mode 100644 index 00000000..9d55f0cd --- /dev/null +++ b/test/json/schema/empty_0001_pass.json @@ -0,0 +1 @@ +[1,"a", {}] diff --git a/test/json/schema/empty_0002_pass.json b/test/json/schema/empty_0002_pass.json new file mode 100644 index 00000000..810c96ee --- /dev/null +++ b/test/json/schema/empty_0002_pass.json @@ -0,0 +1 @@ +"foo" diff --git a/test/json/schema/string_length.schema b/test/json/schema/string_length.schema new file mode 100644 index 00000000..c5465346 --- /dev/null +++ b/test/json/schema/string_length.schema @@ -0,0 +1,7 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + + "type": "string", + "minLength": 2, + "maxLength": 6 +} diff --git a/test/json/schema/string_length_0001_pass.json b/test/json/schema/string_length_0001_pass.json new file mode 100644 index 00000000..cb5537d5 --- /dev/null +++ b/test/json/schema/string_length_0001_pass.json @@ -0,0 +1 @@ +"ab" diff --git a/test/json/schema/string_length_0002_pass.json b/test/json/schema/string_length_0002_pass.json new file mode 100644 index 00000000..5655d601 --- /dev/null +++ b/test/json/schema/string_length_0002_pass.json @@ -0,0 +1 @@ +"abcdef" diff --git a/test/json/schema/string_length_0003_fail.json b/test/json/schema/string_length_0003_fail.json new file mode 100644 index 00000000..231f150c --- /dev/null +++ b/test/json/schema/string_length_0003_fail.json @@ -0,0 +1 @@ +"a" diff --git a/test/json/schema/string_length_0004_fail.json b/test/json/schema/string_length_0004_fail.json new file mode 100644 index 00000000..363e68c4 --- /dev/null +++ b/test/json/schema/string_length_0004_fail.json @@ -0,0 +1 @@ +"abcdefg" diff --git a/test/json/schema/type_any.schema b/test/json/schema/type_any.schema new file mode 100644 index 00000000..2a2c1435 --- /dev/null +++ b/test/json/schema/type_any.schema @@ -0,0 +1 @@ +{"type": ["object", "array", "string", "number", "boolean", "null"]} diff --git a/test/json/schema/type_any_0001_pass.json b/test/json/schema/type_any_0001_pass.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/test/json/schema/type_any_0001_pass.json @@ -0,0 +1 @@ +{} diff --git a/test/json/schema/type_any_0002_pass.json b/test/json/schema/type_any_0002_pass.json new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/test/json/schema/type_any_0002_pass.json @@ -0,0 +1 @@ +[] diff --git a/test/json/schema/type_any_0003_pass.json b/test/json/schema/type_any_0003_pass.json new file mode 100644 index 00000000..810c96ee --- /dev/null +++ b/test/json/schema/type_any_0003_pass.json @@ -0,0 +1 @@ +"foo" diff --git a/test/json/schema/type_any_0004_pass.json b/test/json/schema/type_any_0004_pass.json new file mode 100644 index 00000000..6324d401 --- /dev/null +++ b/test/json/schema/type_any_0004_pass.json @@ -0,0 +1 @@ +3.14 diff --git a/test/json/schema/type_any_0005_pass.json b/test/json/schema/type_any_0005_pass.json new file mode 100644 index 00000000..27ba77dd --- /dev/null +++ b/test/json/schema/type_any_0005_pass.json @@ -0,0 +1 @@ +true diff --git a/test/json/schema/type_any_0006_pass.json b/test/json/schema/type_any_0006_pass.json new file mode 100644 index 00000000..19765bd5 --- /dev/null +++ b/test/json/schema/type_any_0006_pass.json @@ -0,0 +1 @@ +null diff --git a/test/json/schema/type_number.schema b/test/json/schema/type_number.schema new file mode 100644 index 00000000..33909a9d --- /dev/null +++ b/test/json/schema/type_number.schema @@ -0,0 +1 @@ +{"type":"number"} diff --git a/test/json/schema/type_number_0001_pass.json b/test/json/schema/type_number_0001_pass.json new file mode 100644 index 00000000..6324d401 --- /dev/null +++ b/test/json/schema/type_number_0001_pass.json @@ -0,0 +1 @@ +3.14 diff --git a/test/json/schema/type_number_0002_pass.json b/test/json/schema/type_number_0002_pass.json new file mode 100644 index 00000000..00750edc --- /dev/null +++ b/test/json/schema/type_number_0002_pass.json @@ -0,0 +1 @@ +3 diff --git a/test/json/schema/type_number_0003_fail.json b/test/json/schema/type_number_0003_fail.json new file mode 100644 index 00000000..810c96ee --- /dev/null +++ b/test/json/schema/type_number_0003_fail.json @@ -0,0 +1 @@ +"foo" diff --git a/test/json/schema/type_object.schema b/test/json/schema/type_object.schema new file mode 100644 index 00000000..8d33e0b5 --- /dev/null +++ b/test/json/schema/type_object.schema @@ -0,0 +1 @@ +{"type":"object"} diff --git a/test/json/schema/type_object_0001_pass.json b/test/json/schema/type_object_0001_pass.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/test/json/schema/type_object_0001_pass.json @@ -0,0 +1 @@ +{} diff --git a/test/json/schema/type_object_0002_fail.json b/test/json/schema/type_object_0002_fail.json new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/test/json/schema/type_object_0002_fail.json @@ -0,0 +1 @@ +[] diff --git a/tools/json-schema.cpp b/tools/json-schema.cpp index 71d8f203..7849a670 100644 --- a/tools/json-schema.cpp +++ b/tools/json-schema.cpp @@ -14,31 +14,20 @@ * You should have received a copy of the GNU General Public License * along with libgim. If not, see . * - * Copyright 2012 Danny Robson + * Copyright 2015 Danny Robson */ #include "json/except.hpp" #include "json/tree.hpp" - -#include "debug.hpp" -#include "maths.hpp" - -#include -#include -#include -#include -#include -#include -#include +#include "json/schema.hpp" #include -using namespace std; -using namespace std::placeholders; +namespace fs = boost::filesystem; enum { - ARG_COMMAND, + ARG_CMD, ARG_SCHEMA, ARG_INPUT, @@ -46,563 +35,17 @@ enum { }; -void -print_usage (int argc, char **argv) { - if (argc <= ARG_COMMAND) - abort (); - - std::cerr << "Usage: " << argv[ARG_COMMAND] << " \n"; -} - - -const char* -type_to_string (const json::tree::node &node) { - if (node.is_array ()) return "array"; - if (node.is_boolean ()) return "boolean"; - if (node.is_null ()) return "null"; - if (node.is_number ()) return "number"; - if (node.is_object ()) return "object"; - if (node.is_string ()) return "string"; - - unreachable (); -} - - -bool -is_node_valid (const json::tree::node &node, - const json::tree::object &schema); - - -bool -is_type_valid (const json::tree::node &node, - const json::tree::node &type) { - if (type.is_array ()) { - return any_of (type.as_array ().begin (), - type.as_array ().end (), - bind (is_type_valid, ref (node), _1)); - } - - if (!type.is_string ()) - throw json::schema_error ("schema type requires array, string, or object"); - - static const auto ANY_VALIDATOR = [] (const json::tree::node &) { return true; }; - static const auto INT_VALIDATOR = [] (const json::tree::node &n) { - return n.is_number () && is_integer (n.as_number ()); - }; - - static const map> TYPE_VALIDATORS ({ - { "array", bind (&json::tree::node::is_array, _1) }, - { "boolean", bind (&json::tree::node::is_boolean, _1) }, - { "null", bind (&json::tree::node::is_null, _1) }, - { "number", bind (&json::tree::node::is_number, _1) }, - { "object", bind (&json::tree::node::is_object, _1) }, - { "string", bind (&json::tree::node::is_string, _1) }, - { "any", ANY_VALIDATOR }, - { "integer", INT_VALIDATOR }, - }); - - auto pos = TYPE_VALIDATORS.find (type.as_string ().native ()); - if (pos == TYPE_VALIDATORS.end ()) { - std::cerr << "warning: unknown type " << type.as_string ().native () << ". assuming valid.\n"; - return true; - } - - return pos->second(node); -} - - -bool -is_disallow_valid (const json::tree::node &node, - const json::tree::node &constraint) - { return !is_type_valid (node, constraint); } - - -bool -is_enum_valid (const json::tree::node &node, - const json::tree::node &constraint) { - if (!constraint.is_array ()) - throw json::schema_error ("enum validation requires an array"); - - const json::tree::array &valids = constraint.as_array (); - return valids.end () != std::find (valids.begin (), - valids.end (), - node); -} - - -bool -is_enum_valid (const json::tree::string &node, - const json::tree::node &constraint) { - return is_enum_valid (static_cast (node), constraint); -} - - -bool -is_always_valid (const json::tree::node &, - const json::tree::node &) - { return true; } - - -/*static const map COMMON_VALIDATORS ({ - { "description", &is_always_valid }, - { "disallow", &is_disallow_valid }, - { "enum", &is_enum_valid }, - { "title", &is_always_valid }, - { "type", &is_type_valid }, -});*/ - - - -bool -is_boolean_valid (const json::tree::node &node, - const json::tree::object &) - { return node.is_boolean (); } - - -bool -is_null_valid (const json::tree::node &node, - const json::tree::object &) - { return node.is_null (); } - - -// -// JSON number -// - -bool -is_minimum_valid (const json::tree::number &node, - const json::tree::node &constraint) { - return constraint["minimum"].as_number () <= node; -} - - -bool -is_maximum_valid (const json::tree::number &node, - const json::tree::node &constraint) { - return constraint["maximum"].as_number () >= node; -} - - -bool -is_exclusive_minimum_valid (const json::tree::number &node, - const json::tree::node &constraint) { - return constraint["exclusiveMinimum"].as_number () < node; -} - - -bool -is_exclusive_maximum_valid (const json::tree::number &node, - const json::tree::node &constraint) { - return constraint["exclusiveMaximum"].as_number () > node; -} - - -bool -is_divisible_by_valid (const json::tree::number &node, - const json::tree::node &constraint) { - return exactly_equal (fmod (node.native (), - constraint["divisibleBy"].as_number ()), - 0.0); -} - - -bool -is_number_valid (const json::tree::number &node, - const json::tree::object &schema) { - typedef bool (*number_validator_t)(const json::tree::number&, const json::tree::node&); - static const map VALIDATORS = { - { "minimum", &is_minimum_valid }, - { "maximum", &is_maximum_valid }, - { "exclusiveMinimum", &is_exclusive_minimum_valid }, - { "exclusiveMaximum", &is_exclusive_maximum_valid }, - { "divisibleBy", &is_divisible_by_valid }, - }; - - for (const auto &i: schema) { - const std::string &key = i.first; - const auto &validator = VALIDATORS.find (key); - if (validator == VALIDATORS.end ()) { - std::cerr << "Unknown validation constraint: " << key << "\n"; - continue; - } - - if (!validator->second (node, schema)) - return false; - } - - return true; -} - - -// -// JSON string -// - - -bool -is_min_length_valid (const json::tree::string &node, - const json::tree::node &constraint) { - if (!is_integer (constraint)) - return false; - - return node.size () >= constraint.as_number (); -} - - -bool -is_max_length_valid (const json::tree::string &node, - const json::tree::node &constraint) { - if (!is_integer (constraint)) - return false; - - return node.size () <= constraint.as_number (); -} - - -bool -is_pattern_valid (const json::tree::string &node, - const json::tree::node &constraint) { - if (!constraint.is_string ()) - return false; - - regex pattern (constraint.as_string ().native (), - regex_constants::ECMAScript); - return regex_match (node.native (), pattern); -} - - -bool -is_string_valid (const json::tree::string &node, - const json::tree::object &schema) { - typedef bool (*string_validator_t)(const json::tree::string&, const json::tree::node&); - static const map VALIDATORS = { - { "minLength", &is_min_length_valid }, - { "maxLength", &is_max_length_valid }, - { "pattern", &is_pattern_valid }, - { "enum", &is_enum_valid }, - }; - - for (const json::tree::object::const_iterator::value_type &i: schema) { - const std::string &key = i.first; - const json::tree::node &constraint = *i.second; - - auto validator = VALIDATORS.find (key); - if (validator == VALIDATORS.end ()) { - std::cerr << "Unknown validation constraint: " << key << "\n"; - continue; - } - - if (!validator->second (node, constraint)) { - std::cerr << "Failed string constraint: " << key << "\n"; - return false; - } - } - - return true; -} - - -bool -is_string_valid (const json::tree::node &node, - const json::tree::object &schema) { - if (!node.is_string ()) - return false; - return is_string_valid (node.as_string (), schema); -} - -// -// JSON array -// - - -bool -is_max_items_valid (const json::tree::array &node, - const json::tree::node &constraint) { - if (!constraint.is_number () && is_integer (constraint.as_number ())) - throw json::schema_error ("max_items should be an integer"); - - return node.size () <= constraint.as_number (); -} - - -bool -is_min_items_valid (const json::tree::array &node, - const json::tree::node &constraint) { - if (!constraint.is_number () && is_integer (constraint.as_number ())) - throw json::schema_error ("min_items should be an integer"); - - return node.size () >= constraint.as_number (); -} - - -bool -is_unique_items_valid (const json::tree::array &node, - const json::tree::node &constraint) { - if (!constraint.is_boolean ()) - throw json::schema_error ("uniqueItems must be a boolean"); - - if (node.size () < 2) - return true; - - - for (json::tree::array::const_iterator i = node.begin (); i != node.end () - 1; ++i) { - if (find (i + 1, node.end (), *i) != node.end ()) - return false; - } - - return true; -} - - -bool -is_items_valid (const json::tree::array &node, - const json::tree::node &_schema) { - if (!_schema.is_object ()) - throw json::schema_error ("array_items constraint must be an object"); - const json::tree::object &schema = _schema.as_object (); - - for (const json::tree::node &i: node) - if (!is_node_valid (i, schema)) - return false; - - return true; - - not_implemented (); - return false; -} - - -bool -is_additional_items_valid (const json::tree::array &, - const json::tree::node &) { - not_implemented (); - return false; -} - - -bool -is_array_valid (const json::tree::array &node, - const json::tree::object &schema) { - CHECK (node.is_array ()); - - typedef bool (*array_validator_t)(const json::tree::array&, const json::tree::node&); - static const map VALIDATORS ({ - { "items", &is_items_valid }, - { "minItems", &is_min_items_valid }, - { "maxItems", &is_max_items_valid }, - { "uniqueItems", &is_unique_items_valid }, - { "additionalItems", &is_additional_items_valid }, - }); - - for (const json::tree::object::const_iterator::value_type &i: schema) { - const std::string &key = i.first; - const json::tree::node &constraint = *i.second; - - auto validator = VALIDATORS.find (key); - if (validator == VALIDATORS.end ()) { - std::cerr << "Ignoring unknown contraint key: " << key << "\n"; - continue; - } - - if (!validator->second (node, constraint)) { - std::cerr << "Failed validating array constraint: " << key << "\n"; - return false; - } - } - - return true; -} - - -// -// JSON object -// - -bool -is_properties_valid (const json::tree::object &node, - const json::tree::object &schema) { - for (const json::tree::object::const_iterator::value_type &element: node) { - const std::string &key = element.first; - const json::tree::node &val = *element.second; - - if (!schema.has (key)) { - std::cerr << "[warning] no constraint found for key: " << key << "\n"; - continue; - } - - if (!is_node_valid (val, schema[key].as_object ())) { - std::cerr << "failed validation on property: " << key << "\n"; - return false; - } - } - - return true; -} - - -bool -is_properties_valid (const json::tree::object &node, - const json::tree::node &constraint) { - CHECK (node.is_object ()); - - if (!constraint.is_object ()) - throw json::schema_error ("properties needs an object"); - - return is_properties_valid (node, constraint.as_object ()); -} - - -bool -is_object_valid (const json::tree::object &node, - const json::tree::object &schema) { - typedef bool (*object_validator_t)(const json::tree::object&, const json::tree::node&); - static const map VALIDATORS = { - { "properties", &is_properties_valid }, - //{ "patternProperties", &is_pattern_properties_valid }, - //{ "additionalProperties", &is_additionaL_properties_valid }, - }; - - for (const json::tree::object::const_iterator::value_type &i: schema) { - const std::string &name = i.first; - const json::tree::node &constraint = *i.second; - - auto validator = VALIDATORS.find (name); - if (validator == VALIDATORS.end ()) { - std::cerr << "Unknown constraint name \"" << name << "\". Ignoring.\n"; - continue; - } - - if (!validator->second(node, constraint)) { - std::cerr << "Failed validation on: " << name << "\n"; - return false; - } - } - - return true; -} - - -bool -is_object_valid (const json::tree::node &node, - const json::tree::object &schema) { - if (!node.is_object ()) - return false; - - return is_object_valid (node.as_object (), schema); -} - - -// -// JSON node -// - -bool -is_node_valid (const json::tree::node &node, - const json::tree::object &schema) { - if (schema.has ("$ref")) { - const std::string &uri = schema["$ref"].as_string (); - std::cerr << "loading referenced schema: " << uri << "\n"; - - if (uri[0] == '#') { - std::cerr << "[error] schema fragments are not supported\n"; - return false; - } - - auto referenced = json::tree::parse (boost::filesystem::path (uri)); - return is_node_valid (node, referenced->as_object ()); - } - - if (schema.has ("type") && - !is_type_valid (node, schema["type"])) - { - std::cerr << "node type is \"" << type_to_string (node) << "\", expected " << schema["type"] << "\n"; - return false; - } - -#define IS_VALID(T) \ - do { \ - if (node.is_##T ()) { \ - if (!is_##T##_valid (node.as_##T (), schema)) { \ - std::cerr << "Failed validation as " #T "\n"; \ - return false; \ - } \ - return true; \ - } \ - } while (0) - - IS_VALID(array); - IS_VALID(boolean); - IS_VALID(null); - IS_VALID(number); - IS_VALID(object); - IS_VALID(string); - -#undef IS_VALID - - return false; - - - /*static const map VALIDATORS ({ - { "description", &is_always_valid }, - { "disallow", &is_disallow_valid }, - { "enum", &is_enum_valid }, - { "title", &is_always_valid }, - { "type", &is_type_valid }, - });*/ - - - - //"required"; - - return false; -} - - -bool -is_root_valid (const json::tree::node &node, - const json::tree::object &schema) { - if (!node.is_array () && !node.is_object ()) - return false; - return is_node_valid (node, schema); -} - - -// -// Driver -// - int main (int argc, char **argv) { - // Basic argument checking if (argc != NUM_ARGS) { - print_usage (argc, argv); + std::cerr << argv[ARG_CMD] << " \n"; return EXIT_FAILURE; } - // Load the schema and input - unique_ptr schema, input; - try { - schema = json::tree::parse (boost::filesystem::path (argv[ARG_SCHEMA])); - input = json::tree::parse (boost::filesystem::path (argv[ARG_INPUT])); - } catch (const json::parse_error &err) { - std::cerr << "malformed json for schema or input. " << err.what () << "\n"; - return EXIT_FAILURE; - } + auto schema = json::tree::parse (fs::path (argv[ARG_SCHEMA])); + auto input = json::tree::parse (fs::path (argv[ARG_INPUT])); - // Check schema is valid - if (!schema->is_object ()) { - std::cerr << "Schema should be an object\n"; - return EXIT_FAILURE; - } - - const json::tree::object &schema_object = schema->as_object (); - - // Check input is valid - if (!is_node_valid (*input, schema_object)) { - std::cerr << "input does not satisfy the schema\n"; - return EXIT_FAILURE; - } - - return EXIT_SUCCESS; + bool success = json::schema::validate (*input, schema->as_object ()); + std::cerr << (success ? "success\n" : "failure\n"); + return success ? EXIT_SUCCESS : EXIT_FAILURE; } -