From 5db72ab9d3b8e7989bccaca377d5e5a6e7da339c Mon Sep 17 00:00:00 2001 From: Makkar <124033019+hiteshmk05@users.noreply.github.com> Date: Tue, 26 May 2026 22:28:46 +0530 Subject: [PATCH] add: C++26 annotations support for rename and skip (#2730) --- include/simdjson/annotations.h | 63 ++++++++ .../simdjson/generic/builder/dependencies.h | 1 + .../simdjson/generic/builder/json_builder.h | 36 +++-- .../simdjson/generic/ondemand/dependencies.h | 1 + .../generic/ondemand/std_deserialize.h | 6 +- tests/builder/CMakeLists.txt | 1 + .../static_reflection_annotations_tests.cpp | 146 ++++++++++++++++++ 7 files changed, 236 insertions(+), 18 deletions(-) create mode 100644 include/simdjson/annotations.h create mode 100644 tests/builder/static_reflection_annotations_tests.cpp diff --git a/include/simdjson/annotations.h b/include/simdjson/annotations.h new file mode 100644 index 000000000..1f6adef4d --- /dev/null +++ b/include/simdjson/annotations.h @@ -0,0 +1,63 @@ +#ifndef SIMDJSON_ANNOTATIONS_H +#define SIMDJSON_ANNOTATIONS_H + +#if SIMDJSON_STATIC_REFLECTION + +#include +#include + +namespace simdjson { + +// Structural compile-time string — char array avoids the pointer-based +// 'reflect_constant failed' that occurs with const char* / string_view members. +template +struct fixed_string { + char data[N]; + + consteval fixed_string(const char (&s)[N]) noexcept { + for (size_t i = 0; i < N; ++i) { data[i] = s[i]; } + } + + consteval std::string_view view() const noexcept { return {data, N - 1}; } + + consteval bool operator==(const fixed_string&) const noexcept = default; +}; + +// Usage: [[= simdjson::rename<"first_name">]] std::string firstName; +namespace detail { + template + struct rename_t { + static constexpr auto name = Name; + }; +} // namespace detail + +template +inline constexpr detail::rename_t rename{}; + +// Usage: [[= simdjson::skip]] int internalCache; +namespace detail { + struct skip_tag {}; +} // namespace detail + +inline constexpr detail::skip_tag skip{}; + +// Returns the JSON key for a reflected data member. +template +consteval const char* get_json_key_name() { + template for (constexpr auto ann : + std::define_static_array(std::meta::annotations_of(dm))) { + constexpr auto ann_type = std::meta::type_of(ann); + if constexpr (std::meta::has_template_arguments(ann_type) && + std::meta::template_of(ann_type) == ^^detail::rename_t) { + constexpr auto args = + std::define_static_array(std::meta::template_arguments_of(ann_type)); + return std::define_static_string([:args[0]:].view()); + } + } + return std::define_static_string(std::meta::identifier_of(dm)); +} + +} // namespace simdjson + +#endif // SIMDJSON_STATIC_REFLECTION +#endif // SIMDJSON_ANNOTATIONS_H \ No newline at end of file diff --git a/include/simdjson/generic/builder/dependencies.h b/include/simdjson/generic/builder/dependencies.h index a5a26abf6..e20509d8e 100644 --- a/include/simdjson/generic/builder/dependencies.h +++ b/include/simdjson/generic/builder/dependencies.h @@ -10,5 +10,6 @@ // Otherwise, amalgamation will fail. #include "simdjson/concepts.h" #include "simdjson/dom/fractured_json.h" +#include "simdjson/annotations.h" #endif // SIMDJSON_GENERIC_BUILDER_DEPENDENCIES_H \ No newline at end of file diff --git a/include/simdjson/generic/builder/json_builder.h b/include/simdjson/generic/builder/json_builder.h index f3c8c8050..eefde5443 100644 --- a/include/simdjson/generic/builder/json_builder.h +++ b/include/simdjson/generic/builder/json_builder.h @@ -4,6 +4,7 @@ #define SIMDJSON_GENERIC_STRING_BUILDER_H #include "simdjson/generic/builder/json_string_builder.h" #include "simdjson/concepts.h" +#include "simdjson/annotations.h" #endif // SIMDJSON_CONDITIONAL_INCLUDE #if SIMDJSON_STATIC_REFLECTION @@ -246,26 +247,29 @@ simdjson_really_inline constexpr void atom(writer &w, const T &t) { // through the writer's local pos. For arithmetic fields, the integer // write happens directly via write_uint_jeaiii on w.ptr+w.pos, so pos // never round-trips through memory. - int i = 0; + bool first = true; if (!w.ensure(1)) return; w.ptr[w.pos++] = '{'; template for (constexpr auto dm : std::define_static_array(std::meta::nonstatic_data_members_of(^^T, std::meta::access_context::unchecked()))) { - constexpr auto first_key = std::define_static_string( - constevalutil::consteval_to_quoted_escaped(std::meta::identifier_of(dm)) + ":"); - constexpr auto rest_key = std::define_static_string( - std::string(",") + constevalutil::consteval_to_quoted_escaped(std::meta::identifier_of(dm)) + ":"); - constexpr size_t first_key_len = std::char_traits::length(first_key); - constexpr size_t rest_key_len = std::char_traits::length(rest_key); - if (!w.ensure(rest_key_len)) return; - if (i == 0) { - std::memcpy(w.ptr + w.pos, first_key, first_key_len); - w.pos += first_key_len; - } else { - std::memcpy(w.ptr + w.pos, rest_key, rest_key_len); - w.pos += rest_key_len; + if constexpr (std::meta::annotations_of_with_type(dm, ^^simdjson::detail::skip_tag).empty()) { + constexpr const char* key_name = simdjson::get_json_key_name(); + constexpr auto first_key = std::define_static_string( + constevalutil::consteval_to_quoted_escaped(key_name) + ":"); + constexpr auto rest_key = std::define_static_string( + std::string(",") + constevalutil::consteval_to_quoted_escaped(key_name) + ":"); + constexpr size_t first_key_len = std::char_traits::length(first_key); + constexpr size_t rest_key_len = std::char_traits::length(rest_key); + if (!w.ensure(rest_key_len)) return; + if (first) { + std::memcpy(w.ptr + w.pos, first_key, first_key_len); + w.pos += first_key_len; + } else { + std::memcpy(w.ptr + w.pos, rest_key, rest_key_len); + w.pos += rest_key_len; + } + first = false; + atom(w, t.[:dm:]); } - atom(w, t.[:dm:]); - i++; }; if (!w.ensure(1)) return; w.ptr[w.pos++] = '}'; diff --git a/include/simdjson/generic/ondemand/dependencies.h b/include/simdjson/generic/ondemand/dependencies.h index 3c4fdc3a6..4914c20d8 100644 --- a/include/simdjson/generic/ondemand/dependencies.h +++ b/include/simdjson/generic/ondemand/dependencies.h @@ -14,5 +14,6 @@ #include "simdjson/padded_string_view.h" #include "simdjson/internal/dom_parser_implementation.h" #include "simdjson/jsonpathutil.h" +#include "simdjson/annotations.h" #endif // SIMDJSON_GENERIC_ONDEMAND_DEPENDENCIES_H \ No newline at end of file diff --git a/include/simdjson/generic/ondemand/std_deserialize.h b/include/simdjson/generic/ondemand/std_deserialize.h index 36350ce85..38f5bc5ef 100644 --- a/include/simdjson/generic/ondemand/std_deserialize.h +++ b/include/simdjson/generic/ondemand/std_deserialize.h @@ -6,6 +6,7 @@ #include "simdjson/generic/ondemand/object.h" #include "simdjson/generic/ondemand/array.h" #include "simdjson/generic/ondemand/base.h" +#include "simdjson/annotations.h" #endif // SIMDJSON_CONDITIONAL_INCLUDE #include @@ -277,8 +278,9 @@ error_code tag_invoke(deserialize_tag, ValT &val, T &out) noexcept { SIMDJSON_TRY(val.get_object().get(obj)); } template for (constexpr auto mem : std::define_static_array(std::meta::nonstatic_data_members_of(^^T, std::meta::access_context::unchecked()))) { - if constexpr (!std::meta::is_const(mem) && std::meta::is_public(mem)) { - constexpr std::string_view key = std::define_static_string(std::meta::identifier_of(mem)); + if constexpr (!std::meta::is_const(mem) && std::meta::is_public(mem) + && std::meta::annotations_of_with_type(mem, ^^simdjson::detail::skip_tag).empty()) { + constexpr std::string_view key = simdjson::get_json_key_name(); if constexpr (concepts::optional_type) { // for optional members, it's ok if the key is missing auto error = obj[key].get(out.[:mem:]); diff --git a/tests/builder/CMakeLists.txt b/tests/builder/CMakeLists.txt index df8501cb9..4f85ad44e 100644 --- a/tests/builder/CMakeLists.txt +++ b/tests/builder/CMakeLists.txt @@ -8,6 +8,7 @@ if(SIMDJSON_STATIC_REFLECTION) add_cpp_test(static_reflection_edge_cases_tests LABELS ondemand acceptance per_implementation) add_cpp_test(static_reflection_enum_tests LABELS ondemand acceptance per_implementation) add_cpp_test(static_reflection_fractured_json_tests LABELS ondemand acceptance per_implementation) + add_cpp_test(static_reflection_annotations_tests LABELS ondemand acceptance per_implementation) endif(SIMDJSON_STATIC_REFLECTION) # Copy the simdjson dll into the tests directory if(MSVC AND BUILD_SHARED_LIBS) diff --git a/tests/builder/static_reflection_annotations_tests.cpp b/tests/builder/static_reflection_annotations_tests.cpp new file mode 100644 index 000000000..65c91cdea --- /dev/null +++ b/tests/builder/static_reflection_annotations_tests.cpp @@ -0,0 +1,146 @@ +#include "simdjson.h" +#include "test_builder.h" +#include +#include + +using namespace simdjson; + +#if SIMDJSON_STATIC_REFLECTION + +struct RenamedFields { + [[= simdjson::rename<"first_name">]] std::string firstName = ""; + [[= simdjson::rename<"last_name">]] std::string lastName = ""; + int age = 0; +}; + +struct SkippedField { + std::string name = ""; + [[= simdjson::skip]] int internalCache = 0; +}; + +struct MixedAnnotations { + [[= simdjson::rename<"user_name">]] std::string userName = ""; + [[= simdjson::skip]] int sessionToken = 0; + int age = 0; +}; + +#endif // SIMDJSON_STATIC_REFLECTION + +namespace annotation_tests { + +bool rename_serialize_test() { + TEST_START(); +#if SIMDJSON_STATIC_REFLECTION + RenamedFields r{"Alice", "Smith", 30}; + std::string out; + ASSERT_SUCCESS(simdjson::to_json(r).get(out)); + ASSERT_EQUAL(out, "{\"first_name\":\"Alice\",\"last_name\":\"Smith\",\"age\":30}"); +#endif + TEST_SUCCEED(); +} + +bool rename_deserialize_test() { + TEST_START(); +#if SIMDJSON_STATIC_REFLECTION + std::string json = R"({"first_name":"Bob","last_name":"Jones","age":25})"; + simdjson::ondemand::parser parser; + simdjson::ondemand::document doc; + ASSERT_SUCCESS(parser.iterate(simdjson::pad(json)).get(doc)); + RenamedFields r; + ASSERT_SUCCESS(doc.get().get(r)); + ASSERT_EQUAL(r.firstName, "Bob"); + ASSERT_EQUAL(r.lastName, "Jones"); + ASSERT_EQUAL(r.age, 25); +#endif + TEST_SUCCEED(); +} + +bool rename_roundtrip_test() { + TEST_START(); +#if SIMDJSON_STATIC_REFLECTION + RenamedFields original{"Carol", "White", 40}; + std::string json; + ASSERT_SUCCESS(simdjson::to_json(original).get(json)); + simdjson::ondemand::parser parser; + simdjson::ondemand::document doc; + ASSERT_SUCCESS(parser.iterate(simdjson::pad(json)).get(doc)); + RenamedFields result; + ASSERT_SUCCESS(doc.get().get(result)); + ASSERT_EQUAL(result.firstName, original.firstName); + ASSERT_EQUAL(result.lastName, original.lastName); + ASSERT_EQUAL(result.age, original.age); +#endif + TEST_SUCCEED(); +} + +bool skip_serialize_test() { + TEST_START(); +#if SIMDJSON_STATIC_REFLECTION + SkippedField s{"Alice", 999}; + std::string out; + ASSERT_SUCCESS(simdjson::to_json(s).get(out)); + ASSERT_EQUAL(out, "{\"name\":\"Alice\"}"); +#endif + TEST_SUCCEED(); +} + +bool skip_deserialize_ignores_field_test() { + TEST_START(); +#if SIMDJSON_STATIC_REFLECTION + // The key "internalCache" is present in JSON but the field is annotated skip - + // it should be ignored and the field should keep its default value. + std::string json = R"({"name":"Carol","internalCache":999})"; + simdjson::ondemand::parser parser; + simdjson::ondemand::document doc; + ASSERT_SUCCESS(parser.iterate(simdjson::pad(json)).get(doc)); + SkippedField s; + ASSERT_SUCCESS(doc.get().get(s)); + ASSERT_EQUAL(s.name, "Carol"); + ASSERT_EQUAL(s.internalCache, 0); +#endif + TEST_SUCCEED(); +} + +bool mixed_annotations_serialize_test() { + TEST_START(); +#if SIMDJSON_STATIC_REFLECTION + MixedAnnotations m{"dave", 12345, 28}; + std::string out; + ASSERT_SUCCESS(simdjson::to_json(m).get(out)); + // sessionToken must not appear; userName must appear as "user_name" + ASSERT_EQUAL(out, "{\"user_name\":\"dave\",\"age\":28}"); +#endif + TEST_SUCCEED(); +} + +bool mixed_annotations_deserialize_test() { + TEST_START(); +#if SIMDJSON_STATIC_REFLECTION + std::string json = R"({"user_name":"eve","age":35})"; + simdjson::ondemand::parser parser; + simdjson::ondemand::document doc; + ASSERT_SUCCESS(parser.iterate(simdjson::pad(json)).get(doc)); + MixedAnnotations m; + ASSERT_SUCCESS(doc.get().get(m)); + ASSERT_EQUAL(m.userName, "eve"); + ASSERT_EQUAL(m.age, 35); + ASSERT_EQUAL(m.sessionToken, 0); +#endif + TEST_SUCCEED(); +} + +bool run_all() { + return rename_serialize_test() + && rename_deserialize_test() + && rename_roundtrip_test() + && skip_serialize_test() + && skip_deserialize_ignores_field_test() + && mixed_annotations_serialize_test() + && mixed_annotations_deserialize_test(); +} + +} // namespace annotation_tests + +int main() { + return annotation_tests::run_all() ? EXIT_SUCCESS : EXIT_FAILURE; +}