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

## All fields need a valid default value

A struct that contains an `rfl::DefaultVal` is default-constructed before it is read. The fields found in the input are then written into it. This means that every field of the struct needs a valid default value, even a required field that is in the input.

This matters when the default value of a field's type is not valid, for instance when the field has a validator:

```cpp
struct Person {
rfl::DefaultVal<std::string> name = "Homer Simpson";
rfl::Validator<std::vector<std::string>, rfl::Size<rfl::Minimum<1>>> favorite_foods;
};

// Fails, even though favorite_foods is in the input.
rfl::json::read<Person>(R"({"favorite_foods":["donuts","pizza"]})");
```

An empty vector does not satisfy `rfl::Size<rfl::Minimum<1>>`, so `Person{}` throws an exception and you get the following error:

```
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.
```

To avoid this, give the field a valid default value:

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

The field is still required. If `favorite_foods` is missing in the input, you get the error `Field named 'favorite_foods' not found.` The default value is only used for a missing field if you pass the `rfl::DefaultIfMissing` processor.

The same restriction applies to every struct that is read with the `rfl::DefaultIfMissing` processor, whether it contains an `rfl::DefaultVal` or not.

## 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
4 changes: 4 additions & 0 deletions docs/content/processors.md
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,10 @@ 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. Every field
therefore needs a valid default value, even if the field is in the input.
This is explained in the documentation on `rfl::DefaultVal`.

### `rfl::DurationsAsCount`

By default, `std::chrono::duration` types are written as an object containing
Expand Down
43 changes: 31 additions & 12 deletions include/rfl/parsing/Parser_default.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
#define RFL_PARSING_PARSER_DEFAULT_HPP_

#include <map>
#include <string>
#include <type_traits>

#include "../Result.hpp"
#include "../always_false.hpp"
#include "../internal/HasFromReflectionMethod.hpp"
#include "../internal/always_inline.hpp"
#include "../internal/cold.hpp"
#include "../internal/default_if_missing_v.hpp"
#include "../internal/has_default_val_v.hpp"
#include "../internal/has_reflection_method_v.hpp"
Expand Down Expand Up @@ -720,22 +722,39 @@ struct Parser {
static Result<T> read_struct_with_default(const R& _r,
const InputVarType& _var) {
try {
auto t = T{}; // This might fail, for instance if the default value does
// not satisfy the validator, but in that case we will just
// return the error.
auto view = ProcessorsType::template process<T>(to_view(t));
using ViewType = decltype(view);
auto err =
Parser<R, W, ViewType, ProcessorsType>::read_view_with_default(
_r, _var, &view);
if (err) [[unlikely]] {
return error(std::move(*err));
// This might fail, for instance if the default value of a field does not
// satisfy its validator. The outer handler is for this line only.
auto t = T{};
try {
auto view = ProcessorsType::template process<T>(to_view(t));
using ViewType = decltype(view);
auto err =
Parser<R, W, ViewType, ProcessorsType>::read_view_with_default(
_r, _var, &view);
if (err) [[unlikely]] {
return error(std::move(*err));
}
return t;
} catch (std::exception& e) {
return error(e.what());
}
return t;
} catch (std::exception& e) {
return error(e.what());
return error(default_construction_error(e.what()));
}
}

/// The error returned by read_struct_with_default when T{} throws. On its
/// own, the exception reads as if the input was wrong, so we name the struct
/// and explain why it was default-constructed.
RFL_COLD static Error default_construction_error(const char* _what) {
const std::string reason = internal::has_default_val_v<T>
? "it contains rfl::DefaultVal"
: "it is read with rfl::DefaultIfMissing";
return Error(
"Could not default-construct " + type_name_t<T>().str() +
", which is required because " + reason +
". Give every field a valid default value. Original error: " + _what);
}
};

} // namespace rfl::parsing
Expand Down
147 changes: 147 additions & 0 deletions tests/json/test_default_construct_error.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
#include <gtest/gtest.h>

#include <rfl.hpp>
#include <rfl/json.hpp>
#include <stdexcept>
#include <string>
#include <vector>

namespace test_default_construct_error {

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;
};

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

struct Pet {
std::string name;
Foods favorite_foods;
};

struct Family {
std::string town;
Person head;
};

inline int no_default_port() { throw std::runtime_error("No default port"); }

