Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
97 changes: 97 additions & 0 deletions universal/include/userver/formats/parse/try_parse.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#pragma once
#include <type_traits>
#include <string>
#include <cstdint>
#include <limits>
#include <userver/formats/parse/to.hpp>
#include <userver/utils/meta.hpp>
#include <userver/utils/impl/type_list.hpp>
#include <userver/formats/common/meta.hpp>
#include <userver/formats/parse/common_containers.hpp>


USERVER_NAMESPACE_BEGIN

namespace formats::parse {

namespace impl {

template <typename T, typename Value>
constexpr inline bool Is(Value&& value) {
if constexpr(std::is_same_v<T, bool>) {
return value.IsBool();
} else if constexpr(meta::kIsInteger<T>) {
return (std::is_unsigned_v<T> && sizeof(T) == sizeof(std::uint64_t)) ? value.IsUInt64() : value.IsInt64();
} else if constexpr(std::is_convertible_v<T, std::string>) {
return value.IsString();
} else if constexpr(std::is_convertible_v<T, double>) {
return value.IsDouble();
} else if constexpr(meta::kIsRange<T>) {
return value.IsArray();
} else {
return value.IsObject();
}
}
bool CheckInBounds(const auto& x, const auto& min, const auto& max) {
if (x < min || x > max) {
return false;
};
return true;
Comment on lines +19 to +22
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can simplified. The other changes may (or may not) render this comment obsolete, though

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return x >= min && x <= max

};
inline constexpr utils::impl::TypeList<bool, double, std::string> kBaseTypes;

} // namespace impl


template <typename T, typename Value>
constexpr inline std::enable_if_t<
utils::impl::AnyOf(utils::impl::IsSameCarried<T>(), impl::kBaseTypes) ||
meta::kIsInteger<T>, std::optional<T>>
TryParse(Value&& value, userver::formats::parse::To<T>) {
if(!impl::Is<T>(value)) {
return std::nullopt;
}
return value.template As<T>();
}

template <typename T, typename Value>
constexpr inline std::optional<std::optional<T>> TryParse(Value&& value, userver::formats::parse::To<std::optional<T>>) {
return TryParse(std::forward<Value>(value), userver::formats::parse::To<T>{});
}
template <typename Value>
constexpr inline std::optional<float> TryParse(Value&& value, userver::formats::parse::To<float>) {
auto response = value.template As<double>();
if(impl::CheckInBounds(response, std::numeric_limits<float>::lowest(),
std::numeric_limits<float>::max())) {
return static_cast<float>(response);
};
return std::nullopt;
};

template <typename T, typename Value>
constexpr inline std::enable_if_t<meta::kIsRange<T> && !meta::kIsMap<T> &&
!std::is_same_v<T, boost::uuids::uuid> &&
!utils::impl::AnyOf(utils::impl::IsConvertableCarried<T>(), impl::kBaseTypes) &&
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not be needed if a separate std::string overload is split off

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, TryParse for generic ranges is fine, it's just that this specific line is not needed anymore

!std::is_convertible_v<
T&, utils::impl::strong_typedef::StrongTypedefTag&>,
std::optional<T>>
TryParse(Value&& from, To<T>) {
T response;
auto inserter = std::inserter(response, response.end());
using ValueType = meta::RangeValueType<T>;
for(const auto& item : from) {
auto insert = TryParse(item, userver::formats::parse::To<ValueType>{});
if(!insert) {
return std::nullopt;
};
*inserter = *insert;
++inserter;
};
return response;
}



} // namespace formats::parse

USERVER_NAMESPACE_END
296 changes: 296 additions & 0 deletions universal/include/userver/formats/universal/common_containers.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,296 @@
#pragma once
#include <userver/formats/universal/universal.hpp>
#include <userver/formats/common/items.hpp>
#include <userver/formats/parse/try_parse.hpp>
#include <format>
#include <userver/utils/regex.hpp>

USERVER_NAMESPACE_BEGIN
namespace formats::universal {
template <typename T>
struct Max : public impl::Param<T> {
inline constexpr Max(const T& value) :
impl::Param<T>(value) {}
inline constexpr std::string Check(const T& value) const {
return value <= this->value ? "" : this->Error(value);
}
template <typename Value>
inline constexpr std::enable_if_t<meta::kIsRange<Value>, std::string>
Check(const Value& value) const {
for(const auto& element : value) {
if(element > this->value) {
return this->Error(element);
}
}
return "";
}
inline constexpr std::string Error(const T& value) const {
return std::format("{} > {}", value, this->value);
}
};

struct MinElements : public impl::Param<std::size_t> {
inline constexpr MinElements(const std::size_t& value) :
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Parameters of guaranteed std::is_arithmetic types (as well as std::string_view) should be passed by value, also bool below, etc.

impl::Param<std::size_t>(value) {}
template <typename Value>
inline constexpr std::string Check(const Value& value) const {
if(value.size() >= this->value) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: std::size(value)

return "";
}
return this->Error(value);
}
template <typename Value>
inline constexpr auto Error(const Value&) const {
return "Error";
}
};

