From 82381165e831d6110c70b731f9d84e0d0cc76d5a Mon Sep 17 00:00:00 2001 From: segoon Date: Thu, 28 Nov 2024 20:34:13 +0300 Subject: [PATCH] feat chaotic: more stuff for openapi parameters commit_hash:33b332173cadcb98aee398cc4d24484abff1dbf5 --- .mapping.json | 1 + .../userver/chaotic/openapi/parameters.hpp | 46 +++++-- .../chaotic/openapi/parameters_read.hpp | 86 ++++++++++++- .../chaotic/openapi/parameters_write.hpp | 113 ++++++++++++++---- .../src/chaotic/openapi/parameters_read.cpp | 37 ++++++ .../chaotic/openapi/parameters_read_test.cpp | 67 ++++++++++- .../src/chaotic/openapi/parameters_write.cpp | 21 ++-- .../chaotic/openapi/parameters_write_test.cpp | 86 +++++++++++-- 8 files changed, 398 insertions(+), 59 deletions(-) create mode 100644 chaotic/src/chaotic/openapi/parameters_read.cpp diff --git a/.mapping.json b/.mapping.json index a1d8abea7100..e6c75f87fd6c 100644 --- a/.mapping.json +++ b/.mapping.json @@ -184,6 +184,7 @@ "chaotic/src/chaotic/io/userver/utils/datetime/time_point_tz.cpp":"taxi/uservices/userver/chaotic/src/chaotic/io/userver/utils/datetime/time_point_tz.cpp", "chaotic/src/chaotic/io/userver/utils/datetime/time_point_tz_iso_basic.cpp":"taxi/uservices/userver/chaotic/src/chaotic/io/userver/utils/datetime/time_point_tz_iso_basic.cpp", "chaotic/src/chaotic/openapi/parameters.cpp":"taxi/uservices/userver/chaotic/src/chaotic/openapi/parameters.cpp", + "chaotic/src/chaotic/openapi/parameters_read.cpp":"taxi/uservices/userver/chaotic/src/chaotic/openapi/parameters_read.cpp", "chaotic/src/chaotic/openapi/parameters_read_test.cpp":"taxi/uservices/userver/chaotic/src/chaotic/openapi/parameters_read_test.cpp", "chaotic/src/chaotic/openapi/parameters_write.cpp":"taxi/uservices/userver/chaotic/src/chaotic/openapi/parameters_write.cpp", "chaotic/src/chaotic/openapi/parameters_write_test.cpp":"taxi/uservices/userver/chaotic/src/chaotic/openapi/parameters_write_test.cpp", diff --git a/chaotic/include/userver/chaotic/openapi/parameters.hpp b/chaotic/include/userver/chaotic/openapi/parameters.hpp index 512a63799d2d..365be2d34e79 100644 --- a/chaotic/include/userver/chaotic/openapi/parameters.hpp +++ b/chaotic/include/userver/chaotic/openapi/parameters.hpp @@ -1,6 +1,6 @@ #pragma once -#include +#include #include USERVER_NAMESPACE_BEGIN @@ -17,10 +17,24 @@ enum class In { using Name = const char* const; -enum class ParameterType { - kTrivial, - kArray, - // kObject, +template +inline constexpr bool kIsTrivialRawType = std::is_integral_v; + +template <> +inline constexpr bool kIsTrivialRawType = true; + +template <> +inline constexpr bool kIsTrivialRawType = true; + +template <> +inline constexpr bool kIsTrivialRawType = true; + +template +struct TrivialParameterBase { + using RawType = RawType_; + using UserType = UserType_; + + static_assert(kIsTrivialRawType); }; template @@ -28,22 +42,28 @@ struct TrivialParameter { static constexpr auto kIn = In_; static constexpr auto& kName = Name_; - using RawType = RawType_; - using UserType = UserType_; - static constexpr ParameterType Type = ParameterType::kTrivial; + using Base = TrivialParameterBase; + + static_assert(kIn != In::kQueryExplode); }; -template -struct ArrayParameter { - static constexpr auto kIn = In_; - static constexpr auto& kName = Name_; +template +struct ArrayParameterBase { static constexpr char kDelimiter = Delimiter_; + static constexpr auto kIn = In_; using RawType = std::vector; using RawItemType = RawItemType_; using UserType = std::vector; using UserItemType = UserItemType_; - static constexpr ParameterType Type = ParameterType::kArray; +}; + +template +struct ArrayParameter { + static constexpr auto& kName = Name_; + static constexpr auto kIn = In_; + + using Base = ArrayParameterBase; }; } // namespace chaotic::openapi diff --git a/chaotic/include/userver/chaotic/openapi/parameters_read.hpp b/chaotic/include/userver/chaotic/openapi/parameters_read.hpp index 0338f9fd78ad..c21911619773 100644 --- a/chaotic/include/userver/chaotic/openapi/parameters_read.hpp +++ b/chaotic/include/userver/chaotic/openapi/parameters_read.hpp @@ -1,13 +1,29 @@ #pragma once +#include #include +#include #include +#include USERVER_NAMESPACE_BEGIN namespace chaotic::openapi { +/* + * All parameters are parsed according to the following pipeline: + * + * [easy] -> str -> raw -> user + * + * Where: + * - [easy] is curl easy handler + * - str is `string` or `vector of strings` + * - raw is one of JSON Schema types (e.g. boolean, integer or string) + * - user is a type shown to the user (x-usrv-cpp-type value or same as raw) + * + */ + template auto GetParameter(std::string_view name, const server::http::HttpRequest& source) { if constexpr (In_ == In::kPath) { @@ -24,15 +40,75 @@ auto GetParameter(std::string_view name, const server::http::HttpRequest& source } } +namespace parse { +template +struct To {}; +} // namespace parse + template -auto ParseParameter(const T& raw_value) { - return raw_value; +std::enable_if_t, T> FromStr(std::string&& s, parse::To) { + return utils::FromString(s); } +std::string FromStr(std::string&& str_value, parse::To); + +bool FromStr(std::string&& str_value, parse::To); + +double FromStr(std::string&& str_value, parse::To); + +template +struct ParseParameter { + static std::string Parse(typename Parameter::RawType&& t) { + static_assert(sizeof(t) && false, "Cannot find `ParseParameter`"); + return {}; + } +}; + +template +struct ParseParameter> { + static UserType Parse(std::string&& str_value) { + auto raw_value = openapi::FromStr(std::move(str_value), parse::To()); + return Convert(std::move(raw_value), convert::To()); + } +}; + +namespace impl { + +void SplitByDelimiter(std::string_view str, char delimiter, utils::function_ref); + +} + +template +struct ParseParameter> { + static auto Parse(std::string&& str_value) { + openapi::ParseParameter> parser; + + std::vector result; + impl::SplitByDelimiter(str_value, Delimiter, [&result, &parser](std::string str) { + result.emplace_back(parser.Parse(std::move(str))); + }); + return result; + } +}; + +template +struct ParseParameter> { + static auto Parse(std::vector&& str_value) { + openapi::ParseParameter> parser; + + std::vector result; + result.reserve(str_value.size()); + for (auto&& str_item : str_value) { + result.emplace_back(parser.Parse(std::move(str_item))); + } + return result; + } +}; + template -auto ReadParameter(const server::http::HttpRequest& source) { - auto raw_value = USERVER_NAMESPACE::chaotic::openapi::GetParameter(Parameter::kName, source); - return USERVER_NAMESPACE::chaotic::openapi::ParseParameter(raw_value); +typename Parameter::Base::UserType ReadParameter(const server::http::HttpRequest& source) { + auto str_or_array_value = openapi::GetParameter(Parameter::kName, source); + return openapi::ParseParameter::Parse(std::move(str_or_array_value)); } } // namespace chaotic::openapi diff --git a/chaotic/include/userver/chaotic/openapi/parameters_write.hpp b/chaotic/include/userver/chaotic/openapi/parameters_write.hpp index a2d9a616ec20..81e16cad89c6 100644 --- a/chaotic/include/userver/chaotic/openapi/parameters_write.hpp +++ b/chaotic/include/userver/chaotic/openapi/parameters_write.hpp @@ -1,16 +1,33 @@ #pragma once #include +#include +#include #include #include #include +#include +#include #include USERVER_NAMESPACE_BEGIN namespace chaotic::openapi { +/* + * All parameters are serialized according to the following pipeline: + * + * user -> raw -> str -> [easy] + * + * Where: + * - user is a type shown to the user (x-usrv-cpp-type value or same as raw) + * - raw is one of JSON Schema types (e.g. boolean, integer or string) + * - str is `string` or `vector of strings` + * - [easy] is curl easy handler + * + */ + /// @brief curl::easy abstraction class ParameterSinkBase { public: @@ -48,52 +65,102 @@ class ParameterSinkHttpClient final : public ParameterSinkBase { void ValidatePathVariableValue(std::string_view name, std::string_view value); -template -void SetParameter(Name& name, RawType&& raw_value, ParameterSinkBase& dest) { +template +void SetParameter(Name& name, StrType&& str_value, ParameterSinkBase& dest) { + static_assert(std::is_same_v || std::is_same_v>); + if constexpr (In == In::kPath) { - USERVER_NAMESPACE::chaotic::openapi::ValidatePathVariableValue(name, raw_value); - dest.SetPath(name, std::forward(raw_value)); + USERVER_NAMESPACE::chaotic::openapi::ValidatePathVariableValue(name, str_value); + dest.SetPath(name, std::forward(str_value)); } else if constexpr (In == In::kCookie) { - dest.SetCookie(name, std::forward(raw_value)); + dest.SetCookie(name, std::forward(str_value)); } else if constexpr (In == In::kHeader) { - dest.SetHeader(name, std::forward(raw_value)); + dest.SetHeader(name, std::forward(str_value)); } else if constexpr (In == In::kQuery) { - dest.SetQuery(name, std::forward(raw_value)); + dest.SetQuery(name, std::forward(str_value)); } else { static_assert(In == In::kQueryExplode, "Unknown 'In'"); - dest.SetMultiQuery(name, std::forward(raw_value)); + dest.SetMultiQuery(name, std::forward(str_value)); } } template -std::enable_if_t, std::string> ToRawParameter(T s) { +std::enable_if_t, std::string> ToStrParameter(T s) noexcept { return std::to_string(s); } template -std::enable_if_t, std::string> ToRawParameter(T s) { +std::enable_if_t, std::string> ToStrParameter(T s) noexcept { return ToString(s); } -std::string ToRawParameter(std::string&& s); +std::string ToStrParameter(bool value) noexcept; -std::vector ToRawParameter(std::vector&& s); +std::string ToStrParameter(double value) noexcept; -template -auto SerializeParameter(const typename Parameter::UserType& value) { - if constexpr (Parameter::Type == ParameterType::kTrivial) { - // TODO: Convert() - return USERVER_NAMESPACE::chaotic::openapi::ToRawParameter(typename Parameter::UserType{value}); - } else if constexpr (Parameter::Type == ParameterType::kArray) { - // TODO: Convert() - return USERVER_NAMESPACE::utils::text::Join(value, std::string(1, Parameter::kDelimiter)); +std::string ToStrParameter(std::string&& s) noexcept; + +std::vector ToStrParameter(std::vector&& s) noexcept; + +template +std::enable_if_t, std::string>, std::vector> ToStrParameter( + T&& collection +) noexcept { + std::vector result; + result.reserve(collection.size()); + for (auto&& item : collection) { + result.emplace_back(ToStrParameter(item)); } + return result; } template -void WriteParameter(const typename Parameter::UserType& value, ParameterSinkBase& dest) { - auto raw_value = USERVER_NAMESPACE::chaotic::openapi::SerializeParameter(value); - USERVER_NAMESPACE::chaotic::openapi::SetParameter(Parameter::kName, std::move(raw_value), dest); +struct SerializeParameter { + static std::string Serialize(const typename Parameter::UserType&) { + static_assert(sizeof(Parameter) && false, "No SerializeParameter specialization found for `Parameter`"); + return {}; + } +}; + +template +struct SerializeParameter> { + static auto Serialize(const T& value) { return ToStrParameter(T{value}); } +}; + +template +struct SerializeParameter> { + static auto Serialize(const UserType& value) { return ToStrParameter(Convert(value, convert::To{})); } +}; + +template +struct SerializeParameter> { + static std::vector Serialize(const std::vector& collection) { + // Note: ignore Delimiter + std::vector result; + result.reserve(collection.size()); + for (const auto& item : collection) { + result.emplace_back(SerializeParameter>().Serialize(item)); + } + return result; + } +}; + +template +struct SerializeParameter> { + static_assert(In_ != In::kQueryExplode); + + std::string operator()(const UserType& item) const { return ToStrParameter(Convert(item, convert::To{})); } + + static std::string Serialize(const std::vector& value) { + using ProjectingView = utils::impl::ProjectingView, SerializeParameter>; + return fmt::to_string(fmt::join(ProjectingView{value, SerializeParameter{}}, std::string(1, Delimiter))); + } +}; + +template +void WriteParameter(const typename Parameter::Base::UserType& value, ParameterSinkBase& dest) { + auto str_value = openapi::SerializeParameter::Serialize(value); + openapi::SetParameter(Parameter::kName, std::move(str_value), dest); } } // namespace chaotic::openapi diff --git a/chaotic/src/chaotic/openapi/parameters_read.cpp b/chaotic/src/chaotic/openapi/parameters_read.cpp new file mode 100644 index 000000000000..e62bf4c10c02 --- /dev/null +++ b/chaotic/src/chaotic/openapi/parameters_read.cpp @@ -0,0 +1,37 @@ +#include + +#include + +#include + +USERVER_NAMESPACE_BEGIN + +namespace chaotic::openapi { + +namespace impl { + +void SplitByDelimiter(std::string_view str, char delimiter, utils::function_ref func) { + for (auto it = boost::algorithm::make_split_iterator(str, boost::token_finder([delimiter](char c) { + return c == delimiter; + })); + it != decltype(it){}; + ++it) { + func(std::string{it->begin(), it->end()}); + } +} + +} // namespace impl + +std::string FromStr(std::string&& str_value, parse::To) { return std::move(str_value); } + +bool FromStr(std::string&& str_value, parse::To) { + if (str_value == "true") return true; + if (str_value == "false") return false; + throw std::runtime_error("Unknown bool value: " + str_value); +} + +double FromStr(std::string&& str_value, parse::To) { return utils::FromString(str_value); } + +} // namespace chaotic::openapi + +USERVER_NAMESPACE_END diff --git a/chaotic/src/chaotic/openapi/parameters_read_test.cpp b/chaotic/src/chaotic/openapi/parameters_read_test.cpp index 8b8caacd8baf..c11b4e162517 100644 --- a/chaotic/src/chaotic/openapi/parameters_read_test.cpp +++ b/chaotic/src/chaotic/openapi/parameters_read_test.cpp @@ -1,5 +1,6 @@ #include +#include #include #include #include @@ -20,7 +21,7 @@ UTEST(OpenapiParametersRead, SourceHttpRequest) { static constexpr co::Name kName{"foo"}; - auto query_multi = co::ReadParameter>(source); + auto query_multi = co::ReadParameter>(source); auto query = co::ReadParameter>(source); auto path = co::ReadParameter>(source); auto header = co::ReadParameter>(source); @@ -33,4 +34,68 @@ UTEST(OpenapiParametersRead, SourceHttpRequest) { EXPECT_EQ(cookie, "cookie"); } +UTEST(OpenapiParametersRead, TypeBoolean) { + auto request = server::http::HttpRequestBuilder().AddRequestArg("foo", "true").Build(); + static constexpr co::Name kName{"foo"}; + + bool foo{co::ReadParameter>(*request)}; + EXPECT_EQ(foo, true); +} + +UTEST(OpenapiParametersRead, TypeDouble) { + auto request = server::http::HttpRequestBuilder().AddRequestArg("foo", "12.2").Build(); + static constexpr co::Name kName{"foo"}; + + double foo{co::ReadParameter>(*request)}; + EXPECT_EQ(foo, 12.2); +} + +UTEST(OpenapiParametersRead, UserType) { + auto request = server::http::HttpRequestBuilder().AddRequestArg("foo", "12.2").Build(); + static constexpr co::Name kName{"foo"}; + + using Decimal = decimal64::Decimal<10>; + Decimal foo{co::ReadParameter>(*request)}; + EXPECT_EQ(foo, Decimal{"12.2"}); +} + +UTEST(OpenapiParametersRead, TypeInt) { + auto request = server::http::HttpRequestBuilder().AddRequestArg("foo", "12").Build(); + static constexpr co::Name kName{"foo"}; + + int foo{co::ReadParameter>(*request)}; + EXPECT_EQ(foo, 12); +} + +UTEST(OpenapiParametersRead, TypeArrayOfString) { + auto request = server::http::HttpRequestBuilder().AddRequestArg("foo", "bar,baz").Build(); + static constexpr co::Name kName{"foo"}; + + std::vector foo{ + co::ReadParameter>(*request)}; + EXPECT_EQ(foo, (std::vector{"bar", "baz"})); +} + +UTEST(OpenapiParametersRead, TypeArrayOfUser) { + auto request = server::http::HttpRequestBuilder().AddRequestArg("foo", "1.2,3.4").Build(); + static constexpr co::Name kName{"foo"}; + + using Decimal = decimal64::Decimal<10>; + std::vector foo{ + co::ReadParameter>(*request)}; + EXPECT_EQ(foo, (std::vector{Decimal{"1.2"}, Decimal{"3.4"}})); +} + +UTEST(OpenapiParametersRead, TypeStringExplode) { + auto request = server::http::HttpRequestBuilder() + .AddRequestArg("foo", "1") + .AddRequestArg("foo", "2") + .AddRequestArg("foo", "3") + .Build(); + static constexpr co::Name kName{"foo"}; + + std::vector foo{co::ReadParameter>(*request)}; + EXPECT_EQ(foo, (std::vector{1, 2, 3})); +} + USERVER_NAMESPACE_END diff --git a/chaotic/src/chaotic/openapi/parameters_write.cpp b/chaotic/src/chaotic/openapi/parameters_write.cpp index bd96b78c26a4..2d36488072be 100644 --- a/chaotic/src/chaotic/openapi/parameters_write.cpp +++ b/chaotic/src/chaotic/openapi/parameters_write.cpp @@ -9,17 +9,18 @@ namespace chaotic::openapi { ParameterSinkHttpClient::ParameterSinkHttpClient(clients::http::Request& request, std::string&& url_pattern) : url_pattern_(std::move(url_pattern)), request_(request) {} -void ParameterSinkHttpClient::SetCookie(std::string_view name, std::string&& value) { cookies_.emplace(name, value); } +void ParameterSinkHttpClient::SetCookie(std::string_view name, std::string&& value) { + cookies_.emplace(name, std::move(value)); +} -void ParameterSinkHttpClient::SetHeader(std::string_view name, std::string&& value) { headers_.emplace(name, value); } +void ParameterSinkHttpClient::SetHeader(std::string_view name, std::string&& value) { + headers_.emplace(name, std::move(value)); +} void ParameterSinkHttpClient::SetPath(Name& name, std::string&& value) { // Note: passing tmp value to fmt::arg() is OK and not a UAF // since fmt::dynamic_format_arg_store copies the data into itself. - - // Note2: an ugly std::string{}.c_str() is needed because - // fmt::arg() wants char* :-( - path_vars_.push_back(fmt::arg(name, value)); + path_vars_.push_back(fmt::arg(name, std::move(value))); } void ParameterSinkHttpClient::SetQuery(std::string_view name, std::string&& value) { @@ -38,9 +39,13 @@ void ParameterSinkHttpClient::Flush() { request_.cookies(std::move(cookies_)); } -std::string ToRawParameter(std::string&& s) { return s; } +std::string ToStrParameter(bool value) noexcept { return value ? "true" : "false"; } + +std::string ToStrParameter(double value) noexcept { return fmt::to_string(value); } + +std::string ToStrParameter(std::string&& s) noexcept { return std::move(s); } -std::vector ToRawParameter(std::vector&& s) { return s; } +std::vector ToStrParameter(std::vector&& s) noexcept { return std::move(s); } void ValidatePathVariableValue(std::string_view name, std::string_view value) { if (value.find('/') != std::string::npos || value.find('?') != std::string::npos) { diff --git a/chaotic/src/chaotic/openapi/parameters_write_test.cpp b/chaotic/src/chaotic/openapi/parameters_write_test.cpp index 0b4152fe5f05..31435d746474 100644 --- a/chaotic/src/chaotic/openapi/parameters_write_test.cpp +++ b/chaotic/src/chaotic/openapi/parameters_write_test.cpp @@ -2,6 +2,7 @@ #include +#include #include #include #include @@ -67,12 +68,33 @@ UTEST(OpenapiParameters, QueryExplode) { ParameterSinkMock sink; EXPECT_CALL(sink, SetMultiQuery("test", (std::vector{"foo", "bar"}))); - co::WriteParameter>>( + co::WriteParameter>( std::vector{"foo", "bar"}, sink ); } -UTEST(OpenapiParameters, QueryArray) { +UTEST(OpenapiParameters, QueryExplodeInteger) { + static constexpr co::Name kName{"test"}; + + ParameterSinkMock sink; + EXPECT_CALL(sink, SetMultiQuery("test", (std::vector{"1", "2"}))); + + co::WriteParameter>(std::vector{1, 2}, sink); +} + +UTEST(OpenapiParameters, QueryExplodeUser) { + static constexpr co::Name kName{"test"}; + + ParameterSinkMock sink; + EXPECT_CALL(sink, SetMultiQuery("test", (std::vector{"1.2", "3.4"}))); + + using Decimal = decimal64::Decimal<10>; + co::WriteParameter>( + std::vector{Decimal{"1.2"}, Decimal{"3.4"}}, sink + ); +} + +UTEST(OpenapiParameters, CookieArray) { static constexpr co::Name kName{"test"}; ParameterSinkMock sink; @@ -81,18 +103,64 @@ UTEST(OpenapiParameters, QueryArray) { co::WriteParameter>({"foo", "bar"}, sink); } -enum class Enum { - kValue, -}; -std::string ToString(Enum) { return "value"; } +UTEST(OpenapiParameters, QueryArrayOfInteger) { + static constexpr co::Name kName{"test"}; + + ParameterSinkMock sink; + EXPECT_CALL(sink, SetQuery("test", std::string{"1,2"})); + + co::WriteParameter>({1, 2}, sink); +} + +UTEST(OpenapiParameters, QueryArrayOfUserTypes) { + static constexpr co::Name kName{"test"}; + + ParameterSinkMock sink; + EXPECT_CALL(sink, SetQuery("test", std::string{"1.1,2.2"})); + + using Decimal = decimal64::Decimal<10>; + co::WriteParameter>( + {Decimal{"1.1"}, Decimal{"2.2"}}, sink + ); +} UTEST(OpenapiParameters, UserType) { static constexpr co::Name kName{"test"}; ParameterSinkMock sink; - EXPECT_CALL(sink, SetCookie("test", std::string{"value"})); + EXPECT_CALL(sink, SetCookie("test", std::string{"1.1"})); + + using Decimal = decimal64::Decimal<10>; + co::WriteParameter>(Decimal{"1.1"}, sink); +} + +UTEST(OpenapiParameters, TypeBoolean) { + static constexpr co::Name kName{"test"}; + + ParameterSinkMock sink; + EXPECT_CALL(sink, SetCookie("test", std::string{"true"})); - co::WriteParameter>(Enum::kValue, sink); + bool bool_var = true; + co::WriteParameter>(bool_var, sink); +} + +UTEST(OpenapiParameters, TypeDouble) { + static constexpr co::Name kName{"test"}; + + ParameterSinkMock sink; + EXPECT_CALL(sink, SetCookie("test", std::string{"2.1"})); + + double double_var = 2.1; + co::WriteParameter>(double_var, sink); +} + +UTEST(OpenapiParameters, TypeInt) { + static constexpr co::Name kName{"test"}; + + ParameterSinkMock sink; + EXPECT_CALL(sink, SetCookie("test", std::string{"1"})); + int int_var = 1; + co::WriteParameter>(int_var, sink); } UTEST(OpenapiParameters, SinkHttpClient) { @@ -126,7 +194,7 @@ UTEST(OpenapiParameters, SinkHttpClient) { static constexpr co::Name kVar2{ "var2", }; - co::WriteParameter>>( + co::WriteParameter>( std::vector{"foo", "bar"}, sink ); co::WriteParameter>("foo1", sink);