diff --git a/Makefile.am b/Makefile.am
index 34f745d0..07cb3677 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -105,7 +105,8 @@ libutil_la_LIBADD = $(BOOST_SYSTEM_LIB) $(BOOST_SYSTEM_LIB)
bin_PROGRAMS = \
json-clean \
- json-validate
+ json-validate \
+ json-schema
json_clean_SOURCES = json/clean.cpp
json_clean_DEPENDENCIES = $(top_builddir)/.libs/libutil.la
diff --git a/json/schema.cpp b/json/schema.cpp
new file mode 100644
index 00000000..0777ccdb
--- /dev/null
+++ b/json/schema.cpp
@@ -0,0 +1,615 @@
+/*
+ * 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 2012 Danny Robson
+ */
+
+
+#include "../json.hpp"
+
+#include "../maths.hpp"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+using namespace std;
+using namespace std::placeholders;
+
+enum {
+ ARG_COMMAND,
+ ARG_SCHEMA,
+ ARG_INPUT,
+
+ NUM_ARGS
+};
+
+
+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::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::node &node,
+ const json::object &schema);
+
+
+bool
+is_type_valid (const json::node &node,
+ const json::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::node &) { return true; };
+ static const auto INT_VALIDATOR = [] (const json::node &n) {
+ return n.is_number () && is_integer (n.as_number ());
+ };
+
+ static const map> TYPE_VALIDATORS ({
+ { "array", bind (&json::node::is_array, _1) },
+ { "boolean", bind (&json::node::is_boolean, _1) },
+ { "null", bind (&json::node::is_null, _1) },
+ { "number", bind (&json::node::is_number, _1) },
+ { "object", bind (&json::node::is_object, _1) },
+ { "string", bind (&json::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::node &node,
+ const json::node &constraint)
+ { return !is_type_valid (node, constraint); }
+
+
+bool
+is_enum_valid (const json::node &node,
+ const json::node &constraint) {
+ if (!constraint.is_array ())
+ throw json::schema_error ("enum validation requires an array");
+
+ const json::array &valids = constraint.as_array ();
+ return valids.end () != std::find (valids.begin (),
+ valids.end (),
+ node);
+}
+
+
+bool
+is_enum_valid (const json::string &node,
+ const json::node &constraint) {
+ return is_enum_valid (static_cast (node), constraint);
+}
+
+
+bool
+is_always_valid (const json::node &,
+ const json::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::node &node,
+ const json::object &)
+ { return node.is_boolean (); }
+
+
+bool
+is_null_valid (const json::node &node,
+ const json::object &)
+ { return node.is_null (); }
+
+
+//
+// JSON number
+//
+
+bool
+is_minimum_valid (const json::number &node,
+ const json::node &constraint) {
+ return constraint["minimum"].as_number () <= node;
+}
+
+
+bool
+is_maximum_valid (const json::number &node,
+ const json::node &constraint) {
+ return constraint["maximum"].as_number () >= node;
+}
+
+
+bool
+is_exclusive_minimum_valid (const json::number &node,
+ const json::node &constraint) {
+ return constraint["exclusiveMinimum"].as_number () < node;
+}
+
+
+bool
+is_exclusive_maximum_valid (const json::number &node,
+ const json::node &constraint) {
+ return constraint["exclusiveMaximum"].as_number () > node;
+}
+
+
+bool
+is_divisible_by_valid (const json::number &node,
+ const json::node &constraint) {
+ return exactly_equal (fmod (node.native (),
+ constraint["divisibleBy"].as_number ()),
+ 0.0);
+}
+
+
+bool
+is_number_valid (const json::number &node,
+ const json::object &schema) {
+ typedef bool (*number_validator_t)(const json::number&, const json::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;
+ }
+
+ const json::node &val = *i.second;
+ if (!validator->second (node, schema))
+ return false;
+ }
+
+ return true;
+}
+
+
+bool
+is_number_valid (const json::node &node,
+ const json::object &schema) {
+ not_implemented ();
+ return true;
+}
+
+
+//
+// JSON string
+//
+
+
+bool
+is_min_length_valid (const json::string &node,
+ const json::node &constraint) {
+ if (!is_integer (constraint))
+ return false;
+
+ return node.size () >= constraint.as_number ();
+}
+
+
+bool
+is_max_length_valid (const json::string &node,
+ const json::node &constraint) {
+ if (!is_integer (constraint))
+ return false;
+
+ return node.size () <= constraint.as_number ();
+}
+
+
+bool
+is_pattern_valid (const json::string &node,
+ const json::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::string &node,
+ const json::object &schema) {
+ typedef bool (*string_validator_t)(const json::string&, const json::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::object::const_iterator::value_type &i: schema) {
+ const std::string &key = i.first;
+ const json::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::node &node,
+ const json::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::array &node,
+ const json::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::array &node,
+ const json::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::array &node,
+ const json::node &constraint) {
+ if (!constraint.is_boolean ())
+ throw json::schema_error ("uniqueItems must be a boolean");
+
+ if (node.size () < 2)
+ return true;
+
+
+ for (json::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::array &node,
+ const json::node &_schema) {
+ if (!_schema.is_object ())
+ throw json::schema_error ("array_items constraint must be an object");
+ const json::object &schema = _schema.as_object ();
+
+ for (const json::node &i: node)
+ if (!is_node_valid (i, schema))
+ return false;
+
+ return true;
+
+ not_implemented ();
+ return false;
+}
+
+
+bool
+is_additional_items_valid (const json::array &node,
+ const json::node &constraint) {
+ not_implemented ();
+ return false;
+}
+
+
+bool
+is_array_valid (const json::array &node,
+ const json::object &schema) {
+ check_hard (node.is_array ());
+
+ typedef bool (*array_validator_t)(const json::array&, const json::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::object::const_iterator::value_type &i: schema) {
+ const std::string &key = i.first;
+ const json::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::object &node,
+ const json::object &schema) {
+ for (const json::object::const_iterator::value_type &element: node) {
+ const std::string &key = element.first;
+ const json::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::object &node,
+ const json::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::object &node,
+ const json::object &schema) {
+ typedef bool (*object_validator_t)(const json::object&, const json::node&);
+ static const map VALIDATORS = {
+ { "properties", &is_properties_valid },
+ //{ "patternProperties", &is_pattern_properties_valid },
+ //{ "additionalProperties", &is_additionaL_properties_valid },
+ };
+
+ for (const json::object::const_iterator::value_type &i: schema) {
+ const std::string &name = i.first;
+ const json::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::node &node,
+ const json::object &schema) {
+ if (!node.is_object ())
+ return false;
+
+ return is_object_valid (node.as_object (), schema);
+}
+
+
+//
+// JSON node
+//
+
+bool
+is_node_valid (const json::node &node,
+ const json::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::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::node &node,
+ const json::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);
+ return EXIT_FAILURE;
+ }
+
+ // Load the schema and input
+ unique_ptr schema, input;
+ try {
+ schema = json::parse (boost::filesystem::path (argv[ARG_SCHEMA]));
+ input = json::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;
+ }
+
+ // Check schema is valid
+ if (!schema->is_object ()) {
+ std::cerr << "Schema should be an object\n";
+ return EXIT_FAILURE;
+ }
+
+ const json::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;
+}
+