From 82003c15268ae69b1a34a15585742a2dd23937c1 Mon Sep 17 00:00:00 2001 From: sha512sum Date: Wed, 27 Dec 2023 14:41:55 +0000 Subject: [PATCH] Add Universal Serialization --- .../userver/formats/parse/try_parse.hpp | 97 ++++++ .../formats/universal/common_containers.hpp | 296 ++++++++++++++++++ .../userver/formats/universal/universal.hpp | 235 ++++++++++++++ .../userver/utils/constexpr_string.hpp | 59 ++++ .../include/userver/utils/impl/type_list.hpp | 46 +++ universal/src/formats/json/universal_test.cpp | 202 ++++++++++++ 6 files changed, 935 insertions(+) create mode 100644 universal/include/userver/formats/parse/try_parse.hpp create mode 100644 universal/include/userver/formats/universal/common_containers.hpp create mode 100644 universal/include/userver/formats/universal/universal.hpp create mode 100644 universal/include/userver/utils/constexpr_string.hpp create mode 100644 universal/include/userver/utils/impl/type_list.hpp create mode 100644 universal/src/formats/json/universal_test.cpp diff --git a/universal/include/userver/formats/parse/try_parse.hpp b/universal/include/userver/formats/parse/try_parse.hpp new file mode 100644 index 000000000000..20ea92db7ac9 --- /dev/null +++ b/universal/include/userver/formats/parse/try_parse.hpp @@ -0,0 +1,97 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +USERVER_NAMESPACE_BEGIN + +namespace formats::parse { + +namespace impl { + +template +constexpr inline bool Is(Value&& value) { + if constexpr(std::is_same_v) { + return value.IsBool(); + } else if constexpr(meta::kIsInteger) { + return (std::is_unsigned_v && sizeof(T) == sizeof(std::uint64_t)) ? value.IsUInt64() : value.IsInt64(); + } else if constexpr(std::is_convertible_v) { + return value.IsString(); + } else if constexpr(std::is_convertible_v) { + return value.IsDouble(); + } else if constexpr(meta::kIsRange) { + 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; +}; +inline constexpr utils::impl::TypeList kBaseTypes; + +} // namespace impl + + +template +constexpr inline std::enable_if_t< + utils::impl::AnyOf(utils::impl::IsSameCarried(), impl::kBaseTypes) || + meta::kIsInteger, std::optional> +TryParse(Value&& value, userver::formats::parse::To) { + if(!impl::Is(value)) { + return std::nullopt; + } + return value.template As(); +} + +template +constexpr inline std::optional> TryParse(Value&& value, userver::formats::parse::To>) { + return TryParse(std::forward(value), userver::formats::parse::To{}); +} +template +constexpr inline std::optional TryParse(Value&& value, userver::formats::parse::To) { + auto response = value.template As(); + if(impl::CheckInBounds(response, std::numeric_limits::lowest(), + std::numeric_limits::max())) { + return static_cast(response); + }; + return std::nullopt; +}; + +template +constexpr inline std::enable_if_t && !meta::kIsMap && + !std::is_same_v && + !utils::impl::AnyOf(utils::impl::IsConvertableCarried(), impl::kBaseTypes) && + !std::is_convertible_v< + T&, utils::impl::strong_typedef::StrongTypedefTag&>, + std::optional> +TryParse(Value&& from, To) { + T response; + auto inserter = std::inserter(response, response.end()); + using ValueType = meta::RangeValueType; + for(const auto& item : from) { + auto insert = TryParse(item, userver::formats::parse::To{}); + if(!insert) { + return std::nullopt; + }; + *inserter = *insert; + ++inserter; + }; + return response; +} + + + +} // namespace formats::parse + +USERVER_NAMESPACE_END diff --git a/universal/include/userver/formats/universal/common_containers.hpp b/universal/include/userver/formats/universal/common_containers.hpp new file mode 100644 index 000000000000..2d819169caa8 --- /dev/null +++ b/universal/include/userver/formats/universal/common_containers.hpp @@ -0,0 +1,296 @@ +#pragma once +#include +#include +#include +#include +#include + +USERVER_NAMESPACE_BEGIN +namespace formats::universal { +template +struct Max : public impl::Param { + inline constexpr Max(const T& value) : + impl::Param(value) {} + inline constexpr std::string Check(const T& value) const { + return value <= this->value ? "" : this->Error(value); + } + template + inline constexpr std::enable_if_t, 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 { + inline constexpr MinElements(const std::size_t& value) : + impl::Param(value) {} + template + inline constexpr std::string Check(const Value& value) const { + if(value.size() >= this->value) { + return ""; + } + return this->Error(value); + } + template + inline constexpr auto Error(const Value&) const { + return "Error"; + } +}; + +template +struct Min : public impl::Param { + inline constexpr Min(const T& value) : + impl::Param(value) {} + inline constexpr std::string Check(const T& value) const { + return value >= this->value ? "" : this->Error(value); + }; + template + inline constexpr std::enable_if_t, 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 +struct Default : public impl::EmptyCheck, public impl::Param { + inline constexpr Default(const T& value) : + impl::Param(value) {} +}; +template +static const utils::regex kRegex(Pattern); + +struct Pattern : public impl::EmptyCheck, public impl::Param { + constexpr inline Pattern(const utils::regex& regex) : + impl::Param(®ex) {} + 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 { + constexpr inline Additional(const bool& value) : + impl::Param(value) {} +}; +template <> +struct FieldConfig { + std::optional> Maximum; + std::optional> Minimum; + template + constexpr int Read(Value&& value) const { + constexpr auto name = boost::pfr::get_name(); + return value[name].template As(); + }; + template + constexpr std::optional TryRead(Value&& value) const { + constexpr auto name = boost::pfr::get_name(); + return parse::TryParse(value[name], parse::To{}); + }; + constexpr auto Write(const int& value, std::string_view fieldName, const auto&, auto& builder) const { + builder[static_cast(fieldName)] = value; + }; + inline constexpr std::string_view Check(const int&) const { + return ""; + } + +}; +template <> +struct FieldConfig> { + std::optional Pattern; + std::optional> Default; + template + constexpr std::optional Read(Value&& value) const { + constexpr auto name = boost::pfr::get_name(); + if(!value.HasMember(name)) { + return std::nullopt; + }; + return value[name].template As(); + }; + template + constexpr std::optional> TryRead(Value&& value) const { + constexpr auto name = boost::pfr::get_name(); + auto response = parse::TryParse(value[name], parse::To{}); + if(response) { + return response; + } + if(this->Default) { + return this->Default->value; + } + return std::nullopt; + } + constexpr auto Write(const std::optional& value, std::string_view fieldName, const auto&, auto& builder) const { + if(value) { + builder[static_cast(fieldName)] = *value; + }; + }; + inline constexpr std::string_view Check(const std::string&) const { + return ""; + } + +}; +template <> +struct FieldConfig { + std::optional Pattern; + template + constexpr std::string Read(Value&& value) const { + constexpr auto name = boost::pfr::get_name(); + return value[name].template As(); + }; + template + constexpr std::optional TryRead(Value&& value) const { + constexpr auto name = boost::pfr::get_name(); + return parse::TryParse(value[name], parse::To{}); + }; + constexpr auto Write(std::string_view value, std::string_view fieldName, const auto&, auto& builder) const { + builder[static_cast(fieldName)] = value; + }; + inline constexpr std::string_view Check(std::string_view) const { + return ""; + } + +}; + +template <> +struct FieldConfig> { + std::optional> Maximum; + std::optional> Minimum; + std::optional> Default; + template + constexpr std::optional Read(Value&& value) const { + constexpr auto name = boost::pfr::get_name(); + if(value.HasMember(name)) { + return value[name].template As(); + } + if(this->Default) { + return this->Default->value; + } + return std::nullopt; + } + template + constexpr std::optional> TryRead(Value&& value) const { + constexpr auto name = boost::pfr::get_name(); + auto response = parse::TryParse(value[name], parse::To{}); + if(response) { + return response; + } + if(this->Default) { + return this->Default->value; + } + return {{}}; + } + constexpr auto Write(const std::optional& value, std::string_view fieldName, const auto&, auto& builder) const { + if(value) { + builder[static_cast(fieldName)] = *value; + return; + } + if(this->Default) { + builder[static_cast(fieldName)] = this->Default->value; + } + } + + inline constexpr std::string_view Check(const std::optional&) const { + return ""; + } + +}; +template +struct FieldConfig> { + std::optional Additional; + using kType = std::unordered_map; + template + 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(); + for(const auto& [name, value2] : userver::formats::common::Items(std::forward(value))) { + auto it = std::find(fields.begin(), fields.end(), name); + if(it == fields.end()) { + response.emplace(name, value2.template As()); + } + } + return response; + } + template + inline constexpr std::optional TryRead(Value2&& value) const { + if(!this->Additional) { + throw std::runtime_error("Invalid Flags"); + } + kType response; + constexpr auto fields = boost::pfr::names_as_array(); + constexpr auto name = boost::pfr::get_name(); + for(const auto& [name2, value2] : userver::formats::common::Items(std::forward(value))) { + if(std::find(fields.begin(), fields.end(), name2) == fields.end()) { + auto New = parse::TryParse(value2, parse::To{}); + if(!New) { + return std::nullopt; + }; + response.emplace(name, *New); + } + } + 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 +struct FieldConfig> { + std::optional MinimalElements; + FieldConfig Items; + template + inline constexpr auto Read(Value&& value) const { + constexpr auto name = boost::pfr::get_name(); + std::vector response; + for(const auto& element : value[name]) { + auto New = element.template As(); + response.push_back(std::move(New)); + } + return response; + } + template + inline constexpr std::optional> TryRead(Value&& value) const { + std::vector response; + constexpr auto name = boost::pfr::get_name(); + for(const auto& element : value[name]) { + auto New = parse::TryParse(element, parse::To{}); + if(!New) { + return std::nullopt; + } + response.push_back(std::move(*New)); + } + return response; + } + inline constexpr std::string Check(const std::vector& obj) const { + std::string error; + for(const auto& element : obj) { + error += impl::UniversalCheckField(element, this->Items); + } + return error; + } + +}; +} // namespace formats::universal +USERVER_NAMESPACE_END diff --git a/universal/include/userver/formats/universal/universal.hpp b/universal/include/userver/formats/universal/universal.hpp new file mode 100644 index 000000000000..0548f33c37fb --- /dev/null +++ b/universal/include/userver/formats/universal/universal.hpp @@ -0,0 +1,235 @@ +#pragma once +#include +#include +#include +#include +#include +#include + + +USERVER_NAMESPACE_BEGIN + +namespace formats::universal { + + +template +struct FieldConfig { + constexpr inline std::string_view Check(const T&) const { + return ""; + } +}; +namespace impl { + +template +using kFieldTypeOnIndex = std::remove_reference_t(std::declval()))>; + +template +consteval std::size_t getFieldIndexByName(std::string_view fieldName) { + constexpr auto names = boost::pfr::names_as_array(); + return std::find(names.begin(), names.end(), fieldName) - names.begin(); +}; + +struct Disabled {}; + + +template +struct Param { + T value; + constexpr Param(const T& value) : + value(value) {}; +}; + +template +struct Wrapper { + static constexpr auto kValue = Value; +}; + + + +template +inline constexpr auto RemoveOptional(const std::optional& value) { + auto response = *value; + if constexpr(meta::kIsOptional) { + return RemoveOptional(response); + } else { + return response; + } +} +template +inline constexpr auto RemoveOptional(const T& value) { + return value; +} + + +template +struct HasCheck : public std::false_type {}; + +template +struct HasCheck().Check(std::declval()))>> : public std::true_type {}; + +template +inline constexpr Result add(T&&... args) { + if constexpr(sizeof...(T) != 0) { + return (args + ...); + } else { + return {}; + } +} + + + +template +inline constexpr std::string UniversalCheckField( + T&& response, + FieldConfig config) { + using FieldType = std::remove_cvref_t; + auto error = [&](std::index_sequence) -> std::string { + auto f = [&](Wrapper) -> std::string { + auto check = boost::pfr::get(config); + if constexpr(HasCheck::value) { + if constexpr(meta::kIsOptional) { + if(response && check && !check->Check(RemoveOptional(response)).empty()) { + return check->Check(RemoveOptional(response)); + } + } else { + if(check && !check->Check(response).empty()) { + return check->Check(response); + } + } + } + return ""; + }; + return add(f(Wrapper{})...); + }(std::make_index_sequence>()); + if constexpr(meta::kIsOptional>) { + if(response) { + error += config.Check(*response); + } + } else { + error += config.Check(std::forward(response)); + } + return error; +} + + +template +inline constexpr auto UniversalParseField( + T&& value, + FieldConfig> config) { + auto response = config.template Read(value); + const auto error = UniversalCheckField(response, config); + if(!error.empty()) { + throw std::runtime_error(error); + } + return response; +} + +template +inline constexpr auto UniversalTryParseField( + T&& value, + FieldConfig> config) { + auto response = config.template TryRead(value); + return [&]() -> decltype(response) { + if(UniversalCheckField(response, std::move(config)).empty()) { + return response; + } + return std::nullopt; + }(); +} + +template +inline constexpr auto UniversalSerializeField( + T&& value, + Builder& builder, + FieldConfig> config) { + UniversalCheckField(value, config); + config.template Write(std::forward(value), boost::pfr::get_name(), boost::pfr::names_as_array(), builder); +} +struct EmptyCheck { + template + inline constexpr std::string Check(Value&&) const { + return ""; + } +}; +} // namespace impl + +template +impl::Disabled kSerialization; + +template +auto kDeserialization = kSerialization; + + +template +struct SerializationConfig { + using kFieldsConfigType = decltype([](std::index_sequence){ + return std::type_identity>...>>(); + }(std::make_index_sequence>()))::type; + + public: + template + inline constexpr auto& With(FieldConfig(fieldName)>>&& fieldConfig) { + constexpr auto Index = impl::getFieldIndexByName(fieldName); + static_assert(Index != boost::pfr::tuple_size_v, "Field Not Found"); + std::get(this->fieldsConfig) = std::move(fieldConfig); + return *this; + } + constexpr SerializationConfig() : fieldsConfig({}) {}; + + template + constexpr auto Get() const { + return std::get(this->fieldsConfig); + }; + private: + kFieldsConfigType fieldsConfig; + +}; + + + +} // namespace formats::universal + + + +namespace formats::parse { + +template +inline constexpr std::enable_if_t), universal::impl::Disabled>, T> +Parse(Value&& value, To) { + return [&](std::index_sequence){ + auto config = universal::kSerialization; + return T{universal::impl::UniversalParseField(value, config.template Get())...}; + }(std::make_index_sequence>()); +} + +template +inline constexpr std::enable_if_t), universal::impl::Disabled>, std::optional> +TryParse(Value&& value, To) { + return [&](std::index_sequence) -> std::optional { + auto config = universal::kSerialization; + auto tup = std::make_tuple(universal::impl::UniversalTryParseField(value, config.template Get())...); + if((std::get(tup) && ...)) { + return T{*std::get(tup)...}; + }; + return std::nullopt; + }(std::make_index_sequence>()); +} + +} // namespace formats::parse +namespace formats::serialize { + +template +inline constexpr std::enable_if_t>), universal::impl::Disabled>, Value> +Serialize(T&& obj, To) { + using Type = std::remove_cvref_t; + return [&](std::index_sequence){ + typename Value::Builder builder; + auto config = universal::kSerialization; + (universal::impl::UniversalSerializeField(boost::pfr::get(obj), builder, config.template Get()), ...); + return builder.ExtractValue(); + }(std::make_index_sequence>()); +} + +} // namespace formats::serialize + +USERVER_NAMESPACE_END diff --git a/universal/include/userver/utils/constexpr_string.hpp b/universal/include/userver/utils/constexpr_string.hpp new file mode 100644 index 000000000000..81b946fa33e2 --- /dev/null +++ b/universal/include/userver/utils/constexpr_string.hpp @@ -0,0 +1,59 @@ +#pragma once +#include +#include +USERVER_NAMESPACE_BEGIN + +namespace utils { + +template +struct ConstexprString { + + static constexpr auto kSize = Size; + constexpr operator std::string_view() const { + return std::string_view{this->contents_.data(), Size - 1}; + }; + constexpr auto c_str() const -> const char* { + return this->contents_.data(); + }; + constexpr std::size_t size() const { + return Size - 1; + }; + constexpr const char* data() const { + return this->contents_.begin(); + }; + + constexpr operator char&() const { + return this->contents_.data(); + }; + + friend constexpr bool operator==(const ConstexprString& string, const char* other) { + return std::string_view(string) == std::string_view(other); + }; + template + friend constexpr auto& operator<<(Stream& stream, const ConstexprString& string) { + stream << static_cast(string); + return stream; + }; + + constexpr ConstexprString(const char (&str)[Size]) noexcept { + std::copy_n(str, Size, std::begin(this->contents_)); + }; + constexpr ConstexprString(std::array data) noexcept : contents_(data) {}; + template + constexpr ConstexprString operator+(const ConstexprString& other) const noexcept { + return + [&](std::index_sequence, std::index_sequence){ + return ConstexprString({this->contents_[I]..., other.contents_[I2]...}); + }(std::make_index_sequence(), std::make_index_sequence()); + }; + template + constexpr ConstexprString operator+(const char(&str)[OtherSize]) const noexcept { + return *this + ConstexprString{str}; + }; + std::array contents_; +}; +template +ConstexprString(char const (&)[n]) -> ConstexprString; +} // namespace utils +USERVER_NAMESPACE_END + diff --git a/universal/include/userver/utils/impl/type_list.hpp b/universal/include/userver/utils/impl/type_list.hpp new file mode 100644 index 000000000000..eaf71d031211 --- /dev/null +++ b/universal/include/userver/utils/impl/type_list.hpp @@ -0,0 +1,46 @@ +#pragma once +#include + + +USERVER_NAMESPACE_BEGIN + +namespace utils::impl { + +template +struct TypeList {}; + + +template +consteval auto size(TypeList) { + return sizeof...(Ts); +} + + +template