From 7b31fc7828456133a8ab7a919978d122ec5254a2 Mon Sep 17 00:00:00 2001 From: Griger5 Date: Sun, 9 Aug 2026 18:22:57 +0200 Subject: [PATCH] add: ordered_json support, tests for the new functionality --- CMakeLists.txt | 1 + .../pybind11_json/pybind11_ordered_json.hpp | 249 ++++++++ test/CMakeLists.txt | 1 + test/test_pybind11_ordered_json.cpp | 560 ++++++++++++++++++ 4 files changed, 811 insertions(+) create mode 100644 include/pybind11_json/pybind11_ordered_json.hpp create mode 100644 test/test_pybind11_ordered_json.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 0be4838..80fe223 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,6 +39,7 @@ set(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/lib; ${CMAKE_INSTALL_PREFIX}/${ set(PYBIND11_JSON_HEADERS include/pybind11_json/pybind11_json.hpp + include/pybind11_json/pybind11_ordered_json.hpp ) add_library(${PROJECT_NAME} INTERFACE) diff --git a/include/pybind11_json/pybind11_ordered_json.hpp b/include/pybind11_json/pybind11_ordered_json.hpp new file mode 100644 index 0000000..53a0922 --- /dev/null +++ b/include/pybind11_json/pybind11_ordered_json.hpp @@ -0,0 +1,249 @@ +/*************************************************************************** +* Copyright (c) 2026, Martin Renou, Gracjan Adamus * +* * +* Distributed under the terms of the BSD 3-Clause License. * +* * +* The full license is in the file LICENSE, distributed with this software. * +****************************************************************************/ + +#ifndef PYBIND11_JSON_HPP +#define PYBIND11_JSON_HPP + +#include +#include +#include + +#include "nlohmann/json.hpp" + +#include "pybind11/pybind11.h" + +namespace py = pybind11; +namespace nl = nlohmann; + +namespace pyjson +{ + inline py::object from_ordered_json(const nl::ordered_json& j) + { + if (j.is_null()) + { + return py::none(); + } + else if (j.is_boolean()) + { + return py::bool_(j.get()); + } + else if (j.is_number_unsigned()) + { + return py::int_(j.get()); + } + else if (j.is_number_integer()) + { + return py::int_(j.get()); + } + else if (j.is_number_float()) + { + return py::float_(j.get()); + } + else if (j.is_string()) + { + return py::str(j.get()); + } + else if (j.is_array()) + { + py::list obj(j.size()); + for (std::size_t i = 0; i < j.size(); i++) + { + obj[i] = from_ordered_json(j[i]); + } + return obj; + } + else // Object + { + py::dict obj; + for (nl::ordered_json::const_iterator it = j.cbegin(); it != j.cend(); ++it) + { + obj[py::str(it.key())] = from_ordered_json(it.value()); + } + return obj; + } + } + + inline nl::ordered_json to_ordered_json(const py::handle& obj, std::set& refs) + { + if (obj.ptr() == nullptr || obj.is_none()) + { + return nullptr; + } + if (py::isinstance(obj)) + { + return obj.cast(); + } + if (py::isinstance(obj)) + { + try + { + nl::ordered_json::number_integer_t s = obj.cast(); + if (py::int_(s).equal(obj)) + { + return s; + } + } + catch (...) + { + } + try + { + nl::ordered_json::number_unsigned_t u = obj.cast(); + if (py::int_(u).equal(obj)) + { + return u; + } + } + catch (...) + { + } + throw std::runtime_error("to_ordered_json received an integer out of range for both nl::ordered_json::number_integer_t and nl::ordered_json::number_unsigned_t type: " + py::repr(obj).cast()); + } + if (py::isinstance(obj)) + { + return obj.cast(); + } + if (py::isinstance(obj)) + { + py::module base64 = py::module::import("base64"); + return base64.attr("b64encode")(obj).attr("decode")("utf-8").cast(); + } + if (py::isinstance(obj)) + { + return obj.cast(); + } + if (py::isinstance(obj) || py::isinstance(obj)) + { + auto insert_ret = refs.insert(obj.ptr()); + if (!insert_ret.second) { + throw std::runtime_error("Circular reference detected"); + } + + auto out = nl::ordered_json::array(); + for (const py::handle value : obj) + { + out.push_back(to_ordered_json(value, refs)); + } + + refs.erase(insert_ret.first); + + return out; + } + if (py::isinstance(obj)) + { + auto insert_ret = refs.insert(obj.ptr()); + if (!insert_ret.second) { + throw std::runtime_error("Circular reference detected"); + } + + auto out = nl::ordered_json::object(); + for (const py::handle key : obj) + { + out[py::str(key).cast()] = to_ordered_json(obj[key], refs); + } + + refs.erase(insert_ret.first); + + return out; + } + + throw std::runtime_error("to_ordered_json not implemented for this type of object: " + py::repr(obj).cast()); + } + + inline nl::ordered_json to_ordered_json(const py::handle& obj) + { + std::set refs; + return to_ordered_json(obj, refs); + } + +} + +// nlohmann_json serializers +namespace nlohmann +{ + #define MAKE_NLJSON_SERIALIZER_DESERIALIZER(T) \ + template <> \ + struct adl_serializer \ + { \ + inline static void to_json(ordered_json& j, const T& obj) \ + { \ + j = pyjson::to_ordered_json(obj); \ + } \ + \ + inline static T from_json(const ordered_json& j) \ + { \ + return pyjson::from_ordered_json(j); \ + } \ + } + + #define MAKE_NLJSON_SERIALIZER_ONLY(T) \ + template <> \ + struct adl_serializer \ + { \ + inline static void to_json(ordered_json& j, const T& obj) \ + { \ + j = pyjson::to_ordered_json(obj); \ + } \ + } + + MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::object); + + MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::bool_); + MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::int_); + MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::float_); + MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::str); + + MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::list); + MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::tuple); + MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::dict); + + MAKE_NLJSON_SERIALIZER_ONLY(py::handle); + MAKE_NLJSON_SERIALIZER_ONLY(py::detail::item_accessor); + MAKE_NLJSON_SERIALIZER_ONLY(py::detail::list_accessor); + MAKE_NLJSON_SERIALIZER_ONLY(py::detail::tuple_accessor); + MAKE_NLJSON_SERIALIZER_ONLY(py::detail::sequence_accessor); + MAKE_NLJSON_SERIALIZER_ONLY(py::detail::str_attr_accessor); + MAKE_NLJSON_SERIALIZER_ONLY(py::detail::obj_attr_accessor); + + #undef MAKE_NLJSON_SERIALIZER + #undef MAKE_NLJSON_SERIALIZER_ONLY +} + +// pybind11 caster +namespace pybind11 +{ + namespace detail + { + template <> struct type_caster + { + public: + PYBIND11_TYPE_CASTER(nl::ordered_json, _("ordered_json")); + + bool load(handle src, bool) + { + try + { + value = pyjson::to_ordered_json(src); + return true; + } + catch (...) + { + return false; + } + } + + static handle cast(nl::ordered_json src, return_value_policy /* policy */, handle /* parent */) + { + object obj = pyjson::from_ordered_json(src); + return obj.release(); + } + }; + } +} + +#endif diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 8e4af36..57064f2 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -73,6 +73,7 @@ include_directories(${GTEST_INCLUDE_DIRS} SYSTEM) set(PYBIND11_JSON_TESTS test_pybind11_json.cpp + test_pybind11_ordered_json.cpp ) add_executable(test_pybind11_json ${PYBIND11_JSON_TESTS}) diff --git a/test/test_pybind11_ordered_json.cpp b/test/test_pybind11_ordered_json.cpp new file mode 100644 index 0000000..55d0f05 --- /dev/null +++ b/test/test_pybind11_ordered_json.cpp @@ -0,0 +1,560 @@ +/*************************************************************************** +* Copyright (c) 2019, Martin Renou * +* * +* Distributed under the terms of the BSD 3-Clause License. * +* * +* The full license is in the file LICENSE, distributed with this software. * +****************************************************************************/ + +#include +#include +#include +#include + +#include "gtest/gtest.h" + +#include "pybind11_json/pybind11_ordered_json.hpp" + +#include "pybind11/embed.h" + +namespace py = pybind11; +namespace nl = nlohmann; + +using namespace pybind11::literals; + +inline py::module create_module(const std::string& module_name) +{ + return py::module_::create_extension_module(module_name.c_str(), nullptr, new py::module_::module_def); +} + +TEST(nljson_serializers_to_orderedjson, none) +{ + py::scoped_interpreter guard; + py::object obj1; + py::object obj2 = py::none(); + nl::ordered_json j1 = obj1; + nl::ordered_json j2 = obj2; + ASSERT_TRUE(j1.is_null()); + ASSERT_TRUE(j2.is_null()); +} + +TEST(nljson_serializers_to_orderedjson, bool_) +{ + py::scoped_interpreter guard; + py::bool_ obj(false); + nl::ordered_json j = obj; + + ASSERT_TRUE(j.is_boolean()); + ASSERT_FALSE(j.get()); +} + +TEST(nljson_serializers_to_orderedjson, integer) +{ + py::scoped_interpreter guard; + py::int_ obj(36); + nl::ordered_json j = obj; + + ASSERT_TRUE(j.is_number_integer()); + ASSERT_EQ(j.get(), 36); + + py::int_ obj_integer_min(std::numeric_limits::min()); + nl::ordered_json j_integer_min = obj_integer_min; + + ASSERT_TRUE(j_integer_min.is_number_integer()); + ASSERT_EQ(j_integer_min.get(), std::numeric_limits::min()); + + py::int_ obj_unsigned_max(std::numeric_limits::max()); + nl::ordered_json j_unsigned_max = obj_unsigned_max; + + ASSERT_TRUE(j_unsigned_max.is_number_unsigned()); + ASSERT_EQ(j_unsigned_max.get(), std::numeric_limits::max()); + + py::int_ obj_integer_border = py::int_(std::numeric_limits::max()).attr("__add__")(1); + nl::ordered_json j_integer_border = obj_integer_border; + + ASSERT_TRUE(j_integer_border.is_number_unsigned()); + ASSERT_EQ(j_integer_min.get(), (nl::ordered_json::number_unsigned_t)(std::numeric_limits::max()) + 1); + + py::int_ obj_small_outside = obj_integer_min.attr("__sub__")(1); + ASSERT_THROW(nl::ordered_json j_small_outside = obj_small_outside, std::runtime_error); + + py::int_ obj_large_outside=obj_unsigned_max.attr("__add__")(1); + ASSERT_THROW(nl::ordered_json j_large_outside = obj_large_outside, std::runtime_error); +} + +TEST(nljson_serializers_to_orderedjson, float_) +{ + py::scoped_interpreter guard; + py::float_ obj(36.37); + nl::ordered_json j = obj; + + ASSERT_TRUE(j.is_number_float()); + ASSERT_EQ(j.get(), 36.37); + + py::float_ obj_inf(INFINITY); + nl::ordered_json j_inf = obj_inf; + + ASSERT_TRUE(j_inf.is_number_float()); + ASSERT_EQ(j_inf.get(), INFINITY); + + py::float_ obj_nan(NAN); + nl::ordered_json j_nan = obj_nan; + + ASSERT_TRUE(j_nan.is_number_float()); + ASSERT_TRUE(isnan(j_nan.get())); +} + +TEST(nljson_serializers_to_orderedjson, string) +{ + py::scoped_interpreter guard; + py::str obj("Hello"); + nl::ordered_json j = obj; + + ASSERT_TRUE(j.is_string()); + ASSERT_EQ(j.get(), "Hello"); +} + +TEST(nljson_serializers_to_orderedjson, list) +{ + py::scoped_interpreter guard; + py::list obj; + obj.append(py::int_(36)); + obj.append(py::str("Hello World")); + obj.append(py::bool_(false)); + nl::ordered_json j = obj; + + ASSERT_TRUE(j.is_array()); + ASSERT_EQ(j[0].get(), 36); + ASSERT_EQ(j[1].get(), "Hello World"); + ASSERT_EQ(j[2].get(), false); +} + +TEST(nljson_serializers_to_orderedjson, empty_list) +{ + py::scoped_interpreter guard; + py::list obj; + nl::ordered_json j = obj; + + ASSERT_TRUE(j.is_array()); + ASSERT_TRUE(j.empty()); +} + +TEST(nljson_serializers_to_orderedjson, tuple) +{ + py::scoped_interpreter guard; + py::tuple obj = py::make_tuple(1234, "hello", false); + nl::ordered_json j = obj; + + ASSERT_TRUE(j.is_array()); + ASSERT_EQ(j[0].get(), 1234); + ASSERT_EQ(j[1].get(), "hello"); + ASSERT_EQ(j[2].get(), false); +} + +TEST(nljson_serializers_to_orderedjson, dict) +{ + py::scoped_interpreter guard; + py::dict obj("number"_a=1234, "hello"_a="world"); + nl::ordered_json j = obj; + + ASSERT_TRUE(j.is_object()); + ASSERT_EQ(j["number"].get(), 1234); + ASSERT_EQ(j["hello"].get(), "world"); +} + +TEST(nljson_serializers_to_orderedjson, empty_dict) +{ + py::scoped_interpreter guard; + py::dict obj; + nl::ordered_json j = obj; + + ASSERT_TRUE(j.is_object()); + ASSERT_TRUE(j.empty()); +} + +TEST(nljson_serializers_to_orderedjson, nested) +{ + py::scoped_interpreter guard; + py::dict obj( + "list"_a=py::make_tuple(1234, "hello", false), + "dict"_a=py::dict("a"_a=12, "b"_a=13), + "hello"_a="world", + "world"_a=py::none() + ); + nl::ordered_json j = obj; + + ASSERT_TRUE(j.is_object()); + ASSERT_EQ(j["list"][0].get(), 1234); + ASSERT_EQ(j["list"][1].get(), "hello"); + ASSERT_EQ(j["list"][2].get(), false); + ASSERT_EQ(j["dict"]["a"].get(), 12); + ASSERT_EQ(j["dict"]["b"].get(), 13); + ASSERT_EQ(j["world"], nullptr); +} + +TEST(nljson_serializers_to_orderedjson, handle) +{ + py::scoped_interpreter guard; + py::list obj; + obj.append(py::make_tuple(1234, "hello", false)); + obj.append(py::dict("a"_a=12, "b"_a=13)); + obj.append("world"); + obj.append(py::none()); + + for (py::handle handle : obj) + { + nl::ordered_json j = handle; + + ASSERT_TRUE(j.is_array() || j.is_object() || j.is_string() || j.is_null()); + } +} + +TEST(nljson_serializers_to_orderedjson, list_accessor) +{ + py::scoped_interpreter guard; + py::list obj; + obj.append(py::make_tuple(1234, "hello", false)); + obj.append(py::dict("a"_a=12, "b"_a=13)); + obj.append("world"); + obj.append(py::none()); + + nl::ordered_json j = obj[0]; + ASSERT_TRUE(j.is_array()); + + j = obj[1]; + ASSERT_TRUE(j.is_object()); + + j = py::make_tuple(1234, "hello", false)[0]; + ASSERT_TRUE(j.is_number_integer()); + ASSERT_EQ(j.get(), 1234); +} + +TEST(nljson_serializers_to_orderedjson, tuple_accessor) +{ + py::scoped_interpreter guard; + py::tuple obj = py::make_tuple(1234, "hello", false); + + nl::ordered_json j = obj[0]; + ASSERT_TRUE(j.is_number_integer()); + ASSERT_EQ(j.get(), 1234); + + j = obj[1]; + ASSERT_TRUE(j.is_string()); + ASSERT_EQ(j.get(), "hello"); +} + +TEST(nljson_serializers_to_orderedjson, item_accessor) +{ + py::scoped_interpreter guard; + py::dict obj = py::dict("a"_a=12, "b"_a="hello"); + + nl::ordered_json j = obj["a"]; + ASSERT_TRUE(j.is_number()); + ASSERT_EQ(j.get(), 12); + + j = obj["b"]; + ASSERT_TRUE(j.is_string()); + ASSERT_EQ(j.get(), "hello"); +} + +TEST(nljson_serializers_to_orderedjson, str_attr_accessor) +{ + py::scoped_interpreter guard; + py::module sys = py::module::import("sys"); + + nl::ordered_json j = sys.attr("base_prefix"); + ASSERT_TRUE(j.is_string()); +} + +TEST(nljson_serializers_to_orderedjson, obj_attr_accessor) +{ + py::scoped_interpreter guard; + py::module sys = py::module::import("sys"); + + py::str base_prefix = "base_prefix"; + + nl::ordered_json j = sys.attr(base_prefix); + ASSERT_TRUE(j.is_string()); +} + +TEST(nljson_serializers_from_orderedjson, none) +{ + py::scoped_interpreter guard; + nl::ordered_json j = "null"_json; + py::object obj = j; + + ASSERT_TRUE(obj.is_none()); +} + +TEST(nljson_serializers_from_orderedjson, bool_) +{ + py::scoped_interpreter guard; + nl::ordered_json j = "false"_json; + py::object obj = j; + + ASSERT_TRUE(py::isinstance(obj)); + ASSERT_FALSE(obj.cast()); + + py::bool_ obj2 = j; + + ASSERT_FALSE(obj2.cast()); +} + +TEST(nljson_serializers_from_orderedjson, integer) +{ + py::scoped_interpreter guard; + nl::ordered_json j = "36"_json; + py::object obj = j; + + ASSERT_TRUE(py::isinstance(obj)); + ASSERT_EQ(obj.cast(), 36); + + py::int_ obj2 = j; + + ASSERT_EQ(obj2.cast(), 36); +} + +TEST(nljson_serializers_from_orderedjson, integer_large_unsigned) +{ + // Note: if the asserts below error, the large number is printed as "-1" with + // an overflow error. This is only in the output step in pybind. Calling + // py::print on the objects shows the correct large unsigned integer. + + py::scoped_interpreter guard; + uint64_t original = 13625394757606569013ull; + py::int_ py_orig = original; + nl::ordered_json j = original; + py::object obj = j; + + ASSERT_TRUE(py::isinstance(obj)); + ASSERT_EQ(obj.cast(), original); + + py::int_ obj2 = j; + + // Use .equal to compare values not pointers + ASSERT_TRUE(obj2.equal(py_orig)); +} + +TEST(nljson_serializers_from_orderedjson, float_) +{ + py::scoped_interpreter guard; + nl::ordered_json j = "36.2"_json; + py::object obj = j; + + ASSERT_TRUE(py::isinstance(obj)); + ASSERT_EQ(obj.cast(), 36.2); + + py::float_ f_obj = j; + + ASSERT_EQ(f_obj.cast(), 36.2); + + nl::ordered_json j_inf(INFINITY); + py::object obj_inf = j_inf; + + ASSERT_TRUE(py::isinstance(obj_inf)); + ASSERT_EQ(obj_inf.cast(), INFINITY); + + py::float_ f_obj_inf = j_inf; + + ASSERT_EQ(f_obj_inf.cast(), INFINITY); + + nl::ordered_json j_nan(NAN); + py::object obj_nan = j_nan; + + ASSERT_TRUE(py::isinstance(obj_nan)); + ASSERT_TRUE(isnan(obj_nan.cast())); + + py::float_ f_obj_nan = j_nan; + + ASSERT_TRUE(isnan(f_obj_nan.cast())); +} + +TEST(nljson_serializers_from_orderedjson, string) +{ + py::scoped_interpreter guard; + nl::ordered_json j = "\"Hello World!\""_json; + py::object obj = j; + + ASSERT_TRUE(py::isinstance(obj)); + ASSERT_EQ(obj.cast(), "Hello World!"); + + py::str obj2 = j; + + ASSERT_EQ(obj2.cast(), "Hello World!"); +} + +TEST(nljson_serializers_from_orderedjson, list) +{ + py::scoped_interpreter guard; + nl::ordered_json j = "[1234, \"Hello World!\", false]"_json; + py::object obj = j; + + ASSERT_TRUE(py::isinstance(obj)); + ASSERT_EQ(py::list(obj)[0].cast(), 1234); + ASSERT_EQ(py::list(obj)[1].cast(), "Hello World!"); + ASSERT_EQ(py::list(obj)[2].cast(), false); + + py::list obj2 = j; + + ASSERT_EQ(py::list(obj2)[0].cast(), 1234); + ASSERT_EQ(py::list(obj2)[1].cast(), "Hello World!"); + ASSERT_EQ(py::list(obj2)[2].cast(), false); +} + +TEST(nljson_serializers_from_orderedjson, dict) +{ + py::scoped_interpreter guard; + nl::ordered_json j = "{\"a\": 1234, \"b\":\"Hello World!\", \"c\":false}"_json; + py::object obj = j; + + ASSERT_TRUE(py::isinstance(obj)); + ASSERT_EQ(py::dict(obj)["a"].cast(), 1234); + ASSERT_EQ(py::dict(obj)["b"].cast(), "Hello World!"); + ASSERT_EQ(py::dict(obj)["c"].cast(), false); + + py::dict obj2 = j; + + ASSERT_EQ(py::dict(obj2)["a"].cast(), 1234); + ASSERT_EQ(py::dict(obj2)["b"].cast(), "Hello World!"); + ASSERT_EQ(py::dict(obj2)["c"].cast(), false); +} + +TEST(nljson_serializers_from_orderedjson, nested) +{ + py::scoped_interpreter guard; + nl::ordered_json j = R"({ + "baz": ["one", "two", "three"], + "foo": 1, + "bar": {"a": 36, "b": false}, + "hey": null + })"_json; + py::object obj = j; + + ASSERT_TRUE(py::isinstance(obj)); + ASSERT_TRUE(py::isinstance(py::dict(obj)["baz"])); + ASSERT_TRUE(py::isinstance(py::dict(obj)["foo"])); + py::list baz = py::dict(obj)["baz"]; + py::int_ foo = py::dict(obj)["foo"]; + py::dict bar = py::dict(obj)["bar"]; + ASSERT_EQ(baz[0].cast(), "one"); + ASSERT_EQ(baz[1].cast(), "two"); + ASSERT_EQ(baz[2].cast(), "three"); + ASSERT_EQ(foo.cast(), 1); + ASSERT_EQ(bar["a"].cast(), 36); + ASSERT_FALSE(bar["b"].cast()); + ASSERT_TRUE(py::dict(obj)["hey"].is_none()); +} + +TEST(nljson_serializers_to_orderedjson, retains_order) +{ + py::scoped_interpreter guard; + py::dict obj; + obj["zebra"] = 1; + obj["apple"] = 2; + obj["mango"] = 3; + obj["banana"] = 4; + + nl::ordered_json j = obj; + + ASSERT_TRUE(j.is_object()); + ASSERT_EQ(j.size(), 4); + + auto it = j.begin(); + + ASSERT_EQ(it.key(), "zebra"); + ASSERT_EQ(it.value().get(), 1); + ++it; + + ASSERT_EQ(it.key(), "apple"); + ASSERT_EQ(it.value().get(), 2); + ++it; + + ASSERT_EQ(it.key(), "mango"); + ASSERT_EQ(it.value().get(), 3); + ++it; + + ASSERT_EQ(it.key(), "banana"); + ASSERT_EQ(it.value().get(), 4); + ++it; + + ASSERT_TRUE(it == j.end()); +} + +TEST(nljson_serializers_from_orderedjson, retains_order) +{ + py::scoped_interpreter guard; + + nl::ordered_json j = nl::ordered_json::parse(R"({ + "zebra": 1, + "apple": 2, + "mango": 3, + "banana": 4 + })"); + + py::dict obj = j; + + ASSERT_TRUE(py::isinstance(obj)); + ASSERT_EQ(obj.size(), 4); + + py::list keys = py::list(obj.attr("keys")()); + + ASSERT_EQ(keys[0].cast(), "zebra"); + ASSERT_EQ(keys[1].cast(), "apple"); + ASSERT_EQ(keys[2].cast(), "mango"); + ASSERT_EQ(keys[3].cast(), "banana"); +} + +inline const nl::ordered_json& test_fromto_orderedjson(const nl::ordered_json& json) +{ + return json; +} + +TEST(pybind11_caster_to_orderedjson, dict) +{ + py::scoped_interpreter guard; + py::module m = create_module("test"); + + m.def("to_json", &test_fromto_orderedjson); + + // Simulate calling this binding from Python with a dictionary as argument + py::dict obj("number"_a=1234, "hello"_a="world"); + nl::ordered_json j = m.attr("to_json")(obj); + + ASSERT_TRUE(j.is_object()); + ASSERT_EQ(j["number"].get(), 1234); + ASSERT_EQ(j["hello"].get(), "world"); +} + +TEST(pybind11_caster_from_orderedjson, dict) +{ + py::scoped_interpreter guard; + py::module m = create_module("test"); + + m.def("from_json", &test_fromto_orderedjson); + + // Simulate calling this binding from Python, getting back a py::object + py::dict obj("number"_a=1234, "hello"_a="world"); + py::dict j = m.attr("from_json")(obj); + + ASSERT_EQ(j["number"].cast(), 1234); + ASSERT_EQ(j["hello"].cast(), "world"); +} + +TEST(pybind11_caster_to_orderedjson, recursive_dict) +{ + py::scoped_interpreter guard; + py::module m = create_module("test"); + + m.def("to_json", &test_fromto_orderedjson); + + // Simulate calling this binding from Python with a dictionary as argument + py::dict obj_inner("number"_a=1234, "hello"_a="world"); + py::dict obj; + obj["first"] = obj_inner; + obj["second"] = obj_inner; + + ASSERT_NO_THROW(m.attr("to_json")(obj)); + + obj["second"]["recur"] = obj_inner; + ASSERT_ANY_THROW(m.attr("to_json")(obj)); +}