struct Server {
rfl::DefaultVal<std::string> host = "localhost";
int port = no_default_port();
};

bool contains(const std::string& _str, const std::string& _substr) {
return _str.find(_substr) != std::string::npos;
}

const std::string validation_error =
"Size validation failed: Value expected to be greater than or equal to 1, "
"but got 0.";

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

ASSERT_FALSE(res && true);
const auto what = res.error().what();
EXPECT_TRUE(contains(what, "default-construct")) << what;
EXPECT_TRUE(contains(what, "Person")) << what;
EXPECT_TRUE(contains(what, "rfl::DefaultVal")) << what;
EXPECT_TRUE(contains(what, "valid default value")) << what;
EXPECT_TRUE(contains(what, validation_error)) << what;
}

TEST(json, test_default_construct_error_with_default_if_missing) {
const auto json_string = R"({"name":"Bart","favorite_foods":["pizza"]})";

EXPECT_TRUE(rfl::json::read<Pet>(json_string) && true);

const auto res = rfl::json::read<Pet, rfl::DefaultIfMissing>(json_string);

ASSERT_FALSE(res && true);
const auto what = res.error().what();
EXPECT_TRUE(contains(what, "default-construct")) << what;
EXPECT_TRUE(contains(what, "Pet")) << what;
EXPECT_TRUE(contains(what, "rfl::DefaultIfMissing")) << what;
EXPECT_TRUE(contains(what, validation_error)) << what;
}

TEST(json, test_default_construct_error_in_nested_struct) {
const auto res = rfl::json::read<Family>(
R"({"town":"Springfield","head":{"favorite_foods":["donuts"]}})");

ASSERT_FALSE(res && true);
const auto what = res.error().what();
EXPECT_TRUE(contains(what, "Failed to parse field 'head': ")) << what;

const auto details = res.error().details();
ASSERT_EQ(details.size(), 1);
EXPECT_EQ(details[0].path,
std::vector<rfl::ErrorDetail::PathSegment>({"head"}));
EXPECT_TRUE(contains(details[0].message, "default-construct"));
EXPECT_TRUE(contains(details[0].message, "Person"));
EXPECT_TRUE(contains(details[0].message, validation_error));
}

TEST(json, test_default_construct_error_without_validator) {
const auto res = rfl::json::read<Server>(R"({"port":8080})");

ASSERT_FALSE(res && true);
const auto what = res.error().what();
EXPECT_TRUE(contains(what, "default-construct")) << what;
EXPECT_TRUE(contains(what, "Server")) << what;
EXPECT_TRUE(contains(what, "No default port")) << what;
}

TEST(json, test_default_construct_error_valid_default) {
const auto homer = rfl::json::read<PersonWithValidDefault>(
R"({"favorite_foods":["donuts","pizza"]})")
.value();

EXPECT_EQ(homer.name.value(), "Homer Simpson");
EXPECT_EQ(homer.favorite_foods.value(),
std::vector<std::string>({"donuts", "pizza"}));

// The initializer does not make the field optional.
const auto missing = rfl::json::read<PersonWithValidDefault>(R"({})");

ASSERT_FALSE(missing && true);
EXPECT_EQ(missing.error().what(), "Field named 'favorite_foods' not found.");

// With rfl::DefaultIfMissing, the initializer is the value of a missing
// field.
const auto with_default =
rfl::json::read<PersonWithValidDefault, rfl::DefaultIfMissing>(R"({})")
.value();

EXPECT_EQ(with_default.favorite_foods.value(),
std::vector<std::string>({"none"}));
}

TEST(json, test_default_construct_error_not_used_for_later_failures) {
const auto wrong_type =
rfl::json::read<PersonWithValidDefault>(R"({"favorite_foods":"donuts"})");

ASSERT_FALSE(wrong_type && true);
EXPECT_TRUE(contains(wrong_type.error().what(),
"Failed to parse field 'favorite_foods': "))
<< wrong_type.error().what();
EXPECT_FALSE(contains(wrong_type.error().what(), "default-construct"))
<< wrong_type.error().what();

const auto invalid =
rfl::json::read<PersonWithValidDefault>(R"({"favorite_foods":[]})");

ASSERT_FALSE(invalid && true);
EXPECT_EQ(invalid.error().what(),
"Failed to parse field 'favorite_foods': " + validation_error);
}
} // namespace test_default_construct_error