template <typename T>
struct Min : public impl::Param<T> {
inline constexpr Min(const T& value) :
impl::Param<T>(value) {}
inline constexpr std::string Check(const T& value) const {
return value >= this->value ? "" : this->Error(value);
};
template <typename Value>
inline constexpr std::enable_if_t<meta::kIsRange<Value>, std::string>
Check(const Value& value) const {
for(const auto& element : value) {
if(element < this->value) {
return this->Error(element);
}
}
return "";
}
inline constexpr auto Error(const T& value) const {
return std::format("{} < {}", value, this->value);
};
};
template <typename T>
struct Default : public impl::EmptyCheck, public impl::Param<T> {
inline constexpr Default(const T& value) :
impl::Param<T>(value) {}
};
template <utils::ConstexprString Pattern>
static const utils::regex kRegex(Pattern);

struct Pattern : public impl::EmptyCheck, public impl::Param<const utils::regex*> {
constexpr inline Pattern(const utils::regex& regex) :
impl::Param<const utils::regex*>(&regex) {}
constexpr inline std::string Check(std::string_view str) const {
return utils::regex_match(str, *this->value) ? "" : this->Error(str);
}
constexpr inline std::string Error(std::string_view) const {
return "Error";
}
};
struct Additional : public impl::EmptyCheck, public impl::Param<bool> {
constexpr inline Additional(const bool& value) :
impl::Param<bool>(value) {}
};
template <>
struct FieldConfig<int> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should not really be int, rather this specialization should be for all kIsInteger || is_floating_point

Also, it should not really be std::vector and std::unordered_map, but meta::* stuff, just like constraints of Parse from parse/common_containers.hpp

requires? There is also a typename Enable technique from fmt

std::optional<Max<int>> Maximum;
std::optional<Min<int>> Minimum;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sometimes it's desired to have a JSON field with a C++-reserved name, e.g. "default". In this case we should choose an alternative field name, e.g. default_value, and override the JSON name for the field. For this we need std::optional<std::string_view> Name for every single-field FieldConfig. Not applicable to maps with their Additional

template <typename MainClass, auto I, typename Value>
constexpr int Read(Value&& value) const {
constexpr auto name = boost::pfr::get_name<I, MainClass>();
return value[name].template As<int>();
};
template <typename MainClass, auto I, typename Value>
constexpr std::optional<int> TryRead(Value&& value) const {
constexpr auto name = boost::pfr::get_name<I, MainClass>();
return parse::TryParse(value[name], parse::To<int>{});
};
constexpr auto Write(const int& value, std::string_view fieldName, const auto&, auto& builder) const {
builder[static_cast<std::string>(fieldName)] = value;
};
inline constexpr std::string_view Check(const int&) const {
return "";
}

};
template <>
struct FieldConfig<std::optional<std::string>> {
std::optional<Pattern> Pattern;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's also add an equivalent of OpenAPI's minLength, maxLength. According to the spec, they should check the number of Unicode code points. text_light.hpp should have something for this

std::optional<Default<std::string>> Default;
template <typename MainClass, auto I, typename Value>
constexpr std::optional<std::string> Read(Value&& value) const {
constexpr auto name = boost::pfr::get_name<I, MainClass>();
if(!value.HasMember(name)) {
return std::nullopt;
};
return value[name].template As<std::string>();
};
template <typename MainClass, auto I, typename Value>
constexpr std::optional<std::optional<std::string>> TryRead(Value&& value) const {
constexpr auto name = boost::pfr::get_name<I, MainClass>();
auto response = parse::TryParse(value[name], parse::To<std::string>{});
if(response) {
return response;
}
if(this->Default) {
return this->Default->value;
}
return std::nullopt;
}
constexpr auto Write(const std::optional<std::string>& value, std::string_view fieldName, const auto&, auto& builder) const {
if(value) {
builder[static_cast<std::string>(fieldName)] = *value;
};
};
inline constexpr std::string_view Check(const std::string&) const {
return "";
}

};
template <>
struct FieldConfig<std::string> {
std::optional<Pattern> Pattern;
template <typename MainClass, auto I, typename Value>
constexpr std::string Read(Value&& value) const {
constexpr auto name = boost::pfr::get_name<I, MainClass>();
return value[name].template As<std::string>();
};
template <typename MainClass, auto I, typename Value>
constexpr std::optional<std::string> TryRead(Value&& value) const {
constexpr auto name = boost::pfr::get_name<I, MainClass>();
return parse::TryParse(value[name], parse::To<std::string>{});
};
constexpr auto Write(std::string_view value, std::string_view fieldName, const auto&, auto& builder) const {
builder[static_cast<std::string>(fieldName)] = value;
};
inline constexpr std::string_view Check(std::string_view) const {
return "";
}

};

