From e34975d1091dc9d050482defa66fff389efa4f44 Mon Sep 17 00:00:00 2001 From: Danny Robson Date: Fri, 20 Apr 2012 18:20:49 +1000 Subject: [PATCH] Add a simple json-schema validator. Does not handle $ref clauses, fragments, format fields, and a bunch of other more minor details. But it should hold for simple self-contained json objects. --- Makefile.am | 3 +- json/schema.cpp | 615 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 617 insertions(+), 1 deletion(-) create mode 100644 json/schema.cpp 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; +} +