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