diff --git a/score/json/BUILD b/score/json/BUILD index a6e082e2b7..e4a6ea7067 100644 --- a/score/json/BUILD +++ b/score/json/BUILD @@ -39,6 +39,27 @@ config_setting( visibility = ["@score_baselibs//score/json/internal/parser:__subpackages__"], ) +string_flag( + name = "writer_library", + build_setting_default = "json_serialize", + values = [ + "json_serialize", + "vajson", + ], +) + +config_setting( + name = "writer_library_json_serialize", + flag_values = {":writer_library": "json_serialize"}, + visibility = ["@score_baselibs//score/json/internal/writer:__subpackages__"], +) + +config_setting( + name = "writer_library_vajson", + flag_values = {":writer_library": "vajson"}, + visibility = ["@score_baselibs//score/json/internal/writer:__subpackages__"], +) + cc_library( name = "parser_interface", srcs = ["i_json_parser.cpp"], @@ -116,8 +137,8 @@ cc_library( visibility = ["@score_baselibs//score/json:__subpackages__"], deps = [ ":writer_interface", - "@score_baselibs//score/json/internal/writer/json_serialize", - "@score_baselibs//score/language/safecpp/safe_math", + "@score_baselibs//score/json/internal/writer", + "@score_baselibs//score/json/internal/writer:writer_backend", ], ) @@ -207,6 +228,10 @@ cc_test( "test_warnings", "aborts_upon_exception", ], + local_defines = select({ + ":writer_library_json_serialize": ["WRITER_JSON_SERIALIZE"], + ":writer_library_vajson": ["WRITER_VAJSON"], + }), tags = ["unit"], visibility = [ "@score_baselibs//score/json:__pkg__", diff --git a/score/json/README.md b/score/json/README.md index bbc329812a..c7cf6134e3 100644 --- a/score/json/README.md +++ b/score/json/README.md @@ -18,10 +18,12 @@ - [Requirements](#requirements) - [Assumptions of Use](#assumptions-of-use) - [Selecting base library](#selecting-base-library) + - [Selecting writer library](#selecting-writer-library) This JSON library is designed as an abstraction layer which can switch to using other parsers/serializers under the hood. At the moment it uses vaJson from Vector for parsing, -which is ASIL D certified. For serialization this library uses a custom implementation. +which is ASIL D certified. For serialization a custom implementation is used by default, with vaJson +available as an alternative, [selectable via a feature flag](#selecting-writer-library). This library requires to be ASIL B certified, so it can be used in other ASIL B certified components. @@ -398,7 +400,7 @@ since they would require high amount of work. Please be aware and note: For the most up to date information on Requirements please follow the provided links to CodeBeamer. Adaptive Platform SW Safety Requirements -- [The users of the vaJson ibrary SHALL guarantee the integrity of the data used to initialize the `amsr::json::JsonData` buffer.](broken_link_c/issue/6576406) +- [The users of the vaJson Library SHALL guarantee the integrity of the data used to initialize the `score::json::vajson::JsonData` buffer.](broken_link_c/issue/6576406) [SW Component Requirements](broken_link_c/tracker/566325?workingSetId=-1&layout_name=document&subtreeRoot=5310849) - The JSON-Library shall support at least the functional set of RFC-8259. @@ -438,3 +440,15 @@ In order to make use of nlohmann json library, feature flag needs to be set. Ple bazel test --config=spp_host_clang //score/json/... --//score/json:base_library="nlohmann" nlohmann json library do not supports hexadecimal. As it is not part of json standard. + +## Selecting writer library + +Independently of the parser, the serialization backend is selected by the `writer_library` flag. It accepts +`json_serialize` (the default, a custom implementation) and `vajson` (the vector json library). The parser flag +`base_library` has no influence on serialization. + +bazel test --config=spp_host_clang //score/json/... --//score/json:writer_library="vajson" + +Mind that the two backends differ in the representation they emit: `json_serialize` pretty-prints with a four +space indentation, whereas `vajson` emits compact JSON without any insignificant whitespace between tokens. Both +produce valid, equivalent JSON, but consumers comparing serialized output byte-wise are affected by the choice. diff --git a/score/json/internal/writer/BUILD b/score/json/internal/writer/BUILD new file mode 100644 index 0000000000..0942081923 --- /dev/null +++ b/score/json/internal/writer/BUILD @@ -0,0 +1,38 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("@rules_cc//cc:defs.bzl", "cc_library") +load("@score_baselibs//score/language/safecpp:toolchain_features.bzl", "COMPILER_WARNING_FEATURES") + +# Declarations of the serialization backend seam. Exactly one backend provides the definitions; which one is +# selected by the `//score/json:writer_library` flag below. +cc_library( + name = "writer_backend", + hdrs = ["writer_backend.h"], + features = COMPILER_WARNING_FEATURES, + tags = ["FFI"], + visibility = ["@score_baselibs//score/json:__subpackages__"], + deps = [ + "@score_baselibs//score/json/internal/model", + "@score_baselibs//score/result", + ], +) + +alias( + name = "writer", + actual = select({ + "@score_baselibs//score/json:writer_library_json_serialize": "@score_baselibs//score/json/internal/writer/json_serialize:json_serialize_backend", + "@score_baselibs//score/json:writer_library_vajson": "@score_baselibs//score/json/internal/writer/vajson:vajson_backend", + }), + visibility = ["@score_baselibs//score/json:__pkg__"], +) diff --git a/score/json/internal/writer/json_serialize/BUILD b/score/json/internal/writer/json_serialize/BUILD index 310dda7963..57a0afe47b 100644 --- a/score/json/internal/writer/json_serialize/BUILD +++ b/score/json/internal/writer/json_serialize/BUILD @@ -31,6 +31,20 @@ cc_library( ], ) +cc_library( + name = "json_serialize_backend", + srcs = ["json_serialize_backend.cpp"], + features = COMPILER_WARNING_FEATURES, + tags = ["FFI"], + visibility = ["@score_baselibs//score/json/internal/writer:__pkg__"], + deps = [ + ":json_serialize", + "@score_baselibs//score/json/internal/writer:writer_backend", + "@score_baselibs//score/language/futurecpp", + "@score_baselibs//score/language/safecpp/safe_math", + ], +) + cc_test( name = "json_serialize_unit_test", srcs = [ diff --git a/score/json/internal/writer/json_serialize/json_serialize_backend.cpp b/score/json/internal/writer/json_serialize/json_serialize_backend.cpp new file mode 100644 index 0000000000..7f34015f00 --- /dev/null +++ b/score/json/internal/writer/json_serialize/json_serialize_backend.cpp @@ -0,0 +1,262 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "score/json/internal/writer/writer_backend.h" + +#include "score/json/internal/writer/json_serialize/json_serialize.h" +#include "score/language/safecpp/safe_math/safe_math.h" + +#include +#include + +#include +#include +#include +#include +// std::locale provides functionalities (e.g. facets) to customize parts of iostream implementation. In this case it is +// used for serializing integers efficiently. An implementation without std::locale might require (massive) amounts of +// code duplication. Furthermore, this header is allowed to be used for character conversion purposes if and it must be +// ensured that libcatalog is not linked using the target toolchain, which is the case for libjson. +// Reference: broken_link_c/issue/4600528 +// NOLINTNEXTLINE(score-banned-include): see rationale above +#include +#include +#include + +namespace +{ + +template +// Coverity thinks this function is unused, but it is used in for calculating kIntBufLen. +// coverity[autosar_cpp14_a0_1_3_violation] +constexpr std::size_t max_dec_digits() noexcept +{ + U v = std::numeric_limits::max(); + std::size_t n = 1U; + while (v >= 10U) + { + v /= 10U; + // There is no known type that could represent enough digits to overflow std::size_t + // coverity[autosar_cpp14_a4_7_1_violation] + ++n; + } + return n; +} + +template +inline constexpr std::size_t kIntBufLen = max_dec_digits>>() + 1U; + +// Rationale: noexcept safe magnitude conversion; uses safe_math assertions; +// COVERITY: autosar_cpp14_a15_5_3_violation, uncaught_exception +// policy requires terminate on failure; no exceptions thrown. +template +// coverity[autosar_cpp14_a15_5_3_violation] +// coverity[uncaught_exception] +inline auto abs_magnitude_unsigned(T val) noexcept +{ + using U = std::make_unsigned_t>; + static_assert(std::is_integral_v, "integral only"); + static_assert(std::is_integral_v, "U must be integral"); + static_assert(std::numeric_limits::digits >= std::numeric_limits>::digits, + "U must represent full magnitude of T"); + + // Coverity doesn't know constexpr if statements + // coverity[autosar_cpp14_a7_1_8_violation] + if constexpr (std::is_signed_v) + { + const auto abs_val = score::safe_math::Abs(val); + const auto cast_res = score::safe_math::Cast(abs_val); + SCORE_LANGUAGE_FUTURECPP_ASSERT_PRD_MESSAGE(cast_res.has_value(), + "Safe cast failed in abs_magnitude_unsigned (signed)"); + return cast_res.value(); + } + else + { + const auto cast_res = score::safe_math::Cast(val); + SCORE_LANGUAGE_FUTURECPP_ASSERT_PRD_MESSAGE(cast_res.has_value(), + "Safe cast failed in abs_magnitude_unsigned (unsigned)"); + return cast_res.value(); + } +} + +template +[[nodiscard]] std::string_view integer_to_chars(std::array>& buffer, const T val) noexcept +{ + using U = std::make_unsigned_t>; + static_assert(std::is_integral_v && std::is_integral_v, "integral only"); + static_assert(!std::is_same_v, bool>, "bool not supported"); + static_assert(std::numeric_limits::digits >= std::numeric_limits>::digits, + "U must represent full magnitude of T"); + + const bool is_negative = (std::is_signed_v && (val < static_cast(0))); + U x = abs_magnitude_unsigned(val); + + // no range checks on it because the function declaration ensures enough buffer space for the given type + auto it = buffer.end(); + do + { + // Rationale: Converting unsigned digit (x % 10U) to signed int8_t is safe because + // the modulo operation guarantees the value is in range [0, 9], which fits in both + // uint8_t and int8_t. The signed type is needed for consistent char arithmetic with '0'. + // coverity[autosar_cpp14_m5_0_9_violation] + const auto digit = static_cast(x % static_cast(10U)); + it = std::prev(it); + // coverity[autosar_cpp14_m5_0_9_violation] + *it = static_cast(static_cast('0') + digit); + x /= 10U; + } while (x > static_cast(0U)); + if (is_negative) + { + it = std::prev(it); + *it = '-'; + } + + return std::string_view(&*it, static_cast(std::distance(it, buffer.end()))); +} + +// This specialization of std::num_put ignores parameter widths (see std::setw) as this feature is neither useful +// nor used for serializing JSON. +class OptimizedNumPut : public std::num_put +{ + public: + // Coverity thinks this function is unused, wheras it is used for std::locale + // coverity[autosar_cpp14_a0_1_3_violation] + using std::num_put::num_put; + + protected: + using std::num_put::do_put; + + // Coverity thinks this function is unused, wheras it is used for std::locale + // coverity[autosar_cpp14_a0_1_3_violation] + iter_type do_put(iter_type out, std::ios_base& s, char_type fill, long v) const override + { + return OptimizedPutForInts(out, s, fill, v); + } + // Coverity thinks this function is unused, wheras it is used for std::locale + // coverity[autosar_cpp14_a0_1_3_violation] + iter_type do_put(iter_type out, std::ios_base& s, char_type fill, unsigned long v) const override + { + return OptimizedPutForInts(out, s, fill, v); + } + // Coverity thinks this function is unused, wheras it is used for std::locale + // coverity[autosar_cpp14_a0_1_3_violation] + // LCOV_EXCL_START see SCORE_LANGUAGE_FUTURECPP_UNREACHABLE_MESSAGE + iter_type do_put(iter_type out, std::ios_base& s, char_type fill, long long v) const override + { + SCORE_LANGUAGE_FUTURECPP_UNREACHABLE_MESSAGE( + "This code is unreachable with tested toolchains and target platforms"); + return OptimizedPutForInts(out, s, fill, v); + } + // LCOV_EXCL_STOP + // Coverity thinks this function is unused, wheras it is used for std::locale + // coverity[autosar_cpp14_a0_1_3_violation] + // LCOV_EXCL_START see SCORE_LANGUAGE_FUTURECPP_UNREACHABLE_MESSAGE + iter_type do_put(iter_type out, std::ios_base& s, char_type fill, unsigned long long v) const override + { + SCORE_LANGUAGE_FUTURECPP_UNREACHABLE_MESSAGE( + "This code is unreachable with tested toolchains and target platforms"); + return OptimizedPutForInts(out, s, fill, v); + } + // LCOV_EXCL_STOP + + private: + template + iter_type OptimizedPutForInts(iter_type out, std::ios_base&, char_type, T val) const + { + std::array> buf{}; + const auto sv = integer_to_chars(buf, val); + return std::copy(sv.begin(), sv.end(), out); + } +}; + +template +score::Result SerializeToStreamInternal(std::ostream& out_stream, const T& json_data) +{ + score::json::JsonSerialize serializer{out_stream}; + return serializer << json_data; +} + +template +score::Result SerializeToBufferInternal(const T& json_data) +{ + // This line must be hit when the function is called. Since other parts of this function show line coverage, + // this line must also be hit. Missing coverage is due to a bug in the coverage tool + std::ostringstream string_stream{}; // LCOV_EXCL_LINE + + // NOLINTBEGIN(score-no-dynamic-raw-memory) See rationale below + // Rationale: std::num_put is reference counted and std::locale does manage it. + // Explanation for Coverity Suppression for AUTOSAR A3-3-2: Static locale with custom facet requires runtime + // initialization. std::locale constructor with facet cannot be constexpr. Thread-safe due to function-local static + // initialization guarantee (C++11 §6.7 [stmt.dcl]/4). coverity[autosar_cpp14_a3_3_2_violation] + const static std::locale loc(std::locale(), new OptimizedNumPut()); + // NOLINTEND(score-no-dynamic-raw-memory) See rationale above + + score::cpp::ignore = string_stream.imbue(loc); + + score::json::JsonSerialize serializer{string_stream}; + auto result = serializer << json_data; + // LCOV_EXCL_BR_START Error path currently not reachable + if (!result.has_value()) + // LCOV_EXCL_BR_STOP + { + return score::Unexpected{result.error()}; + } + + return string_stream.str(); +} + +} // namespace + +namespace score +{ +namespace json +{ +namespace internal +{ +namespace writer +{ + +score::Result SerializeToStream(std::ostream& out_stream, const score::json::Object& json_data) +{ + return SerializeToStreamInternal(out_stream, json_data); +} + +score::Result SerializeToStream(std::ostream& out_stream, const score::json::List& json_data) +{ + return SerializeToStreamInternal(out_stream, json_data); +} + +score::Result SerializeToStream(std::ostream& out_stream, const score::json::Any& json_data) +{ + return SerializeToStreamInternal(out_stream, json_data); +} + +score::Result SerializeToBuffer(const score::json::Object& json_data) +{ + return SerializeToBufferInternal(json_data); +} + +score::Result SerializeToBuffer(const score::json::List& json_data) +{ + return SerializeToBufferInternal(json_data); +} + +score::Result SerializeToBuffer(const score::json::Any& json_data) +{ + return SerializeToBufferInternal(json_data); +} + +} // namespace writer +} // namespace internal +} // namespace json +} // namespace score diff --git a/score/json/internal/writer/vajson/BUILD b/score/json/internal/writer/vajson/BUILD new file mode 100644 index 0000000000..3107530b96 --- /dev/null +++ b/score/json/internal/writer/vajson/BUILD @@ -0,0 +1,59 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("@rules_cc//cc:defs.bzl", "cc_library", "cc_test") +load("@score_baselibs//score/language/safecpp:toolchain_features.bzl", "COMPILER_WARNING_FEATURES") + +cc_library( + name = "vajson_serialize", + srcs = ["vajson_serialize.cpp"], + hdrs = ["vajson_serialize.h"] + glob([ + "writer/**/*.h", + ]), + features = COMPILER_WARNING_FEATURES, + tags = ["FFI"], + visibility = ["@score_baselibs//score/json:__subpackages__"], + deps = [ + "@score_baselibs//score/json/internal/model", + "@score_baselibs//score/json/internal/parser/vajson/vajson_impl", + "@score_baselibs//score/language/futurecpp", + "@score_baselibs//score/result", + ], +) + +cc_library( + name = "vajson_backend", + srcs = ["vajson_backend.cpp"], + features = COMPILER_WARNING_FEATURES, + tags = ["FFI"], + visibility = [ + # Coverage scope root (//tools/coverage, eclipse-score/baselibs#512). + "//tools/coverage:__pkg__", + "@score_baselibs//score/json/internal/writer:__pkg__", + ], + deps = [ + ":vajson_serialize", + "@score_baselibs//score/json/internal/writer:writer_backend", + ], +) + +cc_test( + name = "vajson_serialize_test", + srcs = ["vajson_serialize_test.cpp"], + features = COMPILER_WARNING_FEATURES + ["aborts_upon_exception"], + tags = ["unit"], + deps = [ + ":vajson_serialize", + "@googletest//:gtest_main", + ], +) diff --git a/score/json/internal/writer/vajson/vajson_backend.cpp b/score/json/internal/writer/vajson/vajson_backend.cpp new file mode 100644 index 0000000000..9fb50bedf3 --- /dev/null +++ b/score/json/internal/writer/vajson/vajson_backend.cpp @@ -0,0 +1,63 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "score/json/internal/writer/writer_backend.h" + +#include "score/json/internal/writer/vajson/vajson_serialize.h" + +namespace +{ + +template +score::Result SerializeToStreamInternal(std::ostream& out_stream, const T& json_data) +{ + score::json::VajsonSerialize serializer{out_stream}; + return serializer << json_data; +} + +} // namespace + +namespace score::json::internal::writer +{ + +score::Result SerializeToStream(std::ostream& out_stream, const score::json::Object& json_data) +{ + return SerializeToStreamInternal(out_stream, json_data); +} + +score::Result SerializeToStream(std::ostream& out_stream, const score::json::List& json_data) +{ + return SerializeToStreamInternal(out_stream, json_data); +} + +score::Result SerializeToStream(std::ostream& out_stream, const score::json::Any& json_data) +{ + return SerializeToStreamInternal(out_stream, json_data); +} + +score::Result SerializeToBuffer(const score::json::Object& json_data) +{ + return score::json::VajsonToBuffer(json_data); +} + +score::Result SerializeToBuffer(const score::json::List& json_data) +{ + return score::json::VajsonToBuffer(json_data); +} + +score::Result SerializeToBuffer(const score::json::Any& json_data) +{ + return score::json::VajsonToBuffer(json_data); +} + +} // namespace score::json::internal::writer diff --git a/score/json/internal/writer/vajson/vajson_serialize.cpp b/score/json/internal/writer/vajson/vajson_serialize.cpp new file mode 100644 index 0000000000..9bfc846445 --- /dev/null +++ b/score/json/internal/writer/vajson/vajson_serialize.cpp @@ -0,0 +1,76 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/json/internal/writer/vajson/vajson_serialize.h" + +#include +#include +#include +namespace +{ +template +score::Result SerializeToStream(std::ostream& out_stream, const T& json_data) +{ + auto serializer = score::json::vajson::DocumentSerializer{std::ref(out_stream)}; + static_cast(std::move(serializer) << json_data); + + // A default constructed Result already carries the success state. + score::Result result{}; + if (out_stream.fail()) + { + result = + score::MakeUnexpected(score::json::Error::kUnknownError, "vaJSON serializer failed to write to stream"); + } + + return result; +} +template +score::Result SerializeToBuffer(const T& json_data) +{ + std::ostringstream out_stream{}; + + // and_then keeps a single exit point: the buffer is only extracted once serialization succeeded, + // and any error is forwarded unchanged. + return SerializeToStream(out_stream, json_data).and_then([&out_stream](auto&&...) -> score::Result { + return out_stream.str(); + }); +} +} // namespace + +namespace score::json +{ +VajsonSerialize::VajsonSerialize(std::ostream& out_stream) noexcept : out_stream_{out_stream} {} +score::Result VajsonSerialize::operator<<(const score::json::Object& json_data) +{ + return SerializeToStream(out_stream_, json_data); +} +score::Result VajsonSerialize::operator<<(const score::json::List& json_data) +{ + return SerializeToStream(out_stream_, json_data); +} +score::Result VajsonSerialize::operator<<(const score::json::Any& json_data) +{ + return SerializeToStream(out_stream_, json_data); +} +score::Result VajsonToBuffer(const score::json::Object& json_data) +{ + return SerializeToBuffer(json_data); +} +score::Result VajsonToBuffer(const score::json::List& json_data) +{ + return SerializeToBuffer(json_data); +} +score::Result VajsonToBuffer(const score::json::Any& json_data) +{ + return SerializeToBuffer(json_data); +} +} // namespace score::json diff --git a/score/json/internal/writer/vajson/vajson_serialize.h b/score/json/internal/writer/vajson/vajson_serialize.h new file mode 100644 index 0000000000..b0a4d6865a --- /dev/null +++ b/score/json/internal/writer/vajson/vajson_serialize.h @@ -0,0 +1,196 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_LIB_JSON_INTERNAL_WRITER_VAJSON_VAJSON_SERIALIZE_H +#define SCORE_LIB_JSON_INTERNAL_WRITER_VAJSON_VAJSON_SERIALIZE_H +#include "score/json/internal/model/any.h" +#include "score/json/internal/model/error.h" +#include "score/json/internal/writer/vajson/writer/serializers/structures/generic_value_serializer_impl.h" +#include "score/json/internal/writer/vajson/writer/serializers/structures/key_serializer.h" +#include "score/result/result.h" +#include +#include +#include +#include +#include +#include +#include + +namespace score::json +{ + +namespace internal::writer::vajson +{ +class ObjectKeySerializer final +{ + public: + auto operator()(const score::memory::StringComparisonAdaptor& key) const noexcept -> score::json::vajson::JKeyType + { + return score::json::vajson::JKey(key.GetAsStringView()); + } +}; +template +auto SerializeValue(score::json::vajson::GenericValueSerializer&& serializer, + const score::json::Any& value) noexcept -> + typename score::json::vajson::GenericValueSerializer::Next; +template +auto SerializeNumber(score::json::vajson::GenericValueSerializer&& serializer, + const score::json::Number& value) noexcept -> + typename score::json::vajson::GenericValueSerializer::Next +{ + // The serializer is consumed by whichever alternative matches, so the outcome is parked in an + // optional to keep a single exit point. Number::As() re-parses the value on every call, hence the + // chain stays an else-if: the alternatives must be probed lazily, in order. + std::optional::Next> serialized{}; + + if (const auto unsigned_value = value.As(); unsigned_value.has_value()) + { + serialized.emplace(std::move(serializer) << score::json::vajson::JNumber(*unsigned_value)); + } + else if (const auto signed_value = value.As(); signed_value.has_value()) + { + serialized.emplace(std::move(serializer) << score::json::vajson::JNumber(*signed_value)); + } + else if (const auto float_value = value.As(); float_value.has_value()) + { + serialized.emplace(std::move(serializer) << score::json::vajson::JNumber(*float_value)); + } + else if (const auto double_value = value.As(); double_value.has_value()) + { + serialized.emplace(std::move(serializer) << score::json::vajson::JNumber(*double_value)); + } + else + { + serialized.emplace(std::move(serializer) << score::json::vajson::JNull()); + } + + return *std::move(serialized); +} +template +auto SerializeList(score::json::vajson::GenericValueSerializer&& serializer, + const score::json::List& list) noexcept -> + typename score::json::vajson::GenericValueSerializer::Next +{ + return std::move(serializer) << score::json::vajson::JArray( + [&list](score::json::vajson::ArrayStart array_serializer) noexcept { + auto next = std::move(array_serializer); + for (const auto& value : list) + { + next = SerializeValue(std::move(next), value); + } + }); +} +template +auto SerializeObject(score::json::vajson::GenericValueSerializer&& serializer, + const score::json::Object& object) noexcept -> + typename score::json::vajson::GenericValueSerializer::Next +{ + return std::move(serializer) << score::json::vajson::JObject( + [&object](score::json::vajson::ObjectStart object_serializer) noexcept { + auto next = std::move(object_serializer); + for (const auto& element : object) + { + auto value_serializer = std::move(next) << ObjectKeySerializer{}(element.first); + next = SerializeValue(std::move(value_serializer), element.second); + } + return next; + }); +} +template +auto SerializeValue(score::json::vajson::GenericValueSerializer&& serializer, + const score::json::Any& value) noexcept -> + typename score::json::vajson::GenericValueSerializer::Next +{ + // See SerializeNumber() for why the outcome is parked in an optional rather than returned directly. + std::optional::Next> serialized{}; + + if (const auto object = value.As(); object.has_value()) + { + serialized.emplace(SerializeObject(std::move(serializer), object->get())); + } + else if (const auto list = value.As(); list.has_value()) + { + serialized.emplace(SerializeList(std::move(serializer), list->get())); + } + else if (const auto string_value = value.As(); string_value.has_value()) + { + serialized.emplace(std::move(serializer) << score::json::vajson::JString(string_value->get())); + } + else if (const auto null_value = value.As(); null_value.has_value()) + { + score::cpp::ignore = null_value; + serialized.emplace(std::move(serializer) << score::json::vajson::JNull()); + } + else if (const auto number = value.As(); number.has_value()) + { + serialized.emplace(SerializeNumber(std::move(serializer), number->get())); + } + else if (const auto boolean = value.As(); boolean.has_value()) + { + serialized.emplace(std::move(serializer) << score::json::vajson::JBool(*boolean)); + } + else + { + // Any holds a bool, a Number, a std::string, a Null, an Object or a List, so every alternative has been + // probed by now and this branch cannot be reached. Writing null keeps the output valid JSON in case an + // alternative is added to Any without being handled here. + serialized.emplace(std::move(serializer) << score::json::vajson::JNull()); /* LCOV_EXCL_LINE */ + } + + return *std::move(serialized); +} +} // namespace internal::writer::vajson + +class VajsonSerialize final +{ + public: + explicit VajsonSerialize(std::ostream& out_stream) noexcept; + ~VajsonSerialize() noexcept = default; + VajsonSerialize(const VajsonSerialize&) = delete; + VajsonSerialize(VajsonSerialize&&) noexcept = default; + VajsonSerialize& operator=(const VajsonSerialize&) = delete; + VajsonSerialize& operator=(VajsonSerialize&&) = delete; + score::Result operator<<(const score::json::Object& json_data); + score::Result operator<<(const score::json::List& json_data); + score::Result operator<<(const score::json::Any& json_data); + + private: + std::ostream& out_stream_; +}; +score::Result VajsonToBuffer(const score::json::Object& json_data); +score::Result VajsonToBuffer(const score::json::List& json_data); +score::Result VajsonToBuffer(const score::json::Any& json_data); +} // namespace score::json + +namespace score::json::vajson +{ +template +auto operator<<(GenericValueSerializer&& serializer, const score::json::Object& value) noexcept -> + typename GenericValueSerializer::Next +{ + return score::json::internal::writer::vajson::SerializeObject(std::move(serializer), value); +} +template +auto operator<<(GenericValueSerializer&& serializer, const score::json::List& value) noexcept -> + typename GenericValueSerializer::Next +{ + return score::json::internal::writer::vajson::SerializeList(std::move(serializer), value); +} +template +auto operator<<(GenericValueSerializer&& serializer, const score::json::Any& value) noexcept -> + typename GenericValueSerializer::Next +{ + return score::json::internal::writer::vajson::SerializeValue(std::move(serializer), value); +} +} // namespace score::json::vajson + +#endif // SCORE_LIB_JSON_INTERNAL_WRITER_VAJSON_VAJSON_SERIALIZE_H diff --git a/score/json/internal/writer/vajson/vajson_serialize_test.cpp b/score/json/internal/writer/vajson/vajson_serialize_test.cpp new file mode 100644 index 0000000000..6f17efd9ce --- /dev/null +++ b/score/json/internal/writer/vajson/vajson_serialize_test.cpp @@ -0,0 +1,176 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/json/internal/writer/vajson/vajson_serialize.h" + +#include + +#include +#include +#include +#include +#include +#include + +namespace score::json +{ +namespace +{ +TEST(VajsonSerializeTest, SerializesNestedAnyToCompactJson) +{ + Object nested_object{}; + nested_object["number"] = Any{std::int32_t{7}}; + List list{}; + list.emplace_back(Any{Null{}}); + list.emplace_back(Any{std::move(nested_object)}); + Object root{}; + root["boolean"] = Any{true}; + root["list"] = Any{std::move(list)}; + root["string"] = Any{std::string{"line1\n\"quoted\"\\line2"}}; + const auto result = VajsonToBuffer(Any{std::move(root)}); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, + std::string{ + "{\"boolean\":true,\"list\":[null,{\"number\":7}],\"string\":\"line1\\n\\\"quoted\\\"\\\\line2\"}"}); +} +TEST(VajsonSerializeTest, SerializesObjectKeysUsingStringComparisonAdaptor) +{ + Object object{}; + object[std::string_view{"alpha"}] = Any{std::string{"a"}}; + object["beta"] = Any{std::uint32_t{2U}}; + const auto result = VajsonToBuffer(object); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, std::string{"{\"alpha\":\"a\",\"beta\":2}"}); +} +// RFC 8259, section 7 does not allow characters in the range U+0000 to U+001F to appear unescaped in a string. +TEST(VajsonSerializeTest, EscapesControlCharactersWithoutShortEscapeSequence) +{ + Object object{}; + object["value"] = Any{std::string{"\x01\x0b\x1f"}}; + const auto result = VajsonToBuffer(object); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, std::string{"{\"value\":\"\\u0001\\u000b\\u001f\"}"}); +} +TEST(VajsonSerializeTest, EscapesNullCharacterInsideString) +{ + Object object{}; + object["value"] = Any{std::string{std::string_view{"a\0b", 3U}}}; + const auto result = VajsonToBuffer(object); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, std::string{"{\"value\":\"a\\u0000b\"}"}); +} +TEST(VajsonSerializeTest, EscapesControlCharactersInObjectKeys) +{ + Object object{}; + object[std::string{ + "a\x1e" + "b"}] = Any{std::string{"v"}}; + const auto result = VajsonToBuffer(object); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, std::string{"{\"a\\u001eb\":\"v\"}"}); +} +TEST(VajsonSerializeTest, PrefersShortEscapeSequencesOverUnicodeEscapes) +{ + Object object{}; + object["value"] = Any{std::string{"\b\f\n\r\t"}}; + const auto result = VajsonToBuffer(object); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, std::string{"{\"value\":\"\\b\\f\\n\\r\\t\"}"}); +} +TEST(VajsonSerializeTest, EscapesEveryControlCharacterAndNothingElse) +{ + std::string value{}; + for (std::uint32_t character{0U}; character <= 0x7FU; ++character) + { + value.push_back(static_cast(character)); + } + Object object{}; + object["value"] = Any{value}; + const auto result = VajsonToBuffer(object); + ASSERT_TRUE(result.has_value()); + + // No unescaped control character may survive in the output. + for (const char serialized : *result) + { + EXPECT_GE(std::char_traits::to_int_type(serialized), 0x20) + << "unescaped control character in serialized output"; + } + // The printable characters are written as they are, with only the quote and the backslash escaped. + EXPECT_NE(result->find("!\\\"#$%&'()*+,-./0123456789:;<=>?"), std::string::npos); + EXPECT_NE(result->find("[\\\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\x7f"), std::string::npos); +} +TEST(VajsonSerializeTest, PassesMultiByteUtf8CharactersThrough) +{ + Object object{}; + object["value"] = Any{std::string{"\u00e4\u20ac"}}; + const auto result = VajsonToBuffer(object); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, std::string{"{\"value\":\"\u00e4\u20ac\"}"}); +} +TEST(VajsonSerializeTest, SerializesFiniteDouble) +{ + Object object{}; + object["number"] = Any{double{1.5}}; + const auto result = VajsonToBuffer(object); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, std::string{"{\"number\":1.5}"}); +} +// RFC 8259, section 6 has no representation for infinity or NaN, hence they cannot be serialized. +TEST(VajsonSerializeTest, RejectsInfiniteDouble) +{ + Object object{}; + object["number"] = Any{std::numeric_limits::infinity()}; + EXPECT_FALSE(VajsonToBuffer(object).has_value()); +} +TEST(VajsonSerializeTest, RejectsNegativeInfiniteFloat) +{ + Object object{}; + object["number"] = Any{-std::numeric_limits::infinity()}; + EXPECT_FALSE(VajsonToBuffer(object).has_value()); +} +TEST(VajsonSerializeTest, RejectsNotANumber) +{ + List list{}; + list.emplace_back(Any{std::numeric_limits::quiet_NaN()}); + EXPECT_FALSE(VajsonToBuffer(list).has_value()); +} +TEST(VajsonSerializeTest, RejectsNotANumberOnStream) +{ + Object object{}; + object["number"] = Any{std::numeric_limits::quiet_NaN()}; + std::ostringstream out_stream{}; + VajsonSerialize serializer{out_stream}; + const auto result = serializer << object; + EXPECT_FALSE(result.has_value()); + // The non-representable number itself must not have been written. + EXPECT_EQ(out_stream.str().find("nan"), std::string::npos); +} +TEST(VajsonSerializeTest, SerializesBothBooleanValues) +{ + List list{}; + list.emplace_back(Any{true}); + list.emplace_back(Any{false}); + const auto result = VajsonToBuffer(list); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, std::string{"[true,false]"}); +} +TEST(VajsonSerializeTest, SerializesTopLevelList) +{ + List list{}; + list.emplace_back(Any{std::uint8_t{5U}}); + list.emplace_back(Any{std::string{"value"}}); + const auto result = VajsonToBuffer(list); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, std::string{"[5,\"value\"]"}); +} +} // namespace +} // namespace score::json diff --git a/score/json/internal/writer/vajson/writer/serializers/structures/generic_value_serializer.h b/score/json/internal/writer/vajson/writer/serializers/structures/generic_value_serializer.h new file mode 100644 index 0000000000..b98a2932a4 --- /dev/null +++ b/score/json/internal/writer/vajson/writer/serializers/structures/generic_value_serializer.h @@ -0,0 +1,236 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +/// \file +/// \brief Serializer for generic JSON value types. +/// \details Provides serializers for Null, Bool, Number, String, Array, and Object types. + +#ifndef SCORE_LIB_JSON_INTERNAL_WRITER_VAJSON_WRITER_SERIALIZERS_STRUCTURES_GENERIC_VALUE_SERIALIZER_H +#define SCORE_LIB_JSON_INTERNAL_WRITER_VAJSON_WRITER_SERIALIZERS_STRUCTURES_GENERIC_VALUE_SERIALIZER_H + +#include +#include +#include +#include +#include +#include + +#include "score/json/internal/parser/vajson/vajson_impl/util/json_error_domain.h" +#include "score/json/internal/parser/vajson/vajson_impl/util/types.h" +#include "score/json/internal/writer/vajson/writer/serializers/structures/serializer.h" +#include "score/json/internal/writer/vajson/writer/serializers/util/escaped_json_string.h" +#include "score/json/internal/writer/vajson/writer/types/array_type.h" +#include "score/json/internal/writer/vajson/writer/types/basic_types.h" +#include "score/json/internal/writer/vajson/writer/types/object_type.h" + +namespace score::json::vajson +{ +/// \brief A serializer for JSON value types +/// \tparam Return Type of the return value of a << operation. Must be one of the following types: - Unit: Serializer +/// has no follow-up state (outermost element). - Self or GenericValueSerializer: Next element is another value +/// (e.g. inside arrays). - KeySerializer: Next element is a key. +template +class GenericValueSerializer final +{ + public: + /// \brief Type of the return value + /// \details Set the type of the return value to be either its own type GenericValueSerializer (for arrays or a + /// specified type) or the type specified by Return. + using Next = typename std::conditional_t, GenericValueSerializer, Return>; + + /// \brief Constructs a GenericValueSerializer from an output stream + /// \details Do not create an instance of GenericValueSerializer directly, use the aliases in + /// score/json/internal/writer/vajson/writer/serializers/structures/serializer.h + /// \param[in] os Output stream to write into. + /// \param[in] state of the Serializer. + /// \param[in] bom The BOM type to write. + explicit GenericValueSerializer(WriterType os, + SerializerState state = SerializerState::kEmpty, + EncodingType bom = EncodingType::kNone) noexcept + : os_(os), serializer_state_{state} + { + this->WriteBom(bom); + } + + /// \brief Default move constructor + GenericValueSerializer(GenericValueSerializer&&) noexcept = default; + + /// \brief Default move assignment + /// \return A reference to the moved into object. + auto operator=(GenericValueSerializer&&) & noexcept -> GenericValueSerializer& = default; + + // Deleted copy constructor/assignment operator. + GenericValueSerializer(const GenericValueSerializer&) = delete; + auto operator=(const GenericValueSerializer&) -> GenericValueSerializer& = delete; + + /// \brief Default DTOR + ~GenericValueSerializer() noexcept = default; + + /// \brief Serializes a null value + /// \return The succeeding serializer. + // coverity[autosar_cpp14_m9_3_3_violation] + auto operator<<(JNullType) && noexcept -> Next + { + return this->Serialize([this]() noexcept { + constexpr auto null_str = "null"sv; + this->os_.get().write(null_str.data(), null_str.size()); + }); + } + + /// \brief Serializes a boolean value + /// \param[in] b Boolean value to serialize. + /// \return The succeeding serializer. + // coverity[autosar_cpp14_m9_3_3_violation] + auto operator<<(JBoolType b) && noexcept -> Next + { + const std::string_view value{b.value ? "true" : "false"}; + // NOLINTNEXTLINE(whitespace/line_length) + // coverity[autosar_cpp14_m8_5_1_violation] + return this->Serialize([this, value]() noexcept { + this->os_.get().write(value.data(), static_cast(value.size())); + }); + } + + /// \brief Serializes a number value + /// \details The JSON grammar of RFC 8259, section 6 only covers finite numbers, so infinity and NaN have no + /// representation. Such a value is not written at all, instead the output stream is put into the failed + /// state, which the enclosing serializer reports as an error to its caller. + /// \tparam T Type of number. + /// \param[in] number value to serialize. + /// \return The succeeding serializer. + template + // coverity[autosar_cpp14_m9_3_3_violation] + auto operator<<(JNumberType number) && noexcept -> Next + { + return this->Serialize([this, number]() noexcept { + const T value = number.GetValue(); + + if (!IsFinite(value)) + { + this->os_.get().setstate(std::ios_base::failbit); + } + else + { + // Buffer size: max 24 chars for double, ~20 for int64, extra space for safety + std::array buffer{}; + + const auto conversion_result = std::to_chars(buffer.data(), buffer.data() + buffer.size(), value); + + AssertCondition(conversion_result.ec == std::errc{}, + "GenericValueSerializer: Could not convert number to textual representation."); + + this->os_.get().write(buffer.data(), + static_cast(conversion_result.ptr - buffer.data())); + } + }); + } + + /// \brief Serializes a string value + /// \details + /// - Add quotes to the begin and end of the string. + /// - Serialize the escaped string as a JSON string value. + /// \param[in] string value to serialize. + /// \return The succeeding serializer. + // coverity[autosar_cpp14_m9_3_3_violation] + auto operator<<(JStringType string) && noexcept -> Next + { + return this->Serialize([this, string]() noexcept { + this->os_.get().put('"'); + this->os_.get() << internal::EscapedJsonString(string); + this->os_.get().put('"'); + }); + } + + /// \brief Serializes a series of serializable values + /// \details + /// - Add an opening square bracket. + /// - Serialize every value as a JSON value. + /// - Add a closing square bracket. + /// \tparam Fn Type of serializer function. + /// \param[in] tuple to serialize. + /// \return The succeeding serializer. + /// \pre The function contained in the argument does not throw any exceptions + template + // coverity[autosar_cpp14_m9_3_3_violation] + auto operator<<(JArrayType tuple) && noexcept -> Next + { + return this->Serialize([this, tuple]() noexcept { + this->os_.get().put('['); + static_cast(tuple.fn(ArrayStart(this->os_.get()))); + this->os_.get().put(']'); + }); + } + + /// \brief Serializes an object + /// \tparam Fn Type of serializer function. + /// \param[in] object to serialize. + /// \return The succeeding serializer. + /// \pre The function contained in the argument does not throw any exceptions + template + auto operator<<(JObjectType object) && noexcept -> Next; + + private: + /// \brief Checks whether a number is representable as a JSON number + /// \details RFC 8259, section 6 only allows finite numbers. Only floating point values can be non-finite, + /// integral values are always representable. + /// \tparam T Type of number. + /// \param[in] value Number to check. + /// \return True if the value is finite, false otherwise. + template + static auto IsFinite(const T value) noexcept -> bool + { + return !std::is_floating_point_v || std::isfinite(value); + } + + /// \brief Serializes a value + /// \details + /// - If another element was serialized before: + /// - Add a comma. + /// - Execute the given serializer function. + /// \tparam Fn Type of function. + /// \param[in] fn Serializer call function. + /// \return The succeeding serializer. + /// \pre The passed function does not throw any exceptions + template + auto Serialize(Fn&& fn) const noexcept -> Next + { + if (this->serializer_state_ == SerializerState::kNonEmpty) + { + this->os_.get().put(','); + } + std::forward(fn)(); + return Next(this->os_.get(), SerializerState::kNonEmpty); + } + + /// \brief Writes the requested BOM type + /// \details + /// - Write the requested BOM to the stream. + /// \param[in] type The BOM type to write. + void WriteBom(EncodingType type) const noexcept + { + if (type == EncodingType::kUtf8) + { + constexpr std::string_view kUtf8Bom{"\xEF\xBB\xBF"sv}; + this->os_.get().write(kUtf8Bom.data(), kUtf8Bom.size()); + } + } + + /// \brief Output stream to write into + WriterType os_; + + /// \brief Serializer state + SerializerState serializer_state_; +}; + +} // namespace score::json::vajson + +#endif // SCORE_LIB_JSON_INTERNAL_WRITER_VAJSON_WRITER_SERIALIZERS_STRUCTURES_GENERIC_VALUE_SERIALIZER_H diff --git a/score/json/internal/writer/vajson/writer/serializers/structures/generic_value_serializer_impl.h b/score/json/internal/writer/vajson/writer/serializers/structures/generic_value_serializer_impl.h new file mode 100644 index 0000000000..7c3b14cc50 --- /dev/null +++ b/score/json/internal/writer/vajson/writer/serializers/structures/generic_value_serializer_impl.h @@ -0,0 +1,49 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +/// \file +/// \brief Implementation of methods for generic value serializer type. + +#ifndef SCORE_LIB_JSON_INTERNAL_WRITER_VAJSON_WRITER_SERIALIZERS_STRUCTURES_GENERIC_VALUE_SERIALIZER_IMPL_H +#define SCORE_LIB_JSON_INTERNAL_WRITER_VAJSON_WRITER_SERIALIZERS_STRUCTURES_GENERIC_VALUE_SERIALIZER_IMPL_H + +#include "score/json/internal/writer/vajson/writer/serializers/structures/generic_value_serializer.h" +#include "score/json/internal/writer/vajson/writer/serializers/structures/key_serializer.h" + +namespace score::json::vajson +{ +/// \brief Serializes another object into the output stream +/// \details +/// - Assert that the current state allows adding an object. +/// - Add an opening curly bracket. +/// - Serialize the object as a JSON object. +/// - Add a closing curly bracket. +template +template +// coverity[autosar_cpp14_m9_3_3_violation] +auto GenericValueSerializer::operator<<(JObjectType object) && noexcept + -> GenericValueSerializer::Next +{ + return this->Serialize([this, &object]() noexcept { + /// \brief Defines the return value type + using ReturnType = decltype(object.fn(ObjectStart(this->os_.get()))); + static_assert(std::is_same::value, "Cannot close object in current state"); + + this->os_.get().put('{'); + static_cast(object.fn(ObjectStart(this->os_.get()))); + this->os_.get().put('}'); + }); +} + +} // namespace score::json::vajson + +#endif // SCORE_LIB_JSON_INTERNAL_WRITER_VAJSON_WRITER_SERIALIZERS_STRUCTURES_GENERIC_VALUE_SERIALIZER_IMPL_H diff --git a/score/json/internal/writer/vajson/writer/serializers/structures/key_serializer.h b/score/json/internal/writer/vajson/writer/serializers/structures/key_serializer.h new file mode 100644 index 0000000000..25242342f4 --- /dev/null +++ b/score/json/internal/writer/vajson/writer/serializers/structures/key_serializer.h @@ -0,0 +1,98 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +/// \file +/// \brief Serializer for JSON keys. + +#ifndef SCORE_LIB_JSON_INTERNAL_WRITER_VAJSON_WRITER_SERIALIZERS_STRUCTURES_KEY_SERIALIZER_H +#define SCORE_LIB_JSON_INTERNAL_WRITER_VAJSON_WRITER_SERIALIZERS_STRUCTURES_KEY_SERIALIZER_H + +#include "score/json/internal/parser/vajson/vajson_impl/util/types.h" +#include "score/json/internal/writer/vajson/writer/serializers/structures/serializer.h" +#include "score/json/internal/writer/vajson/writer/serializers/util/escaped_json_string.h" +#include "score/json/internal/writer/vajson/writer/types/basic_types.h" + +namespace score::json::vajson +{ +/// \brief A serializer for JSON keys +/// \details This class only allows adding a key into the object and always returns a value serializer to only allow a +/// value for the next concatenation operation. +class KeySerializer final +{ + public: + /// \brief Serializer state after adding a key + using Next = ObjectSerializerValue; + + /// \brief Constructs a KeySerializer from an output stream + /// \details Do not create an instance of KeySerializer directly, use the aliases in + /// score/json/internal/writer/vajson/writer/serializers/structures/serializer.h + /// \param[in] os Output stream to write into. + /// \param[in] state of the Serializer. + explicit KeySerializer(WriterType os, SerializerState state = SerializerState::kEmpty) noexcept + : os_(os), serializer_state_{state} + { + } + + /// \brief Default move constructor + KeySerializer(KeySerializer&&) noexcept = default; + + /// \brief Default move assignment + /// \return A reference to the moved into object. + auto operator=(KeySerializer&&) & noexcept -> KeySerializer& = default; + + // Deleted copy constructor copy assignment operator. + KeySerializer(const KeySerializer&) = delete; + auto operator=(const KeySerializer&) -> KeySerializer& = delete; + + /// \brief Default DTOR + ~KeySerializer() noexcept = default; + + /// \brief Serializes a key + /// \details + /// - Add a comma, if necessary. + /// - Serialize the key. + /// \param[in] key to serialize. + /// \return The succeeding serializer. + auto operator<<(JKeyType key) const&& noexcept -> Next + { + this->WriteComma(); + + this->os_.get().put('"'); + this->os_.get() << internal::EscapedJsonString(key); + constexpr auto colon_str = R"(":)"sv; + this->os_.get().write(colon_str.data(), colon_str.size()); + + return Next(this->os_.get()); + } + + private: + /// \brief Adds a comma to the stream, if necessary + /// \details + /// - If another element was serialized before, add a comma. + void WriteComma() const noexcept + { + if (this->serializer_state_ == SerializerState::kNonEmpty) + { + this->os_.get().put(','); + } + } + + /// \brief Output stream to write into + WriterType os_; + + /// \brief Serializer state + SerializerState serializer_state_; +}; + +} // namespace score::json::vajson + +#endif // SCORE_LIB_JSON_INTERNAL_WRITER_VAJSON_WRITER_SERIALIZERS_STRUCTURES_KEY_SERIALIZER_H diff --git a/score/json/internal/writer/vajson/writer/serializers/structures/serializer.h b/score/json/internal/writer/vajson/writer/serializers/structures/serializer.h new file mode 100644 index 0000000000..45b3da2594 --- /dev/null +++ b/score/json/internal/writer/vajson/writer/serializers/structures/serializer.h @@ -0,0 +1,81 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +/// \file +/// \brief Contains common types and forward declarations for JSON serializers. +#ifndef SCORE_LIB_JSON_INTERNAL_WRITER_VAJSON_WRITER_SERIALIZERS_STRUCTURES_SERIALIZER_H +#define SCORE_LIB_JSON_INTERNAL_WRITER_VAJSON_WRITER_SERIALIZERS_STRUCTURES_SERIALIZER_H + +#include +#include + +#include "score/json/internal/parser/vajson/vajson_impl/util/types.h" + +namespace score::json::vajson +{ +/// \brief State of the object to be serialized +/// \details Indicates if the object is empty or not. Commas should only appended if the object is not empty. +enum class SerializerState : bool +{ + kEmpty, + kNonEmpty +}; + +/// \brief An empty type that signifies that the serializer has no follow-up state +class Unit +{ + public: + /// \brief Constructs a Unit type from an output stream + /// \details This satisfies the 'Next' state interface for serializers. + explicit Unit(std::reference_wrapper, SerializerState = SerializerState::kEmpty) noexcept {} +}; + +/// \brief A marker struct that only tells the GenericValueSerializer to return itself after using operator<<() +class Self +{ +}; + +/// \brief Forward declaration for the GenericValueSerializer +template +class GenericValueSerializer; + +/// \brief A serializer type for single values +using ValueSerializer = GenericValueSerializer; + +/// \brief A serializer type for JSON documents +/// \details Intentionally a using to make it obvious that a JSON document must start with a single value. +using DocumentSerializer = ValueSerializer; + +/// \brief Forward declaration for the KeySerializer +class KeySerializer; + +/// \brief A serializer type for the start of JSON objects +/// \details Typedef for the initial Object serializer state where only a key is allowed. +using ObjectStart = KeySerializer; + +/// \brief A serializer type for JSON objects +/// \details This class only allows adding a value into the object and the next concatenation will only allow a key. +using ObjectSerializerValue = GenericValueSerializer; + +/// \brief A serializer type for JSON arrays +/// \details Serializes multiple, potentially inhomogeneous values. +using ArraySerializer = GenericValueSerializer<>; + +/// \brief A serializer type for the start of JSON arrays +/// \details Typedef for the initial Array serializer state. +using ArrayStart = ArraySerializer; + +/// \brief Type of the output writer +using WriterType = std::reference_wrapper; +} // namespace score::json::vajson + +#endif // SCORE_LIB_JSON_INTERNAL_WRITER_VAJSON_WRITER_SERIALIZERS_STRUCTURES_SERIALIZER_H diff --git a/score/json/internal/writer/vajson/writer/serializers/util/escaped_json_string.h b/score/json/internal/writer/vajson/writer/serializers/util/escaped_json_string.h new file mode 100644 index 0000000000..5680ba4b02 --- /dev/null +++ b/score/json/internal/writer/vajson/writer/serializers/util/escaped_json_string.h @@ -0,0 +1,160 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +/// \file +/// \brief Serializer for JSON string literals. + +#ifndef SCORE_LIB_JSON_INTERNAL_WRITER_VAJSON_WRITER_SERIALIZERS_UTIL_ESCAPED_JSON_STRING_H +#define SCORE_LIB_JSON_INTERNAL_WRITER_VAJSON_WRITER_SERIALIZERS_UTIL_ESCAPED_JSON_STRING_H + +#include +#include +#include +#include +#include +#include + +#include "score/json/internal/parser/vajson/vajson_impl/util/types.h" +#include "score/json/internal/writer/vajson/writer/serializers/structures/serializer.h" +#include "score/json/internal/writer/vajson/writer/types/basic_types.h" + +namespace score::json::vajson::internal +{ +/// \brief An escaped JSON string type +class EscapedJsonString +{ + public: + /// \brief Constructs an EscapedJsonString from a JSON key + /// \param[in] key to serialize. + explicit EscapedJsonString(JKeyType key) noexcept : EscapedJsonString(key.GetValue()) {} + + /// \brief Constructs an EscapedJsonString from a JSON string + /// \param[in] string to serialize. + explicit EscapedJsonString(JStringType string) noexcept : EscapedJsonString(string.GetValue()) {} + + /// \brief Returns the contained string + /// \return The contained string. + [[nodiscard]] auto GetValue() const noexcept -> std::string_view + { + return this->value_; + } + + private: + /// \brief Constructs an EscapedJsonString from a StringView + /// \param[in] value The value to serialize. + explicit EscapedJsonString(std::string_view value) noexcept : value_{value} {} + + /// \brief Value to write as a JSON string literal + std::string_view value_; +}; + +/// \brief First character that RFC 8259, section 7 allows to appear unescaped in a JSON string +constexpr std::char_traits::int_type kFirstUnescapedCharacter{0x20}; + +/// \brief Writes a control character as a six character \uXXXX escape sequence +/// \details RFC 8259, section 7 requires characters in the range U+0000 to U+001F to be escaped. Those without a +/// two character escape sequence must be written in the \uXXXX notation. +/// \param[in] os Output stream to write into. +/// \param[in] value Value of the control character to escape, must be below kFirstUnescapedCharacter. +inline void WriteUnicodeEscape(std::ostream& os, const std::char_traits::int_type value) noexcept +{ + constexpr std::string_view kHexDigits{"0123456789abcdef"}; + constexpr std::char_traits::int_type kNibbleMask{0x0F}; + constexpr std::char_traits::int_type kNibbleWidth{4}; + constexpr std::size_t kUnicodeEscapeLength{6U}; + + // Only characters below U+0020 reach this function, hence the two upper hexadecimal digits are always zero. + const std::array escape{ + '\\', + 'u', + '0', + '0', + kHexDigits[static_cast((value >> kNibbleWidth) & kNibbleMask)], + kHexDigits[static_cast(value & kNibbleMask)]}; + os.write(escape.data(), static_cast(escape.size())); +} + +// NOLINTNEXTLINE(whitespace/line_length) +// coverity[autosar_cpp14_m5_0_16_violation] +/// \brief Serializes an escaped string literal type +/// \details +/// - If the string contains a character that has a two character escape sequence in JSON: +/// - Serialize the escaped character. +/// - Otherwise, if the character is a control character (U+0000 to U+001F), which RFC 8259, section 7 does not +/// allow to appear unescaped: +/// - Serialize the character as a \uXXXX escape sequence. +/// - Otherwise: +/// - Serialize the character directly. +/// \param[in] os Output stream to write into. +/// \param[in] string to escape and serialize. +/// \return A reference to the output stream. +auto inline operator<<(std::ostream& os, EscapedJsonString string) noexcept -> std::ostream& +{ + for (const char ch : string.GetValue()) + { + const auto value = std::char_traits::to_int_type(ch); + switch (value) + { + case std::char_traits::to_int_type('"'): + { + os.write("\\\"", 2); + break; + } + case std::char_traits::to_int_type('\\'): + { + os.write("\\\\", 2); + break; + } + case std::char_traits::to_int_type('\b'): + { + os.write("\\b", 2); + break; + } + case std::char_traits::to_int_type('\f'): + { + os.write("\\f", 2); + break; + } + case std::char_traits::to_int_type('\n'): + { + os.write("\\n", 2); + break; + } + case std::char_traits::to_int_type('\r'): + { + os.write("\\r", 2); + break; + } + case std::char_traits::to_int_type('\t'): + { + os.write("\\t", 2); + break; + } + default: + if (value < kFirstUnescapedCharacter) + { + WriteUnicodeEscape(os, value); + } + else + { + os.put(ch); + } + break; + } + } + + return os; +} + +} // namespace score::json::vajson::internal + +#endif // SCORE_LIB_JSON_INTERNAL_WRITER_VAJSON_WRITER_SERIALIZERS_UTIL_ESCAPED_JSON_STRING_H diff --git a/score/json/internal/writer/vajson/writer/types/array_type.h b/score/json/internal/writer/vajson/writer/types/array_type.h new file mode 100644 index 0000000000..9df11a0e00 --- /dev/null +++ b/score/json/internal/writer/vajson/writer/types/array_type.h @@ -0,0 +1,112 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +/// \file +/// \brief A collection of serializers for range-based containers. +/// \details Provides serializers for arrays and tuples. + +#ifndef SCORE_LIB_JSON_INTERNAL_WRITER_VAJSON_WRITER_TYPES_ARRAY_TYPE_H +#define SCORE_LIB_JSON_INTERNAL_WRITER_VAJSON_WRITER_TYPES_ARRAY_TYPE_H + +#include +#include +#include +#include + +#include "score/json/internal/writer/vajson/writer/serializers/structures/serializer.h" +#include "score/json/internal/writer/vajson/writer/types/basic_types.h" + +namespace score::json::vajson +{ +inline namespace types +{ +/// \brief A serializer type for a JSON array from a homogeneous C++ range +/// \tparam Range Type of range. +/// \tparam Fn The function type for this serializer. +template +class RangeSerializer final +{ + public: + /// \brief Constructs a RangeSerializer + /// \tparam Fn1 Type of function. + /// \param[in] range to serialize. + /// \param[in] fn Function used to serialize. Must not throw exceptions. + template + RangeSerializer(const Range& range, Fn1&& fn) noexcept : container_{range}, function_{std::forward(fn)} + { + } + + /// \brief Call operator + /// \details + /// - Serialize every element of the array as a JSON value. + /// \tparam AS Type of array serializer. + /// \param[in] as Array serializer to write into. + /// \pre The function contained in the class does not throw any exceptions + template + void operator()(AS as) const noexcept + { + for (const auto& value : this->container_.get()) + { + as = std::move(as) << this->function_(value); + } + } + + private: + /// \brief Container instance to be serialized + std::reference_wrapper container_; + + /// \brief Function to serialize single items with + Fn function_; +}; + +/// \brief Serialize an ad-hoc defined Tuple as heterogeneous array +/// \tparam Fn The function type that defines the serialization. +template +struct JArrayType final +{ + /// \brief Wrapped function value + Fn fn; +}; + +/// \brief Serializes an ad-hoc defined Tuple as a heterogeneous array +/// \details The function can be used to define a tuple by adding values. +/// \tparam Fn Type of serializer function. Must take an ArrayStart&& and return the follow-up serializer. +/// \param[in] fn Function used to serialize the tuple. +/// \return A serializable Tuple type. +/// \pre The passed function does not throw any exceptions +template >> +auto JArray(Fn&& fn) noexcept -> JArrayType +{ // coverity[autosar_cpp14_a13_3_1_violation] + return {std::forward(fn)}; +} + +/// \brief Serializes a homogeneous C++ range as a JSON array +/// \tparam Range Type of range. +/// \tparam Fn Type of value serializer function. Must take the range's value type and return a JSON type. +/// \param[in] range instance to be serialized. +/// \param[in] fn Function used to serialize single elements. +/// \return A serializable JSON array. +/// \pre The passed range & function do not throw any exceptions +template > +auto JArray(const Range& range, Fn&& fn = IdSerializer{}) noexcept -> JArrayType> +{ + return {RangeSerializer{range, std::forward(fn)}}; +} + +// clang-format off +} // inline namespace types +// clang-format off +} // namespace score::json::vajson + + + +#endif // SCORE_LIB_JSON_INTERNAL_WRITER_VAJSON_WRITER_TYPES_ARRAY_TYPE_H diff --git a/score/json/internal/writer/vajson/writer/types/basic_types.h b/score/json/internal/writer/vajson/writer/types/basic_types.h new file mode 100644 index 0000000000..718ea17cca --- /dev/null +++ b/score/json/internal/writer/vajson/writer/types/basic_types.h @@ -0,0 +1,206 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +/// \file +/// \brief A collection of serializers for basic JSON types. +/// \details Provides serializers for Null, Bool, Key, Number, and String types. + +#ifndef SCORE_LIB_JSON_INTERNAL_WRITER_VAJSON_WRITER_TYPES_BASIC_TYPES_H +#define SCORE_LIB_JSON_INTERNAL_WRITER_VAJSON_WRITER_TYPES_BASIC_TYPES_H + +#include +#include +#include +#include + +#include "score/json/internal/parser/vajson/vajson_impl/util/types.h" + +namespace score::json::vajson +{ +inline namespace types +{ +/// \brief A Null type +struct JNullType final +{ +}; + +/// \brief Serializes a Null value +/// \return The serializable null type. +constexpr inline auto JNull() noexcept -> JNullType +{ + return JNullType{}; +} + +/// \brief A Bool type +struct JBoolType final +{ + /// \brief Wrapped bool value + bool value; +}; + +/// \brief Serializes a Bool value +/// \param[in] b Bool value to serialize. +/// \return The serializable bool type. +constexpr inline auto JBool(bool b) noexcept -> JBoolType +{ + return {b}; +} + +/// \brief A Key type +class JKeyType final +{ + public: + /// \brief Constructs a Key type + /// \param[in] s Key to serialize. + explicit constexpr JKeyType(std::string_view s) noexcept : value_(s) {} + + /// \brief Returns the contained value + /// \return The value. + [[nodiscard]] auto GetValue() const noexcept -> std::string_view + { + return this->value_; + } + + private: + /// \brief Wrapped string value + std::string_view value_; +}; + +/// \brief Serializes a Key value +/// \param[in] s Key to serialize. +/// \return The serializable key type. +constexpr auto JKey(std::string_view s) noexcept -> JKeyType +{ + return JKeyType{s}; +} + +/// \brief Serializes a Key value +/// \param[in] s Key to serialize. +/// \return The serializable key type. +inline auto JKey(const std::string& s) noexcept -> JKeyType +{ + return JKey(std::string_view(s)); +} + +inline namespace literals +{ +/// \brief Serializes a Key value +/// \param[in] s String literal to serialize. +/// \param[in] size Size of the pointer. +/// \return The serializable key type. +// coverity[autosar_cpp14_a13_1_3_violation] +constexpr auto operator""_key(const char* s, std::size_t size) noexcept -> JKeyType +{ + return JKey(score::safecpp::zstring_view{s, size}); +} + +// clang-format off +} // namespace literals +// clang-format on + +/// \brief A Number type +/// \details bool is excluded: it is a JSON boolean, not a JSON number, and has no std::to_chars overload. +/// Use JBool instead. +/// \tparam N Type of number. +template && !std::is_same_v>> +class JNumberType final +{ + public: + /// \brief Constructs a Number type + /// \param[in] num Number to serialize. + constexpr explicit JNumberType(N num) noexcept : value_(num) {} + + /// \brief Returns the contained value + /// \return The value. + [[nodiscard]] auto GetValue() const noexcept -> N + { + return this->value_; + } + + private: + /// \brief Wrapped number value + N value_; +}; + +/// \brief Serializes a Number value +/// \details bool is excluded: it is a JSON boolean, not a JSON number. Use JBool instead. +/// \tparam N Type of number. +/// \param[in] n The number to serialize. +/// \return The serializable number type. +template && !std::is_same_v>> +constexpr auto JNumber(N n) noexcept -> JNumberType +{ + return JNumberType{n}; +} + +/// \brief A String type +class JStringType final +{ + public: + /// \brief Constructs a String type + /// \param[in] s String to serialize. + constexpr explicit JStringType(std::string_view s) noexcept : value_(s) {} + + /// \brief Returns the contained value + /// \return The value. + [[nodiscard]] auto GetValue() const noexcept -> std::string_view + { + return this->value_; + } + + private: + /// \brief Wrapped string value + std::string_view value_; +}; + +/// \brief Serializes a String value +/// \param[in] s String to serialize. +/// \return The serializable string type. +constexpr auto JString(std::string_view s) noexcept -> JStringType +{ + return JStringType(s); +} + +/// \brief Serializes a String value +/// \param[in] s String to serialize. +/// \return The serializable string type. +inline auto JString(const std::string& s) noexcept -> JStringType +{ + return JString(std::string_view{s}); +} + +/// \brief A function object used to serialize predefined serializers +/// \tparam Container Type of container to serialize. +template +class IdSerializer +{ + public: + /// \brief Value Type of container + using value_type = typename Container::value_type; + + /// \brief Returns the unchanged value + /// \param[in] v Value to return. + /// \return The unchanged value. + template + auto operator()(Value&& v) const noexcept -> Value + { + return std::forward(v); + } +}; + +// clang-format off +} // namespace types +// // clang-format on +} // namespace score::json::vajson + + +#endif // SCORE_LIB_JSON_INTERNAL_WRITER_VAJSON_WRITER_TYPES_BASIC_TYPES_H diff --git a/score/json/internal/writer/vajson/writer/types/object_type.h b/score/json/internal/writer/vajson/writer/types/object_type.h new file mode 100644 index 0000000000..1d37711aae --- /dev/null +++ b/score/json/internal/writer/vajson/writer/types/object_type.h @@ -0,0 +1,52 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +/// \file +/// \brief A collection of serializers for objects. +/// \details Provides serializers for Object types. + +#ifndef SCORE_LIB_JSON_INTERNAL_WRITER_VAJSON_WRITER_TYPES_OBJECT_TYPE_H +#define SCORE_LIB_JSON_INTERNAL_WRITER_VAJSON_WRITER_TYPES_OBJECT_TYPE_H + +#include +#include + +namespace score::json::vajson +{ +inline namespace types +{ +/// \brief An Object type +/// \tparam Fn Type of serializer function. +template +struct JObjectType final +{ + /// \brief Function used to serialize the object + Fn fn; +}; + +/// \brief Serializes an object value +/// \tparam Fn Type of serializer function. Must take an ObjectStart&& and return the follow-up serializer. +/// \param[in] fn Function used to serialize the object. +/// \return The serializable object type. +/// \pre The passed function does not throw any exceptions +template >> +auto JObject(Fn&& fn) noexcept -> JObjectType +{ // coverity[autosar_cpp14_a13_3_1_violation] + return {std::forward(fn)}; +} + +// clang-format off +} // inline namespace types +// clang-format on +} // namespace score::json::vajson + +#endif // SCORE_LIB_JSON_INTERNAL_WRITER_VAJSON_WRITER_TYPES_OBJECT_TYPE_H diff --git a/score/json/internal/writer/writer_backend.h b/score/json/internal/writer/writer_backend.h new file mode 100644 index 0000000000..b7ce10ebc1 --- /dev/null +++ b/score/json/internal/writer/writer_backend.h @@ -0,0 +1,61 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SCORE_LIB_JSON_INTERNAL_WRITER_WRITER_BACKEND_H +#define SCORE_LIB_JSON_INTERNAL_WRITER_WRITER_BACKEND_H + +#include "score/json/internal/model/any.h" +#include "score/result/result.h" + +#include +#include + +namespace score +{ +namespace json +{ +namespace internal +{ +namespace writer +{ + +/// \brief Backend seam for JSON serialization. +/// +/// Exactly one backend provides definitions for the declarations below. Which one is linked is decided at build +/// time by the `//score/json:writer_library` flag, resolved through the `//score/json/internal/writer:writer` +/// alias -- the same mechanism the parser uses via `//score/json:base_library`. +/// +/// Note that the emitted representation is backend specific: `json_serialize` pretty-prints with a four space +/// indentation, whereas `vajson` emits compact JSON without insignificant whitespace. + +/// \brief Serializes json_data into out_stream +/// \param out_stream The stream to write the serialized representation to +/// \param json_data The data to serialize +/// \return empty result on success, error otherwise +score::Result SerializeToStream(std::ostream& out_stream, const score::json::Object& json_data); +score::Result SerializeToStream(std::ostream& out_stream, const score::json::List& json_data); +score::Result SerializeToStream(std::ostream& out_stream, const score::json::Any& json_data); + +/// \brief Serializes json_data into a string +/// \param json_data The data to serialize +/// \return the serialized representation on success, error otherwise +score::Result SerializeToBuffer(const score::json::Object& json_data); +score::Result SerializeToBuffer(const score::json::List& json_data); +score::Result SerializeToBuffer(const score::json::Any& json_data); + +} // namespace writer +} // namespace internal +} // namespace json +} // namespace score + +#endif // SCORE_LIB_JSON_INTERNAL_WRITER_WRITER_BACKEND_H diff --git a/score/json/json_writer.cpp b/score/json/json_writer.cpp index 25543fd54f..1e2580153c 100644 --- a/score/json/json_writer.cpp +++ b/score/json/json_writer.cpp @@ -14,25 +14,11 @@ #include "score/json/json_writer.h" #include "score/json/i_json_writer.h" #include "score/json/internal/model/error.h" -#include "score/json/internal/writer/json_serialize/json_serialize.h" -#include "score/language/safecpp/safe_math/safe_math.h" +#include "score/json/internal/writer/writer_backend.h" -#include - -#include -#include -#include -#include -// std::locale provides functionalities (e.g. facets) to customize parts of iostream implementation. In this case it is -// used for serializing integers efficiently. An implementation without std::locale might require (massive) amounts of -// code duplication. Furthermore, this header is allowed to be used for character conversion purposes if and it must be -// ensured that libcatalog is not linked using the target toolchain, which is the case for libjson. -// Reference: broken_link_c/issue/4600528 -// NOLINTNEXTLINE(score-banned-include): see rationale above -#include -#include +#include +#include #include -#include namespace { @@ -50,153 +36,9 @@ score::Result ToFileInternal(const T& json_data, return score::Result{score::unexpect, error}; } - score::json::JsonSerialize serializer{**file}; - return serializer << json_data; -} - -template -// Coverity thinks this function is unused, but it is used in for calculating kIntBufLen. -// coverity[autosar_cpp14_a0_1_3_violation] -constexpr std::size_t max_dec_digits() noexcept -{ - U v = std::numeric_limits::max(); - std::size_t n = 1U; - while (v >= 10U) - { - v /= 10U; - // There is no known type that could represent enough digits to overflow std::size_t - // coverity[autosar_cpp14_a4_7_1_violation] - ++n; - } - return n; -} - -template -inline constexpr std::size_t kIntBufLen = max_dec_digits>>() + 1U; - -// Rationale: noexcept safe magnitude conversion; uses safe_math assertions; -// COVERITY: autosar_cpp14_a15_5_3_violation, uncaught_exception -// policy requires terminate on failure; no exceptions thrown. -template -// coverity[autosar_cpp14_a15_5_3_violation] -// coverity[uncaught_exception] -inline auto abs_magnitude_unsigned(T val) noexcept -{ - using U = std::make_unsigned_t>; - static_assert(std::is_integral_v, "integral only"); - static_assert(std::is_integral_v, "U must be integral"); - static_assert(std::numeric_limits::digits >= std::numeric_limits>::digits, - "U must represent full magnitude of T"); - - // Coverity doesn't know constexpr if statements - // coverity[autosar_cpp14_a7_1_8_violation] - if constexpr (std::is_signed_v) - { - const auto abs_val = score::safe_math::Abs(val); - const auto cast_res = score::safe_math::Cast(abs_val); - SCORE_LANGUAGE_FUTURECPP_ASSERT_PRD_MESSAGE(cast_res.has_value(), - "Safe cast failed in abs_magnitude_unsigned (signed)"); - return cast_res.value(); - } - else - { - const auto cast_res = score::safe_math::Cast(val); - SCORE_LANGUAGE_FUTURECPP_ASSERT_PRD_MESSAGE(cast_res.has_value(), - "Safe cast failed in abs_magnitude_unsigned (unsigned)"); - return cast_res.value(); - } -} - -template -[[nodiscard]] std::string_view integer_to_chars(std::array>& buffer, const T val) noexcept -{ - using U = std::make_unsigned_t>; - static_assert(std::is_integral_v && std::is_integral_v, "integral only"); - static_assert(!std::is_same_v, bool>, "bool not supported"); - static_assert(std::numeric_limits::digits >= std::numeric_limits>::digits, - "U must represent full magnitude of T"); - - const bool is_negative = (std::is_signed_v && (val < static_cast(0))); - U x = abs_magnitude_unsigned(val); - - // no range checks on it because the function declaration ensures enough buffer space for the given type - auto it = buffer.end(); - do - { - // Rationale: Converting unsigned digit (x % 10U) to signed int8_t is safe because - // the modulo operation guarantees the value is in range [0, 9], which fits in both - // uint8_t and int8_t. The signed type is needed for consistent char arithmetic with '0'. - // coverity[autosar_cpp14_m5_0_9_violation] - const auto digit = static_cast(x % static_cast(10U)); - it = std::prev(it); - // coverity[autosar_cpp14_m5_0_9_violation] - *it = static_cast(static_cast('0') + digit); - x /= 10U; - } while (x > static_cast(0U)); - if (is_negative) - { - it = std::prev(it); - *it = '-'; - } - - return std::string_view(&*it, static_cast(std::distance(it, buffer.end()))); + return score::json::internal::writer::SerializeToStream(**file, json_data); } -// This specialization of std::num_put ignores parameter widths (see std::setw) as this feature is neither useful -// nor used for serializing JSON. -class OptimizedNumPut : public std::num_put -{ - public: - // Coverity thinks this function is unused, wheras it is used for std::locale - // coverity[autosar_cpp14_a0_1_3_violation] - using std::num_put::num_put; - - protected: - using std::num_put::do_put; - - // Coverity thinks this function is unused, wheras it is used for std::locale - // coverity[autosar_cpp14_a0_1_3_violation] - iter_type do_put(iter_type out, std::ios_base& s, char_type fill, long v) const override - { - return OptimizedPutForInts(out, s, fill, v); - } - // Coverity thinks this function is unused, wheras it is used for std::locale - // coverity[autosar_cpp14_a0_1_3_violation] - iter_type do_put(iter_type out, std::ios_base& s, char_type fill, unsigned long v) const override - { - return OptimizedPutForInts(out, s, fill, v); - } - // Coverity thinks this function is unused, wheras it is used for std::locale - // coverity[autosar_cpp14_a0_1_3_violation] - // LCOV_EXCL_START see SCORE_LANGUAGE_FUTURECPP_UNREACHABLE_MESSAGE - iter_type do_put(iter_type out, std::ios_base& s, char_type fill, long long v) const override - { - SCORE_LANGUAGE_FUTURECPP_UNREACHABLE_MESSAGE( - "This code is unreachable with tested toolchains and target platforms"); - return OptimizedPutForInts(out, s, fill, v); - } - // LCOV_EXCL_STOP - // Coverity thinks this function is unused, wheras it is used for std::locale - // coverity[autosar_cpp14_a0_1_3_violation] - // LCOV_EXCL_START see SCORE_LANGUAGE_FUTURECPP_UNREACHABLE_MESSAGE - iter_type do_put(iter_type out, std::ios_base& s, char_type fill, unsigned long long v) const override - { - SCORE_LANGUAGE_FUTURECPP_UNREACHABLE_MESSAGE( - "This code is unreachable with tested toolchains and target platforms"); - return OptimizedPutForInts(out, s, fill, v); - } - // LCOV_EXCL_STOP - - private: - template - iter_type OptimizedPutForInts(iter_type out, std::ios_base&, char_type, T val) const - { - std::array> buf{}; - const auto sv = integer_to_chars(buf, val); - return std::copy(sv.begin(), sv.end(), out); - } -}; - template score::Result ToFileInternalAtomic(const T& json_data, const std::string_view& file_path, @@ -209,8 +51,7 @@ score::Result ToFileInternalAtomic(const T& json_data, return score::json::MakeError(score::json::Error::kInvalidFilePath, err.UserMessage()); }) .and_then([&json_data](auto filestream) -> score::Result { - score::json::JsonSerialize serializer{*filestream}; - auto serializer_result = serializer << json_data; + auto serializer_result = score::json::internal::writer::SerializeToStream(*filestream, json_data); return filestream->Close().and_then([serializer_result](auto&&...) noexcept { return serializer_result; }); @@ -220,30 +61,7 @@ score::Result ToFileInternalAtomic(const T& json_data, template score::Result ToBufferInternal(const T& json_data) { - // This line must be hit when the function is called. Since other parts of this function show line coverage, - // this line must also be hit. Missing coverage is due to a bug in the coverage tool - std::ostringstream string_stream{}; // LCOV_EXCL_LINE - - // NOLINTBEGIN(score-no-dynamic-raw-memory) See rationale below - // Rationale: std::num_put is reference counted and std::locale does manage it. - // Explanation for Coverity Suppression for AUTOSAR A3-3-2: Static locale with custom facet requires runtime - // initialization. std::locale constructor with facet cannot be constexpr. Thread-safe due to function-local static - // initialization guarantee (C++11 §6.7 [stmt.dcl]/4). coverity[autosar_cpp14_a3_3_2_violation] - const static std::locale loc(std::locale(), new OptimizedNumPut()); - // NOLINTEND(score-no-dynamic-raw-memory) See rationale above - - score::cpp::ignore = string_stream.imbue(loc); - - score::json::JsonSerialize serializer{string_stream}; - auto result = serializer << json_data; - // LCOV_EXCL_BR_START Error path currently not reachable - if (!result.has_value()) - // LCOV_EXCL_BR_STOP - { - return score::Unexpected{result.error()}; - } - - return string_stream.str(); + return score::json::internal::writer::SerializeToBuffer(json_data); } } // namespace diff --git a/score/json/json_writer_test.cpp b/score/json/json_writer_test.cpp index bedc5da246..93dd0e34be 100644 --- a/score/json/json_writer_test.cpp +++ b/score/json/json_writer_test.cpp @@ -35,6 +35,15 @@ using ::testing::ByMove; using ::testing::Return; using ::testing::StrEq; +// The selected serialization backend decides the emitted representation: json_serialize pretty-prints with a four +// space indentation, whereas vajson emits compact JSON without insignificant whitespace. Which backend is linked is +// chosen by the //score/json:writer_library flag and communicated here via local_defines. +#if defined(WRITER_VAJSON) +constexpr auto kKeySeparator = "\":"; +#else +constexpr auto kKeySeparator = "\": "; +#endif + class TestJsonList : public json::List { public: @@ -47,6 +56,9 @@ class TestJsonList : public json::List emplace_back(std::move(obj)); } +#if defined(WRITER_VAJSON) + static constexpr auto expected = R"([1234,"string",{"key":"value"}])"; +#else static constexpr auto expected = R"([ 1234, "string", @@ -54,6 +66,7 @@ class TestJsonList : public json::List "key": "value" } ])"; +#endif }; class TestJsonObject : public json::Object @@ -65,10 +78,14 @@ class TestJsonObject : public json::Object emplace("num", score::json::Any{1}); } +#if defined(WRITER_VAJSON) + static constexpr auto expected = R"({"num":1,"string":"foo"})"; +#else static constexpr auto expected = R"({ "num": 1, "string": "foo" })"; +#endif }; class TestJsonAny : public json::Any @@ -232,8 +249,8 @@ class JsonWriterIntegerTest : public ::testing::Test // The Number variant currently supports std::int{8,16,32,64}_t and std::uint{8,16,32,64}_t. // Types such as long long and unsigned long long can only be tested on platforms where the above typedefs resolve to // them (usually <64-bit platforms). -// This test suite covers all integral types not causing a -Wsign-promo error exercised by our num_put overrides and -// accepted by the production JSON implementation. +// This test suite covers all integral types not causing a -Wsign-promo error accepted by the production JSON +// implementation. using IntegralTypes = ::testing::Types; TYPED_TEST_SUITE(JsonWriterIntegerTest, IntegralTypes, ); @@ -276,26 +293,34 @@ TYPED_TEST(JsonWriterIntegerTest, FormatsIntegralValuesCorrectly) const std::string json_str = *result; // Verify all values are formatted correctly - EXPECT_NE(std::string::npos, json_str.find(std::string{"\"zero\": "} + std::to_string(static_cast(0)))); - EXPECT_NE(std::string::npos, json_str.find(std::string{"\"positive\": "} + std::to_string(static_cast(12345)))); + EXPECT_NE(std::string::npos, + json_str.find(std::string{"\"zero"} + kKeySeparator + std::to_string(static_cast(0)))); + EXPECT_NE(std::string::npos, + json_str.find(std::string{"\"positive"} + kKeySeparator + std::to_string(static_cast(12345)))); - EXPECT_NE(std::string::npos, json_str.find(std::string{"\"p9\": "} + std::to_string(static_cast(9)))); - EXPECT_NE(std::string::npos, json_str.find(std::string{"\"p10\": "} + std::to_string(static_cast(10)))); - EXPECT_NE(std::string::npos, json_str.find(std::string{"\"p11\": "} + std::to_string(static_cast(11)))); + EXPECT_NE(std::string::npos, + json_str.find(std::string{"\"p9"} + kKeySeparator + std::to_string(static_cast(9)))); + EXPECT_NE(std::string::npos, + json_str.find(std::string{"\"p10"} + kKeySeparator + std::to_string(static_cast(10)))); + EXPECT_NE(std::string::npos, + json_str.find(std::string{"\"p11"} + kKeySeparator + std::to_string(static_cast(11)))); if constexpr (std::numeric_limits::is_signed) { EXPECT_NE(std::string::npos, - json_str.find(std::string{"\"negative\": "} + std::to_string(static_cast(-12345)))); + json_str.find(std::string{"\"negative"} + kKeySeparator + std::to_string(static_cast(-12345)))); + EXPECT_NE(std::string::npos, + json_str.find(std::string{"\"min"} + kKeySeparator + std::to_string(std::numeric_limits::min()))); + EXPECT_NE(std::string::npos, + json_str.find(std::string{"\"m9"} + kKeySeparator + std::to_string(static_cast(-9)))); + EXPECT_NE(std::string::npos, + json_str.find(std::string{"\"m10"} + kKeySeparator + std::to_string(static_cast(-10)))); EXPECT_NE(std::string::npos, - json_str.find(std::string{"\"min\": "} + std::to_string(std::numeric_limits::min()))); - EXPECT_NE(std::string::npos, json_str.find(std::string{"\"m9\": "} + std::to_string(static_cast(-9)))); - EXPECT_NE(std::string::npos, json_str.find(std::string{"\"m10\": "} + std::to_string(static_cast(-10)))); - EXPECT_NE(std::string::npos, json_str.find(std::string{"\"m11\": "} + std::to_string(static_cast(-11)))); + json_str.find(std::string{"\"m11"} + kKeySeparator + std::to_string(static_cast(-11)))); } EXPECT_NE(std::string::npos, - json_str.find(std::string{"\"max\": "} + std::to_string(std::numeric_limits::max()))); + json_str.find(std::string{"\"max"} + kKeySeparator + std::to_string(std::numeric_limits::max()))); } } // namespace diff --git a/tools/coverage/BUILD b/tools/coverage/BUILD index 5fdf5ffd28..d604bc602a 100644 --- a/tools/coverage/BUILD +++ b/tools/coverage/BUILD @@ -53,6 +53,7 @@ score_coverage_scope( "//score/json:interface", "//score/json:json_serializer", "//score/json/internal/parser/vajson:vajson_parser", + "//score/json/internal/writer/vajson:vajson_backend", "//score/language/rust/memres", "//score/language/rust/stop_token", "//score/language/safecpp/aborts_upon_exception:abortsuponexception",