template <>
struct FieldConfig<std::optional<int>> {
std::optional<Max<int>> Maximum;
std::optional<Min<int>> Minimum;
std::optional<Default<int>> Default;
template <typename MainClass, auto I, typename Value>
constexpr std::optional<int> Read(Value&& value) const {
constexpr auto name = boost::pfr::get_name<I, MainClass>();
if(value.HasMember(name)) {
return value[name].template As<int>();
}
if(this->Default) {
return this->Default->value;
}
return std::nullopt;
}
template <typename MainClass, auto I, typename Value>
constexpr std::optional<std::optional<int>> TryRead(Value&& value) const {
constexpr auto name = boost::pfr::get_name<I, MainClass>();
auto response = parse::TryParse(value[name], parse::To<int>{});
if(response) {
return response;
}
if(this->Default) {
return this->Default->value;
}
return {{}};
}
constexpr auto Write(const std::optional<int>& value, std::string_view fieldName, const auto&, auto& builder) const {
if(value) {
builder[static_cast<std::string>(fieldName)] = *value;
return;
}
if(this->Default) {
builder[static_cast<std::string>(fieldName)] = this->Default->value;
}
}

inline constexpr std::string_view Check(const std::optional<int>&) const {
return "";
}

};
template <typename Value>
struct FieldConfig<std::unordered_map<std::string, Value>> {
std::optional<Additional> Additional;
using kType = std::unordered_map<std::string, Value>;
template <typename MainClass, auto I, typename Value2>
inline constexpr kType Read(Value2&& value) const {
if(!this->Additional) {
throw std::runtime_error("Invalid Flags");
}
kType response;
constexpr auto fields = boost::pfr::names_as_array<MainClass>();
for(const auto& [name, value2] : userver::formats::common::Items(std::forward<Value2>(value))) {
auto it = std::find(fields.begin(), fields.end(), name);
if(it == fields.end()) {
response.emplace(name, value2.template As<Value>());
}
}
return response;
}
template <typename MainClass, auto I, typename Value2>
inline constexpr std::optional<kType> TryRead(Value2&& value) const {
if(!this->Additional) {
throw std::runtime_error("Invalid Flags");
}
kType response;
constexpr auto fields = boost::pfr::names_as_array<MainClass>();
constexpr auto name = boost::pfr::get_name<I, MainClass>();
for(const auto& [name2, value2] : userver::formats::common::Items(std::forward<Value2>(value))) {
if(std::find(fields.begin(), fields.end(), name2) == fields.end()) {
auto New = parse::TryParse(value2, parse::To<Value>{});
Copy link
Member

@Anton3 Anton3 Dec 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use ADL-found TryParse, without parse::, to allow customization for user types

if(!New) {
return std::nullopt;
};
response.emplace(name, *New);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move

}
}
return response;
}
inline constexpr std::string_view Check(const kType&) const {
return "";
}
constexpr auto Write(const kType& value, std::string_view, const auto&, auto& builder) const {
for(const auto& [name, value2] : value) {
builder[name] = value2;
};
};
};
template <typename Element>
struct FieldConfig<std::vector<Element>> {
std::optional<MinElements> MinimalElements;
FieldConfig<Element> Items;
template <typename MainClass, auto I, typename Value>
inline constexpr auto Read(Value&& value) const {
constexpr auto name = boost::pfr::get_name<I, MainClass>();
std::vector<Element> response;
for(const auto& element : value[name]) {
auto New = element.template As<Element>();
response.push_back(std::move(New));
}
return response;
}
template <typename MainClass, auto I, typename Value>
inline constexpr std::optional<std::vector<Element>> TryRead(Value&& value) const {
std::vector<Element> response;
constexpr auto name = boost::pfr::get_name<I, MainClass>();
for(const auto& element : value[name]) {
auto New = parse::TryParse(element, parse::To<Element>{});
if(!New) {
return std::nullopt;
}
response.push_back(std::move(*New));
}
return response;
}
inline constexpr std::string Check(const std::vector<Element>& obj) const {
std::string error;
for(const auto& element : obj) {
error += impl::UniversalCheckField(element, this->Items);
}
return error;
}

};
} // namespace formats::universal
USERVER_NAMESPACE_END
Loading