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
46 changes: 46 additions & 0 deletions docs/content/default_val.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,52 @@ const auto homer = rfl::json::read<Person, rfl::NoFieldNames>(R"(["Homer"])").va
// homer.town == ""
```

## Fields without a valid default value

A struct that contains an `rfl::DefaultVal` is default-constructed before it is read, so that the fields missing from the input keep their default values. The fields found in the input are then replaced by the values read from it.

Some fields have no valid default value, for instance a validator that an empty vector does not satisfy:

```cpp
using Foods = rfl::Validator<std::vector<std::string>, rfl::Size<rfl::Minimum<1>>>;

struct Person {
rfl::DefaultVal<std::string> name = "Homer Simpson";
Foods favorite_foods;
};

const auto homer = rfl::json::read<Person>(R"({"favorite_foods":["donuts","pizza"]})");
```

`Person{}` throws, but `read` then constructs the struct again with the check of such a validator deferred until the input has been read. If `favorite_foods` is in the input, its invalid default value has been replaced. If it is missing, you get the error `Field named 'favorite_foods' not found.` Outside of `read`, `Person{}` still throws.

A field whose default value is invalid must not be missing, even where a missing field would otherwise get its default value, that is, for an `rfl::DefaultVal` or with the `rfl::DefaultIfMissing` processor:

```
Field named 'favorite_foods' not found, and its default value is invalid: Size validation failed: Value expected to be greater than or equal to 1, but got 0.
```

Only a validator that is default-constructed in place inside the struct is deferred, also inside nested structs, `std::optional`, `std::variant`, `std::array` or `rfl::Variant`. While the struct is constructed, it holds its invalid value, which a default member initializer that reads it would see. A default member initializer that catches the exception of such a validator in order to fall back to another value might not get the exception either, depending on whether the compiler constructs the validator directly inside the struct. The struct cannot be read, and you get the error below, if the invalid default value is constructed elsewhere and then moved into the struct (as in `rfl::DefaultVal<Foods> favorite_foods = Foods();` or inside `rfl::Tuple`), if it is on the heap (as in `std::vector<Foods>(1)`), or if `Person{}` throws for another reason:

```
Could not default-construct Person, which is required because it contains rfl::DefaultVal. Give every field a valid default value. Original error: Size validation failed: Value expected to be greater than or equal to 1, but got 0.
```

One case is not detected: if a default member initializer copies a validator whose default value is invalid, as in `rfl::DefaultVal<Foods> other_foods = favorite_foods;`, the copy is not checked, and it keeps the invalid value if `other_foods` is missing from the input.

In all of these cases, you can give the field a valid default value:

```cpp
struct Person {
rfl::DefaultVal<std::string> name = "Homer Simpson";
Foods favorite_foods = std::vector<std::string>({"none"});
};
```

The field is still required. The default value is only used for a missing field if you pass the `rfl::DefaultIfMissing` processor.

All of this applies to every struct that is read with the `rfl::DefaultIfMissing` processor, whether it contains an `rfl::DefaultVal` or not. The JSON schema of a struct that cannot be default-constructed contains no default values.

## When to use

Use `rfl::DefaultVal` when you want a field to be optional at the input side but still available as a value on the resulting object (no std::optional or pointer indirection). It is particularly useful for fields with sensible defaults (for example, a common last name, a default configuration value, or empty containers/strings).
Expand Down
7 changes: 7 additions & 0 deletions docs/content/processors.md
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,13 @@ have gotten had you read the following JSON string:
Because you have not passed a default value to town, the default value
of the type is used instead.

Note that the struct is default-constructed before it is read. A field
whose default value is invalid, such as a validator that the default value
of its type does not satisfy, can be read, but it must not be missing: you
get the error `Field named '...' not found, and its default value is
invalid: ...`. The documentation on `rfl::DefaultVal` explains this and the
cases in which such a struct cannot be read at all.

### `rfl::DurationsAsCount`

By default, `std::chrono::duration` types are written as an object containing
Expand Down
3 changes: 2 additions & 1 deletion include/rfl/DefaultVal.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ struct DefaultVal {
/// @param _value The value to convert and store (will be moved)
template <class U>
requires(std::is_convertible_v<U, Type>)
DefaultVal(U&& _value) noexcept : value_(std::forward<U>(_value)) {}
DefaultVal(U&& _value) noexcept(std::is_nothrow_constructible_v<Type, U&&>)
: value_(std::forward<U>(_value)) {}

/// Constructs from a DefaultVal with compatible type.
/// @tparam U Type convertible to T
Expand Down
27 changes: 25 additions & 2 deletions include/rfl/Validator.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@

#include "AllOf.hpp"
#include "Result.hpp"
#include "internal/DeferredDefaults.hpp"
#include "internal/HasValidation.hpp"
#include "internal/cold.hpp"

namespace rfl {

Expand Down Expand Up @@ -52,8 +54,10 @@ struct Validator {
}

/// Default constructor - validates a default-constructed T.
/// @throws std::exception if validation of the default value fails
Validator() : value_(ValidationType::validate(T()).value()) {}
/// @throws std::exception if validation of the default value fails, unless
/// the parser is default-constructing a struct to read into, see
/// internal::DeferredDefaults.
Validator() : value_(valid_or_deferred(this)) {}

/// Move constructor.
/// @param _other The validator to move from
Expand Down Expand Up @@ -197,6 +201,25 @@ struct Validator {

Validator(AlreadyValidated, T&& _value) : value_(std::move(_value)) {}

/// Returns the validated default value. If it is invalid, this throws,
/// unless the parser defers its validation, see internal::DeferredDefaults.
/// _self is the Validator under construction. It is not a pointer to const,
/// because GCC would then warn that it is used uninitialized.
static T valid_or_deferred(void* _self) {
auto res = ValidationType::validate(T());
if (!res) [[unlikely]] {
return deferred_or_throw(_self, res.error());
}
return *std::move(res);
}

RFL_COLD static T deferred_or_throw(void* _self, const Error& _err) {
if (!internal::DeferredDefaults::defer(_self, _err)) {
Result<T>(error(_err)).value();
}
return T();
}

/// Wraps the result of a validation without throwing if it failed.
static Result<Validator> wrap_validated(Result<T>&& _res) {
return std::move(_res).transform(
Expand Down
5 changes: 2 additions & 3 deletions include/rfl/Variant.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,10 @@ class Variant {
public:
/// @brief Default-constructs the Variant, initializing it with the first
/// alternative type.
/// @details The first alternative type is default-constructed and stored in
/// the variant.
/// @details The first alternative type is default-constructed in place.
Variant() : index_(IndexType()), data_(DataType()) {
using FirstAlternative = internal::nth_element_t<0, AlternativeTypes...>;
move_from_type(FirstAlternative());
new (data_.data()) FirstAlternative();
}

/// @brief Copy-constructs the Variant from another Variant.
Expand Down
82 changes: 82 additions & 0 deletions include/rfl/internal/DeferredDefaults.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#ifndef RFL_INTERNAL_DEFERREDDEFAULTS_HPP_
#define RFL_INTERNAL_DEFERREDDEFAULTS_HPP_

#include <cstddef>
#include <functional>
#include <vector>

#include "../Result.hpp"

namespace rfl::internal {

/// Parser::read_struct_with_default default-constructs the struct it reads
/// into, to obtain the default values of the fields that are missing from the
/// input. If an rfl::Validator in it has an invalid default value, that
/// throws, even if the input contains the field. The parser then constructs
/// the struct again while a DeferredDefaults is active on this thread. A
/// Validator inside the struct then keeps its invalid default value instead
/// of throwing and is recorded here, so that the parser can check after
/// reading that the value has been replaced by one from the input. Validators
/// anywhere else, such as temporaries or on the heap, throw as usual.
class DeferredDefaults {
public:
struct Record {
/// The address of the Validator.
const void* address;

/// Why its default value is invalid.
Error error;
};

/// Makes _deferred the active DeferredDefaults of this thread for as long
/// as the Scope lives.
class Scope {
public:
explicit Scope(DeferredDefaults* _deferred) noexcept : prev_(active()) {
active() = _deferred;
}

~Scope() { active() = prev_; }

Scope(const Scope&) = delete;
Scope& operator=(const Scope&) = delete;

private:
DeferredDefaults* prev_;
};

/// _begin and _size describe the storage of the struct.
DeferredDefaults(const void* _begin, size_t _size) noexcept
: begin_(static_cast<const unsigned char*>(_begin)),
end_(begin_ + _size) {}

/// Called by rfl::Validator() if its default value is invalid. Returns
/// whether the Validator keeps the value instead of throwing.
static bool defer(const void* _address, const Error& _error) {
auto* deferred = active();
const auto* p = static_cast<const unsigned char*>(_address);
const auto less = std::less<const unsigned char*>();
if (!deferred || less(p, deferred->begin_) || !less(p, deferred->end_)) {
return false;
}
deferred->records_.push_back(Record{_address, _error});
return true;
}

const std::vector<Record>& records() const noexcept { return records_; }

private:
static DeferredDefaults*& active() noexcept {
static thread_local DeferredDefaults* deferred = nullptr;
return deferred;
}

private:
const unsigned char* begin_;
const unsigned char* end_;
std::vector<Record> records_;
};

} // namespace rfl::internal

#endif
38 changes: 29 additions & 9 deletions include/rfl/parsing/NamedTupleParser.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#define RFL_PARSING_NAMEDTUPLEPARSER_HPP_

#include <array>
#include <exception>
#include <map>
#include <sstream>
#include <type_traits>
Expand Down Expand Up @@ -176,23 +177,24 @@ struct NamedTupleParser {
* @param _r The reader to use.
* @param _var The input variable to read from.
* @param _view The view to read into.
* @param _found If not null, receives which fields the input contains.
* @return An optional error.
*/
static std::optional<Error> read_view_with_default(
const R& _r, const InputVarType& _var,
NamedTuple<FieldTypes...>* _view) noexcept {
const R& _r, const InputVarType& _var, NamedTuple<FieldTypes...>* _view,
std::array<bool, size_>* _found = nullptr) noexcept {
if constexpr (_no_field_names) {
auto arr = _r.to_array(_var);
if (!arr) [[unlikely]] {
return arr.error();
}
return read_object_or_array_with_default(_r, *arr, _view);
return read_object_or_array_with_default(_r, *arr, _view, _found);
} else {
auto obj = _r.to_object(_var);
if (!obj) [[unlikely]] {
return obj.error();
}
return read_object_or_array_with_default(_r, *obj, _view);
return read_object_or_array_with_default(_r, *obj, _view, _found);
}
}

Expand Down Expand Up @@ -353,11 +355,26 @@ struct NamedTupleParser {
make_nested_error(std::string(current_name), stream.str()));

} else if constexpr (!internal::has_default_val_v<NamedTupleType>) {
if constexpr (!std::is_const_v<ValueType>) {
::new (rfl::get<_i>(_view)) ValueType();
using NonConstT = std::remove_const_t<ValueType>;
const auto ptr = const_cast<NonConstT*>(rfl::get<_i>(_view));
if constexpr (std::is_nothrow_default_constructible_v<NonConstT>) {
::new (ptr) NonConstT();
} else {
using NonConstT = std::remove_const_t<ValueType>;
::new (const_cast<NonConstT*>(rfl::get<_i>(_view))) NonConstT();
// The default value can throw, for instance a Validator whose
// default value is invalid.
try {
::new (ptr) NonConstT();
} catch (std::exception& e) {
constexpr auto current_name =
internal::nth_element_t<_i, FieldTypes...>::name();
_errors->emplace_back(make_nested_error(
std::string(current_name),
"Field named '" + std::string(current_name) +
"' not found, and its default value could not be "
"constructed: " +
e.what()));
return;
}
}
std::get<_i>(*_set) = true;
}
Expand Down Expand Up @@ -420,7 +437,7 @@ struct NamedTupleParser {

static std::optional<Error> read_object_or_array_with_default(
const R& _r, const InputObjectOrArrayType& _obj_or_arr,
NamedTupleType* _view) noexcept {
NamedTupleType* _view, std::array<bool, size_>* _found) noexcept {
FieldErrors<ProcessorsType> errors;
const auto reader = ViewReaderWithDefaultType(&_r, _view, &errors);
if constexpr (_no_field_names) {
Expand All @@ -434,6 +451,9 @@ struct NamedTupleParser {
return err;
}
}
if (_found) [[unlikely]] {
*_found = reader.found();
}
if constexpr (internal::has_default_val_v<NamedTupleType> &&
!internal::default_if_missing_v<ProcessorsType>) {
if (!should_stop_on_field_error<ProcessorsType>(errors)) {
Expand Down
11 changes: 9 additions & 2 deletions include/rfl/parsing/ParserSkip.hpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#ifndef RFL_PARSING_PARSER_SKIP_HPP_
#define RFL_PARSING_PARSER_SKIP_HPP_

#include <exception>
#include <map>
#include <type_traits>

Expand Down Expand Up @@ -39,8 +40,14 @@ struct ParserSkip {
static Result<internal::Skip<T, _skip_serialization, _skip_deserialization>>
read(const R& _r, const InputVarType& _var) noexcept {
if constexpr (_skip_deserialization) {
return internal::Skip<T, _skip_serialization, _skip_deserialization>(
std::remove_cvref_t<T>());
// The default value can throw, for instance a Validator whose default
// value is invalid.
try {
return internal::Skip<T, _skip_serialization, _skip_deserialization>(
std::remove_cvref_t<T>());
} catch (std::exception& e) {
return error(e.what());
}
} else {
const auto to_skip = [&](auto&& _t) {
return internal::Skip<T, _skip_serialization, _skip_deserialization>(
Expand Down
Loading
Loading