From f656f535e13741a08031db096da70218f8bff22f Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Tue, 31 Aug 2021 15:04:06 +0200 Subject: [PATCH] :construction: begin BON8 implementation --- .../nlohmann/detail/input/input_adapters.hpp | 2 +- .../nlohmann/detail/output/binary_writer.hpp | 200 ++++++++++- include/nlohmann/json.hpp | 17 + single_include/nlohmann/json.hpp | 219 +++++++++++- test/src/unit-bon8.cpp | 315 ++++++++++++++++++ 5 files changed, 739 insertions(+), 14 deletions(-) create mode 100644 test/src/unit-bon8.cpp diff --git a/include/nlohmann/detail/input/input_adapters.hpp b/include/nlohmann/detail/input/input_adapters.hpp index 6df58a1cd..1aed3664e 100644 --- a/include/nlohmann/detail/input/input_adapters.hpp +++ b/include/nlohmann/detail/input/input_adapters.hpp @@ -23,7 +23,7 @@ namespace nlohmann namespace detail { /// the supported input formats -enum class input_format_t { json, cbor, msgpack, ubjson, bson }; +enum class input_format_t { json, cbor, msgpack, ubjson, bson, bon8 }; //////////////////// // input adapters // diff --git a/include/nlohmann/detail/output/binary_writer.hpp b/include/nlohmann/detail/output/binary_writer.hpp index 42fd50e32..8a999a613 100644 --- a/include/nlohmann/detail/output/binary_writer.hpp +++ b/include/nlohmann/detail/output/binary_writer.hpp @@ -923,6 +923,158 @@ class binary_writer } } + /*! + @param[in] j JSON value to serialize + */ + void write_bon8(const BasicJsonType& j) + { + switch (j.type()) + { + case value_t::null: + { + oa->write_character(to_char_type(0xFA)); + break; + } + + case value_t::boolean: + { + oa->write_character(j.m_value.boolean + ? to_char_type(0xF9) + : to_char_type(0xF8)); + break; + } + + case value_t::number_float: + { + // special values + if (j.m_value.number_float == -1.0) + { + oa->write_character(to_char_type(0xFB)); + break; + } + if (j.m_value.number_float == 0.0) + { + oa->write_character(to_char_type(0xFC)); + break; + } + if (j.m_value.number_float == 1.0) + { + oa->write_character(to_char_type(0xFD)); + break; + } + + // write float with prefix + write_compact_float(j.m_value.number_float, detail::input_format_t::bon8); + break; + } + + case value_t::string: + { + // empty string: use end-of-text symbol + if (j.m_value.string->empty()) + { + oa->write_character(to_char_type(0xFF)); + break; + } + + // write strings as is + oa->write_characters( + reinterpret_cast(j.m_value.string->c_str()), + j.m_value.string->size()); + break; + } + + case value_t::array: + { + const auto N = j.m_value.array->size(); + if (N <= 4) + { + // start array with count (80..84) + oa->write_character(to_char_type(0x80 + N)); + } + else + { + // start array + oa->write_character(to_char_type(0x85)); + } + + // write each element + for (std::size_t i = 0; i < N; ++i) + { + const auto& el = j.m_value.array->operator[](i); + + // check if 0xFF after nonempty string and string is required + if (i > 0) + { + const auto& prev = j.m_value.array->operator[](i - 1); + if (el.is_string() && prev.is_string() && !prev.m_value.string->empty()) + { + oa->write_character(to_char_type(0xFF)); + } + } + + write_bon8(el); + } + + if (N > 4) + { + // end of container + oa->write_character(to_char_type(0xFE)); + } + break; + } + + case value_t::object: + { + const auto N = j.m_value.object->size(); + if (N <= 4) + { + // start object with count (86..8A) + oa->write_character(to_char_type(0x86 + N)); + } + else + { + // start object + oa->write_character(to_char_type(0x8B)); + } + + // write each element + for (auto it = j.m_value.object->begin(); it != j.m_value.object->end(); ++it) + { + const auto& key = it->first; + const auto& value = it->second; + + write_bon8(key); + + // check if we need a 0xFF separator between key and value + if (!key.empty() && value.is_string()) + { + oa->write_character(to_char_type(0xFF)); + } + + write_bon8(value); + + // check if we need a 0xFF separator between the value and the next key + if (value.is_string() && !value.m_value.string->empty() && std::next(it) != j.m_value.object->end()) + { + oa->write_character(to_char_type(0xFF)); + } + } + + if (N > 4) + { + // end of container + oa->write_character(to_char_type(0xFE)); + } + break; + } + + case value_t::discarded: + default: + break; + } + } + private: ////////// // BSON // @@ -1524,6 +1676,20 @@ class binary_writer return 'D'; // float 64 } + ////////// + // BON8 // + ////////// + + static constexpr CharType get_bon8_float_prefix(float /*unused*/) + { + return to_char_type(0x8E); + } + + static constexpr CharType get_bon8_float_prefix(double /*unused*/) + { + return to_char_type(0x8F); + } + /////////////////////// // Utility functions // /////////////////////// @@ -1566,16 +1732,38 @@ class binary_writer static_cast(n) <= static_cast((std::numeric_limits::max)()) && static_cast(static_cast(n)) == static_cast(n)) { - oa->write_character(format == detail::input_format_t::cbor - ? get_cbor_float_prefix(static_cast(n)) - : get_msgpack_float_prefix(static_cast(n))); + switch (format) + { + case input_format_t::cbor: + get_cbor_float_prefix(static_cast(n)); + break; + case input_format_t::msgpack: + get_msgpack_float_prefix(static_cast(n)); + break; + case input_format_t::bon8: + get_bon8_float_prefix(static_cast(n)); + break; + default: + break; + } write_number(static_cast(n)); } else { - oa->write_character(format == detail::input_format_t::cbor - ? get_cbor_float_prefix(n) - : get_msgpack_float_prefix(n)); + switch (format) + { + case input_format_t::cbor: + get_cbor_float_prefix(n); + break; + case input_format_t::msgpack: + get_msgpack_float_prefix(n); + break; + case input_format_t::bon8: + get_bon8_float_prefix(n); + break; + default: + break; + } write_number(n); } #ifdef __GNUC__ diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index fc3e60828..650730509 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -7611,6 +7611,23 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec binary_writer(o).write_bson(j); } + static std::vector to_bon8(const basic_json& j) + { + std::vector result; + to_bon8(j, result); + return result; + } + + static void to_bon8(const basic_json& j, detail::output_adapter o) + { + binary_writer(o).write_bon8(j); + } + + static void to_bon8(const basic_json& j, detail::output_adapter o) + { + binary_writer(o).write_bon8(j); + } + /*! @brief create a JSON value from an input in CBOR format diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 8959265da..8c7b3d1b6 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -5354,7 +5354,7 @@ namespace nlohmann namespace detail { /// the supported input formats -enum class input_format_t { json, cbor, msgpack, ubjson, bson }; +enum class input_format_t { json, cbor, msgpack, ubjson, bson, bon8 }; //////////////////// // input adapters // @@ -14495,6 +14495,158 @@ class binary_writer } } + /*! + @param[in] j JSON value to serialize + */ + void write_bon8(const BasicJsonType& j) + { + switch (j.type()) + { + case value_t::null: + { + oa->write_character(to_char_type(0xFA)); + break; + } + + case value_t::boolean: + { + oa->write_character(j.m_value.boolean + ? to_char_type(0xF9) + : to_char_type(0xF8)); + break; + } + + case value_t::number_float: + { + // special values + if (j.m_value.number_float == -1.0) + { + oa->write_character(to_char_type(0xFB)); + break; + } + if (j.m_value.number_float == 0.0) + { + oa->write_character(to_char_type(0xFC)); + break; + } + if (j.m_value.number_float == 1.0) + { + oa->write_character(to_char_type(0xFD)); + break; + } + + // write float with prefix + write_compact_float(j.m_value.number_float, detail::input_format_t::bon8); + break; + } + + case value_t::string: + { + // empty string: use end-of-text symbol + if (j.m_value.string->empty()) + { + oa->write_character(to_char_type(0xFF)); + break; + } + + // write strings as is + oa->write_characters( + reinterpret_cast(j.m_value.string->c_str()), + j.m_value.string->size()); + break; + } + + case value_t::array: + { + const auto N = j.m_value.array->size(); + if (N <= 4) + { + // start array with count (80..84) + oa->write_character(to_char_type(0x80 + N)); + } + else + { + // start array + oa->write_character(to_char_type(0x85)); + } + + // write each element + for (std::size_t i = 0; i < N; ++i) + { + const auto& el = j.m_value.array->operator[](i); + + // check if 0xFF after nonempty string and string is required + if (i > 0) + { + const auto& prev = j.m_value.array->operator[](i - 1); + if (el.is_string() && prev.is_string() && !prev.m_value.string->empty()) + { + oa->write_character(to_char_type(0xFF)); + } + } + + write_bon8(el); + } + + if (N > 4) + { + // end of container + oa->write_character(to_char_type(0xFE)); + } + break; + } + + case value_t::object: + { + const auto N = j.m_value.object->size(); + if (N <= 4) + { + // start object with count (86..8A) + oa->write_character(to_char_type(0x86 + N)); + } + else + { + // start object + oa->write_character(to_char_type(0x8B)); + } + + // write each element + for (auto it = j.m_value.object->begin(); it != j.m_value.object->end(); ++it) + { + const auto& key = it->first; + const auto& value = it->second; + + write_bon8(key); + + // check if we need a 0xFF separator between key and value + if (!key.empty() && value.is_string()) + { + oa->write_character(to_char_type(0xFF)); + } + + write_bon8(value); + + // check if we need a 0xFF separator between the value and the next key + if (value.is_string() && !value.m_value.string->empty() && std::next(it) != j.m_value.object->end()) + { + oa->write_character(to_char_type(0xFF)); + } + } + + if (N > 4) + { + // end of container + oa->write_character(to_char_type(0xFE)); + } + break; + } + + case value_t::discarded: + default: + break; + } + } + private: ////////// // BSON // @@ -15096,6 +15248,20 @@ class binary_writer return 'D'; // float 64 } + ////////// + // BON8 // + ////////// + + static constexpr CharType get_bon8_float_prefix(float /*unused*/) + { + return to_char_type(0x8E); + } + + static constexpr CharType get_bon8_float_prefix(double /*unused*/) + { + return to_char_type(0x8F); + } + /////////////////////// // Utility functions // /////////////////////// @@ -15138,16 +15304,38 @@ class binary_writer static_cast(n) <= static_cast((std::numeric_limits::max)()) && static_cast(static_cast(n)) == static_cast(n)) { - oa->write_character(format == detail::input_format_t::cbor - ? get_cbor_float_prefix(static_cast(n)) - : get_msgpack_float_prefix(static_cast(n))); + switch (format) + { + case input_format_t::cbor: + get_cbor_float_prefix(static_cast(n)); + break; + case input_format_t::msgpack: + get_msgpack_float_prefix(static_cast(n)); + break; + case input_format_t::bon8: + get_bon8_float_prefix(static_cast(n)); + break; + default: + break; + } write_number(static_cast(n)); } else { - oa->write_character(format == detail::input_format_t::cbor - ? get_cbor_float_prefix(n) - : get_msgpack_float_prefix(n)); + switch (format) + { + case input_format_t::cbor: + get_cbor_float_prefix(n); + break; + case input_format_t::msgpack: + get_msgpack_float_prefix(n); + break; + case input_format_t::bon8: + get_bon8_float_prefix(n); + break; + default: + break; + } write_number(n); } #ifdef __GNUC__ @@ -25016,6 +25204,23 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec binary_writer(o).write_bson(j); } + static std::vector to_bon8(const basic_json& j) + { + std::vector result; + to_bon8(j, result); + return result; + } + + static void to_bon8(const basic_json& j, detail::output_adapter o) + { + binary_writer(o).write_bon8(j); + } + + static void to_bon8(const basic_json& j, detail::output_adapter o) + { + binary_writer(o).write_bon8(j); + } + /*! @brief create a JSON value from an input in CBOR format diff --git a/test/src/unit-bon8.cpp b/test/src/unit-bon8.cpp new file mode 100644 index 000000000..3c17ba46b --- /dev/null +++ b/test/src/unit-bon8.cpp @@ -0,0 +1,315 @@ +/* + __ _____ _____ _____ + __| | __| | | | JSON for Modern C++ (test suite) +| | |__ | | | | | | version 3.10.2 +|_____|_____|_____|_|___| https://github.com/nlohmann/json + +Licensed under the MIT License . +SPDX-License-Identifier: MIT +Copyright (c) 2013-2019 Niels Lohmann . + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#include "doctest_compatibility.h" + +#include +using nlohmann::json; + +#include +#include +#include +#include +#include "test_utils.hpp" + +namespace +{ +class SaxCountdown +{ + public: + explicit SaxCountdown(const int count) : events_left(count) + {} + + bool null() + { + return events_left-- > 0; + } + + bool boolean(bool /*unused*/) + { + return events_left-- > 0; + } + + bool number_integer(json::number_integer_t /*unused*/) + { + return events_left-- > 0; + } + + bool number_unsigned(json::number_unsigned_t /*unused*/) + { + return events_left-- > 0; + } + + bool number_float(json::number_float_t /*unused*/, const std::string& /*unused*/) + { + return events_left-- > 0; + } + + bool string(std::string& /*unused*/) + { + return events_left-- > 0; + } + + bool binary(std::vector& /*unused*/) + { + return events_left-- > 0; + } + + bool start_object(std::size_t /*unused*/) + { + return events_left-- > 0; + } + + bool key(std::string& /*unused*/) + { + return events_left-- > 0; + } + + bool end_object() + { + return events_left-- > 0; + } + + bool start_array(std::size_t /*unused*/) + { + return events_left-- > 0; + } + + bool end_array() + { + return events_left-- > 0; + } + + bool parse_error(std::size_t /*unused*/, const std::string& /*unused*/, const json::exception& /*unused*/) // NOLINT(readability-convert-member-functions-to-static) + { + return false; + } + + private: + int events_left = 0; +}; +} // namespace + +TEST_CASE("BON8") +{ + SECTION("individual values") + { + SECTION("discarded") + { + // discarded values are not serialized + json j = json::value_t::discarded; + const auto result = json::to_bon8(j); + CHECK(result.empty()); + } + + SECTION("null") + { + json j = nullptr; + std::vector expected = {0xFA}; + const auto result = json::to_bon8(j); + CHECK(result == expected); + } + + SECTION("boolean") + { + SECTION("true") + { + json j = true; + std::vector expected = {0xF9}; + const auto result = json::to_bon8(j); + CHECK(result == expected); + } + + SECTION("false") + { + json j = false; + std::vector expected = {0xF8}; + const auto result = json::to_bon8(j); + CHECK(result == expected); + } + } + + SECTION("floating-point numbers") + { + SECTION("-1.0") + { + json j = -1.0; + std::vector expected = {0xFB}; + const auto result = json::to_bon8(j); + CHECK(result == expected); + } + + SECTION("0.0") + { + json j = 0.0; + std::vector expected = {0xFC}; + const auto result = json::to_bon8(j); + CHECK(result == expected); + } + + SECTION("1.0") + { + json j = 1.0; + std::vector expected = {0xFD}; + const auto result = json::to_bon8(j); + CHECK(result == expected); + } + } + + SECTION("string") + { + SECTION("empty string") + { + json j = ""; + std::vector expected = {0xFF}; + const auto result = json::to_bon8(j); + CHECK(result == expected); + } + + SECTION("other strings") + { + json j = "This is a string."; + std::vector expected = {'T', 'h', 'i', 's', ' ', 'i', 's', ' ', 'a', ' ', 's', 't', 'r', 'i', 'n', 'g', '.'}; + const auto result = json::to_bon8(j); + CHECK(result == expected); + } + } + + SECTION("array") + { + SECTION("array with count") + { + SECTION("empty array") + { + json j = json::array(); + std::vector expected = {0x80}; + const auto result = json::to_bon8(j); + CHECK(result == expected); + } + + SECTION("[false]") + { + json j = {false}; + std::vector expected = {0x81, 0xF8}; + const auto result = json::to_bon8(j); + CHECK(result == expected); + } + + SECTION("[false, null]") + { + json j = {false, nullptr}; + std::vector expected = {0x82, 0xF8, 0xFA}; + const auto result = json::to_bon8(j); + CHECK(result == expected); + } + + SECTION("[false, null, true]") + { + json j = {false, nullptr, true}; + std::vector expected = {0x83, 0xF8, 0xFA, 0xF9}; + const auto result = json::to_bon8(j); + CHECK(result == expected); + } + + SECTION("[false, null, true, 1.0]") + { + json j = {false, nullptr, true, 1.0}; + std::vector expected = {0x84, 0xF8, 0xFA, 0xF9, 0xFD}; + const auto result = json::to_bon8(j); + CHECK(result == expected); + } + + SECTION("[\"s\", \"s\"]") + { + json j = {"s", "s"}; + std::vector expected = {0x82, 's', 0xFF, 's'}; + const auto result = json::to_bon8(j); + CHECK(result == expected); + } + + SECTION("[\"\", \"s\"]") + { + json j = {"", "s"}; + std::vector expected = {0x82, 0xFF, 's'}; + const auto result = json::to_bon8(j); + CHECK(result == expected); + } + } + + SECTION("array without count") + { + SECTION("[false, null, true, 1.0, [], 0.0]") + { + json j = {false, nullptr, true, 1.0, json::array(), 0.0}; + std::vector expected = {0x85, 0xF8, 0xFA, 0xF9, 0xFD, 0x80, 0xFC, 0xFE}; + const auto result = json::to_bon8(j); + CHECK(result == expected); + } + } + } + + SECTION("object") + { + SECTION("object with count") + { + SECTION("empty object") + { + json j = json::object(); + std::vector expected = {0x86}; + const auto result = json::to_bon8(j); + CHECK(result == expected); + } + + SECTION("{\"foo\": null}") + { + json j = {{"foo", nullptr}}; + std::vector expected = {0x87, 'f', 'o', 'o', 0xFA}; + const auto result = json::to_bon8(j); + CHECK(result == expected); + } + + SECTION("{\"\": true, \"foo\": null}") + { + json j = {{"", true}, {"foo", nullptr}}; + std::vector expected = {0x88, 0xFF, 0xF9, 'f', 'o', 'o', 0xFA}; + const auto result = json::to_bon8(j); + CHECK(result == expected); + } + + SECTION("{\"a\": \"\", \"c\": \"d\"}") + { + json j = {{"a", ""}, {"c", "d"}}; + std::vector expected = {0x88, 'a', 0xFF, 0xFF, 'c', 0xFF, 'd'}; + const auto result = json::to_bon8(j); + CHECK(result == expected); + } + } + } + } +}