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