json/schema: validate now throws on error

This commit is contained in:
Danny Robson 2015-03-23 18:42:00 +11:00
parent d102410567
commit 90552732d7
3 changed files with 111 additions and 93 deletions

View File

@ -28,16 +28,25 @@
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
static bool validate (json::tree::node&, const json::tree::object&); struct length_error : public json::schema_error {
using schema_error::schema_error;
};
struct format_error : public json::schema_error {
using schema_error::schema_error;
};
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
static bool static void validate (json::tree::node&, const json::tree::object&);
///////////////////////////////////////////////////////////////////////////////
static void
validate (json::tree::object &node, validate (json::tree::object &node,
const json::tree::object &schema) const json::tree::object &schema)
{ {
bool success = true;
auto properties = schema.find ("properties"); auto properties = schema.find ("properties");
auto additional = schema.find ("additionalProperties"); auto additional = schema.find ("additionalProperties");
auto pattern = schema.find ("patternProperties"); auto pattern = schema.find ("patternProperties");
@ -46,11 +55,12 @@ validate (json::tree::object &node,
for (const auto &kv: properties->second->as_object ()) { for (const auto &kv: properties->second->as_object ()) {
auto p = node.find (kv.first); auto p = node.find (kv.first);
if (p != node.cend ()) if (p != node.cend ())
success = success && validate (*p->second, kv.second->as_object ()); validate (*p->second, kv.second->as_object ());
else { else {
try { try {
node.insert (kv.first, (*kv.second)["default"].clone ()); node.insert (kv.first, (*kv.second)["default"].clone ());
success = success && validate (node[kv.first], kv.second->as_object ()); validate (node[kv.first], kv.second->as_object ());
continue;
} catch (const json::key_error&) } catch (const json::key_error&)
{ ; } { ; }
} }
@ -70,28 +80,27 @@ validate (json::tree::object &node,
// create the defaulted entries. // create the defaulted entries.
auto maxProperties = schema.find ("maxProperties"); auto maxProperties = schema.find ("maxProperties");
if (maxProperties != schema.cend ()) if (maxProperties != schema.cend ())
success = success && node.size () <= maxProperties->second->as_uint (); if (node.size () > maxProperties->second->as_uint ())
throw json::schema_error ("maxProperties");
auto minProperties = schema.find ("minProperties"); auto minProperties = schema.find ("minProperties");
if (minProperties != schema.cend ()) if (minProperties != schema.cend ())
success = success && node.size () >= minProperties->second->as_uint (); if (node.size () < minProperties->second->as_uint ())
throw json::schema_error ("minProperties");
auto required = schema.find ("required"); auto required = schema.find ("required");
if (required != schema.cend ()) if (required != schema.cend ())
for (const auto &i: required->second->as_array ()) for (const auto &i: required->second->as_array ())
success = success && node.has (i.as_string ()); if (!node.has (i.as_string ()))
throw json::schema_error ("required");
return success;
} }
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
static bool static void
validate (json::tree::array &node, validate (json::tree::array &node,
const json::tree::object &schema) const json::tree::object &schema)
{ {
bool success = true;
// attempt to match the item and additionalItem schemas // attempt to match the item and additionalItem schemas
auto items = schema.find ("items"); auto items = schema.find ("items");
auto additional = schema.find ("additionalItems"); auto additional = schema.find ("additionalItems");
@ -100,24 +109,25 @@ validate (json::tree::array &node,
// items is an object, test all elements with it as a schema // items is an object, test all elements with it as a schema
if (items->second->is_object ()) { if (items->second->is_object ()) {
for (auto &i: node) for (auto &i: node)
success = success && validate (i, items->second->as_object ()); validate (i, items->second->as_object ());
// items is a list of schemas, test n-elements with it as a schema // items is a list of schemas, test n-elements with it as a schema
} else if (items->second->is_array ()) { } else if (items->second->is_array ()) {
const auto &itemArray = items->second->as_array (); const auto &itemArray = items->second->as_array ();
size_t i = 0; size_t i = 0;
for (; i < itemArray.size () && i < node.size (); ++i) for (; i < itemArray.size () && i < node.size (); ++i)
success = success && validate (node[i], itemArray[i].as_object ()); validate (node[i], itemArray[i].as_object ());
// we've exhausted the schema list, use the additional schema // we've exhausted the schema list, use the additional schema
if (i == itemArray.size ()) { if (i == itemArray.size ()) {
if (additional->second->is_boolean ()) { if (additional->second->is_boolean ()) {
success = success && additional->second->as_boolean (); if (!additional->second->as_boolean ())
throw json::schema_error ("additional");
} else if (additional->second->is_object ()) { } else if (additional->second->is_object ()) {
for ( ; i < node.size (); ++i) for ( ; i < node.size (); ++i)
success = success && validate (node[i], additional->second->as_object ()); validate (node[i], additional->second->as_object ());
} else { } else {
success = false; throw json::schema_error ("items");
} }
} }
} }
@ -125,32 +135,28 @@ validate (json::tree::array &node,
auto maxItems = schema.find ("maxItems"); auto maxItems = schema.find ("maxItems");
if (maxItems != schema.cend ()) if (maxItems != schema.cend ())
success = success && node.size () <= maxItems->second->as_uint (); if (node.size () > maxItems->second->as_uint ())
throw json::schema_error ("maxItems");
auto minItems = schema.find ("minItems"); auto minItems = schema.find ("minItems");
if (minItems != schema.cend ()) if (minItems != schema.cend ())
success = success && node.size () >= minItems->second->as_uint (); if (node.size () < minItems->second->as_uint ())
throw json::schema_error ("minItems");
// check all element are unique // check all element are unique
// XXX: uses a naive n^2 brute force search on equality because it's 2am // 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. // and I don't want to write a type aware comparator for the sort.
auto unique = schema.find ("uniqueItems"); auto unique = schema.find ("uniqueItems");
if (unique != schema.cend () && unique->second->as_boolean ()) { if (unique != schema.cend () && unique->second->as_boolean ())
for (size_t a = 0; a < node.size (); ++a) for (size_t a = 0; a < node.size (); ++a)
for (size_t b = a + 1; b < node.size (); ++b) for (size_t b = a + 1; b < node.size (); ++b)
if (node[a] == node[b]) { if (node[a] == node[b])
success = false; throw json::schema_error ("uniqueItems");
goto notunique;
}
notunique: ;
}
return success;
} }
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
static bool static void
validate (json::tree::string &node, validate (json::tree::string &node,
const json::tree::object &schema) const json::tree::object &schema)
{ {
@ -161,10 +167,10 @@ validate (json::tree::string &node,
if (maxLength != schema.cend ()) { if (maxLength != schema.cend ()) {
auto cmp = maxLength->second->as_number ().native (); auto cmp = maxLength->second->as_number ().native ();
if (!is_integer (cmp)) if (!is_integer (cmp))
return false; throw length_error ("maxLength");
if (val.size () > cmp) if (val.size () > cmp)
return false; throw length_error ("maxLength");
} }
// check length is greater than a maximum // check length is greater than a maximum
@ -172,10 +178,10 @@ validate (json::tree::string &node,
if (minLength != schema.cend ()) { if (minLength != schema.cend ()) {
auto cmp = minLength->second->as_number ().native (); auto cmp = minLength->second->as_number ().native ();
if (!is_integer (cmp)) if (!is_integer (cmp))
return false; throw length_error ("minLength");
if (val.size () < cmp) if (val.size () < cmp)
return false; throw length_error ("minLength");
} }
// check the string conforms to a regex // check the string conforms to a regex
@ -185,15 +191,13 @@ validate (json::tree::string &node,
std::regex r (pattern->second->as_string ().native (), std::regex r (pattern->second->as_string ().native (),
std::regex_constants::ECMAScript); std::regex_constants::ECMAScript);
if (!std::regex_search (val, r)) if (!std::regex_search (val, r))
return false; throw format_error ("pattern");
} }
return true;
} }
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
static bool static void
validate (json::tree::number &node, validate (json::tree::number &node,
const json::tree::object &schema) const json::tree::object &schema)
{ {
@ -205,7 +209,7 @@ validate (json::tree::number &node,
auto div = mult->second->as_number ().native (); auto div = mult->second->as_number ().native ();
if (val <= 0 || almost_equal (val, div)) if (val <= 0 || almost_equal (val, div))
return false; throw json::schema_error ("multipleOf");
} }
// check maximum holds. exclusive requires max condition. // check maximum holds. exclusive requires max condition.
@ -214,13 +218,13 @@ validate (json::tree::number &node,
if (max != schema.end ()) { if (max != schema.end ()) {
auto cmp = max->second->as_number ().native (); auto cmp = max->second->as_number ().native ();
if (exclusiveMax != schema.end () && exclusiveMax->second->as_boolean () && val <= cmp) if (exclusiveMax != schema.end () && exclusiveMax->second->as_boolean () && val >= cmp)
return false; throw json::schema_error ("exclusiveMax");
else if (val < cmp) else if (val > cmp)
return false; throw json::schema_error ("maximum");
} else { } else {
if (exclusiveMax != schema.cend ()) if (exclusiveMax != schema.cend ())
return false; throw json::schema_error ("exclusiveMax");
} }
// check minimum holds. exclusive requires min condition // check minimum holds. exclusive requires min condition
@ -229,35 +233,29 @@ validate (json::tree::number &node,
if (min != schema.end ()) { if (min != schema.end ()) {
auto cmp = min->second->as_number ().native (); auto cmp = min->second->as_number ().native ();
if (exclusiveMin != schema.end () && exclusiveMin->second->as_boolean () && val >= cmp) if (exclusiveMin != schema.end () && exclusiveMin->second->as_boolean () && val <= cmp)
return false; throw json::schema_error ("exclusiveMin");
else if (val > cmp) else if (val < cmp)
return false; throw json::schema_error ("minimum");
} else { } else {
if (exclusiveMin != schema.cend ()) if (exclusiveMin != schema.cend ())
return false; throw json::schema_error ("exclusiveMin");
} }
return true;
} }
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
static bool static void
validate (json::tree::boolean&, validate (json::tree::boolean&,
const json::tree::object&) const json::tree::object&)
{ { ; }
return true;
}
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
static bool static void
validate (json::tree::null&, validate (json::tree::null&,
const json::tree::object&) const json::tree::object&)
{ { ; }
return true;
}
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
@ -278,7 +276,7 @@ to_string (json::tree::type_t t)
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
static bool static void
validate (json::tree::node &node, validate (json::tree::node &node,
const json::tree::object &schema) const json::tree::object &schema)
{ {
@ -289,47 +287,50 @@ validate (json::tree::node &node,
enumPos->second->as_array ().cend (), enumPos->second->as_array ().cend (),
node); node);
if (pos == enumPos->second->as_array ().cend ()) if (pos == enumPos->second->as_array ().cend ())
return false; throw json::schema_error ("enum");
} }
// check the value is the correct type
auto type = schema.find ("type"); auto type = schema.find ("type");
if (type != schema.cend ()) { if (type != schema.cend ()) {
// check against a single named type
if (type->second->is_string ()) { if (type->second->is_string ()) {
auto a = type->second->as_string (); auto a = type->second->as_string ();
auto b = to_string (node.type ()); auto b = to_string (node.type ());
if (a != b) { if (a != b)
std::cerr << a << " != " << b << '\n'; throw json::schema_error ("type");
return false; // check against an array of types
}
} else if (type->second->is_array ()) { } else if (type->second->is_array ()) {
auto pos = std::find_if (type->second->as_array ().begin (), auto pos = std::find_if (type->second->as_array ().begin (),
type->second->as_array ().end (), type->second->as_array ().end (),
[&] (const auto &i) { return i.as_string () == to_string (node.type ()); }); [&] (const auto &i) { return i.as_string () == to_string (node.type ()); });
if (pos == type->second->as_array ().end ()) if (pos == type->second->as_array ().end ())
return false; throw json::schema_error ("type");
} else } else
return false; throw json::schema_error ("type");
} }
auto allOf = schema.find ("allOf"); auto allOf = schema.find ("allOf");
if (allOf != schema.cend ()) { if (allOf != schema.cend ()) {
for (const auto &i: allOf->second->as_array ()) for (const auto &i: allOf->second->as_array ())
if (!validate (node, i.as_object ())) validate (node, i.as_object ());
return false;
} }
auto anyOf = schema.find ("anyOf"); auto anyOf = schema.find ("anyOf");
if (anyOf != schema.cend ()) { if (anyOf != schema.cend ()) {
bool success = false; bool success = false;
for (const auto &i: anyOf->second->as_array ()) { for (const auto &i: anyOf->second->as_array ()) {
success = validate (node, i.as_object ()); try {
if (success) validate (node, i.as_object ());
success = true;
break; break;
} catch (const json::schema_error&)
{ continue; }
} }
if (!success) if (!success)
return false; throw json::schema_error ("anyOf");
} }
auto oneOf = schema.find ("oneOf"); auto oneOf = schema.find ("oneOf");
@ -337,51 +338,62 @@ validate (json::tree::node &node,
unsigned count = 0; unsigned count = 0;
for (const auto &i: oneOf->second->as_array ()) { for (const auto &i: oneOf->second->as_array ()) {
if (validate (node, i.as_object ())) try {
validate (node, i.as_object ());
count++; count++;
} catch (const json::schema_error&)
{ ; }
if (count > 1) if (count > 1)
return false; throw json::schema_error ("oneOf");
} }
if (count != 1) if (count != 1)
return false; throw json::schema_error ("oneOf");
} }
auto notSchema = schema.find ("not"); auto notSchema = schema.find ("not");
if (notSchema != schema.cend ()) { if (notSchema != schema.cend ()) {
for (const auto &i: notSchema->second->as_array ()) for (const auto &i: notSchema->second->as_array ()) {
if (validate (node, i.as_object ())) bool valid = false;
return false; try {
validate (node, i.as_object ());
valid = true;
} catch (const json::schema_error&)
{ ; }
if (valid)
throw json::schema_error ("not");
}
} }
switch (node.type ()) { switch (node.type ()) {
case json::tree::OBJECT: return validate (node.as_object (), schema); case json::tree::OBJECT: validate (node.as_object (), schema); return;
case json::tree::ARRAY: return validate (node.as_array (), schema); case json::tree::ARRAY: validate (node.as_array (), schema); return;
case json::tree::STRING: return validate (node.as_string (), schema); case json::tree::STRING: validate (node.as_string (), schema); return;
case json::tree::NUMBER: return validate (node.as_number (), schema); case json::tree::NUMBER: validate (node.as_number (), schema); return;
case json::tree::BOOLEAN: return validate (node.as_boolean (), schema); case json::tree::BOOLEAN: validate (node.as_boolean (), schema); return;
case json::tree::NONE: return validate (node.as_null (), schema); case json::tree::NONE: validate (node.as_null (), schema); return;
} }
unreachable (); unreachable ();
return false;
} }
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
bool void
json::schema::validate (json::tree::node &data, json::schema::validate (json::tree::node &data,
const json::tree::object &schema) const json::tree::object &schema)
{ {
auto title = schema.find ("title"); auto title = schema.find ("title");
if (title != schema.cend ()) if (title != schema.cend ())
if (!title->second->is_string ()) if (!title->second->is_string ())
return false; throw json::schema_error ("title");
auto description = schema.find ("description"); auto description = schema.find ("description");
if (description != schema.cend ()) if (description != schema.cend ())
if (!description->second->is_string ()) if (!description->second->is_string ())
return false; throw json::schema_error ("description");
return ::validate (data, schema.as_object ()); return ::validate (data, schema.as_object ());
} }

View File

@ -25,7 +25,7 @@
#include <memory> #include <memory>
namespace json { namespace schema { namespace json { namespace schema {
bool void
validate (json::tree::node &data, validate (json::tree::node &data,
const json::tree::object &schema); const json::tree::object &schema);
} } } }

View File

@ -21,6 +21,7 @@
#include "json/except.hpp" #include "json/except.hpp"
#include "json/tree.hpp" #include "json/tree.hpp"
#include "json/schema.hpp" #include "json/schema.hpp"
#include "json/except.hpp"
#include <boost/filesystem.hpp> #include <boost/filesystem.hpp>
@ -45,7 +46,12 @@ main (int argc, char **argv) {
auto schema = json::tree::parse (fs::path (argv[ARG_SCHEMA])); auto schema = json::tree::parse (fs::path (argv[ARG_SCHEMA]));
auto input = json::tree::parse (fs::path (argv[ARG_INPUT])); auto input = json::tree::parse (fs::path (argv[ARG_INPUT]));
bool success = json::schema::validate (*input, schema->as_object ()); try {
std::cerr << (success ? "success\n" : "failure\n"); json::schema::validate (*input, schema->as_object ());
return success ? EXIT_SUCCESS : EXIT_FAILURE; } catch (const json::error &e) {
std::cerr << "error: " << e.what () << '\n';
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
} }