userver: userver/easy.hpp Source File
Loading...
Searching...
No Matches
easy.hpp
Go to the documentation of this file.
1#pragma once
2
3/// @file userver/easy.hpp
4/// @brief Headers of an library for `easy` prototyping
5
6#include <functional>
7#include <string>
8#include <string_view>
9#include <type_traits>
10
11#include <userver/clients/http/client.hpp>
12#include <userver/components/component_base.hpp>
13#include <userver/components/component_list.hpp>
14#include <userver/formats/json.hpp>
15#include <userver/http/content_type.hpp>
16#include <userver/server/handlers/exceptions.hpp> // out of the box support for server::handlers::ClientError
17#include <userver/server/http/http_request.hpp>
18#include <userver/server/request/request_context.hpp>
19#include <userver/utils/meta_light.hpp>
20
21#include <userver/storages/postgres/cluster.hpp>
22#include <userver/storages/query.hpp>
23
24USERVER_NAMESPACE_BEGIN
25
26/// @brief Top namespace for `easy` library
27namespace easy {
28
29namespace impl {
30
31class DependenciesBase : public components::ComponentBase {
32public:
33 static constexpr std::string_view kName = "easy-dependencies";
34 using components::ComponentBase::ComponentBase;
35 ~DependenciesBase() override;
36};
37
38template <class T>
39struct FirstFunctionArgument;
40
41template <class Return, class First, class... Args>
42struct FirstFunctionArgument<Return(First, Args...) noexcept> {
43 using type = First;
44};
45
46template <class Return, class First, class... Args>
47struct FirstFunctionArgument<Return(First, Args...)> {
48 using type = First;
49};
50
51template <class Return, class Class, class First, class... Args>
52struct FirstFunctionArgument<Return (Class::*)(First, Args...)> {
53 using type = First;
54};
55
56template <class Return, class Class, class First, class... Args>
57struct FirstFunctionArgument<Return (Class::*)(First, Args...) const> {
58 using type = First;
59};
60
61template <class T>
62struct FirstFunctionArgument : FirstFunctionArgument<decltype(&std::decay_t<T>::operator())> {};
63
64template <typename T>
65using FromJsonStringDetector = decltype(FromJsonString(std::string_view{}, formats::parse::To<T>{}));
66
67template <typename T>
68T ParseFromJsonString(std::string_view json) {
69 if constexpr (std::is_same_v<meta::DetectedType<FromJsonStringDetector, T>, T>) {
70 return FromJsonString(json, formats::parse::To<T>{});
71 } else {
72 return formats::json::FromString(json).As<T>();
73 }
74}
75
76} // namespace impl
77
78/// @ingroup userver_components
79///
80/// @brief Factory component for the dependencies from easy library.
81///
82/// This component can be registered in the component list and used by any client. For example:
83///
84/// @snippet libraries/easy/samples/6_pg_service_template_no_http_with/src/main.cpp main
85template <class Dependencies>
86class DependenciesComponent : public impl::DependenciesBase {
87public:
88 /// @ingroup userver_component_names
89 /// @brief The default name of easy::DependenciesComponent
90 static constexpr std::string_view kName = "easy-dependencies";
91
92 DependenciesComponent(const components::ComponentConfig& config, const components::ComponentContext& context)
93 : DependenciesBase(config, context),
94 dependencies_(context)
95 {}
96
97 Dependencies GetDependencies() const { return dependencies_; }
98
99private:
100 Dependencies dependencies_;
101};
102
103/// @brief easy::HttpWith like class with erased dependencies information that should be used only in dependency
104/// registration functions; use easy::HttpWith if not making a new dependency class.
105class HttpBase final {
106public:
107 struct Callback {
108 std::function<std::string(const server::http::HttpRequest&, const impl::DependenciesBase&)> function;
109 std::optional<http::ContentType> content_type;
110 };
111
112 /// Sets the default Content-Type header for all the routes
113 void DefaultContentType(http::ContentType content_type);
114
115 /// Register an HTTP handler by `path` that supports the `methods` HTTP methods
116 void Route(std::string_view path, Callback&& func, std::initializer_list<server::http::HttpMethod> methods);
117
118 /// Append a component to the component list of a service
119 template <class Component>
120 bool TryAddComponent(std::string_view name, std::string_view config) {
121 if (component_list_.Contains(name)) {
122 return false;
123 }
124
125 component_list_.Append<Component>(name);
126 AddComponentConfig(name, config);
127 return true;
128 }
129
130 template <class Component>
131 bool TryAddComponent(std::string_view name) {
132 if (component_list_.Contains(name)) {
133 return false;
134 }
135
136 component_list_.Append<Component>(name);
137 return true;
138 }
139
140 /// Stores the schema for further retrieval from GetDbSchema()
141 void DbSchema(std::string_view schema);
142
143 /// @returns the \b last schema that was provided to the easy::HttpWith or easy::HttpBase
144 static const std::string& GetDbSchema() noexcept;
145
146 /// Set the HTTP server listen port, default is 8080.
147 void Port(std::uint16_t port);
148
149 /// Set the logging level for the service
150 void LogLevel(logging::Level level);
151
152private:
153 template <class>
154 friend class HttpWith;
155
156 void AddComponentConfig(std::string_view name, std::string_view config);
157
158 HttpBase(int argc, const char* const argv[]);
159 ~HttpBase();
160
161 class Handle;
162
163 const int argc_;
164 const char* const* argv_;
165 std::string static_config_;
166 components::ComponentList component_list_;
167
168 std::uint16_t port_ = 8080;
169 logging::Level level_ = logging::Level::kDebug;
170};
171
172/// Class that combines dependencies passed to HttpWith into a single type, that is passed to callbacks.
173///
174/// @see @ref scripts/docs/en/userver/libraries/easy.md
175template <class... Dependency>
176class Dependencies final : public Dependency... {
177public:
178 explicit Dependencies(const components::ComponentContext& context) : Dependency{context}... {}
179
180 static void RegisterOn(HttpBase& app) { (Dependency::RegisterOn(app), ...); }
181};
182
183/// @brief Class for describing the service functionality in simple declarative way that generates static configs,
184/// applies schemas.
185///
186/// @see @ref scripts/docs/en/userver/libraries/easy.md
187template <class Dependency = Dependencies<>>
188class HttpWith final {
189public:
190 /// Helper class that can store any callback of the following signatures:
191 ///
192 /// * formats::json::Value(formats::json::Value, const Dependency&)
193 /// * formats::json::Value(formats::json::Value)
194 /// * formats::json::Value(const HttpRequest&, const Dependency&)
195 /// * std::string(const HttpRequest&, const Dependency&)
196 /// * formats::json::Value(const HttpRequest&)
197 /// * std::string(const HttpRequest&)
198 /// * JsonSerializableStructure(JsonParseableStructure, const Dependency&)
199 /// * JsonSerializableStructure(JsonParseableStructure)
200 ///
201 /// If callback returns formats::json::Value or accepts a JSON parsable structure then the default content type
202 /// is set to `application/json`.
203 class Callback final {
204 public:
205 template <class Function>
206 Callback(Function func);
207
208 HttpBase::Callback Extract() && noexcept { return std::move(callback_); }
209
210 private:
211 static Dependency GetDependencies(const impl::DependenciesBase& deps) {
212 return static_cast<const DependenciesComponent&>(deps).GetDependencies();
213 };
214 HttpBase::Callback callback_;
215 };
216
217 HttpWith(int argc, const char* const argv[])
218 : impl_(argc, argv)
219 {
220 impl_.TryAddComponent<DependenciesComponent>(DependenciesComponent::kName);
221 }
222 ~HttpWith() { Dependency::RegisterOn(impl_); }
223
224 /// @copydoc HttpBase::DefaultContentType
225 HttpWith& DefaultContentType(http::ContentType content_type) {
226 return (impl_.DefaultContentType(content_type), *this);
227 }
228
229 /// @copydoc HttpBase::Route
230 HttpWith& Route(
231 std::string_view path,
232 Callback&& func,
233 std::initializer_list<server::http::HttpMethod> methods =
234 {
235 server::http::HttpMethod::kGet,
236 server::http::HttpMethod::kPost,
237 server::http::HttpMethod::kDelete,
238 server::http::HttpMethod::kPut,
239 server::http::HttpMethod::kPatch,
240 }
241 ) {
242 impl_.Route(path, std::move(func).Extract(), methods);
243 return *this;
244 }
245
246 /// Register an HTTP handler by `path` that supports the HTTP GET method.
247 HttpWith& Get(std::string_view path, Callback&& func) {
248 impl_.Route(path, std::move(func).Extract(), {server::http::HttpMethod::kGet});
249 return *this;
250 }
251
252 /// Register an HTTP handler by `path` that supports the HTTP POST method.
253 HttpWith& Post(std::string_view path, Callback&& func) {
254 impl_.Route(path, std::move(func).Extract(), {server::http::HttpMethod::kPost});
255 return *this;
256 }
257
258 /// Register an HTTP handler by `path` that supports the HTTP DELETE method.
259 HttpWith& Del(std::string_view path, Callback&& func) {
260 impl_.Route(path, std::move(func).Extract(), {server::http::HttpMethod::kDelete});
261 return *this;
262 }
263
264 /// Register an HTTP handler by `path` that supports the HTTP PUT method.
265 HttpWith& Put(std::string_view path, Callback&& func) {
266 impl_.Route(path, std::move(func).Extract(), {server::http::HttpMethod::kPut});
267 return *this;
268 }
269
270 /// Register an HTTP handler by `path` that supports the HTTP PATCH method.
271 HttpWith& Patch(std::string_view path, Callback&& func) {
272 impl_.Route(path, std::move(func).Extract(), {server::http::HttpMethod::kPatch});
273 return *this;
274 }
275
276 /// @copydoc HttpBase::DbSchema
277 HttpWith& DbSchema(std::string_view schema) {
278 impl_.DbSchema(schema);
279 return *this;
280 }
281
282 /// @copydoc HttpBase::Port
283 HttpWith& Port(std::uint16_t port) {
284 impl_.Port(port);
285 return *this;
286 }
287
288 /// @copydoc HttpBase::LogLevel
289 HttpWith& LogLevel(logging::Level level) {
290 impl_.LogLevel(level);
291 return *this;
292 }
293
294private:
295 using DependenciesComponent = easy::DependenciesComponent<Dependency>;
296 HttpBase impl_;
297};
298
299template <class Dependency>
300template <class Function>
301HttpWith<Dependency>::Callback::Callback(Function func) {
302 using server::http::HttpRequest;
303
304 constexpr unsigned kMatches =
305 (std::is_invocable_r_v<formats::json::Value, Function, formats::json::Value, const Dependency&> << 0) |
306 (std::is_invocable_r_v<formats::json::Value, Function, formats::json::Value> << 1) |
307 (std::is_invocable_r_v<formats::json::Value, Function, const HttpRequest&, const Dependency&> << 2) |
308 (std::is_invocable_r_v<std::string, Function, const HttpRequest&, const Dependency&> << 3) |
309 (std::is_invocable_r_v<formats::json::Value, Function, const HttpRequest&> << 4) |
310 (std::is_invocable_r_v<std::string, Function, const HttpRequest&> << 5);
311 constexpr bool has_single_match = (kMatches == 0 || ((kMatches & (kMatches - 1)) == 0));
312 static_assert(
313 has_single_match,
314 "Found more than one matching signature, probably due to `auto` usage in parameters. See "
315 "the easy::HttpWith::Callback docs for info on supported signatures"
316 );
317
318 if constexpr (kMatches & 1) {
319 callback_.content_type = http::content_type::kApplicationJson;
320 callback_.function = [f = std::move(func)](const HttpRequest& req, const impl::DependenciesBase& deps) {
321 return formats::json::ToString(f(formats::json::FromString(req.RequestBody()), GetDependencies(deps)));
322 };
323 } else if constexpr (kMatches & 2) {
324 callback_.content_type = http::content_type::kApplicationJson;
325 callback_.function = [f = std::move(func)](const HttpRequest& req, const impl::DependenciesBase&) {
326 return formats::json::ToString(f(formats::json::FromString(req.RequestBody())));
327 };
328 } else if constexpr (kMatches & 4) {
329 callback_.content_type = http::content_type::kApplicationJson;
330 callback_.function = [f = std::move(func)](const HttpRequest& req, const impl::DependenciesBase& deps) {
331 return formats::json::ToString(f(req, GetDependencies(deps)));
332 };
333 } else if constexpr (kMatches & 8) {
334 callback_.content_type = http::content_type::kApplicationJson;
335 callback_.function = [f = std::move(func)](const HttpRequest& req, const impl::DependenciesBase& deps) {
336 return f(req, GetDependencies(deps));
337 };
338 } else if constexpr (kMatches & 16) {
339 callback_.content_type = http::content_type::kApplicationJson;
340 callback_.function = [f = std::move(func)](const HttpRequest& req, const impl::DependenciesBase&) {
341 return formats::json::ToString(f(req));
342 };
343 } else if constexpr (kMatches & 32) {
344 callback_.function = [f = std::move(func)](const HttpRequest& req, const impl::DependenciesBase&) {
345 return f(req);
346 };
347 } else {
348 using FirstArgument = std::decay_t<typename impl::FirstFunctionArgument<Function>::type>;
349 static_assert(
350 std::is_class_v<FirstArgument>,
351 "First function argument should be a class or structure that is JSON pareseable"
352 );
353
354 callback_.content_type = http::content_type::kApplicationJson;
355 callback_.function = [f = std::move(func)](const HttpRequest& req, const impl::DependenciesBase& deps) {
356 auto arg = impl::ParseFromJsonString<FirstArgument>(req.RequestBody());
357
358 if constexpr (std::is_invocable_v<Function, FirstArgument, const Dependency&>) {
359 return formats::json::ToString(formats::json::ValueBuilder{f(std::move(arg), GetDependencies(deps))}
360 .ExtractValue());
361 } else {
362 static_assert(
363 std::is_invocable_v<Function, FirstArgument>,
364 "Found no matching signature, probably due to second argument of the provided function. See "
365 "the easy::HttpWith::Callback docs for info on supported signatures"
366 );
367 return formats::json::ToString(formats::json::ValueBuilder{f(std::move(arg))}.ExtractValue());
368 }
369 };
370 }
371}
372
373/// @brief Dependency class that provides a PostgreSQL cluster client.
374class PgDep {
375public:
376 explicit PgDep(const components::ComponentContext& context);
377 storages::postgres::Cluster& pg() const noexcept { return *pg_cluster_; }
378 static void RegisterOn(HttpBase& app);
379
380private:
381 storages::postgres::ClusterPtr pg_cluster_;
382};
383
384/// @brief Dependency class that provides a Http client.
385class HttpDep {
386public:
387 explicit HttpDep(const components::ComponentContext& context);
388 clients::http::Client& http() { return http_; }
389 static void RegisterOn(HttpBase& app);
390
391private:
392 clients::http::Client& http_;
393};
394
395} // namespace easy
396
397template <class Dependencies>
398inline constexpr auto
399 components::kConfigFileMode<easy::DependenciesComponent<Dependencies>> = ConfigFileMode::kNotRequired;
400
401USERVER_NAMESPACE_END