add: C++26 annotations support for rename and skip (#2730)

This commit is contained in:
Makkar
2026-05-26 22:28:46 +05:30
committed by GitHub
parent d3d99b8fc5
commit 5db72ab9d3
7 changed files with 236 additions and 18 deletions
+63
View File
@@ -0,0 +1,63 @@
#ifndef SIMDJSON_ANNOTATIONS_H
#define SIMDJSON_ANNOTATIONS_H
#if SIMDJSON_STATIC_REFLECTION
#include <meta>
#include <string_view>
namespace simdjson {
// Structural compile-time string — char array avoids the pointer-based
// 'reflect_constant failed' that occurs with const char* / string_view members.
template <size_t N>
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 <fixed_string Name>
struct rename_t {
static constexpr auto name = Name;
};
} // namespace detail
template <fixed_string Name>
inline constexpr detail::rename_t<Name> 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 <auto dm>
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
@@ -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
+20 -16
View File
@@ -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<char>::length(first_key);
constexpr size_t rest_key_len = std::char_traits<char>::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<dm>();
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<char>::length(first_key);
constexpr size_t rest_key_len = std::char_traits<char>::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++] = '}';
@@ -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
@@ -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 <concepts>
@@ -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<mem>();
if constexpr (concepts::optional_type<decltype(out.[:mem:])>) {
// for optional members, it's ok if the key is missing
auto error = obj[key].get(out.[:mem:]);
+1
View File
@@ -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)
@@ -0,0 +1,146 @@
#include "simdjson.h"
#include "test_builder.h"
#include <string>
#include <optional>
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<RenamedFields>().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<RenamedFields>().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<SkippedField>().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<MixedAnnotations>().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;
}