From 1e297502cde499991cad8f4627838d1cebf033a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Geis?= Date: Thu, 3 Sep 2026 10:59:23 +0900 Subject: [PATCH] fix `NamedTupleParser::to_schema()` for non-`DefaultVal` fields Also properly pass processors when computing the default, so e.g. naming matches the schema. --- include/rfl/parsing/NamedTupleParser.hpp | 36 +++++++++- include/rfl/parsing/schema/Type.hpp | 4 +- src/rfl/json/to_schema.cpp | 5 +- tests/json/test_json_schema_default_vals.cpp | 72 ++++++++++++++++++++ 4 files changed, 113 insertions(+), 4 deletions(-) diff --git a/include/rfl/parsing/NamedTupleParser.hpp b/include/rfl/parsing/NamedTupleParser.hpp index 8542cdb7..45951064 100644 --- a/include/rfl/parsing/NamedTupleParser.hpp +++ b/include/rfl/parsing/NamedTupleParser.hpp @@ -11,12 +11,15 @@ #include "../Result.hpp" #include "../internal/default_if_missing_v.hpp" #include "../internal/has_default_val_v.hpp" +#include "../internal/has_reflection_method_v.hpp" +#include "../internal/has_reflection_type_v.hpp" #include "../internal/is_attribute.hpp" #include "../internal/is_default_val_v.hpp" #include "../internal/is_extra_fields.hpp" #include "../internal/is_skip.hpp" #include "../internal/nth_element_t.hpp" #include "../internal/ptr_cast.hpp" +#include "../num_fields.hpp" #include "../to_view.hpp" #include "AreReaderAndWriter.hpp" #include "Parent.hpp" @@ -37,6 +40,35 @@ namespace rfl { template Generic to_generic(const auto& _t); +namespace internal { + +/// Satisfied by a type that carries a nested `ReflectionType` and can be +/// unwrapped by decomposing it, like `rfl::Validator`. +template +concept IsReflectionTypeWrapper = + // We use `std::is_aggregate_v` and `num_fields` rather than `requires` + // because `requires`, well, requires an expression, but checking this via + // syntax would require a statement `const auto& [_] = _t`, which is not + // allowed in a `requires` clause. + has_reflection_type_v && !has_reflection_method_v && + std::is_aggregate_v && num_fields == 1; + +/// Satisfied when the generic writer can serialize `T`, i.e. when a schema +/// `default` can be produced for it. Every type is writable, except those that +/// can be constructed from `ReflectionType`, but not converted back to it. +template +concept CanWriteGeneric = + !has_reflection_type_v> || + has_reflection_method_v> || + IsReflectionTypeWrapper>; + +/// Satisfied when a schema `default` can be emitted for the field type `T`: +/// it must be a `DefaultVal` that satisfies `CanWriteGeneric`. +template +concept CanEmitDefault = + is_default_val_v && CanWriteGeneric; + +} // namespace internal } // namespace rfl namespace rfl::parsing { @@ -246,12 +278,12 @@ struct NamedTupleParser { if constexpr (!internal::is_skip_v && !internal::is_extra_fields_v) { // Add default value here auto s = Parser::to_schema(_definitions); - if constexpr (!std::is_same_v) { + if constexpr (!std::is_same_v && internal::CanEmitDefault) { s.variant_.visit([&](auto& value) { if constexpr (std::is_same_v, schema::Type::DefaultVal>) { value.default_value_ = - rfl::to_generic((*rfl::get<_i>(*_view)).get()); + rfl::to_generic((*rfl::get<_i>(*_view)).get()); } }); } diff --git a/include/rfl/parsing/schema/Type.hpp b/include/rfl/parsing/schema/Type.hpp index a3621b2a..c81679ce 100644 --- a/include/rfl/parsing/schema/Type.hpp +++ b/include/rfl/parsing/schema/Type.hpp @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -62,7 +63,8 @@ struct RFL_API Type { /// using this or the Optional wrapper. struct DefaultVal { Ref type_; - Generic default_value_; + /// Empty when no default could be produced (distinct from "null"). + std::optional default_value_; }; struct DescribedLiteral { diff --git a/src/rfl/json/to_schema.cpp b/src/rfl/json/to_schema.cpp index b6ed76cd..3c5426e8 100644 --- a/src/rfl/json/to_schema.cpp +++ b/src/rfl/json/to_schema.cpp @@ -224,8 +224,11 @@ schema::Type type_to_json_schema_type(const parsing::schema::Type& _type, } else if constexpr (std::is_same()) { auto res = type_to_json_schema_type(*_t.type_, _no_required); + if (!_t.default_value_.has_value()) { + return res; + } const auto update_prediction = [&](auto _v) -> schema::Type { - _v.annotations.value_.defaultValue = _t.default_value_; + _v.annotations.value_.defaultValue = *_t.default_value_; return schema::Type{_v}; }; return rfl::visit(update_prediction, res.value); diff --git a/tests/json/test_json_schema_default_vals.cpp b/tests/json/test_json_schema_default_vals.cpp index 9236a329..97f2f25e 100644 --- a/tests/json/test_json_schema_default_vals.cpp +++ b/tests/json/test_json_schema_default_vals.cpp @@ -19,6 +19,40 @@ struct DefaultWithConfig { rfl::DefaultVal with_default = Config{443, true}; }; +/// Mixes `DefaultVal` and a plain field. +struct MixedDefaults { + std::string name; + rfl::DefaultVal with_default = 10; +}; + +/// Has `ReflectionType` (and a converting constructor from it), but no +/// `reflection()` method or single `ReflectionType` member, so no default. +struct NonWritable { + int copied; + int missing; + struct Inner { + int value; + }; + using ReflectionType = Inner; + NonWritable() : copied(0), missing(0) {} + explicit NonWritable(ReflectionType&& _r) + : copied(_r.value), missing(0) {} +}; + +struct WithNonWritableDefault { + rfl::DefaultVal inner; +}; + +/// Has snake case fields for testing with `SnakeCaseToCamelCase`. +struct SnakeFields { + int some_value = 7; + bool other_flag = true; +}; + +struct WithProcessedDefault { + rfl::DefaultVal nested = SnakeFields{}; +}; + TEST(json, test_with_default) { auto json_schema = rfl::json::to_schema(); @@ -34,4 +68,42 @@ TEST(json, test_with_default) { EXPECT_EQ(json_schema, expected) << "is " << json_schema; } +TEST(json, test_no_default_when_value_cannot_be_written) { + const auto json_schema = rfl::json::to_schema(); + + EXPECT_EQ(json_schema.find("\"default\""), std::string::npos) + << "expected no default: " << json_schema; + EXPECT_NE(json_schema.find("\"required\":[]"), std::string::npos) + << "expected the field to remain optional: " << json_schema; +} + +TEST(json, test_default_with_processors) { + const auto json_schema = + rfl::json::to_schema(); + + EXPECT_NE(json_schema.find(R"("default":{"someValue":7,"otherFlag":true})"), + std::string::npos) + << "default uses renamed properties: " << json_schema; + EXPECT_EQ(json_schema.find("some_value"), std::string::npos) + << "default was written without the processors: " << json_schema; + + // Same with a nested processor pack. + const auto flat = + rfl::json::to_schema(); + const auto nested = + rfl::json::to_schema>(); + + EXPECT_EQ(flat, nested); +} + +TEST(json, test_with_default_mixed_with_plain_fields) { + const auto json_schema = rfl::json::to_schema(); + + const std::string expected = + R"({"$schema":"https://json-schema.org/draft/2020-12/schema","$ref":"#/$defs/test_schema_default__MixedDefaults","$defs":{"test_schema_default__MixedDefaults":{"type":"object","properties":{"name":{"type":"string"},"with_default":{"type":"integer","default":10}},"required":["name"]}}})"; + + EXPECT_EQ(json_schema, expected) << json_schema; +} + } // namespace test_schema_default