Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions include/rfl/parsing/NamedTupleParser.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -37,6 +40,35 @@ namespace rfl {
template <class... Ps>
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 <class T>
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<T> && !has_reflection_method_v<T> &&
std::is_aggregate_v<T> && num_fields<T> == 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 <class T>
concept CanWriteGeneric =
!has_reflection_type_v<std::remove_cvref_t<T>> ||
has_reflection_method_v<std::remove_cvref_t<T>> ||
IsReflectionTypeWrapper<std::remove_cvref_t<T>>;

/// Satisfied when a schema `default` can be emitted for the field type `T`:
/// it must be a `DefaultVal` that satisfies `CanWriteGeneric`.
template <class T>
concept CanEmitDefault =
is_default_val_v<T> && CanWriteGeneric<typename T::Type>;

} // namespace internal
} // namespace rfl

namespace rfl::parsing {
Expand Down Expand Up @@ -246,12 +278,12 @@ struct NamedTupleParser {
if constexpr (!internal::is_skip_v<U> && !internal::is_extra_fields_v<U>) {
// Add default value here
auto s = Parser<R, W, U, ProcessorsType>::to_schema(_definitions);
if constexpr (!std::is_same_v<View, void>) {
if constexpr (!std::is_same_v<View, void> && internal::CanEmitDefault<U>) {
s.variant_.visit([&](auto& value) {
if constexpr (std::is_same_v<std::remove_cvref_t<decltype(value)>,
schema::Type::DefaultVal>) {
value.default_value_ =
rfl::to_generic((*rfl::get<_i>(*_view)).get());
rfl::to_generic<ProcessorsType>((*rfl::get<_i>(*_view)).get());
}
});
}
Expand Down
4 changes: 3 additions & 1 deletion include/rfl/parsing/schema/Type.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#include <cstddef>
#include <memory>
#include <optional>
#include <string>
#include <vector>

Expand Down Expand Up @@ -62,7 +63,8 @@ struct RFL_API Type {
/// using this or the Optional wrapper.
struct DefaultVal {
Ref<Type> type_;
Generic default_value_;
/// Empty when no default could be produced (distinct from "null").
std::optional<Generic> default_value_;
};

struct DescribedLiteral {
Expand Down
5 changes: 4 additions & 1 deletion src/rfl/json/to_schema.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -224,8 +224,11 @@ schema::Type type_to_json_schema_type(const parsing::schema::Type& _type,

} else if constexpr (std::is_same<T, Type::DefaultVal>()) {
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);
Expand Down
72 changes: 72 additions & 0 deletions tests/json/test_json_schema_default_vals.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,40 @@ struct DefaultWithConfig {
rfl::DefaultVal<Config> with_default = Config{443, true};
};

/// Mixes `DefaultVal` and a plain field.
struct MixedDefaults {
std::string name;
rfl::DefaultVal<int> 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<NonWritable> inner;
};

/// Has snake case fields for testing with `SnakeCaseToCamelCase`.
struct SnakeFields {
int some_value = 7;
bool other_flag = true;
};

struct WithProcessedDefault {
rfl::DefaultVal<SnakeFields> nested = SnakeFields{};
};

TEST(json, test_with_default) {
auto json_schema = rfl::json::to_schema<DefaultValField>();

Expand All @@ -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<WithNonWritableDefault>();

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<WithProcessedDefault, rfl::SnakeCaseToCamelCase>();

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<WithProcessedDefault, rfl::SnakeCaseToCamelCase>();
const auto nested =
rfl::json::to_schema<WithProcessedDefault,
rfl::Processors<rfl::SnakeCaseToCamelCase>>();

EXPECT_EQ(flat, nested);
}

TEST(json, test_with_default_mixed_with_plain_fields) {
const auto json_schema = rfl::json::to_schema<MixedDefaults>();

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
Loading