diff --git a/config/Doxyfile b/config/Doxyfile index 534810d..d69f4d0 100644 --- a/config/Doxyfile +++ b/config/Doxyfile @@ -31,7 +31,7 @@ FULL_PATH_NAMES = YES STRIP_FROM_PATH = include src STRIP_FROM_INC_PATH = include src SHORT_NAMES = NO -JAVADOC_AUTOBRIEF = NO +JAVADOC_AUTOBRIEF = YES QT_AUTOBRIEF = NO MULTILINE_CPP_IS_BRIEF = NO INHERIT_DOCS = YES diff --git a/include/jsonv/all.hpp b/include/jsonv/all.hpp index 75959f8..2fd6b41 100644 --- a/include/jsonv/all.hpp +++ b/include/jsonv/all.hpp @@ -221,6 +221,7 @@ * \see http://json.org/ **/ +#include "coerce.hpp" #include "config.hpp" #include "encode.hpp" #include "forward.hpp" diff --git a/include/jsonv/coerce.hpp b/include/jsonv/coerce.hpp new file mode 100644 index 0000000..38fa696 --- /dev/null +++ b/include/jsonv/coerce.hpp @@ -0,0 +1,127 @@ +/** \file jsonv/coerce.hpp + * A \c jsonv::value has a number of \c as_X operators, which strictly performs a transformation to a C++ data type. + * However, sometimes when working with things like user input, you would like to be more free-form in what you accept + * as "valid." + * + * Copyright (c) 2014 by Travis Gockel. All rights reserved. + * + * This program is free software: you can redistribute it and/or modify it under the terms of the Apache License + * as published by the Apache Software Foundation, either version 2 of the License, or (at your option) any later + * version. + * + * \author Travis Gockel (travis@gockelhut.com) +**/ +#ifndef __JSONV_COERCE_HPP_INCLUDED__ +#define __JSONV_COERCE_HPP_INCLUDED__ + +#include +#include + +#include +#include + +namespace jsonv +{ + +/** \addtogroup Coercion + * \{ +**/ + +/** Can the given \c kind be converted \a from a kind \a to another? + * + * \returns \c true if the corresponding \c coerce_X function for the specified \a to will successfully return if given + * a \c value of the kind \a from; false if there is no such conversion (the \c coerce_X function might + * throw). +**/ +JSONV_PUBLIC bool can_coerce(const kind& from, const kind& to); + +/** Can the given \c value be converted \a from a kind \a to another? + * + * \note + * This is \e not only a convenience function! There is a special case for converting from a \c string into either a + * \c decimal or \c integer where the contents of the string must be considered. This function will look into the given + * \a from and see if it can successfully perfrom the coercion. + * + * \returns \c true if the corresponding \c coerce_X function for the specified \a to will successfully return if given + * the \c value \a from; false if there is no such conversion (the \c coerce_X function will throw). +**/ +JSONV_PUBLIC bool can_coerce(const value& from, const kind& to); + +/** Coerce \a from into a \c null. If \a from is not \c null, this will throw. It is not clear that there is a use for + * this beyond completeness. + * + * \returns \c nullptr if \a from has \c kind::null. + * \throws kind_error if \a from is not \c kind::null. +**/ +JSONV_PUBLIC std::nullptr_t coerce_null(const value& from); + +/** Coerce \a from into a \c map. + * + * \returns a map of the contents of \a from. + * \throws kind_error if \a from is not \c kind::object. +**/ +JSONV_PUBLIC std::map coerce_object(const value& from); + +/** Coerce \a from into a \c vector. + * + * \returns a vector of the contents of \a from. + * \throws kind_error if \a from is not \c kind::array. +**/ +JSONV_PUBLIC std::vector coerce_array(const value& from); + +/** Coerce \a from into an \c std::string. If \a from is already \c kind::string, the value is simply returned. If + * \a from is any other \c kind, the result will be the same as \c to_string. +**/ +JSONV_PUBLIC std::string coerce_string(const value& from); + +/** Coerce \a from into an integer. If \a from is a \c decimal lower than the minimum of \c std::int64_t or higher than + * the maximum of \c std::int64_t, it is clamped to the lowest or highest value, respectively. + * + * \returns + * \c kind is... | Rules + * ------------- | ------------------------------------------------- + * \c null | throws \c kind_error + * \c object | throws \c kind_error + * \c array | throws \c kind_error + * \c string | \c parse(from.as_string()).as_integer() + * \c integer | \c from.as_integer() + * \c decimal | \c std::int64_t(from.as_decimal()) + * \c boolean | \c from.as_boolean() ? 1 : 0 +**/ +JSONV_PUBLIC std::int64_t coerce_integer(const value& from); + +/** Coerce \a from into a \c double. + * + * \returns + * \c kind is... | Rules + * ------------- | ------------------------------------------------- + * \c null | throws \c kind_error + * \c object | throws \c kind_error + * \c array | throws \c kind_error + * \c string | \c parse(from.as_string()).as_decimal() + * \c integer | \c from.as_decimal() + * \c decimal | \c from.as_decimal() + * \c boolean | \c from.as_boolean() ? 1.0 : 0.0 +**/ +JSONV_PUBLIC double coerce_decimal(const value& from); + +/** Coerce \a from into a \c bool. This follows the rules of Python's boolean coercion. + * + * \returns + * \c kind is... | Rules + * ------------- | -------------------------------------------------- + * \c null | \c false + * \c object | \c !from.empty() + * \c array | \c !from.empty() + * \c string | \c !from.empty() (even if the value is \c "false") + * \c integer | \c from != 0 + * \c decimal | \c from != 0.0 + * \c boolean | \c from.as_boolean() +**/ +JSONV_PUBLIC bool coerce_boolean(const value& from); + +/** \} **/ + +} + +#endif/*__JSONV_COERCE_HPP_INCLUDED__*/ diff --git a/include/jsonv/detail/string_ref.hpp b/include/jsonv/detail/string_ref.hpp index db6faaf..7bba93d 100644 --- a/include/jsonv/detail/string_ref.hpp +++ b/include/jsonv/detail/string_ref.hpp @@ -70,7 +70,7 @@ class string_ref string_ref& operator=(const string_ref&) noexcept = default; template - operator std::basic_string, UAllocator>() const + explicit operator std::basic_string, UAllocator>() const { return std::basic_string, UAllocator>(_base, _length); } diff --git a/include/jsonv/value.hpp b/include/jsonv/value.hpp index 682a680..dc8cb36 100644 --- a/include/jsonv/value.hpp +++ b/include/jsonv/value.hpp @@ -731,14 +731,14 @@ class JSONV_PUBLIC value * * \throws kind_error if the kind is not an object. **/ - size_type count(const string_ref& key) const; + size_type count(const std::string& key) const; /** Attempt to locate a key-value pair with the provided \a key in this object. * * \throws kind_error if the kind is not an object. **/ - object_iterator find(const string_ref& key); - const_object_iterator find(const string_ref& key) const; + object_iterator find(const std::string& key); + const_object_iterator find(const std::string& key) const; /** Insert \a pair into this object. If \a hint is provided, this insertion could be optimized. * @@ -770,7 +770,7 @@ class JSONV_PUBLIC value * \returns 1 if \a key was erased; 0 if it did not. * \throws kind_error if the kind is not an object. **/ - size_type erase(const string_ref& key); + size_type erase(const std::string& key); /** Erase the item at the given \a position. * diff --git a/src/jsonv-tests/coerce_tests.cpp b/src/jsonv-tests/coerce_tests.cpp new file mode 100644 index 0000000..a1309fe --- /dev/null +++ b/src/jsonv-tests/coerce_tests.cpp @@ -0,0 +1,223 @@ +/** \file + * + * Copyright (c) 2014 by Travis Gockel. All rights reserved. + * + * This program is free software: you can redistribute it and/or modify it under the terms of the Apache License + * as published by the Apache Software Foundation, either version 2 of the License, or (at your option) any later + * version. + * + * \author Travis Gockel (travis@gockelhut.com) +**/ +#include "test.hpp" + +#include + +#include + +namespace jsonv_test +{ + +using namespace jsonv; + +TEST(coerce_object_valid) +{ + std::map input = { { "x", 1 }, + { "y", 2 }, + { "z", "3" } + }; + auto val = object(std::begin(input), std::end(input)); + auto output = coerce_object(val); + ensure(input == output); +} + +TEST(coerce_object_invalid) +{ + ensure_throws(kind_error, coerce_object(array())); + ensure_throws(kind_error, coerce_object("x")); + ensure_throws(kind_error, coerce_object(1)); + ensure_throws(kind_error, coerce_object(1.2)); + ensure_throws(kind_error, coerce_object(nullptr)); +} + +TEST(coerce_array_valid) +{ + std::vector input = { 1, "blah", 5.6 }; + auto val = array(std::begin(input), std::end(input)); + auto output = coerce_array(val); + ensure(input == output); +} + +TEST(coerce_array_invalid) +{ + ensure_throws(kind_error, coerce_array(object())); + ensure_throws(kind_error, coerce_array("x")); + ensure_throws(kind_error, coerce_array(1)); + ensure_throws(kind_error, coerce_array(1.2)); + ensure_throws(kind_error, coerce_array(nullptr)); +} + +TEST(coerce_string_valid) +{ + ensure_eq(coerce_string(nullptr), "null"); + ensure_eq(coerce_string("blah"), "blah"); +} + +TEST(coerce_integer_null) +{ + ensure_throws(kind_error, coerce_integer(nullptr)); +} + +TEST(coerce_integer_boolean) +{ + ensure_eq(0, coerce_integer(false)); + ensure_eq(1, coerce_integer(true)); +} + +TEST(coerce_integer_integer) +{ + ensure_eq(0, coerce_integer(0)); + ensure_eq(1, coerce_integer(1)); +} + +TEST(coerce_integer_decimal) +{ + ensure_eq(0, coerce_integer(-0.2)); + ensure_eq(7, coerce_integer(7.8)); +} + +TEST(coerce_integer_decimal_clamp_max) +{ + ensure_eq(std::numeric_limits::max(), coerce_integer(18446744074709551600.0)); +} + +TEST(coerce_integer_decimal_clamp_min) +{ + ensure_eq(std::numeric_limits::min(), coerce_integer(-18446744074709551600.0)); +} + +TEST(coerce_integer_string_null) +{ + ensure_throws(kind_error, coerce_integer("null")); +} + +TEST(coerce_integer_string_integer) +{ + ensure_eq(0, coerce_integer("0")); + ensure_eq(1, coerce_integer("1")); +} + +TEST(coerce_integer_string_decimal) +{ + ensure_eq(0, coerce_integer("-0.2")); + ensure_eq(7, coerce_integer("7.8")); +} + +TEST(coerce_integer_string_nested_valid) +{ + ensure_throws(kind_error, coerce_integer("\"5\"")); +} + +TEST(coerce_integer_string_decimal_clamp_max) +{ + ensure_eq(std::numeric_limits::max(), coerce_integer("18446744074709551600.0")); +} + +TEST(coerce_integer_string_decimal_clamp_min) +{ + ensure_eq(std::numeric_limits::min(), coerce_integer("-18446744074709551600.0")); +} + +TEST(coerce_decimal_null) +{ + ensure_throws(kind_error, coerce_decimal(nullptr)); +} + +TEST(coerce_decimal_boolean) +{ + ensure_eq(0.0, coerce_decimal(false)); + ensure_eq(1.0, coerce_decimal(true)); +} + +TEST(coerce_decimal_integer) +{ + ensure_eq(0.0, coerce_decimal(0)); + ensure_eq(1.0, coerce_decimal(1)); +} + +TEST(coerce_decimal_decimal) +{ + ensure_eq(-0.2, coerce_decimal(-0.2)); + ensure_eq(7.8, coerce_decimal(7.8)); +} + +TEST(coerce_decimal_string_null) +{ + ensure_throws(kind_error, coerce_decimal("null")); +} + +TEST(coerce_decimal_string_integer) +{ + ensure_eq(0.0, coerce_decimal("0")); + ensure_eq(1.0, coerce_decimal("1")); +} + +TEST(coerce_decimal_string_decimal) +{ + ensure_eq(-0.2, coerce_decimal("-0.2")); + ensure_eq(7.8, coerce_decimal("7.8")); +} + +TEST(coerce_decimal_string_nested_valid) +{ + ensure_throws(kind_error, coerce_decimal("\"5.0\"")); +} + +TEST(coerce_boolean_null) +{ + ensure(!coerce_boolean(nullptr)); +} + +TEST(coerce_boolean_boolean) +{ + ensure(!coerce_boolean(false)); + ensure(coerce_boolean(true)); +} + +TEST(coerce_boolean_ints) +{ + ensure(coerce_boolean(1)); + ensure(!coerce_boolean(0)); +} + +TEST(coerce_boolean_decimal) +{ + ensure(coerce_boolean(-4.3e9)); + ensure(!coerce_boolean(0.0)); +} + +TEST(coerce_boolean_string) +{ + ensure(coerce_boolean("something")); + ensure(coerce_boolean("false")); + ensure(!coerce_boolean("")); +} + +TEST(coerce_boolean_object) +{ + ensure(coerce_boolean(object({{ "thing", 5 }}))); + ensure(!coerce_boolean(object())); +} + +TEST(coerce_nulls) +{ + ensure(nullptr == coerce_null(nullptr)); + ensure_throws(kind_error, coerce_null(object())); + ensure_throws(kind_error, coerce_null(array())); + ensure_throws(kind_error, coerce_null("string")); + ensure_throws(kind_error, coerce_null(6)); + ensure_throws(kind_error, coerce_null(9.2)); + ensure_throws(kind_error, coerce_null(true)); + ensure_throws(kind_error, coerce_null(false)); +} + +} diff --git a/src/jsonv-tests/value_tests.cpp b/src/jsonv-tests/value_tests.cpp index 0fe480a..3e2364f 100644 --- a/src/jsonv-tests/value_tests.cpp +++ b/src/jsonv-tests/value_tests.cpp @@ -42,6 +42,12 @@ TEST(compare_arrs) ensure_eq(a1234.compare(a123), 1); } +TEST(value_equal_integer_decimal) +{ + ensure_eq(jsonv::value(2), jsonv::value(2.0)); + ensure_eq(jsonv::value(2.0), jsonv::value(2)); +} + TEST(value_store_unordered_map) { std::unordered_map m; diff --git a/src/jsonv/coerce.cpp b/src/jsonv/coerce.cpp new file mode 100644 index 0000000..dcf010c --- /dev/null +++ b/src/jsonv/coerce.cpp @@ -0,0 +1,182 @@ +/** \file + * + * Copyright (c) 2014 by Travis Gockel. All rights reserved. + * + * This program is free software: you can redistribute it and/or modify it under the terms of the Apache License + * as published by the Apache Software Foundation, either version 2 of the License, or (at your option) any later + * version. + * + * \author Travis Gockel (travis@gockelhut.com) +**/ +#include +#include +#include + +#include + +namespace jsonv +{ + +bool can_coerce(const kind& from, const kind& to) +{ + switch (to) + { + case kind::null: + case kind::object: + case kind::array: + // object, array and null cannot be coerced to, so the kinds must match + return from == to; + case kind::string: + case kind::boolean: + return true; + case kind::decimal: + case kind::integer: + return from == kind::decimal || from == kind::integer; + default: + // can't coerce to a corrupt kind + return false; + } +} + +bool can_coerce(const value& from, const kind& to) +{ + if (can_coerce(from.get_kind(), to)) + { + return true; + } + else if (from.get_kind() == kind::string && (to == kind::decimal || to == kind::integer)) + { + // Actually attempt the conversion from string into the proper number. If it succeeds, we can coerce the string. + try + { + if (to == kind::decimal) + coerce_decimal(from); + else + coerce_integer(from); + return true; + } + catch (const kind_error&) + { + return false; + } + } + else + { + return false; + } +} + +std::nullptr_t coerce_null(const value& from) +{ + if (from.get_kind() == kind::null) + return nullptr; + else + throw kind_error(std::string("Can only coerce null from a null, but from is of kind ") + + to_string(from.get_kind()) + ); +} + +std::map coerce_object(const value& from) +{ + if (from.get_kind() == kind::object) + return std::map(from.begin_object(), from.end_object()); + else + throw kind_error(std::string("Invalid kind for object: ") + to_string(from.get_kind())); +} + +std::vector coerce_array(const value& from) +{ + if (from.get_kind() == kind::array) + return std::vector(from.begin_array(), from.end_array()); + else + throw kind_error(std::string("Invalid kind for array: ") + to_string(from.get_kind())); +} + +std::string coerce_string(const value& from) +{ + if (from.get_kind() == kind::string) + return from.as_string(); + else + return to_string(from); +} + +std::int64_t coerce_integer(const value& from) +{ + switch (from.get_kind()) + { + case kind::boolean: + return from.as_boolean() ? 1 : 0; + case kind::integer: + return from.as_integer(); + case kind::decimal: + if (from.as_decimal() > double(std::numeric_limits::max())) + return std::numeric_limits::max(); + else + return std::int64_t(from.as_decimal()); + case kind::string: + try + { + value x = parse(from.as_string()); + if (x.get_kind() == kind::integer || x.get_kind() == kind::decimal || x.get_kind() == kind::null) + return coerce_integer(x); + } + catch (const parse_error&) + { } + throw kind_error(std::string("Could not interpret string ") + to_string(from) + " as an integer."); + case kind::null: + case kind::object: + case kind::array: + default: + throw kind_error(std::string("Invalid kind for integer: ") + to_string(from.get_kind())); + } +} + +double coerce_decimal(const value& from) +{ + switch (from.get_kind()) + { + case kind::boolean: + return from.as_boolean() ? 1.0 : 0.0; + case kind::integer: + case kind::decimal: + return from.as_decimal(); + case kind::string: + try + { + value x = parse(from.as_string()); + if (x.get_kind() == kind::integer || x.get_kind() == kind::decimal || x.get_kind() == kind::null) + return x.as_decimal(); + } + catch (const parse_error&) + { } + throw kind_error(std::string("Could not interpret string ") + to_string(from) + " as a decimal."); + case kind::null: + case kind::object: + case kind::array: + default: + throw kind_error(std::string("Invalid kind for decimal: ") + to_string(from.get_kind())); + } +} + +bool coerce_boolean(const value& from) +{ + switch (from.get_kind()) + { + case kind::null: + return false; + case kind::object: + case kind::array: + case kind::string: + return !from.empty(); + case kind::integer: + return from != 0; + case kind::decimal: + return from != 0.0; + case kind::boolean: + return from.as_boolean(); + default: + throw kind_error(std::string("Invalid kind for boolean: ") + to_string(from.get_kind())); + } +} + +} diff --git a/src/jsonv/object.cpp b/src/jsonv/object.cpp index 2778471..e651c82 100644 --- a/src/jsonv/object.cpp +++ b/src/jsonv/object.cpp @@ -234,18 +234,24 @@ const value& value::at(const std::string& key) const return _data.object->_values.at(key); } -value::size_type value::count(const string_ref& key) const +value::size_type value::count(const std::string& key) const { check_type(kind::object, get_kind()); return _data.object->_values.count(key); } -value::object_iterator value::find(const string_ref& key) +value::object_iterator value::find(const std::string& key) { check_type(kind::object, get_kind()); return object_iterator(_data.object->_values.find(key)); } +value::const_object_iterator value::find(const std::string& key) const +{ + check_type(kind::object, get_kind()); + return const_object_iterator(_data.object->_values.find(key)); +} + value::object_iterator value::insert(value::const_object_iterator hint, std::pair pair) { check_type(kind::object, get_kind()); @@ -269,7 +275,7 @@ void value::insert(std::initializer_list> items) _data.object->_values.insert(std::move(pair)); } -value::size_type value::erase(const string_ref& key) +value::size_type value::erase(const std::string& key) { check_type(kind::object, get_kind()); return _data.object->_values.erase(key); diff --git a/src/jsonv/value.cpp b/src/jsonv/value.cpp index 6ee54b4..927ebe5 100644 --- a/src/jsonv/value.cpp +++ b/src/jsonv/value.cpp @@ -363,28 +363,8 @@ bool value::operator==(const value& other) const { if (this == &other && kind_valid(get_kind())) return true; - if (get_kind() != other.get_kind()) - return false; - - switch (get_kind()) - { - case kind::object: - return *_data.object == *_data.object; - case kind::array: - return *_data.array == *_data.array; - case kind::string: - return as_string() == other.as_string(); - case kind::integer: - return as_integer() == other.as_integer(); - case kind::decimal: - return decimal_compare(as_decimal(), other.as_decimal()) == 0; - case kind::boolean: - return as_boolean() == other.as_boolean(); - case kind::null: - return true; - default: - return false; - } + else + return compare(other) == 0; } bool value::operator !=(const value& other) const @@ -395,28 +375,8 @@ bool value::operator !=(const value& other) const if (this == &other) return false; - if (get_kind() != other.get_kind()) - return true; - - switch (get_kind()) - { - case kind::object: - return *_data.object != *_data.object; - case kind::array: - return *_data.array != *_data.array; - case kind::string: - return as_string() != other.as_string(); - case kind::integer: - return as_integer() != other.as_integer(); - case kind::decimal: - return decimal_compare(as_decimal(), other.as_decimal()) != 0; - case kind::boolean: - return as_boolean() != other.as_boolean(); - case kind::null: - return false; - default: - return true; - } + else + return compare(other) != 0; } static int kindval(kind k)