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
11 changes: 8 additions & 3 deletions include/rfl/AllOf.hpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#ifndef RFL_ALLOF_HPP_
#define RFL_ALLOF_HPP_

#include <utility>
#include <vector>

#include "Result.hpp"
Expand All @@ -16,7 +17,7 @@ struct AllOf {
/// @return The value if all constraints pass, otherwise an error
template <class T>
static rfl::Result<T> validate(T _value) noexcept {
return validate_impl<T, C, Cs...>(_value);
return validate_impl<T, C, Cs...>(std::move(_value));
}

/// Converts the validator to a JSON schema type.
Expand All @@ -33,9 +34,13 @@ struct AllOf {
template <class T, class Head, class... Tail>
static rfl::Result<T> validate_impl(T _value) noexcept {
if constexpr (sizeof...(Tail) == 0) {
return Head::validate(_value);
return Head::validate(std::move(_value));
} else {
return Head::validate(_value).and_then(validate_impl<T, Tail...>);
auto res = Head::validate(std::move(_value));
if (!res) {
return res;
}
return validate_impl<T, Tail...>(std::move(*res));
}
}
};
Expand Down
5 changes: 3 additions & 2 deletions include/rfl/PatternValidator.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#include <sstream>
#include <string>
#include <utility>

#if __has_include(<ctre.hpp>)
#include <ctre.hpp>
Expand All @@ -29,10 +30,10 @@ struct PatternValidator {
/// Validates that a string matches the regex pattern.
/// @param _str The string to validate
/// @return Result containing the string if it matches, or an error if it doesn't
static Result<std::string> validate(const std::string& _str) noexcept {
static Result<std::string> validate(std::string _str) noexcept {
if (ctre::match<ctll::fixed_string<_regex.length>{
ctll::construct_from_pointer, _regex.arr_.data()}>(_str)) {
return _str;
return std::move(_str);
} else {
std::stringstream stream;
stream << "String '" << _str << "' did not match format '" << _name.str()
Expand Down
14 changes: 7 additions & 7 deletions include/rfl/Size.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#define RFL_SIZE_HPP_

#include <map>
#include <utility>

#include "Ref.hpp"
#include "Result.hpp"
Expand All @@ -18,13 +19,12 @@ struct Size {
/// @param _t The container whose size will be validated
/// @return The container if validation passes, otherwise an error
template <class T>
static rfl::Result<T> validate(const T& _t) {
const auto to_t = [&](const auto&) { return _t; };
const auto embellish_error = [](const auto& _err) -> Error {
return Error("Size validation failed: " + _err.what());
};
return V::validate(_t.size()).transform(to_t).transform_error(
embellish_error);
static rfl::Result<T> validate(T _t) {
auto res = V::validate(_t.size());
if (!res) {
return error("Size validation failed: " + res.error().what());
}
return std::move(_t);
}

/// Converts the size validator to a JSON schema type.
Expand Down
30 changes: 28 additions & 2 deletions include/rfl/Validator.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,19 @@ struct Validator {
/// if it fails
static Result<Validator> from_value(const T& _value) noexcept {
try {
return Validator(_value);
return wrap_validated(ValidationType::validate(_value));
} catch (std::exception& e) {
return error(e.what());
}
}

/// Exception-free validation from a value (move version).
/// @param _value The value to validate (will be moved)
/// @return Result containing a Validator if validation succeeds, or an error
/// if it fails
static Result<Validator> from_value(T&& _value) noexcept {
try {
return wrap_validated(ValidationType::validate(std::move(_value)));
} catch (std::exception& e) {
return error(e.what());
}
Expand All @@ -54,7 +66,8 @@ struct Validator {
/// Constructs from an rvalue of T (validates the value).
/// @param _value The value to validate and wrap
/// @throws std::exception if validation fails
Validator(T&& _value) : value_(ValidationType::validate(_value).value()) {}
Validator(T&& _value)
: value_(ValidationType::validate(std::move(_value)).value()) {}

/// Constructs from a const reference to T (validates the value).
/// @param _value The value to validate and wrap
Expand Down Expand Up @@ -177,6 +190,19 @@ struct Validator {
/// Necessary for the serialization to work.
const T& reflection() const { return value_; }

private:
/// Tag for constructing a Validator from a value that has already been
/// validated.
struct AlreadyValidated {};

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

/// Wraps the result of a validation without throwing if it failed.
static Result<Validator> wrap_validated(Result<T>&& _res) {
return std::move(_res).transform(
[](T&& _v) { return Validator(AlreadyValidated{}, std::move(_v)); });
}

private:
/// The underlying value.
T value_;
Expand Down
6 changes: 4 additions & 2 deletions include/rfl/parsing/ParserRflVariant.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,6 @@ class ParserRflVariant<R, W, rfl::Variant<AlternativeTypes...>,
} else {
std::optional<rfl::Variant<AlternativeTypes...>> result;
std::vector<std::string> errors;
errors.reserve(sizeof...(AlternativeTypes));
read_variant(
_r, _var, &result, &errors,
std::make_integer_sequence<int, sizeof...(AlternativeTypes)>());
Expand Down Expand Up @@ -227,7 +226,10 @@ class ParserRflVariant<R, W, rfl::Variant<AlternativeTypes...>,
if (res) {
*_result = std::move(*res);
} else {
_errors->emplace_back(std::move(res.error().what()));
// Reserve lazily, so that reading an alternative that matches
// does not allocate.
_errors->reserve(sizeof...(AlternativeTypes));
_errors->emplace_back(std::move(res).error().what());
}
}
}
Expand Down
13 changes: 10 additions & 3 deletions include/rfl/parsing/ParserTaggedUnion.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -239,9 +239,16 @@ struct ParserTaggedUnion<R, W, TaggedUnion<_discriminator, AlternativeTypes...>,
/// Writes a wrapped version of the original object, which contains the tag.
template <class T, class P>
static void write_wrapped(const W& _w, const T& _val, const P& _parent) {
const auto wrapped = wrap_if_necessary<T>(_val);
Parser<R, W, std::remove_cvref_t<decltype(wrapped)>, ProcessorsType>::write(
_w, wrapped, _parent);
if constexpr (named_tuple_t<T>::Names::template contains<
_discriminator>()) {
// The object already contains the tag, so it can be written as it is,
// without the copy that wrap_if_necessary would make.
Parser<R, W, T, ProcessorsType>::write(_w, _val, _parent);
} else {
const auto wrapped = wrap_if_necessary<T>(_val);
Parser<R, W, std::remove_cvref_t<decltype(wrapped)>,
ProcessorsType>::write(_w, wrapped, _parent);
}
}

/// Generates a wrapped version of the original object, which contains the
Expand Down
6 changes: 4 additions & 2 deletions include/rfl/parsing/ParserVariant.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,6 @@ class ParserVariant<R, W, std::variant<AlternativeTypes...>, ProcessorsType> {
} else {
std::optional<std::variant<AlternativeTypes...>> result;
std::vector<std::string> errors;
errors.reserve(sizeof...(AlternativeTypes));
read_variant(
_r, _var, &result, &errors,
std::make_integer_sequence<int, sizeof...(AlternativeTypes)>());
Expand Down Expand Up @@ -269,7 +268,10 @@ class ParserVariant<R, W, std::variant<AlternativeTypes...>, ProcessorsType> {
if (res) {
_result->emplace(std::move(*res));
} else {
_errors->emplace_back(res.error().what());
// Reserve lazily, so that reading an alternative that matches
// does not allocate.
_errors->reserve(sizeof...(AlternativeTypes));
_errors->emplace_back(std::move(res).error().what());
}
}
}
Expand Down
8 changes: 8 additions & 0 deletions include/rfl/parsing/Parser_default.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,14 @@ struct Parser {
using NT = decltype(_named_tuple);
if constexpr (internal::HasFromReflectionMethod<T>) {
return T::from_reflection(std::forward<NT>(_named_tuple));
} else if constexpr (internal::is_literal_v<T>) {
// Unlike the constructor, from_string does not throw when the
// string is not one of the literal's values.
return T::from_string(_named_tuple);
} else if constexpr (internal::is_validator_v<T>) {
// Unlike the constructor, from_value does not throw when the
// validation fails, which makes failing much cheaper.
return T::from_value(std::forward<NT>(_named_tuple));
} else {
return T{std::forward<NT>(_named_tuple)};
}
Expand Down
10 changes: 10 additions & 0 deletions tests/json/test_literal.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,14 @@ TEST(json, test_literal) {
write_and_read(bart,
R"({"firstName":"Bart","lastName":"Simpson","children":[]})");
}

TEST(json, test_literal_reports_unsupported_string) {
const auto res = rfl::json::read<Person>(
R"({"firstName":"Ned","lastName":"Simpson","children":[]})");
ASSERT_FALSE(res);
EXPECT_EQ(res.error().what(),
"Failed to parse field 'firstName': Literal does not support "
"string 'Ned'. The following strings are supported: 'Homer', "
"'Marge', 'Bart', 'Lisa', 'Maggie'.");
}
} // namespace test_literal
45 changes: 45 additions & 0 deletions tests/json/test_tagged_union_copies.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#include <gtest/gtest.h>

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

namespace test_tagged_union_copies {

struct CopyCounter {
static inline int copies = 0;

CopyCounter() = default;
CopyCounter(const CopyCounter&) { ++copies; }
CopyCounter(CopyCounter&&) noexcept = default;
CopyCounter& operator=(const CopyCounter&) {
++copies;
return *this;
}
CopyCounter& operator=(CopyCounter&&) noexcept = default;
~CopyCounter() = default;
};

struct Circle {
rfl::Literal<"Circle"> shape;
double radius;
rfl::Skip<CopyCounter> counter;
};

struct Square {
rfl::Literal<"Square"> shape;
double width;
rfl::Skip<CopyCounter> counter;
};

using Shapes = rfl::TaggedUnion<"shape", Circle, Square>;

TEST(json, test_tagged_union_copies_nothing_when_writing) {
const auto circle = Shapes(Circle{.radius = 2.0});
CopyCounter::copies = 0;
const auto json_string = rfl::json::write(circle);
EXPECT_EQ(json_string, R"({"shape":"Circle","radius":2.0})");
EXPECT_EQ(CopyCounter::copies, 0);
}

} // namespace test_tagged_union_copies
62 changes: 62 additions & 0 deletions tests/json/test_validator_copies.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#include <gtest/gtest.h>

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

namespace test_validator_copies {

struct CopyCounter {
static inline int copies = 0;

CopyCounter() = default;
CopyCounter(const CopyCounter&) { ++copies; }
CopyCounter(CopyCounter&&) noexcept = default;
CopyCounter& operator=(const CopyCounter&) {
++copies;
return *this;
}
CopyCounter& operator=(CopyCounter&&) noexcept = default;
~CopyCounter() = default;
};

struct Element {
int value;
rfl::Skip<CopyCounter> counter;
};

using Elements =
rfl::Validator<std::vector<Element>, rfl::Size<rfl::Minimum<1>>,
rfl::Size<rfl::Maximum<10>>>;

struct Container {
Elements elements;
};

TEST(json, test_validator_copies_nothing_when_reading) {
CopyCounter::copies = 0;
const auto res =
rfl::json::read<Container>(R"({"elements":[{"value":1},{"value":2}]})");
ASSERT_TRUE(res) << res.error().what();
EXPECT_EQ(res->elements->size(), 2);
EXPECT_EQ(CopyCounter::copies, 0);
}

TEST(json, test_validator_copies_nothing_when_moved_in) {
CopyCounter::copies = 0;
auto elements = std::vector<Element>(3);
const auto res = Elements::from_value(std::move(elements));
ASSERT_TRUE(res) << res.error().what();
EXPECT_EQ(CopyCounter::copies, 0);
}

TEST(json, test_validator_reports_error_when_reading) {
const auto res = rfl::json::read<Container>(R"({"elements":[]})");
ASSERT_FALSE(res);
EXPECT_EQ(res.error().what(),
"Failed to parse field 'elements': Size validation failed: Value "
"expected to be greater than or equal to 1, but got 0.");
}

} // namespace test_validator_copies
Loading