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