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