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