From bcf4188d781dcc5560a08330cf29ce609bfc2580 Mon Sep 17 00:00:00 2001 From: Marcus Boerger Date: Fri, 14 Nov 2025 09:20:29 +0100 Subject: [PATCH 1/2] Add Json write support Added `mbo/json` which adds JSon write support for almost any structured type. If anything is missing it can easily be added on the client side. --- CHANGELOG.md | 1 + mbo/json/BUILD.bazel | 49 +++ mbo/json/json.h | 897 ++++++++++++++++++++++++++++++++++++++++++ mbo/json/json_test.cc | 203 ++++++++++ 4 files changed, 1150 insertions(+) create mode 100644 mbo/json/BUILD.bazel create mode 100644 mbo/json/json.h create mode 100644 mbo/json/json_test.cc diff --git a/CHANGELOG.md b/CHANGELOG.md index 167a557..b1d03cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # 0.10.0 * Bumped minimum GCC to 13. +* Added `mbo/json` which adds JSon write support for almost any structured type. If anything is missing it can easily be added on the client side. * Added direct support for `-fno-exceptions` irrespective of config setting. * Added support for ASAN symbolizer with `--config=clang`. * Added AbslStringify and hash support to `NoDestruct`, `RefWrap`, `Required`. diff --git a/mbo/json/BUILD.bazel b/mbo/json/BUILD.bazel new file mode 100644 index 0000000..78721d4 --- /dev/null +++ b/mbo/json/BUILD.bazel @@ -0,0 +1,49 @@ +# SPDX-FileCopyrightText: Copyright (c) The helly25 authors (helly25.com) +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@rules_cc//cc:defs.bzl", "cc_library", "cc_test") + +package(default_visibility = ["//visibility:private"]) + +alias( + name = "json", + actual = "json_cc", +) + +cc_library( + name = "json_cc", + hdrs = ["json.h"], + deps = [ + "//mbo/config:require_cc", + "//mbo/types:cases_cc", + "//mbo/types:compare_cc", + "//mbo/types:stringify_cc", + "//mbo/types:traits_cc", + "//mbo/types:variant_cc", + "@abseil-cpp//absl/container:flat_hash_map", + ], +) + +cc_test( + name = "json_test", + srcs = ["json_test.cc"], + deps = [ + ":json_cc", + "//mbo/testing:matchers_cc", + "//mbo/types:typed_view_cc", + "@googletest//:gtest", + "@googletest//:gtest_main", + ], +) diff --git a/mbo/json/json.h b/mbo/json/json.h new file mode 100644 index 0000000..ceb29e8 --- /dev/null +++ b/mbo/json/json.h @@ -0,0 +1,897 @@ +// SPDX-FileCopyrightText: Copyright (c) The helly25 authors (helly25.com) +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// This header implements `Stringify`. +// +#ifndef MBO_JSON_JSON_H_ +#define MBO_JSON_JSON_H_ + +#include +#include // IWYU pragma: keep +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "absl/container/flat_hash_map.h" +#include "mbo/config/require.h" +#include "mbo/log/demangle.h" +#include "mbo/types/cases.h" +#include "mbo/types/compare.h" +#include "mbo/types/stringify.h" +#include "mbo/types/traits.h" // IWYU pragma: keep +#include "mbo/types/variant.h" + +// NOLINTBEGIN(readability-identifier-naming) + +namespace mbo::json { + +class Json; + +namespace json_internal { + +using Float = double; // Could be `long double` +using SignedInt = int64_t; +using UnsignedInt = uint64_t; + +using Array = std::unique_ptr>; +using Object = absl::flat_hash_map>; +using Variant = std::variant; + +template +concept JsonUseSignedInt = std::is_signed_v && std::integral && std::convertible_to; + +template +concept JsonUseUnsignedInt = + std::is_unsigned_v && std::integral && std::convertible_to; + +template +concept JsonUseFloat = std::floating_point || std::convertible_to; + +} // namespace json_internal + +template +concept JsonUseNull = std::convertible_to; + +template +concept JsonUseArray = std::same_as, json_internal::Array>; + +template +concept JsonUseBool = std::same_as, bool>; + +template +concept JsonUseNumber = json_internal::JsonUseSignedInt || json_internal::JsonUseUnsignedInt + || json_internal::JsonUseFloat; + +template +concept JsonUseObject = std::same_as, Json>; + +template +concept JsonUseString = types::ConstructibleFrom; + +template +concept ConvertibleToJson = JsonUseNull || JsonUseBool || JsonUseNumber || JsonUseString; + +class Json { + public: + using Float = json_internal::Float; + using SignedInt = json_internal::SignedInt; + using UnsignedInt = json_internal::UnsignedInt; + using Array = json_internal::Array; + using Object = json_internal::Object; + using Variant = json_internal::Variant; + + private: + using array_iterator = Array::element_type::iterator; + using const_array_iterator = Array::element_type::const_iterator; + using object_iterator = Object::iterator; + using const_object_iterator = Object::const_iterator; + + // Iterator (const and mutable) for Value iteration over both Arrays and Objects. + template + class value_iterator_t { + private: + using array_iterator = std::conditional_t; + using object_iterator = std::conditional_t; + + using const_iterator = value_iterator_t; + using mutable_iterator = value_iterator_t; + + public: + using iterator_category = std::forward_iterator_tag; // properties are forward only + using difference_type = array_iterator::difference_type; + using value_type = array_iterator::value_type; + using pointer = array_iterator::pointer; + using reference = array_iterator::reference; + using element_type = array_iterator::value_type; // Clang 16 + + ~value_iterator_t() noexcept = default; + value_iterator_t() noexcept = default; // Sadly needed for `std::forward_iterator` + + explicit value_iterator_t(array_iterator arr_it) noexcept : it_(arr_it) {} + + explicit value_iterator_t(object_iterator obj_it) noexcept : it_(obj_it) {} + + value_iterator_t(const value_iterator_t& other) noexcept : it_(other.it_) {} + + value_iterator_t& operator=(const value_iterator_t& other) noexcept { + if (this != &other) { + std::construct_at(this, other.it_); + } + return *this; + } + + value_iterator_t(value_iterator_t&& other) noexcept : it_(other.it_) {} + + value_iterator_t& operator=(value_iterator_t&& other) noexcept { + std::construct_at(this, other.it_); + return *this; + } + + // NOLINTBEGIN(cppcoreguidelines-rvalue-reference-param-not-moved,*explicit-*) + + value_iterator_t(const mutable_iterator& other) noexcept + requires std::same_as + : it_(other.it_) {} + + value_iterator_t& operator=(const mutable_iterator& other) noexcept + requires std::same_as + { + if (this != other) { + std::construct_at(this, other.it_); + } + return *this; + } + + value_iterator_t(mutable_iterator&& other) noexcept + requires std::same_as + : it_(other.it_) {} + + value_iterator_t& operator=(mutable_iterator&& other) noexcept + requires std::same_as + { + std::construct_at(this, other.it_); + return *this; + } + + operator array_iterator() noexcept { return std::get(it_); } + + operator object_iterator() noexcept { return std::get(it_); } + + operator const_array_iterator() const noexcept { return std::get(it_); } + + operator const_object_iterator() const noexcept { return std::get(it_); } + + // NOLINTEND(cppcoreguidelines-rvalue-reference-param-not-moved,*explicit-*) + + reference operator*() const noexcept { + return IsArray() ? *std::get(it_) : *std::get(it_)->second; + } + + pointer operator->() const noexcept { return &**this; } + + explicit operator reference() const noexcept { return **this; } + + value_iterator_t& operator++() noexcept { + if (IsArray()) { + ++std::get(it_); + } else { + ++std::get(it_); + } + return *this; + } + + value_iterator_t operator++(int) noexcept { // NOLINT(cert-dcl21-cpp) + value_iterator_t result = *this; + if (IsArray()) { + ++std::get(it_); + } else { + ++std::get(it_); + } + return result; + } + + friend bool operator==(const value_iterator_t& lhs, const value_iterator_t& rhs) noexcept { + return lhs.it_ == rhs.it_; + } + + private: + bool IsArray() const noexcept { return std::holds_alternative(it_); } + + std::variant it_{}; + }; + + static_assert(std::forward_iterator>); // Required for ValuesViewT + + template + class ValuesViewT : public std::ranges::view_interface> { + private: + using array_iterator = std::conditional_t; + using object_iterator = std::conditional_t; + + public: + using value_iterator = value_iterator_t; + using value_type = value_iterator::value_type; + using reference = value_iterator::reference; + using difference_type = value_iterator::difference_type; + + ValuesViewT() = delete; + + ValuesViewT(const Json& json, array_iterator begin, array_iterator end) : json_(json), begin_(begin), end_(end) {} + + ValuesViewT(const Json& json, object_iterator begin, object_iterator end) : json_(json), begin_(begin), end_(end) {} + + value_iterator begin() const noexcept { return begin_; } + + value_iterator end() const noexcept { return end_; } + + // Add `empty` and `size`, so that GoogleTest works. + bool empty() const noexcept { return json_.empty(); } + + std::size_t size() const noexcept { return json_.size(); } + + private: + const Json& json_; + value_iterator begin_; + value_iterator end_; + }; + + public: + enum class Kind { + kNull, // IsNull + kArray, // IsArray + kBool, // IsBool + kNumber, // IsNumber = IsDouble || IsInteger + kObject, // IsObject + kString, // IsString + }; + + enum class SerializeMode { + kCompact = static_cast(types::Stringify::OutputMode::kJson), + kLine = static_cast(types::Stringify::OutputMode::kJsonLine), + kPretty = static_cast(types::Stringify::OutputMode::kJsonPretty), + }; + + using value_type = Json; + + using reference = Array::element_type::reference; + using const_reference = Array::element_type::const_reference; + using pointer = Array::element_type::pointer; + using const_pointer = Array::element_type::const_pointer; + using size_type = Array::element_type::size_type; + using difference_type = Array::element_type::difference_type; + + using iterator = array_iterator; + using const_iterator = const_array_iterator; + using reverse_iterator = Array::element_type::reverse_iterator; + using const_reverse_iterator = Array::element_type::const_reverse_iterator; + + using value_iterator = value_iterator_t; + using const_value_iterator = value_iterator_t; + + using values_view = ValuesViewT; + using const_values_view = ValuesViewT; + + template + using ToKind = types::Cases< // Ordered most precise to least preceise. + types::IfThen, std::integral_constant>, + types::IfThen, std::integral_constant>, + types::IfThen, std::integral_constant>, + types::IfThen, std::integral_constant>, + types::IfThen, std::integral_constant>, + types::IfThen, std::integral_constant>, + types::IfElse>; + + template + static Kind GetKind(Value&& /*unused*/) { // NOLINT(*-missing-std-forward) + return ToKind::value; + } + + ~Json() noexcept = default; + + Json() noexcept : data_{std::nullopt} {} + + Json(const Json& other) : Json() { + std::visit( + types::Overloaded{ + [&](const std::unique_ptr& other) { MakeObject() = *other; }, + [&](Other&& other) { Json::operator=(std::forward(other)); }}, + other.data_); + } + + Json& operator=(const Json&) = delete; + Json(Json&&) noexcept = default; + Json& operator=(Json&&) noexcept = default; + + explicit Json(std::nullopt_t /*value*/) noexcept : data_{std::nullopt} {} + + template + explicit Json(Value&& value) noexcept : data_{std::nullopt} { + *this = std::forward(value); + } + + Json& operator=(const std::unique_ptr& other) { // NOLINT(misc-no-recursion) + CopyObject(*other); + return *this; + } + + template> + Json& operator=(Value&& value) { // NOLINT(misc-unconventional-assign-operator,*-signature): See above. + CopyFrom(std::forward(value)); + return *this; + } + + explicit Json(std::string_view value) noexcept : data_{std::nullopt} { data_.emplace(value); } + + explicit operator bool() const noexcept { return !IsNull(); } + + bool IsNull() const noexcept { return std::holds_alternative(data_); } + + bool IsBool() const noexcept { return std::holds_alternative(data_); } + + bool IsFalse() const noexcept { return IsBool() && !std::get(data_); } + + bool IsTrue() const noexcept { return IsBool() && std::get(data_); } + + bool IsSignedInt() const noexcept { return std::holds_alternative(data_); } + + bool IsUnsignedInt() const noexcept { return std::holds_alternative(data_); } + + bool IsInteger() const noexcept { return IsSignedInt() || IsUnsignedInt(); } + + bool IsFloat() const noexcept { return std::holds_alternative(data_); } + + bool IsNumber() const noexcept { return IsInteger() || IsFloat(); } + + bool IsString() const noexcept { return std::holds_alternative(data_); } + + bool IsArray() const noexcept { return std::holds_alternative(data_); } + + bool IsObject() const noexcept { return std::holds_alternative(data_); } + + Kind GetKind() const noexcept { + if (IsNull()) { + return Kind::kNull; + } else if (IsArray()) { + return Kind::kArray; + } else if (IsBool()) { + return Kind::kBool; + } else if (IsNumber()) { + return Kind::kNumber; + } else if (IsObject()) { + return Kind::kObject; + } else if (IsString()) { + return Kind::kString; + } + MBO_CONFIG_REQUIRE(false, "Bad type"); + return Kind::kNull; + } + + std::ostream& Stream( + std::ostream& os, + SerializeMode mode = SerializeMode::kCompact, + const types::StringifyRootOptions& root_options = types::StringifyRootOptions{}) const { + ::mbo::types::Stringify stringify{static_cast(mode), root_options}; + if (IsNull()) { + struct Null {}; + + stringify.Stream(os, Null{}); + } else { + MBO_CONFIG_REQUIRE(IsObject(), "Only Objects can be serialized."); + stringify.Stream(os, std::get(data_)); + } + return os; + } + + std::string Serialize( + SerializeMode mode = SerializeMode::kCompact, + const types::StringifyRootOptions& root_options = types::StringifyRootOptions{}) const { + std::stringstream os; + Stream(os, mode, root_options); + return os.str(); + } + + // Change value to a `Null` value. + Json& Reset() { + data_.emplace(std::nullopt); + return *this; + } + + // Change value to an `Array`. + Json& MakeArray() { return MakeType(std::make_unique()); } + + // Change value to an `Object` + Json& MakeObject() { return MakeType(Object{}); } + + template Str> + Json& MakeString(Str&& str = {}) { + return MakeType(std::forward(str)); + } + + bool empty() const noexcept { + if (IsArray()) { + return std::get(data_)->empty(); + } + if (IsObject()) { + return std::get(data_).empty(); + } + return true; + } + + std::size_t size() const noexcept { + if (IsArray()) { + return std::get(data_)->size(); + } + if (IsObject()) { + return std::get(data_).size(); + } + return 0; + } + + void clear() { + if (IsArray()) { + std::get(data_)->clear(); + } else if (IsObject()) { + std::get(data_).clear(); + } else { + Reset(); + } + } + + bool contains(std::string_view property) const noexcept { + return IsObject() && std::get(data_).contains(property); + } + + template + requires(types::ConstructibleFrom) + Json& emplace_back(Arg&& arg) { + MakeArray(); + auto& values = std::get(data_); + values->emplace_back(std::forward(arg)); + return values->back(); + } + + template + requires(types::ConstructibleFrom) + Json& emplace(std::string_view property, Arg&& arg) { + MakeObject(); + auto& properties = std::get(data_); + return *properties.emplace(property, std::make_unique(std::forward(arg))).first->second; + } + + template + requires(types::ConstructibleFrom) + void push_back(Arg&& arg) { + MakeArray(); + auto& values = std::get(data_); + values->emplace_back(std::forward(arg)); + } + + void pop_back() { + if (IsArray()) { + auto& values = std::get(data_); + values->pop_back(); + } else { + MBO_CONFIG_REQUIRE(IsArray(), "Is not an Array or Null."); + } + } + + Json& operator[](std::size_t index) { + MakeArray(); + MBO_CONFIG_REQUIRE(index < size(), "Out of range."); + return (*std::get(data_))[index]; + } + + const Json& operator[](std::size_t index) const { + MBO_CONFIG_REQUIRE(IsArray(), "Is not an Array."); + MBO_CONFIG_REQUIRE(index < size(), "Out of range."); + return (*std::get(data_))[index]; + } + + Json& operator[](std::string_view property) noexcept { + MakeObject(); + auto [it, inserted] = std::get(data_).emplace(property, nullptr); + if (inserted) { + it->second = std::make_unique(); + } + return *it->second; + } + + const Json& operator[](std::string_view property) const noexcept { + MBO_CONFIG_REQUIRE(IsObject(), "Is not an Object."); + MBO_CONFIG_REQUIRE(contains(property), "Property not present:") << "'" << property << "'."; + return *(std::get(data_)).at(property); + } + + Json& at(std::size_t index) { return (*this)[index]; } + + const Json& at(std::size_t index) const { return (*this)[index]; } + + Json& at(std::string_view property) { return (*this)[property]; } + + const Json& at(std::string_view property) const { return (*this)[property]; } + + iterator erase(const_iterator pos) { + MBO_CONFIG_REQUIRE(IsArray(), "Is not an Array."); + return std::get(data_)->erase(pos); + } + + iterator erase(const_iterator first, const_iterator last) { + MBO_CONFIG_REQUIRE(IsArray(), "Is not an Array."); + return std::get(data_)->erase(first, last); + } + + value_iterator erase(const_value_iterator pos) { + if (IsArray()) { + return value_iterator{std::get(data_)->erase(pos)}; + } + MBO_CONFIG_REQUIRE(IsObject(), "Is neither Array nor Object."); + const_value_iterator end(pos); + return value_iterator{std::get(data_).erase(pos, ++end)}; + } + + value_iterator erase(const_value_iterator first, const_value_iterator last) { + if (IsArray()) { + return value_iterator{std::get(data_)->erase(first, last)}; + } + MBO_CONFIG_REQUIRE(IsObject(), "Is neither Array nor Object."); + return value_iterator{std::get(data_).erase(first, last)}; + } + + size_type erase(std::string_view property) { + if (IsObject()) { + return std::get(data_).erase(property); + } + return 0; + } + + void resize(size_type count) { + MBO_CONFIG_REQUIRE(IsArray(), "Is not an Array."); + std::get(data_)->resize(count); + } + + template + void resize(size_type count, const Value& value) { + MBO_CONFIG_REQUIRE(IsArray(), "Is not an Array."); + std::get(data_)->resize(count, value); + } + + iterator begin() { + MBO_CONFIG_REQUIRE(IsArray(), "Is not an Array."); + return std::get(data_)->begin(); + } + + const_iterator begin() const { + MBO_CONFIG_REQUIRE(IsArray(), "Is not an Array."); + return std::get(data_)->begin(); + } + + const_iterator cbegin() { + MBO_CONFIG_REQUIRE(IsArray(), "Is not an Array."); + return std::get(data_)->cbegin(); + } + + iterator end() { + MBO_CONFIG_REQUIRE(IsArray(), "Is not an Array."); + return std::get(data_)->end(); + } + + const_iterator end() const { + MBO_CONFIG_REQUIRE(IsArray(), "Is not an Array."); + return std::get(data_)->end(); + } + + const_iterator cend() { + MBO_CONFIG_REQUIRE(IsArray(), "Is not an Array."); + return std::get(data_)->cend(); + } + + reverse_iterator rbegin() { + MBO_CONFIG_REQUIRE(IsArray(), "Is not an Array."); + return std::get(data_)->rbegin(); + } + + const_reverse_iterator rbegin() const { + MBO_CONFIG_REQUIRE(IsArray(), "Is not an Array."); + return std::get(data_)->rbegin(); + } + + const_reverse_iterator crbegin() { + MBO_CONFIG_REQUIRE(IsArray(), "Is not an Array."); + return std::get(data_)->crbegin(); + } + + reverse_iterator rend() { + MBO_CONFIG_REQUIRE(IsArray(), "Is not an Array."); + return std::get(data_)->rend(); + } + + const_reverse_iterator rend() const { + MBO_CONFIG_REQUIRE(IsArray(), "Is not an Array."); + return std::get(data_)->rend(); + } + + const_reverse_iterator crend() { + MBO_CONFIG_REQUIRE(IsArray(), "Is not an Array."); + return std::get(data_)->crend(); + } + + auto values() -> values_view { + if (IsArray()) { + auto& array = std::get(data_); + MBO_CONFIG_REQUIRE_DEBUG(array != nullptr, "May not be nulltr."); + std::cout << "Array Size: " << array->size() << "\n" << std::flush; + return {*this, array->begin(), array->end()}; + } + MBO_CONFIG_REQUIRE(IsObject(), "Is neither Array nor Object."); + auto& object = std::get(data_); + std::cout << "Object Size: " << object.size() << "\n" << std::flush; + return {*this, object.begin(), object.end()}; + } + + auto values() const -> const_values_view { + if (IsArray()) { + const auto& array = std::get(data_); + return {*this, array->begin(), array->end()}; + } + MBO_CONFIG_REQUIRE(IsObject(), "Is neither Array nor Object."); + const auto& object = std::get(data_); + return {*this, object.begin(), object.end()}; + } + + auto array_values() { + MBO_CONFIG_REQUIRE(IsArray(), "Is not an Array."); + return std::views::all(*std::get(data_)); + } + + auto array_values() const { + MBO_CONFIG_REQUIRE(IsArray(), "Is not an Array."); + return std::views::all(*std::get(data_)); + } + + auto property_names() const { + MBO_CONFIG_REQUIRE(IsObject(), "Is not an Object."); + return std::views::keys(std::get(data_)); + } + + auto property_pairs() { + MBO_CONFIG_REQUIRE(IsObject(), "Is not an Object."); + return std::views::transform( + std::get(data_), + [](Object::reference v) -> std::pair { return {v.first, *v.second}; }); + } + + auto property_pairs() const { + MBO_CONFIG_REQUIRE(IsObject(), "Is not an Object."); + return std::views::transform( + std::get(data_), + [](Object::const_reference v) -> std::pair { return {v.first, *v.second}; }); + } + + auto property_values() { + MBO_CONFIG_REQUIRE(IsObject(), "Is not an Object."); + return std::views::transform(std::get(data_), [](Object::const_reference v) -> Json& { return *v.second; }); + } + + auto property_values() const { + MBO_CONFIG_REQUIRE(IsObject(), "Is not an Object."); + return std::views::transform( + std::get(data_), [](Object::const_reference v) -> const Json& { return *v.second; }); + } + + bool operator==(std::nullopt_t /*unused*/) const noexcept { return IsNull(); } + + std::strong_ordering operator<=>(const Json& other) const noexcept { return Compare(other); } + + bool operator==(const Json& other) const noexcept { return Compare(other) == std::strong_ordering::equal; } + + bool operator<(const Json& other) const noexcept { return Compare(other) == std::strong_ordering::less; } + + template + std::strong_ordering operator<=>(const Value& other) const noexcept { + return Compare(other); + } + + template + bool operator==(const Value& other) const noexcept { + return Compare(other) == std::strong_ordering::equal; + } + + template + bool operator<(const Value& other) const noexcept { + return Compare(other) == std::strong_ordering::less; + } + + friend const Variant& MboTypesStringifyValueAccess( + const Json& json, + const types::StringifyFieldOptions& /*options*/) { + return json.data_; + } + + private: + template + bool IsType() const noexcept { + return std::holds_alternative(data_); + } + + template + inline Json& MakeType(T&& value); + + template> + void CopyFrom(Value&& value) { + if constexpr (JsonUseNull) { + data_.emplace(std::nullopt); + } else if constexpr (JsonUseString) { + data_.emplace(std::forward(value)); + } else if constexpr (json_internal::JsonUseSignedInt) { + data_.emplace(value); + } else if constexpr (json_internal::JsonUseUnsignedInt) { + data_.emplace(value); + } else if constexpr (json_internal::JsonUseFloat) { + data_.emplace(value); + } else if constexpr (JsonUseArray) { + data_.emplace(std::make_unique(*value)); + } else if constexpr (JsonUseBool) { + data_.emplace(value); + } else if constexpr (JsonUseObject) { + CopyObject(value); + } else { + MBO_CONFIG_REQUIRE(false, "Bad type: ") << log::DemangleV(value); + } + } + + void CopyObject(const Json& other) { // NOLINT(misc-no-recursion) + Object& object = data_.emplace(); // NOLINT(*-auto) + for (const auto& [name, value] : std::get(other.data_)) { + std::unique_ptr& ref = object[name]; + if (ref == nullptr) { + ref = std::make_unique(); + } + *ref = value; + } + } + + std::strong_ordering Compare(const Json& other) const noexcept; + + template + std::strong_ordering Compare(const Value& other) const noexcept; + + Variant data_; +}; + +static_assert(types::ThreeWayComparableTo); +static_assert(types::ThreeWayComparableTo); + +namespace json_internal { + +inline std::strong_ordering operator<=>(const std::unique_ptr& lhs, const std::unique_ptr& rhs) { + // Uses `!= nullptr` so that the `nullptr < non-nullptr`. NOLINTNEXTLINE(readability-implicit-bool-conversion) + if (const auto comp = (lhs != nullptr) <=> (rhs != nullptr); comp != std::strong_ordering::equal) { + return comp; + } + if (lhs == nullptr) { + return std::strong_ordering::equal; + } + return types::WeakToStrong(*lhs <=> *rhs); +} + +template +concept IsNullopt = std::same_as, std::nullopt_t>; + +template +concept NotNullopt = !IsNullopt; + +constexpr auto kJsonComparator = mbo::types::Overloaded{ + // NOLINTBEGIN(*-implicit-bool-conversion,*-narrowing-conversions) + // The left hand side is always a variant-member of `Json::Variant`. + [](const std::nullopt_t /*unused*/, const std::nullopt_t /*unused*/) { return std::strong_ordering::equal; }, + [](bool lhs, bool rhs) -> std::strong_ordering { return lhs <=> rhs; }, + [](SignedInt lhs, types::IsArithmetic auto rhs) { return types::CompareArithmetic(lhs, rhs); }, + [](UnsignedInt lhs, types::IsArithmetic auto rhs) { return types::CompareArithmetic(lhs, rhs); }, + [](Float lhs, types::IsArithmetic auto rhs) { return types::CompareArithmetic(lhs, rhs); }, + [](const std::string& lhs, const std::three_way_comparable_with auto& rhs) { + return types::WeakToStrong(lhs <=> rhs); + }, + [](const Json::Array& lhs, const Json::Array& rhs) { return types::WeakToStrong(lhs <=> rhs); }, + [](const Json::Object& lhs, const Json::Object& rhs) -> std::strong_ordering { + auto lhs_it = lhs.begin(); + auto rhs_it = rhs.begin(); + while (lhs_it != lhs.end() && rhs_it != rhs.end()) { + if (auto comp = *lhs_it <=> *rhs_it; comp != std::strong_ordering::equal) { + return comp; + } + ++lhs_it; + ++rhs_it; + } + return lhs.size() <=> rhs.size(); + }, + [](const LhsOther& lhs, const RhsOther& rhs) -> std::strong_ordering { + if constexpr (IsNullopt || IsNullopt) { + return NotNullopt <=> NotNullopt; + } else { + MBO_CONFIG_REQUIRE(false, "Missing explicit comparison implementation: ") + << log::DemangleV(lhs) << " <=> " << log::DemangleV(rhs); + return std::strong_ordering::equal; + } + }, + // NOLINTEND(*-implicit-bool-conversion,*-narrowing-conversions) +}; + +} // namespace json_internal + +template +inline Json& Json::MakeType(T&& value) { // NOLINT(*-function-cognitive-complexity) + if constexpr (types::ConstructibleFrom) { + if (IsString()) { + return *this; + } + MBO_CONFIG_REQUIRE(IsNull(), "Is not an std::string or Null."); + data_.emplace(std::forward(value)); + } else { + if (IsType()) { + return *this; + } + using RawT = std::remove_cvref_t; + if constexpr (std::same_as) { + MBO_CONFIG_REQUIRE(IsNull(), "Is not Null."); + } else if constexpr (std::same_as) { + MBO_CONFIG_REQUIRE(IsNull(), "Is not an Array or Null."); + MBO_CONFIG_REQUIRE(value != nullptr, "May not be a nullptr."); + } else if constexpr (std::same_as) { + MBO_CONFIG_REQUIRE(IsNull(), "Is not a bool or Null."); + } else if constexpr (std::same_as) { + MBO_CONFIG_REQUIRE(IsNull(), "Is not an Float or Null."); + } else if constexpr (std::same_as) { + MBO_CONFIG_REQUIRE(IsNull(), "Is not an SignedInt or Null."); + } else if constexpr (std::same_as) { + MBO_CONFIG_REQUIRE(IsNull(), "Is not an UnsignedInt or Null."); + } else if constexpr (std::same_as) { + MBO_CONFIG_REQUIRE(IsNull(), "Is not an Object or Null."); + } + data_.emplace(std::forward(value)); + } + return *this; +} + +inline std::strong_ordering Json::Compare(const Json& other) const noexcept { + if (auto comp = GetKind() <=> other.GetKind(); comp != std::strong_ordering::equal) { + return comp; + } + return std::visit(json_internal::kJsonComparator, data_, other.data_); +} + +template +inline std::strong_ordering Json::Compare(const Value& other) const noexcept { + if (const auto comp = GetKind() <=> Json::GetKind(other); comp != std::strong_ordering::equal) { + return comp; + } + return std::visit( + [&other](const Lhs& lhs) { return json_internal::kJsonComparator(lhs, other); }, data_); +} + +static_assert(types::HasMboTypesStringifyValueAccess); +static_assert(!types::HasMboTypesStringifyValueAccess); +static_assert(!types::HasMboTypesStringifyValueAccess); + +} // namespace mbo::json + +// NOLINTEND(readability-identifier-naming) + +#endif // MBO_JSON_JSON_H_ diff --git a/mbo/json/json_test.cc b/mbo/json/json_test.cc new file mode 100644 index 0000000..dd5e5a6 --- /dev/null +++ b/mbo/json/json_test.cc @@ -0,0 +1,203 @@ +// SPDX-FileCopyrightText: Copyright (c) The helly25 authors (helly25.com) +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "mbo/json/json.h" + +#include // IWYU pragma: keep +#include // IWYU pragma: keep +#include + +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "mbo/testing/matchers.h" +#include "mbo/types/typed_view.h" + +// NOLINTBEGIN(*-magic-*) + +namespace mbo::json { +namespace { + +using ::mbo::testing::EqualsText; +using ::mbo::testing::IsNullopt; +using ::mbo::types::TypedView; +using ::testing::AnyOf; +using ::testing::ElementsAre; +using ::testing::ElementsAreArray; +using ::testing::IsEmpty; +using ::testing::Not; +using ::testing::Pair; +using ::testing::SizeIs; +using ::testing::UnorderedElementsAreArray; + +struct JsonTest : ::testing::Test {}; + +TEST_F(JsonTest, Test) { + EXPECT_THAT(Json{}, IsNullopt()); + EXPECT_THAT(Json{}.Serialize(), EqualsText(R"({} +)")); +} + +TEST_F(JsonTest, Comparison) { + static_assert(std::three_way_comparable_with); + static_assert(types::ThreeWayComparableTo); + static_assert(types::ThreeWayComparableTo); + EXPECT_THAT(Json{}, std::nullopt); + EXPECT_THAT(Json{}, IsNullopt()); + EXPECT_THAT(Json{1}, Not(std::nullopt)); + EXPECT_THAT(Json{1}, Not(IsNullopt())); + EXPECT_THAT(Json{2}, 2); + EXPECT_THAT(Json{"yes"}, "yes"); + EXPECT_THAT(Json{3}, Not("nope")); +} + +TEST_F(JsonTest, BasicsAndSerialize) { + Json data; + data["foo"] = "bar"; + data["bar"] = "baz"; + ASSERT_THAT(data["bar"], IsEmpty()); + EXPECT_THAT( + data.Serialize(), // + AnyOf( + EqualsText(R"({"bar":"baz","foo":"bar"} +)"), + EqualsText(R"({"foo":"bar","bar":"baz"} +)"))); + EXPECT_THAT(data.Serialize(Json::SerializeMode::kPretty), EqualsText(R"({ + "bar": "baz", + "foo": "bar" +} +)")); + data["null"] = std::nullopt; + ASSERT_THAT(data["null"].IsNull(), true); + ASSERT_THAT(data["null"], IsEmpty()); + EXPECT_THAT(data.Serialize(Json::SerializeMode::kPretty), EqualsText(R"({ + "bar": "baz", + "foo": "bar", + "null": null +} +)")); + data["null"].MakeObject(); + ASSERT_THAT(data["null"].IsObject(), true); + ASSERT_THAT(data["null"], IsEmpty()); + EXPECT_THAT(data.Serialize(Json::SerializeMode::kPretty), EqualsText(R"({ + "bar": "baz", + "foo": "bar", + "null": { + } +} +)")); + data["null"].Reset(); + ASSERT_THAT(data["null"].IsNull(), true); + ASSERT_THAT(data["null"], IsEmpty()); + EXPECT_THAT(data.Serialize(Json::SerializeMode::kPretty), EqualsText(R"({ + "bar": "baz", + "foo": "bar", + "null": null +} +)")); + data["array"].MakeArray(); + ASSERT_THAT(data["array"].IsArray(), true); + EXPECT_THAT(data.Serialize(Json::SerializeMode::kPretty), EqualsText(R"({ + "array": [ + ], + "bar": "baz", + "foo": "bar", + "null": null +} +)")); + data["array"].emplace_back(25); + data["array"].emplace_back("42"); + ASSERT_THAT(data["array"], SizeIs(2)); + EXPECT_THAT(data.Serialize(Json::SerializeMode::kPretty), EqualsText(R"({ + "array": [ + 25, + "42" + ], + "bar": "baz", + "foo": "bar", + "null": null +} +)")); + data["object"].MakeObject(); + ASSERT_THAT(data["object"].IsObject(), true); + ASSERT_THAT(data["object"], IsEmpty()); + EXPECT_THAT(data.Serialize(Json::SerializeMode::kPretty), EqualsText(R"({ + "array": [ + 25, + "42" + ], + "bar": "baz", + "foo": "bar", + "null": null, + "object": { + } +} +)")); + data["object"]["one"] = 33; + data["object"]["two"].MakeString("Two"); + ASSERT_THAT(data["object"], SizeIs(2)); + EXPECT_THAT(data.Serialize(Json::SerializeMode::kPretty), EqualsText(R"({ + "array": [ + 25, + "42" + ], + "bar": "baz", + "foo": "bar", + "null": null, + "object": { + "one": 33, + "two": "Two" + } +} +)")); +} + +TEST_F(JsonTest, ArrayIteration) { + Json json; + json.MakeArray(); + json.push_back(0); + json.push_back("hello"); + json.emplace_back("world"); + json.emplace_back(std::nullopt); + json.emplace_back(true); + json.emplace_back(false); + const std::initializer_list expected = {Json{0}, Json{"hello"}, Json{"world"}, Json{std::nullopt}, + Json{true}, Json{false}}; + const auto elements_are = ElementsAreArray(expected); + EXPECT_THAT(TypedView(json.array_values()), elements_are); + EXPECT_THAT(TypedView(json.array_values()), ElementsAre(0, "hello", "world", std::nullopt, true, false)); + EXPECT_THAT(json, elements_are); + EXPECT_THAT(std::vector(json.begin(), json.end()), elements_are); + const auto values = json.values(); + EXPECT_THAT(values, Not(IsEmpty())); + EXPECT_THAT(values.begin(), Not(values.end())); + EXPECT_THAT(json.values(), elements_are); +} + +TEST_F(JsonTest, PropertyIteration) { + Json json; + json["a"] = 1; + json["b"] = 2; + json["c"] = 3; + EXPECT_THAT(TypedView(json.property_names()), UnorderedElementsAreArray({"a", "b", "c"})); + EXPECT_THAT(TypedView(json.property_pairs()), UnorderedElementsAreArray({Pair("a", 1), Pair("b", 2), Pair("c", 3)})); + EXPECT_THAT(TypedView(json.property_values()), UnorderedElementsAreArray({1, 2, 3})); + // EXPECT_THAT(json.values(), UnorderedElementsAreArray({1, 2, 3})); +} + +} // namespace +} // namespace mbo::json + +// NOLINTEND(*-magic-*) From 514a12f573a3390a6c5b68eb0d746a74bf1c6c40 Mon Sep 17 00:00:00 2001 From: helly25 Date: Fri, 14 Nov 2025 12:39:59 +0100 Subject: [PATCH 2/2] Make GCC happy --- mbo/json/json.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mbo/json/json.h b/mbo/json/json.h index ceb29e8..481b12b 100644 --- a/mbo/json/json.h +++ b/mbo/json/json.h @@ -108,8 +108,8 @@ class Json { template class value_iterator_t { private: - using array_iterator = std::conditional_t; - using object_iterator = std::conditional_t; + using array_iterator = std::conditional_t; + using object_iterator = std::conditional_t; using const_iterator = value_iterator_t; using mutable_iterator = value_iterator_t; @@ -223,8 +223,8 @@ class Json { template class ValuesViewT : public std::ranges::view_interface> { private: - using array_iterator = std::conditional_t; - using object_iterator = std::conditional_t; + using array_iterator = std::conditional_t; + using object_iterator = std::conditional_t; public: using value_iterator = value_iterator_t;