From d5d28607f2a093564b73df9361462e5832d44c6f Mon Sep 17 00:00:00 2001 From: Antony Polukhin Date: Fri, 15 Nov 2024 10:19:19 +0300 Subject: [PATCH 01/19] Easy-peasy library --- libraries/CMakeLists.txt | 4 + libraries/easy/CMakeLists.txt | 11 +++ libraries/easy/__module_deps__.yaml | 11 +++ .../easy/functional_tests/CMakeLists.txt | 6 ++ .../functional_tests/sample/CMakeLists.txt | 6 ++ .../easy/functional_tests/sample/main.cpp | 11 +++ .../sample/static_config.yaml | 56 +++++++++++++ .../functional_tests/sample/tests/conftest.py | 4 + .../sample/tests/test_grpc_reflection.py | 20 +++++ libraries/easy/include/userver/easy.hpp | 41 +++++++++ libraries/easy/library.yaml | 10 +++ libraries/easy/src/easy.cpp | 84 +++++++++++++++++++ 12 files changed, 264 insertions(+) create mode 100644 libraries/easy/CMakeLists.txt create mode 100644 libraries/easy/__module_deps__.yaml create mode 100644 libraries/easy/functional_tests/CMakeLists.txt create mode 100644 libraries/easy/functional_tests/sample/CMakeLists.txt create mode 100644 libraries/easy/functional_tests/sample/main.cpp create mode 100644 libraries/easy/functional_tests/sample/static_config.yaml create mode 100644 libraries/easy/functional_tests/sample/tests/conftest.py create mode 100644 libraries/easy/functional_tests/sample/tests/test_grpc_reflection.py create mode 100644 libraries/easy/include/userver/easy.hpp create mode 100644 libraries/easy/library.yaml create mode 100644 libraries/easy/src/easy.cpp diff --git a/libraries/CMakeLists.txt b/libraries/CMakeLists.txt index 69de58e752b3..e3ac22c9fe42 100644 --- a/libraries/CMakeLists.txt +++ b/libraries/CMakeLists.txt @@ -5,6 +5,10 @@ if (USERVER_FEATURE_S3API) add_subdirectory(s3api) endif() +if (USERVER_FEATURE_EASY) + add_subdirectory(easy) +endif() + if (USERVER_FEATURE_GRPC_REFLECTION) if (NOT USERVER_FEATURE_GRPC) message(FATAL_ERROR "'USERVER_FEATURE_GRPC_REFLECTION' requires 'USERVER_FEATURE_GRPC=ON'") diff --git a/libraries/easy/CMakeLists.txt b/libraries/easy/CMakeLists.txt new file mode 100644 index 000000000000..b5701f717159 --- /dev/null +++ b/libraries/easy/CMakeLists.txt @@ -0,0 +1,11 @@ +project(userver-easy CXX) + +userver_module(easy + SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}" + LINK_LIBRARIES userver::postgresql + UTEST_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/src/*_test.cpp" +) + +if(USERVER_IS_THE_ROOT_PROJECT) + add_subdirectory(functional_tests) +endif() diff --git a/libraries/easy/__module_deps__.yaml b/libraries/easy/__module_deps__.yaml new file mode 100644 index 000000000000..e04680d8449c --- /dev/null +++ b/libraries/easy/__module_deps__.yaml @@ -0,0 +1,11 @@ +# THIS FILE IS AUTOGENERATED, DO NOT EDIT !!! +version: 1 +includes: + - taxi/schemas/schemas/proto/grpc_reflection/__module_deps__.yaml +paths: + - taxi/schemas/schemas/configs/declarations/other + - taxi/schemas/schemas/configs/declarations/userver + - taxi/schemas/schemas/configs/declarations/userver-grpc + - taxi/uservices/libraries/grpc-reflection + - taxi/uservices/userver/core + - taxi/uservices/userver/grpc diff --git a/libraries/easy/functional_tests/CMakeLists.txt b/libraries/easy/functional_tests/CMakeLists.txt new file mode 100644 index 000000000000..22b213490576 --- /dev/null +++ b/libraries/easy/functional_tests/CMakeLists.txt @@ -0,0 +1,6 @@ +project(userver-easy-tests CXX) + +add_custom_target(${PROJECT_NAME}) + +add_subdirectory(sample) +add_dependencies(${PROJECT_NAME} ${PROJECT_NAME}-sample) diff --git a/libraries/easy/functional_tests/sample/CMakeLists.txt b/libraries/easy/functional_tests/sample/CMakeLists.txt new file mode 100644 index 000000000000..9cebfa72de26 --- /dev/null +++ b/libraries/easy/functional_tests/sample/CMakeLists.txt @@ -0,0 +1,6 @@ +project(userver-easy-tests-sample CXX) + +add_executable(${PROJECT_NAME} "main.cpp") +target_link_libraries(${PROJECT_NAME} userver::easy) + +userver_testsuite_add_simple() diff --git a/libraries/easy/functional_tests/sample/main.cpp b/libraries/easy/functional_tests/sample/main.cpp new file mode 100644 index 000000000000..1959e117f843 --- /dev/null +++ b/libraries/easy/functional_tests/sample/main.cpp @@ -0,0 +1,11 @@ +#include // IWYU pragma: keep + +#include + +int main(int argc, char* argv[]) { + return easy::Http(argc, argv) // + .Path("/hello", [](easy::HttpRequest& req) { + return "Hello world"; + }) // + .Run(); +} diff --git a/libraries/easy/functional_tests/sample/static_config.yaml b/libraries/easy/functional_tests/sample/static_config.yaml new file mode 100644 index 000000000000..75c7635ac7fb --- /dev/null +++ b/libraries/easy/functional_tests/sample/static_config.yaml @@ -0,0 +1,56 @@ +components_manager: + task_processors: + main-task-processor: + worker_threads: 8 + fs-task-processor: + worker_threads: 4 + grpc-blocking-task-processor: # For blocking gRPC channel creation + worker_threads: 2 + thread_name: grpc-worker + default_task_processor: main-task-processor + + components: + grpc-reflection-service: + grpc-health: + + testsuite-support: + + # Common configuration for gRPC server + grpc-server: + # The single listening unix socket for incoming RPCs + unix-socket-path: '/tmp/my_socket' + completion-queue-count: 3 + service-defaults: + task-processor: main-task-processor + middlewares: [] + + + + http-client: + fs-task-processor: main-task-processor + + tests-control: + method: POST + path: /tests/{action} + skip-unregistered-testpoints: true + task_processor: main-task-processor + testpoint-timeout: 10s + testpoint-url: $mockserver/testpoint + throttling_enabled: false + + logging: + fs-task-processor: fs-task-processor + loggers: + default: + file_path: "@stderr" + level: debug + overflow_behavior: discard + + dns-client: + fs-task-processor: fs-task-processor + + server: + listener: + port: 8187 + task_processor: main-task-processor + diff --git a/libraries/easy/functional_tests/sample/tests/conftest.py b/libraries/easy/functional_tests/sample/tests/conftest.py new file mode 100644 index 000000000000..41ffab87fc60 --- /dev/null +++ b/libraries/easy/functional_tests/sample/tests/conftest.py @@ -0,0 +1,4 @@ +# /// [registration] +# Adding a plugin from userver/testsuite/pytest_plugins/ +pytest_plugins = ['pytest_userver.plugins.core'] +# /// [registration] diff --git a/libraries/easy/functional_tests/sample/tests/test_grpc_reflection.py b/libraries/easy/functional_tests/sample/tests/test_grpc_reflection.py new file mode 100644 index 000000000000..4a7978bcb4d8 --- /dev/null +++ b/libraries/easy/functional_tests/sample/tests/test_grpc_reflection.py @@ -0,0 +1,20 @@ +# /// [Functional test] +async def test_hello_simple(service_client): + response = await service_client.get('/hello') + assert response.status == 200 + assert 'text/plain' in response.headers['Content-Type'] + assert response.text == 'Hello!\n' + # /// [Functional test] + +async def disable_test_hello_base(service_client): + response = await service_client.get('/hello') + assert response.status == 200 + assert 'text/plain' in response.headers['Content-Type'] + assert response.text == 'Hello, unknown user!\n' + assert 'X-RequestId' not in response.headers.keys(), 'Unexpected header' + + response = await service_client.get('/hello', params={'name': 'userver'}) + assert response.status == 200 + assert 'text/plain' in response.headers['Content-Type'] + assert response.text == 'Hello, userver!\n' + diff --git a/libraries/easy/include/userver/easy.hpp b/libraries/easy/include/userver/easy.hpp new file mode 100644 index 000000000000..a0413b89f39a --- /dev/null +++ b/libraries/easy/include/userver/easy.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include +#include + +#include + +USERVER_NAMESPACE_BEGIN + +/// @brief Top namespace for `easy` library +namespace easy { + +using HttpRequest = server::http::HttpRequest; +using RequestContext = server::request::RequestContext; + +class Http final { +public: + using Callback = std::function functions_; + + void AddHandleConfig(std::string_view path); + + class Handle; + + const int argc_; + const char *const argv_[]; + components::ComponentList component_list_ = MinimalServerComponentList(); +}; + +} // namespace easy + +USERVER_NAMESPACE_END diff --git a/libraries/easy/library.yaml b/libraries/easy/library.yaml new file mode 100644 index 000000000000..e8a02a8eb9c3 --- /dev/null +++ b/libraries/easy/library.yaml @@ -0,0 +1,10 @@ +project-name: userver-grpc-reflection +project-alt-names: + - yandex-userver-grpc-reflection +description: library with reflection implementation + +maintainers: + - Common componets + +libraries: + - userver-core diff --git a/libraries/easy/src/easy.cpp b/libraries/easy/src/easy.cpp new file mode 100644 index 000000000000..061bc6a07e1b --- /dev/null +++ b/libraries/easy/src/easy.cpp @@ -0,0 +1,84 @@ +#include + +#include + +USERVER_NAMESPACE_BEGIN + +namespace easy { + +namespace { + +constexpr std::string_view kConfigBase = R"~( +components_manager: + task_processors: # Task processor is an executor for coroutine tasks + main-task-processor: # Make a task processor for CPU-bound coroutine tasks. + worker_threads: 4 # Process tasks in 4 threads. + + fs-task-processor: # Make a separate task processor for filesystem bound tasks. + worker_threads: 1 + + default_task_processor: main-task-processor # Task processor in which components start. + + components: # Configuring components that were registered via component_list + server: + listener: # configuring the main listening socket... + port: 8080 # ...to listen on this port and... + task_processor: main-task-processor # ...process incoming requests on this task processor. + logging: + fs-task-processor: fs-task-processor + loggers: + default: + file_path: '@stderr' + level: debug + overflow_behavior: discard # Drop logs if the system is too busy to write them down. +)~"; + + +constexpr std::string_view kConfigHandlerTemplate = R"~( + {0}: + path: {0} # Registering handler by URL '{0}'. + method: GET,POST # It will only reply to GET (HEAD) and POST requests. + task_processor: main-task-processor # Run it on CPU bound task processor +)~"; + +} + +class Http::Handle final : public server::handlers::HttpHandlerBase, Function { +public: + Handle(const components::ComponentConfig& config, const components::ComponentContext& context): + HttpHandlerBase(config, context), + callback_{Http::functions_[config.Name()]} + {} + + std::string HandleRequestThrow(const HttpRequest& request, RequestContext&) const override { + return callback_(request); + } +private: + Callback& callback_; +}; + +Http::Http(int argc, const char *const argv[]) : argc_{argc}, argv_{argv}, static_config_{kConfigBase} {} + +Http::~Http() { + functions_.clear(); +} + +Http& Http::Path(std::string_view path, Callback&& func) { + functions_.insert(path, std::move(func)); + component_list_.Append(path); + AddHandleConfig(path); + + return *this; +} + +int Http::Run() { + return utils::DaemonMain(argc_, argv_, component_list_); +} + +void Http::AddHandleConfig(std::string_view path) { + static_config_ += fmt::format(kConfigHandlerTemplate, path); +} + +} // namespace easy + +USERVER_NAMESPACE_END From 93e5379f2bdb3203da1ccc355556f1f89b1665c1 Mon Sep 17 00:00:00 2001 From: Antony Polukhin Date: Fri, 15 Nov 2024 13:12:41 +0300 Subject: [PATCH 02/19] Its alive --- cmake/UserverTestsuite.cmake | 9 +++ cmake/install/userver-easy-config.cmake | 13 +++++ libraries/CMakeLists.txt | 3 + libraries/easy/CMakeLists.txt | 2 +- libraries/easy/__module_deps__.yaml | 11 ---- .../easy/functional_tests/CMakeLists.txt | 6 -- .../easy/functional_tests/sample/main.cpp | 11 ---- .../sample/static_config.yaml | 56 ------------------- .../sample/tests/test_grpc_reflection.py | 20 ------- libraries/easy/include/userver/easy.hpp | 15 +++-- libraries/easy/library.yaml | 10 ---- .../0_hello_world}/CMakeLists.txt | 4 +- libraries/easy/samples/0_hello_world/main.cpp | 13 +++++ .../0_hello_world}/tests/conftest.py | 0 .../samples/0_hello_world/tests/test_basic.py | 7 +++ libraries/easy/samples/CMakeLists.txt | 6 ++ libraries/easy/src/easy.cpp | 50 ++++++++++++++--- .../pytest_userver/plugins/config.py | 10 +++- 18 files changed, 115 insertions(+), 131 deletions(-) create mode 100644 cmake/install/userver-easy-config.cmake delete mode 100644 libraries/easy/__module_deps__.yaml delete mode 100644 libraries/easy/functional_tests/CMakeLists.txt delete mode 100644 libraries/easy/functional_tests/sample/main.cpp delete mode 100644 libraries/easy/functional_tests/sample/static_config.yaml delete mode 100644 libraries/easy/functional_tests/sample/tests/test_grpc_reflection.py delete mode 100644 libraries/easy/library.yaml rename libraries/easy/{functional_tests/sample => samples/0_hello_world}/CMakeLists.txt (51%) create mode 100644 libraries/easy/samples/0_hello_world/main.cpp rename libraries/easy/{functional_tests/sample => samples/0_hello_world}/tests/conftest.py (100%) create mode 100644 libraries/easy/samples/0_hello_world/tests/test_basic.py create mode 100644 libraries/easy/samples/CMakeLists.txt diff --git a/cmake/UserverTestsuite.cmake b/cmake/UserverTestsuite.cmake index 80a3198ce5ee..d6946fb81c55 100644 --- a/cmake/UserverTestsuite.cmake +++ b/cmake/UserverTestsuite.cmake @@ -308,6 +308,7 @@ function(userver_testsuite_add_simple) CONFIG_VARS_PATH DYNAMIC_CONFIG_FALLBACK_PATH SECDIST_PATH + DUMP_CONFIG ) set(multiValueArgs PYTEST_ARGS @@ -358,9 +359,16 @@ function(userver_testsuite_add_simple) set(ARG_TEST_SUFFIX "") endif() + set(DUMP_CONFIG_OPTION "") + if (ARG_DUMP_CONFIG) + set(DUMP_CONFIG_OPTION "--dump-config") + endif() + if(ARG_CONFIG_PATH) get_filename_component(config_path "${ARG_CONFIG_PATH}" REALPATH BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}") + elseif(ARG_DUMP_CONFIG) + set(config_path "${CMAKE_CURRENT_BINARY_DIR}/Testing/Temporary/static_config.yaml") else() foreach(probable_config_path IN ITEMS "${CMAKE_CURRENT_SOURCE_DIR}/configs/static_config.yaml" @@ -460,6 +468,7 @@ function(userver_testsuite_add_simple) "--service-config=${config_path}" "--service-source-dir=${CMAKE_CURRENT_SOURCE_DIR}" "--service-binary=${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}" + "${DUMP_CONFIG_OPTION}" ${pytest_additional_args} ${ARG_PYTEST_ARGS} REQUIREMENTS ${ARG_REQUIREMENTS} diff --git a/cmake/install/userver-easy-config.cmake b/cmake/install/userver-easy-config.cmake new file mode 100644 index 000000000000..40caf93a1c06 --- /dev/null +++ b/cmake/install/userver-easy-config.cmake @@ -0,0 +1,13 @@ +include_guard(GLOBAL) + +if(userver_easy_FOUND) + return() +endif() + +find_package(userver REQUIRED COMPONENTS + core + postgresql +) + +set(userver_easy_FOUND TRUE) + diff --git a/libraries/CMakeLists.txt b/libraries/CMakeLists.txt index e3ac22c9fe42..4c504f678e4d 100644 --- a/libraries/CMakeLists.txt +++ b/libraries/CMakeLists.txt @@ -6,6 +6,9 @@ if (USERVER_FEATURE_S3API) endif() if (USERVER_FEATURE_EASY) + if (NOT USERVER_FEATURE_POSTGRESQL) + message(FATAL_ERROR "'USERVER_FEATURE_EASY' requires 'USERVER_FEATURE_POSTGRESQL=ON'") + endif() add_subdirectory(easy) endif() diff --git a/libraries/easy/CMakeLists.txt b/libraries/easy/CMakeLists.txt index b5701f717159..67e1813b1990 100644 --- a/libraries/easy/CMakeLists.txt +++ b/libraries/easy/CMakeLists.txt @@ -7,5 +7,5 @@ userver_module(easy ) if(USERVER_IS_THE_ROOT_PROJECT) - add_subdirectory(functional_tests) + add_subdirectory(samples) endif() diff --git a/libraries/easy/__module_deps__.yaml b/libraries/easy/__module_deps__.yaml deleted file mode 100644 index e04680d8449c..000000000000 --- a/libraries/easy/__module_deps__.yaml +++ /dev/null @@ -1,11 +0,0 @@ -# THIS FILE IS AUTOGENERATED, DO NOT EDIT !!! -version: 1 -includes: - - taxi/schemas/schemas/proto/grpc_reflection/__module_deps__.yaml -paths: - - taxi/schemas/schemas/configs/declarations/other - - taxi/schemas/schemas/configs/declarations/userver - - taxi/schemas/schemas/configs/declarations/userver-grpc - - taxi/uservices/libraries/grpc-reflection - - taxi/uservices/userver/core - - taxi/uservices/userver/grpc diff --git a/libraries/easy/functional_tests/CMakeLists.txt b/libraries/easy/functional_tests/CMakeLists.txt deleted file mode 100644 index 22b213490576..000000000000 --- a/libraries/easy/functional_tests/CMakeLists.txt +++ /dev/null @@ -1,6 +0,0 @@ -project(userver-easy-tests CXX) - -add_custom_target(${PROJECT_NAME}) - -add_subdirectory(sample) -add_dependencies(${PROJECT_NAME} ${PROJECT_NAME}-sample) diff --git a/libraries/easy/functional_tests/sample/main.cpp b/libraries/easy/functional_tests/sample/main.cpp deleted file mode 100644 index 1959e117f843..000000000000 --- a/libraries/easy/functional_tests/sample/main.cpp +++ /dev/null @@ -1,11 +0,0 @@ -#include // IWYU pragma: keep - -#include - -int main(int argc, char* argv[]) { - return easy::Http(argc, argv) // - .Path("/hello", [](easy::HttpRequest& req) { - return "Hello world"; - }) // - .Run(); -} diff --git a/libraries/easy/functional_tests/sample/static_config.yaml b/libraries/easy/functional_tests/sample/static_config.yaml deleted file mode 100644 index 75c7635ac7fb..000000000000 --- a/libraries/easy/functional_tests/sample/static_config.yaml +++ /dev/null @@ -1,56 +0,0 @@ -components_manager: - task_processors: - main-task-processor: - worker_threads: 8 - fs-task-processor: - worker_threads: 4 - grpc-blocking-task-processor: # For blocking gRPC channel creation - worker_threads: 2 - thread_name: grpc-worker - default_task_processor: main-task-processor - - components: - grpc-reflection-service: - grpc-health: - - testsuite-support: - - # Common configuration for gRPC server - grpc-server: - # The single listening unix socket for incoming RPCs - unix-socket-path: '/tmp/my_socket' - completion-queue-count: 3 - service-defaults: - task-processor: main-task-processor - middlewares: [] - - - - http-client: - fs-task-processor: main-task-processor - - tests-control: - method: POST - path: /tests/{action} - skip-unregistered-testpoints: true - task_processor: main-task-processor - testpoint-timeout: 10s - testpoint-url: $mockserver/testpoint - throttling_enabled: false - - logging: - fs-task-processor: fs-task-processor - loggers: - default: - file_path: "@stderr" - level: debug - overflow_behavior: discard - - dns-client: - fs-task-processor: fs-task-processor - - server: - listener: - port: 8187 - task_processor: main-task-processor - diff --git a/libraries/easy/functional_tests/sample/tests/test_grpc_reflection.py b/libraries/easy/functional_tests/sample/tests/test_grpc_reflection.py deleted file mode 100644 index 4a7978bcb4d8..000000000000 --- a/libraries/easy/functional_tests/sample/tests/test_grpc_reflection.py +++ /dev/null @@ -1,20 +0,0 @@ -# /// [Functional test] -async def test_hello_simple(service_client): - response = await service_client.get('/hello') - assert response.status == 200 - assert 'text/plain' in response.headers['Content-Type'] - assert response.text == 'Hello!\n' - # /// [Functional test] - -async def disable_test_hello_base(service_client): - response = await service_client.get('/hello') - assert response.status == 200 - assert 'text/plain' in response.headers['Content-Type'] - assert response.text == 'Hello, unknown user!\n' - assert 'X-RequestId' not in response.headers.keys(), 'Unexpected header' - - response = await service_client.get('/hello', params={'name': 'userver'}) - assert response.status == 200 - assert 'text/plain' in response.headers['Content-Type'] - assert response.text == 'Hello, userver!\n' - diff --git a/libraries/easy/include/userver/easy.hpp b/libraries/easy/include/userver/easy.hpp index a0413b89f39a..05f6321a1448 100644 --- a/libraries/easy/include/userver/easy.hpp +++ b/libraries/easy/include/userver/easy.hpp @@ -1,9 +1,13 @@ #pragma once +#include +#include #include #include #include +#include +#include USERVER_NAMESPACE_BEGIN @@ -15,25 +19,24 @@ using RequestContext = server::request::RequestContext; class Http final { public: - using Callback = std::function; Http(int argc, const char *const argv[]); ~Http(); Http& Path(std::string_view path, Callback&& func); - int Run(); + [[nodiscard]] int Run(); private: - static std::unordered_map functions_; - void AddHandleConfig(std::string_view path); class Handle; const int argc_; - const char *const argv_[]; - components::ComponentList component_list_ = MinimalServerComponentList(); + const char *const* argv_; + std::string static_config_; + components::ComponentList component_list_; }; } // namespace easy diff --git a/libraries/easy/library.yaml b/libraries/easy/library.yaml deleted file mode 100644 index e8a02a8eb9c3..000000000000 --- a/libraries/easy/library.yaml +++ /dev/null @@ -1,10 +0,0 @@ -project-name: userver-grpc-reflection -project-alt-names: - - yandex-userver-grpc-reflection -description: library with reflection implementation - -maintainers: - - Common componets - -libraries: - - userver-core diff --git a/libraries/easy/functional_tests/sample/CMakeLists.txt b/libraries/easy/samples/0_hello_world/CMakeLists.txt similarity index 51% rename from libraries/easy/functional_tests/sample/CMakeLists.txt rename to libraries/easy/samples/0_hello_world/CMakeLists.txt index 9cebfa72de26..536208b8e008 100644 --- a/libraries/easy/functional_tests/sample/CMakeLists.txt +++ b/libraries/easy/samples/0_hello_world/CMakeLists.txt @@ -1,6 +1,6 @@ -project(userver-easy-tests-sample CXX) +project(userver-easy-samples-hello-world CXX) add_executable(${PROJECT_NAME} "main.cpp") target_link_libraries(${PROJECT_NAME} userver::easy) -userver_testsuite_add_simple() +userver_testsuite_add_simple(DUMP_CONFIG True) diff --git a/libraries/easy/samples/0_hello_world/main.cpp b/libraries/easy/samples/0_hello_world/main.cpp new file mode 100644 index 000000000000..50486291d0e5 --- /dev/null +++ b/libraries/easy/samples/0_hello_world/main.cpp @@ -0,0 +1,13 @@ +// Note: this is for the purposes of samples only +#include + +#include + +int main(int argc, char* argv[]) { + return easy::Http(argc, argv) // + .Path("/hello", [](const easy::HttpRequest& req) { + req.GetHttpResponse().SetContentType(http::content_type::kTextPlain); + return "Hello world"; + }) // + .Run(); +} diff --git a/libraries/easy/functional_tests/sample/tests/conftest.py b/libraries/easy/samples/0_hello_world/tests/conftest.py similarity index 100% rename from libraries/easy/functional_tests/sample/tests/conftest.py rename to libraries/easy/samples/0_hello_world/tests/conftest.py diff --git a/libraries/easy/samples/0_hello_world/tests/test_basic.py b/libraries/easy/samples/0_hello_world/tests/test_basic.py new file mode 100644 index 000000000000..3ea76e627d03 --- /dev/null +++ b/libraries/easy/samples/0_hello_world/tests/test_basic.py @@ -0,0 +1,7 @@ +# /// [Functional test] +async def test_hello_simple(service_client): + response = await service_client.get('/hello') + assert response.status == 200 + assert 'text/plain' in response.headers['Content-Type'] + assert response.text == 'Hello world' + # /// [Functional test] diff --git a/libraries/easy/samples/CMakeLists.txt b/libraries/easy/samples/CMakeLists.txt new file mode 100644 index 000000000000..a2b65677dfa7 --- /dev/null +++ b/libraries/easy/samples/CMakeLists.txt @@ -0,0 +1,6 @@ +project(userver-easy-samples CXX) + +add_custom_target(${PROJECT_NAME}) + +add_subdirectory(0_hello_world) +add_dependencies(${PROJECT_NAME} ${PROJECT_NAME}-hello-world) diff --git a/libraries/easy/src/easy.cpp b/libraries/easy/src/easy.cpp index 061bc6a07e1b..46cdcd8817aa 100644 --- a/libraries/easy/src/easy.cpp +++ b/libraries/easy/src/easy.cpp @@ -1,6 +1,13 @@ #include +#include + +#include + +#include +#include #include +#include USERVER_NAMESPACE_BEGIN @@ -41,30 +48,34 @@ constexpr std::string_view kConfigHandlerTemplate = R"~( task_processor: main-task-processor # Run it on CPU bound task processor )~"; + +std::unordered_map g_http_functions_; + } -class Http::Handle final : public server::handlers::HttpHandlerBase, Function { +class Http::Handle final : public server::handlers::HttpHandlerBase { public: Handle(const components::ComponentConfig& config, const components::ComponentContext& context): HttpHandlerBase(config, context), - callback_{Http::functions_[config.Name()]} + callback_{g_http_functions_[config.Name()]} {} std::string HandleRequestThrow(const HttpRequest& request, RequestContext&) const override { return callback_(request); } private: - Callback& callback_; + Http::Callback& callback_; }; -Http::Http(int argc, const char *const argv[]) : argc_{argc}, argv_{argv}, static_config_{kConfigBase} {} +Http::Http(int argc, const char *const argv[]) : argc_{argc}, argv_{argv}, static_config_{kConfigBase}, + component_list_{components::MinimalServerComponentList()} {} Http::~Http() { - functions_.clear(); + g_http_functions_.clear(); } Http& Http::Path(std::string_view path, Callback&& func) { - functions_.insert(path, std::move(func)); + g_http_functions_.emplace(path, std::move(func)); component_list_.Append(path); AddHandleConfig(path); @@ -72,7 +83,32 @@ Http& Http::Path(std::string_view path, Callback&& func) { } int Http::Run() { - return utils::DaemonMain(argc_, argv_, component_list_); + namespace po = boost::program_options; + + po::variables_map vm; + po::options_description desc("Easy options"); + std::string config_dump; + + // clang-format off + desc.add_options() + ("dump-config", po::value(&config_dump), "path to dump the server config") + ; + // clang-format on + + po::store(po::command_line_parser(argc_, argv_).options(desc).allow_unregistered().run(), vm); + po::notify(vm); + + if (vm.count("dump-config")) { + std::ofstream(config_dump) << static_config_; + return 0; + } + + if (argc_ <= 1) { + components::Run(components::InMemoryConfig{static_config_}, component_list_); + return 0; + } else { + return utils::DaemonMain(argc_, argv_, component_list_); + } } void Http::AddHandleConfig(std::string_view path) { diff --git a/testsuite/pytest_plugins/pytest_userver/plugins/config.py b/testsuite/pytest_plugins/pytest_userver/plugins/config.py index f39a4ce83617..4330ab3a02dc 100644 --- a/testsuite/pytest_plugins/pytest_userver/plugins/config.py +++ b/testsuite/pytest_plugins/pytest_userver/plugins/config.py @@ -7,6 +7,7 @@ import logging import os import pathlib +import subprocess import types import typing @@ -97,13 +98,18 @@ def pytest_addoption(parser) -> None: type=pathlib.Path, help='Path to dynamic config fallback file.', ) + group.addoption( + '--dump-config', + action='store_true', + help='Dump config from binary before running tests', + ) # @endcond @pytest.fixture(scope='session') -def service_config_path(pytestconfig) -> pathlib.Path: +def service_config_path(pytestconfig, service_binary) -> pathlib.Path: """ Returns the path to service.yaml file set by command line `--service-config` option. @@ -112,6 +118,8 @@ def service_config_path(pytestconfig) -> pathlib.Path: @ingroup userver_testsuite_fixtures """ + if pytestconfig.option.dump_config: + subprocess.run([service_binary, '--dump-config', pytestconfig.option.service_config]) return pytestconfig.option.service_config From 18d69bd86bc035d62e379e3ee7dab23b33dc3bd4 Mon Sep 17 00:00:00 2001 From: Antony Polukhin Date: Fri, 15 Nov 2024 13:18:59 +0300 Subject: [PATCH 03/19] start docs --- libraries/CMakeLists.txt | 1 + scripts/docs/en/index.md | 3 +++ scripts/docs/en/userver/build/options.md | 5 +++-- scripts/docs/en/userver/libraries/easy.md | 3 +++ 4 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 scripts/docs/en/userver/libraries/easy.md diff --git a/libraries/CMakeLists.txt b/libraries/CMakeLists.txt index 4c504f678e4d..b26f1ce10073 100644 --- a/libraries/CMakeLists.txt +++ b/libraries/CMakeLists.txt @@ -1,3 +1,4 @@ +option(USERVER_FEATURE_EASY "Build easy HTTP server library" "${USERVER_LIB_ENABLED_DEFAULT}") option(USERVER_FEATURE_S3API "Build S3 api client library" "${USERVER_LIB_ENABLED_DEFAULT}") option(USERVER_FEATURE_GRPC_REFLECTION "Build grpc reflection library" "${USERVER_LIB_ENABLED_DEFAULT}") diff --git a/scripts/docs/en/index.md b/scripts/docs/en/index.md index 8a6f8e042914..6393e5b175d9 100644 --- a/scripts/docs/en/index.md +++ b/scripts/docs/en/index.md @@ -152,6 +152,9 @@ are available at the ## Libraries +### Easy +* @ref scripts/docs/en/userver/libraries/easy.md + ### S3 client * @ref scripts/docs/en/userver/libraries/s3api.md diff --git a/scripts/docs/en/userver/build/options.md b/scripts/docs/en/userver/build/options.md index efcd97eee6e0..55836d82f2d4 100644 --- a/scripts/docs/en/userver/build/options.md +++ b/scripts/docs/en/userver/build/options.md @@ -26,8 +26,9 @@ userver is split into multiple CMake libraries. | `userver::rocks` | `USERVER_FEATURE_ROCKS` | `rocks` | TODO | | `userver::ydb` | `USERVER_FEATURE_YDB` | `ydb` | @ref scripts/docs/en/userver/ydb.md | | `userver::otlp` | `USERVER_FEATURE_OTLP` | `otlp` | @ref opentelemetry "OpenTelemetry Protocol" | -| `userver::s3api` | `USERVER_FEATURE_S3API` | `s3api` | @ref scripts/docs/en/userver/libraries/s3api.md | -| `userver::grpc-reflection` | `USERVER_FEATURE_GRPC_REFLECTION` | `grpc-reflection` | @ref scripts/docs/en/userver/libraries/grpc-reflection.md | +| `userver::easy` | `USERVER_FEATURE_EASY` | `easy` | @ref scripts/docs/en/userver/libraries/easy.md | +| `userver::s3api` | `USERVER_FEATURE_S3API` | `s3api` | @ref scripts/docs/en/userver/libraries/s3api.md | +| `userver::grpc-reflection` | `USERVER_FEATURE_GRPC_REFLECTION` | `grpc-reflection` | @ref scripts/docs/en/userver/libraries/grpc-reflection.md | Make sure to: diff --git a/scripts/docs/en/userver/libraries/easy.md b/scripts/docs/en/userver/libraries/easy.md new file mode 100644 index 000000000000..d623532a1626 --- /dev/null +++ b/scripts/docs/en/userver/libraries/easy.md @@ -0,0 +1,3 @@ +## Easy - library for single file prototyping + +TODO: From 415e16891130095084b14ae33fd5de7ee251875d Mon Sep 17 00:00:00 2001 From: Antony Polukhin Date: Fri, 15 Nov 2024 13:46:33 +0300 Subject: [PATCH 04/19] minor tweaks --- libraries/easy/include/userver/easy.hpp | 3 -- libraries/easy/samples/0_hello_world/main.cpp | 5 ++- libraries/easy/src/easy.cpp | 33 +++++++++---------- 3 files changed, 18 insertions(+), 23 deletions(-) diff --git a/libraries/easy/include/userver/easy.hpp b/libraries/easy/include/userver/easy.hpp index 05f6321a1448..588a2c7b307c 100644 --- a/libraries/easy/include/userver/easy.hpp +++ b/libraries/easy/include/userver/easy.hpp @@ -25,9 +25,6 @@ class Http final { ~Http(); Http& Path(std::string_view path, Callback&& func); - - [[nodiscard]] int Run(); - private: void AddHandleConfig(std::string_view path); diff --git a/libraries/easy/samples/0_hello_world/main.cpp b/libraries/easy/samples/0_hello_world/main.cpp index 50486291d0e5..65c52a68ea79 100644 --- a/libraries/easy/samples/0_hello_world/main.cpp +++ b/libraries/easy/samples/0_hello_world/main.cpp @@ -4,10 +4,9 @@ #include int main(int argc, char* argv[]) { - return easy::Http(argc, argv) // + easy::Http(argc, argv) // .Path("/hello", [](const easy::HttpRequest& req) { req.GetHttpResponse().SetContentType(http::content_type::kTextPlain); return "Hello world"; - }) // - .Run(); + }); } diff --git a/libraries/easy/src/easy.cpp b/libraries/easy/src/easy.cpp index 46cdcd8817aa..eef795e3fb3e 100644 --- a/libraries/easy/src/easy.cpp +++ b/libraries/easy/src/easy.cpp @@ -51,7 +51,7 @@ constexpr std::string_view kConfigHandlerTemplate = R"~( std::unordered_map g_http_functions_; -} +} // anonymous namespace class Http::Handle final : public server::handlers::HttpHandlerBase { public: @@ -71,18 +71,6 @@ Http::Http(int argc, const char *const argv[]) : argc_{argc}, argv_{argv}, stati component_list_{components::MinimalServerComponentList()} {} Http::~Http() { - g_http_functions_.clear(); -} - -Http& Http::Path(std::string_view path, Callback&& func) { - g_http_functions_.emplace(path, std::move(func)); - component_list_.Append(path); - AddHandleConfig(path); - - return *this; -} - -int Http::Run() { namespace po = boost::program_options; po::variables_map vm; @@ -100,15 +88,26 @@ int Http::Run() { if (vm.count("dump-config")) { std::ofstream(config_dump) << static_config_; - return 0; + return; } if (argc_ <= 1) { components::Run(components::InMemoryConfig{static_config_}, component_list_); - return 0; + return; } else { - return utils::DaemonMain(argc_, argv_, component_list_); - } + const auto ret = utils::DaemonMain(argc_, argv_, component_list_); + if (ret != 0) { + std::exit(ret); + } + } +} + +Http& Http::Path(std::string_view path, Callback&& func) { + g_http_functions_.emplace(path, std::move(func)); + component_list_.Append(path); + AddHandleConfig(path); + + return *this; } void Http::AddHandleConfig(std::string_view path) { From e5ae62ca80dde8acbab5728ce901177d842f099c Mon Sep 17 00:00:00 2001 From: Antony Polukhin Date: Fri, 15 Nov 2024 15:28:23 +0300 Subject: [PATCH 05/19] more tests --- libraries/easy/include/userver/easy.hpp | 6 +++++- libraries/easy/samples/0_hello_world/main.cpp | 10 ++++++++-- .../easy/samples/0_hello_world/tests/test_basic.py | 12 ++++++++++++ libraries/easy/src/easy.cpp | 11 ++++++++++- 4 files changed, 35 insertions(+), 4 deletions(-) diff --git a/libraries/easy/include/userver/easy.hpp b/libraries/easy/include/userver/easy.hpp index 588a2c7b307c..819e7665c148 100644 --- a/libraries/easy/include/userver/easy.hpp +++ b/libraries/easy/include/userver/easy.hpp @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -24,7 +25,10 @@ class Http final { Http(int argc, const char *const argv[]); ~Http(); - Http& Path(std::string_view path, Callback&& func); + + Http& DefaultContentType(http::ContentType content_type); + + Http& Route(std::string_view path, Callback&& func); private: void AddHandleConfig(std::string_view path); diff --git a/libraries/easy/samples/0_hello_world/main.cpp b/libraries/easy/samples/0_hello_world/main.cpp index 65c52a68ea79..34e890a1259b 100644 --- a/libraries/easy/samples/0_hello_world/main.cpp +++ b/libraries/easy/samples/0_hello_world/main.cpp @@ -5,8 +5,14 @@ int main(int argc, char* argv[]) { easy::Http(argc, argv) // - .Path("/hello", [](const easy::HttpRequest& req) { - req.GetHttpResponse().SetContentType(http::content_type::kTextPlain); + .DefaultContentType(http::content_type::kTextPlain) + .Route("/hello", [](const easy::HttpRequest& req) { return "Hello world"; + }) + .Route("/hello/to/{user}", [](const easy::HttpRequest& req) { + return "Hello, " + req.GetPathArg("user"); + }) + .Route("/hi", [](const easy::HttpRequest& req) { + return "Hi, " + req.GetArg("name"); }); } diff --git a/libraries/easy/samples/0_hello_world/tests/test_basic.py b/libraries/easy/samples/0_hello_world/tests/test_basic.py index 3ea76e627d03..696228ccff0c 100644 --- a/libraries/easy/samples/0_hello_world/tests/test_basic.py +++ b/libraries/easy/samples/0_hello_world/tests/test_basic.py @@ -5,3 +5,15 @@ async def test_hello_simple(service_client): assert 'text/plain' in response.headers['Content-Type'] assert response.text == 'Hello world' # /// [Functional test] + +async def test_hello_to(service_client): + response = await service_client.get('/hello/to/flusk') + assert response.status == 200 + assert 'text/plain' in response.headers['Content-Type'] + assert response.text == 'Hello, flusk' + +async def test_hi(service_client): + response = await service_client.get('/hi?name=pal') + assert response.status == 200 + assert 'text/plain' in response.headers['Content-Type'] + assert response.text == 'Hi, pal' diff --git a/libraries/easy/src/easy.cpp b/libraries/easy/src/easy.cpp index eef795e3fb3e..4c412bd67fd7 100644 --- a/libraries/easy/src/easy.cpp +++ b/libraries/easy/src/easy.cpp @@ -50,6 +50,7 @@ constexpr std::string_view kConfigHandlerTemplate = R"~( std::unordered_map g_http_functions_; +std::optional default_content_type_; } // anonymous namespace @@ -61,6 +62,9 @@ class Http::Handle final : public server::handlers::HttpHandlerBase { {} std::string HandleRequestThrow(const HttpRequest& request, RequestContext&) const override { + if (default_content_type_) { + request.GetHttpResponse().SetContentType(*default_content_type_); + } return callback_(request); } private: @@ -102,7 +106,12 @@ Http::~Http() { } } -Http& Http::Path(std::string_view path, Callback&& func) { +Http& Http::DefaultContentType(http::ContentType content_type) { + default_content_type_ = content_type; + return *this; +} + +Http& Http::Route(std::string_view path, Callback&& func) { g_http_functions_.emplace(path, std::move(func)); component_list_.Append(path); AddHandleConfig(path); From e8e6b1ff4a511478df722cbfb390b4bd16b99eba Mon Sep 17 00:00:00 2001 From: Antony Polukhin Date: Fri, 15 Nov 2024 17:49:19 +0300 Subject: [PATCH 06/19] move towards Dependency --- libraries/easy/include/userver/easy.hpp | 65 ++++++++++++++++++- libraries/easy/samples/0_hello_world/main.cpp | 2 +- libraries/easy/src/easy.cpp | 34 ++++++++-- 3 files changed, 93 insertions(+), 8 deletions(-) diff --git a/libraries/easy/include/userver/easy.hpp b/libraries/easy/include/userver/easy.hpp index 819e7665c148..c3aa136668fd 100644 --- a/libraries/easy/include/userver/easy.hpp +++ b/libraries/easy/include/userver/easy.hpp @@ -6,10 +6,13 @@ #include #include +#include #include #include #include +#include + USERVER_NAMESPACE_BEGIN /// @brief Top namespace for `easy` library @@ -18,13 +21,39 @@ namespace easy { using HttpRequest = server::http::HttpRequest; using RequestContext = server::request::RequestContext; -class Http final { +class DependenciesBase: public components::ComponentBase { public: - using Callback = std::function; + static constexpr std::string_view kName = "easy-dependencies"; + using components::ComponentBase::ComponentBase; + virtual ~DependenciesBase(); +}; + +class Callback { +public: + using Underlying = std::function; + + template + Callback(Function func) { + if constexpr (std::is_invocable_r_v) { + func_ = std::move(func); + } else { + static_assert(std::is_invocable_r_v); + func_ = [f = std::move(func)](const HttpRequest& req, const DependenciesBase&) { + return f(req); + }; + } + } + + Underlying Extract() && noexcept { return std::move(func_); } +private: + std::function func_; +}; + +class Http final { +public: Http(int argc, const char *const argv[]); ~Http(); - Http& DefaultContentType(http::ContentType content_type); @@ -40,6 +69,36 @@ class Http final { components::ComponentList component_list_; }; +/// + +template +class CallbackWith : public Callback { +public: + using Callback::Callback; + + CallbackWith(std::function func) + : Callback([f = std::move(func)](const HttpRequest& req, const DependenciesBase& deps) { + return f(req, static_cast(deps)); + }) + {} +}; + +class Postgres : public DependenciesBase { +public: + Postgres(const components::ComponentConfig& config, const components::ComponentContext& context); + + storages::postgres::Cluster& pg() const noexcept { return *pg_cluster_; } + +private: + storages::postgres::ClusterPtr pg_cluster_; +}; + } // namespace easy +template <> +inline constexpr auto components::kConfigFileMode = ConfigFileMode::kNotRequired; + +template <> +inline constexpr auto components::kConfigFileMode = ConfigFileMode::kNotRequired; + USERVER_NAMESPACE_END diff --git a/libraries/easy/samples/0_hello_world/main.cpp b/libraries/easy/samples/0_hello_world/main.cpp index 34e890a1259b..c44b3ab6c10b 100644 --- a/libraries/easy/samples/0_hello_world/main.cpp +++ b/libraries/easy/samples/0_hello_world/main.cpp @@ -6,7 +6,7 @@ int main(int argc, char* argv[]) { easy::Http(argc, argv) // .DefaultContentType(http::content_type::kTextPlain) - .Route("/hello", [](const easy::HttpRequest& req) { + .Route("/hello", [](const easy::HttpRequest& /*req*/) { return "Hello world"; }) .Route("/hello/to/{user}", [](const easy::HttpRequest& req) { diff --git a/libraries/easy/src/easy.cpp b/libraries/easy/src/easy.cpp index 4c412bd67fd7..aa311c5d3b6c 100644 --- a/libraries/easy/src/easy.cpp +++ b/libraries/easy/src/easy.cpp @@ -6,9 +6,12 @@ #include #include +#include #include #include +#include + USERVER_NAMESPACE_BEGIN namespace easy { @@ -48,16 +51,33 @@ constexpr std::string_view kConfigHandlerTemplate = R"~( task_processor: main-task-processor # Run it on CPU bound task processor )~"; +class EmptyDeps final : public DependenciesBase { + using DependenciesBase::DependenciesBase; +}; + +const DependenciesBase& GetDeps(const components::ComponentConfig& config, const components::ComponentContext& context) { + const auto* deps = context.FindComponentOptional(); + if (deps) { + return *deps; + } + + static const EmptyDeps empty{config, context}; + return empty; +} -std::unordered_map g_http_functions_; +std::unordered_map g_http_functions_; std::optional default_content_type_; } // anonymous namespace + +DependenciesBase::~DependenciesBase() = default; + class Http::Handle final : public server::handlers::HttpHandlerBase { public: Handle(const components::ComponentConfig& config, const components::ComponentContext& context): HttpHandlerBase(config, context), + deps_{GetDeps(config, context)}, callback_{g_http_functions_[config.Name()]} {} @@ -65,10 +85,12 @@ class Http::Handle final : public server::handlers::HttpHandlerBase { if (default_content_type_) { request.GetHttpResponse().SetContentType(*default_content_type_); } - return callback_(request); + return callback_(request, deps_); } + private: - Http::Callback& callback_; + const DependenciesBase& deps_; + Callback::Underlying& callback_; }; Http::Http(int argc, const char *const argv[]) : argc_{argc}, argv_{argv}, static_config_{kConfigBase}, @@ -112,7 +134,7 @@ Http& Http::DefaultContentType(http::ContentType content_type) { } Http& Http::Route(std::string_view path, Callback&& func) { - g_http_functions_.emplace(path, std::move(func)); + g_http_functions_.emplace(path, std::move(func).Extract()); component_list_.Append(path); AddHandleConfig(path); @@ -123,6 +145,10 @@ void Http::AddHandleConfig(std::string_view path) { static_config_ += fmt::format(kConfigHandlerTemplate, path); } + +Postgres::Postgres(const components::ComponentConfig& config, const components::ComponentContext& context) +: DependenciesBase{config, context}, pg_cluster_(context.FindComponent("key-value-database").GetCluster()) {} + } // namespace easy USERVER_NAMESPACE_END From ad2bfd28a54ae624ab38c5fd8b9bc46987b7633e Mon Sep 17 00:00:00 2001 From: Antony Polukhin Date: Fri, 15 Nov 2024 19:20:14 +0300 Subject: [PATCH 07/19] WIP --- libraries/easy/include/userver/easy.hpp | 94 ++++++++++++------- libraries/easy/samples/0_hello_world/main.cpp | 2 +- libraries/easy/src/easy.cpp | 25 ++--- 3 files changed, 76 insertions(+), 45 deletions(-) diff --git a/libraries/easy/include/userver/easy.hpp b/libraries/easy/include/userver/easy.hpp index c3aa136668fd..5ac07b4df8dd 100644 --- a/libraries/easy/include/userver/easy.hpp +++ b/libraries/easy/include/userver/easy.hpp @@ -28,38 +28,26 @@ class DependenciesBase: public components::ComponentBase { virtual ~DependenciesBase(); }; -class Callback { -public: - using Underlying = std::function; - - template - Callback(Function func) { - if constexpr (std::is_invocable_r_v) { - func_ = std::move(func); - } else { - static_assert(std::is_invocable_r_v); - func_ = [f = std::move(func)](const HttpRequest& req, const DependenciesBase&) { - return f(req); - }; - } - } +namespace impl { - Underlying Extract() && noexcept { return std::move(func_); } +using UnderlyingCallback = std::function; -private: - std::function func_; -}; +struct AggressiveUnicorn; // Do not unleash + +template +using ConstDependencyRef = std::conditional_t, AggressiveUnicorn, Dependency> const&; -class Http final { +class HttpBase { public: - Http(int argc, const char *const argv[]); - ~Http(); + HttpBase(int argc, const char *const argv[]); + ~HttpBase(); - Http& DefaultContentType(http::ContentType content_type); + void DefaultContentType(http::ContentType content_type); + void Route(std::string_view path, UnderlyingCallback&& func); - Http& Route(std::string_view path, Callback&& func); private: void AddHandleConfig(std::string_view path); + void AddDependencyConfig(std::string_view config); class Handle; @@ -69,20 +57,62 @@ class Http final { components::ComponentList component_list_; }; -/// +} template -class CallbackWith : public Callback { +class Configurator; + +template <> +class Configurator {}; + +template +class HttpWith final : public Configurator { public: - using Callback::Callback; + class Callback { + public: + template + Callback(Function func); - CallbackWith(std::function func) - : Callback([f = std::move(func)](const HttpRequest& req, const DependenciesBase& deps) { - return f(req, static_cast(deps)); - }) - {} + auto Extract() && noexcept { return std::move(func_); } + + private: + impl::UnderlyingCallback func_; + }; + + HttpWith(int argc, const char *const argv[]) : impl_(argc, argv) {} + ~HttpWith() = default; + + HttpWith& DefaultContentType(http::ContentType content_type) { + impl_.DefaultContentType(content_type); + return *this; + } + + HttpWith& Route(std::string_view path, Callback&& func) { + impl_.Route(path, std::move(func).Extract()); + return *this; + } + +private: + impl::HttpBase impl_; }; +template +template +HttpWith::Callback::Callback(Function func) { + if constexpr (std::is_invocable_r_v) { + func_ = std::move(func); + } else if constexpr (std::is_invocable_r_v >) { + func_ = [f = std::move(func)](const HttpRequest& req, const DependenciesBase& deps) { + return f(req, static_cast(deps)); + }; + } else { + static_assert(std::is_invocable_r_v); + func_ = [f = std::move(func)](const HttpRequest& req, const DependenciesBase&) { + return f(req); + }; + } +} + class Postgres : public DependenciesBase { public: Postgres(const components::ComponentConfig& config, const components::ComponentContext& context); diff --git a/libraries/easy/samples/0_hello_world/main.cpp b/libraries/easy/samples/0_hello_world/main.cpp index c44b3ab6c10b..05fec54e35ee 100644 --- a/libraries/easy/samples/0_hello_world/main.cpp +++ b/libraries/easy/samples/0_hello_world/main.cpp @@ -4,7 +4,7 @@ #include int main(int argc, char* argv[]) { - easy::Http(argc, argv) // + easy::HttpWith(argc, argv) // .DefaultContentType(http::content_type::kTextPlain) .Route("/hello", [](const easy::HttpRequest& /*req*/) { return "Hello world"; diff --git a/libraries/easy/src/easy.cpp b/libraries/easy/src/easy.cpp index aa311c5d3b6c..ea9923bd4277 100644 --- a/libraries/easy/src/easy.cpp +++ b/libraries/easy/src/easy.cpp @@ -65,7 +65,7 @@ const DependenciesBase& GetDeps(const components::ComponentConfig& config, const return empty; } -std::unordered_map g_http_functions_; +std::unordered_map g_http_functions_; std::optional default_content_type_; } // anonymous namespace @@ -73,7 +73,9 @@ std::optional default_content_type_; DependenciesBase::~DependenciesBase() = default; -class Http::Handle final : public server::handlers::HttpHandlerBase { +namespace impl { + +class HttpBase::Handle final : public server::handlers::HttpHandlerBase { public: Handle(const components::ComponentConfig& config, const components::ComponentContext& context): HttpHandlerBase(config, context), @@ -90,13 +92,13 @@ class Http::Handle final : public server::handlers::HttpHandlerBase { private: const DependenciesBase& deps_; - Callback::Underlying& callback_; + impl::UnderlyingCallback& callback_; }; -Http::Http(int argc, const char *const argv[]) : argc_{argc}, argv_{argv}, static_config_{kConfigBase}, +HttpBase::HttpBase(int argc, const char *const argv[]) : argc_{argc}, argv_{argv}, static_config_{kConfigBase}, component_list_{components::MinimalServerComponentList()} {} -Http::~Http() { +HttpBase::~HttpBase() { namespace po = boost::program_options; po::variables_map vm; @@ -128,23 +130,22 @@ Http::~Http() { } } -Http& Http::DefaultContentType(http::ContentType content_type) { +void HttpBase::DefaultContentType(http::ContentType content_type) { default_content_type_ = content_type; - return *this; } -Http& Http::Route(std::string_view path, Callback&& func) { - g_http_functions_.emplace(path, std::move(func).Extract()); +void HttpBase::Route(std::string_view path, UnderlyingCallback&& func) { + g_http_functions_.emplace(path, std::move(func)); component_list_.Append(path); AddHandleConfig(path); - - return *this; } -void Http::AddHandleConfig(std::string_view path) { +void HttpBase::AddHandleConfig(std::string_view path) { static_config_ += fmt::format(kConfigHandlerTemplate, path); } +} // namespace impl + Postgres::Postgres(const components::ComponentConfig& config, const components::ComponentContext& context) : DependenciesBase{config, context}, pg_cluster_(context.FindComponent("key-value-database").GetCluster()) {} From 25eb3d6233e1915d0053549f6c9941a02512f234 Mon Sep 17 00:00:00 2001 From: Antony Polukhin Date: Fri, 15 Nov 2024 20:21:37 +0300 Subject: [PATCH 08/19] WIP --- libraries/easy/include/userver/easy.hpp | 62 +++++++++++++++---- libraries/easy/samples/0_hello_world/main.cpp | 2 +- .../samples/0_hello_world/tests/conftest.py | 20 ++++-- libraries/easy/src/easy.cpp | 41 +++++++++--- 4 files changed, 101 insertions(+), 24 deletions(-) diff --git a/libraries/easy/include/userver/easy.hpp b/libraries/easy/include/userver/easy.hpp index 5ac07b4df8dd..573858d46e8d 100644 --- a/libraries/easy/include/userver/easy.hpp +++ b/libraries/easy/include/userver/easy.hpp @@ -45,9 +45,18 @@ class HttpBase { void DefaultContentType(http::ContentType content_type); void Route(std::string_view path, UnderlyingCallback&& func); + void AddComponentsConfig(std::string_view config); + + template + void AppendComponent(std::string_view name) { + component_list_.Append(name); + } + template + void AppendComponent() { + component_list_.Append(); + } private: void AddHandleConfig(std::string_view path); - void AddDependencyConfig(std::string_view config); class Handle; @@ -59,14 +68,22 @@ class HttpBase { } -template -class Configurator; +template +class HttpWith; -template <> -class Configurator {}; +inline void DependenciesRegistration(HttpWith&) {} -template -class HttpWith final : public Configurator { +template +inline void DependenciesRegistration(HttpWith&) { + static_assert(sizeof(T) && false, "Define `void DependenciesRegistration(HttpWith& app)` " + "in the namespace of the your type `T` to automatically add required configurations and " + "components to the `app` for your dependences type `T`. For example:" + "void DependenciesRegistration(HttpWith& app) { app.AppendComponent(); }" + ); +} + +template +class HttpWith final { public: class Callback { public: @@ -80,7 +97,10 @@ class HttpWith final : public Configurator { }; HttpWith(int argc, const char *const argv[]) : impl_(argc, argv) {} - ~HttpWith() = default; + + ~HttpWith() { + DependenciesRegistration(*this); // ADL is intentional + } HttpWith& DefaultContentType(http::ContentType content_type) { impl_.DefaultContentType(content_type); @@ -92,6 +112,23 @@ class HttpWith final : public Configurator { return *this; } + HttpWith& AddComponentsConfig(std::string_view config) { + impl_.AddComponentsConfig(config); + return *this; + } + + template + HttpWith& AppendComponent(std::string_view name) { + impl_.AppendComponent(name); + return *this; + } + + template + HttpWith& AppendComponent() { + impl_.AppendComponent(); + return *this; + } + private: impl::HttpBase impl_; }; @@ -113,9 +150,9 @@ HttpWith::Callback::Callback(Function func) { } } -class Postgres : public DependenciesBase { +class PgDep : public DependenciesBase { public: - Postgres(const components::ComponentConfig& config, const components::ComponentContext& context); + PgDep(const components::ComponentConfig& config, const components::ComponentContext& context); storages::postgres::Cluster& pg() const noexcept { return *pg_cluster_; } @@ -123,12 +160,15 @@ class Postgres : public DependenciesBase { storages::postgres::ClusterPtr pg_cluster_; }; +void DependenciesRegistration(HttpWith& app); + + } // namespace easy template <> inline constexpr auto components::kConfigFileMode = ConfigFileMode::kNotRequired; template <> -inline constexpr auto components::kConfigFileMode = ConfigFileMode::kNotRequired; +inline constexpr auto components::kConfigFileMode = ConfigFileMode::kNotRequired; USERVER_NAMESPACE_END diff --git a/libraries/easy/samples/0_hello_world/main.cpp b/libraries/easy/samples/0_hello_world/main.cpp index 05fec54e35ee..807bc59222ed 100644 --- a/libraries/easy/samples/0_hello_world/main.cpp +++ b/libraries/easy/samples/0_hello_world/main.cpp @@ -4,7 +4,7 @@ #include int main(int argc, char* argv[]) { - easy::HttpWith(argc, argv) // + easy::HttpWith(argc, argv) // .DefaultContentType(http::content_type::kTextPlain) .Route("/hello", [](const easy::HttpRequest& /*req*/) { return "Hello world"; diff --git a/libraries/easy/samples/0_hello_world/tests/conftest.py b/libraries/easy/samples/0_hello_world/tests/conftest.py index 41ffab87fc60..0c2173fcc3cc 100644 --- a/libraries/easy/samples/0_hello_world/tests/conftest.py +++ b/libraries/easy/samples/0_hello_world/tests/conftest.py @@ -1,4 +1,16 @@ -# /// [registration] -# Adding a plugin from userver/testsuite/pytest_plugins/ -pytest_plugins = ['pytest_userver.plugins.core'] -# /// [registration] +import pytest + +from testsuite.databases.pgsql import discover + +pytest_plugins = ['pytest_userver.plugins.postgresql'] + +@pytest.fixture(scope='session') +def pgsql_local(service_source_dir, pgsql_local_create): + with open(service_source_dir.joinpath('schemas/postgresql/1.sql'), 'w') as f: + f.write('CREATE TABLE FOO(int);') + + databases = discover.find_schemas( + 'admin', [service_source_dir.joinpath('schemas/postgresql')], + ) + return pgsql_local_create(list(databases.values())) + diff --git a/libraries/easy/src/easy.cpp b/libraries/easy/src/easy.cpp index ea9923bd4277..75c9bd6a206c 100644 --- a/libraries/easy/src/easy.cpp +++ b/libraries/easy/src/easy.cpp @@ -3,13 +3,15 @@ #include #include +#include #include #include #include #include +#include +#include #include - #include USERVER_NAMESPACE_BEGIN @@ -45,10 +47,10 @@ constexpr std::string_view kConfigBase = R"~( constexpr std::string_view kConfigHandlerTemplate = R"~( - {0}: - path: {0} # Registering handler by URL '{0}'. - method: GET,POST # It will only reply to GET (HEAD) and POST requests. - task_processor: main-task-processor # Run it on CPU bound task processor +{0}: + path: {0} # Registering handler by URL '{0}'. + method: GET,POST # It will only reply to GET (HEAD) and POST requests. + task_processor: main-task-processor # Run it on CPU bound task processor )~"; class EmptyDeps final : public DependenciesBase { @@ -141,14 +143,37 @@ void HttpBase::Route(std::string_view path, UnderlyingCallback&& func) { } void HttpBase::AddHandleConfig(std::string_view path) { - static_config_ += fmt::format(kConfigHandlerTemplate, path); + AddComponentsConfig(fmt::format(kConfigHandlerTemplate, path)); +} + +void HttpBase::AddComponentsConfig(std::string_view config) { + static_config_ += boost::algorithm::replace_all_copy(std::string{config}, "\n", "\n "); } } // namespace impl -Postgres::Postgres(const components::ComponentConfig& config, const components::ComponentContext& context) -: DependenciesBase{config, context}, pg_cluster_(context.FindComponent("key-value-database").GetCluster()) {} +PgDep::PgDep(const components::ComponentConfig& config, const components::ComponentContext& context) +: DependenciesBase{config, context}, pg_cluster_(context.FindComponent("postgres").GetCluster()) {} + +void DependenciesRegistration(HttpWith& app) { + app.AddComponentsConfig(R"~( +postgres: + dbconnection#env: POSTGRESQL + dbconnection#fallback: 'postgresql://testsuite@localhost:15433/postgres' + blocking_task_processor: fs-task-processor + dns_resolver: async + +testsuite-support: + +dns-client: + fs-task-processor: fs-task-processor +)~"); + + app.AppendComponent("postgres"); + app.AppendComponent(); + app.AppendComponent(); +} } // namespace easy From d71efb0a79f5d8081b435e82de66193e075e0f6a Mon Sep 17 00:00:00 2001 From: Antony Polukhin Date: Sun, 17 Nov 2024 11:42:15 +0300 Subject: [PATCH 09/19] WIP --- cmake/install/userver-easy-config.cmake | 5 +---- libraries/easy/include/userver/easy.hpp | 10 +++++++++- libraries/easy/samples/0_hello_world/main.cpp | 18 ++++++++++++++++++ .../samples/0_hello_world/tests/conftest.py | 18 ++++++++++-------- libraries/easy/src/easy.cpp | 8 +++++++- 5 files changed, 45 insertions(+), 14 deletions(-) diff --git a/cmake/install/userver-easy-config.cmake b/cmake/install/userver-easy-config.cmake index 40caf93a1c06..3f8b8775bcdb 100644 --- a/cmake/install/userver-easy-config.cmake +++ b/cmake/install/userver-easy-config.cmake @@ -4,10 +4,7 @@ if(userver_easy_FOUND) return() endif() -find_package(userver REQUIRED COMPONENTS - core - postgresql -) +find_package(userver REQUIRED COMPONENTS postgresql) set(userver_easy_FOUND TRUE) diff --git a/libraries/easy/include/userver/easy.hpp b/libraries/easy/include/userver/easy.hpp index 573858d46e8d..acbecd1ba0db 100644 --- a/libraries/easy/include/userver/easy.hpp +++ b/libraries/easy/include/userver/easy.hpp @@ -55,6 +55,8 @@ class HttpBase { void AppendComponent() { component_list_.Append(); } + + void Schema(std::string_view schema) { schema_ = schema; } private: void AddHandleConfig(std::string_view path); @@ -63,6 +65,7 @@ class HttpBase { const int argc_; const char *const* argv_; std::string static_config_; + std::string schema_; components::ComponentList component_list_; }; @@ -128,6 +131,11 @@ class HttpWith final { impl_.AppendComponent(); return *this; } + + HttpWith& Schema(std::string_view schema) { + impl_.Schema(schema); + return *this; + } private: impl::HttpBase impl_; @@ -140,7 +148,7 @@ HttpWith::Callback::Callback(Function func) { func_ = std::move(func); } else if constexpr (std::is_invocable_r_v >) { func_ = [f = std::move(func)](const HttpRequest& req, const DependenciesBase& deps) { - return f(req, static_cast(deps)); + return f(req, static_cast(deps)); }; } else { static_assert(std::is_invocable_r_v); diff --git a/libraries/easy/samples/0_hello_world/main.cpp b/libraries/easy/samples/0_hello_world/main.cpp index 807bc59222ed..63e2354b759b 100644 --- a/libraries/easy/samples/0_hello_world/main.cpp +++ b/libraries/easy/samples/0_hello_world/main.cpp @@ -3,8 +3,16 @@ #include +constexpr std::string_view kSchema = R"~( +CREATE TABLE IF NOT EXISTS key_value_table ( + key VARCHAR PRIMARY KEY, + value VARCHAR +) +)~"; + int main(int argc, char* argv[]) { easy::HttpWith(argc, argv) // + .Schema(kSchema) .DefaultContentType(http::content_type::kTextPlain) .Route("/hello", [](const easy::HttpRequest& /*req*/) { return "Hello world"; @@ -14,5 +22,15 @@ int main(int argc, char* argv[]) { }) .Route("/hi", [](const easy::HttpRequest& req) { return "Hi, " + req.GetArg("name"); + }) + .Route("/kv", [](const easy::HttpRequest& req, const easy::PgDep& dep) { + const auto& key = req.GetArg("key"); + if (req.GetMethod() == server::http::HttpMethod::kGet) { + auto res = dep.pg().Execute(storages::postgres::ClusterHostType::kSlave, "SELECT value FROM key_value_table WHERE key=$1", key); + return res[0][0].As(); + } else { + dep.pg().Execute(storages::postgres::ClusterHostType::kMaster, "INSERT INTO key_value_table VALUES($1, $2) ON CONFLICT DO UPDATE", key, req.GetArg("name")); + return {}; + } }); } diff --git a/libraries/easy/samples/0_hello_world/tests/conftest.py b/libraries/easy/samples/0_hello_world/tests/conftest.py index 0c2173fcc3cc..4b1bea98ca3f 100644 --- a/libraries/easy/samples/0_hello_world/tests/conftest.py +++ b/libraries/easy/samples/0_hello_world/tests/conftest.py @@ -1,16 +1,18 @@ +# /// [psql prepare] +import os import pytest +import subprocess from testsuite.databases.pgsql import discover pytest_plugins = ['pytest_userver.plugins.postgresql'] -@pytest.fixture(scope='session') -def pgsql_local(service_source_dir, pgsql_local_create): - with open(service_source_dir.joinpath('schemas/postgresql/1.sql'), 'w') as f: - f.write('CREATE TABLE FOO(int);') - databases = discover.find_schemas( - 'admin', [service_source_dir.joinpath('schemas/postgresql')], - ) +@pytest.fixture(scope='session') +def pgsql_local(service_tmpdir, service_binary, pgsql_local_create): + schema_path = service_tmpdir.joinpath('schemas') + os.mkdir(schema_path) + subprocess.run([service_binary, '--dump-schema', schema_path]) + databases = discover.find_schemas('admin', [schema_path]) return pgsql_local_create(list(databases.values())) - + # /// [psql prepare] diff --git a/libraries/easy/src/easy.cpp b/libraries/easy/src/easy.cpp index 75c9bd6a206c..4f6410a001d8 100644 --- a/libraries/easy/src/easy.cpp +++ b/libraries/easy/src/easy.cpp @@ -106,10 +106,12 @@ HttpBase::~HttpBase() { po::variables_map vm; po::options_description desc("Easy options"); std::string config_dump; + std::string schema_dump; // clang-format off desc.add_options() ("dump-config", po::value(&config_dump), "path to dump the server config") + ("dump-schema", po::value(&schema_dump), "path to dump the DB schema") ; // clang-format on @@ -120,7 +122,11 @@ HttpBase::~HttpBase() { std::ofstream(config_dump) << static_config_; return; } - + + if (vm.count("dump-schema")) { + std::ofstream(schema_dump + "/0_pg.sql") << schema_; + } + if (argc_ <= 1) { components::Run(components::InMemoryConfig{static_config_}, component_list_); return; From 1c8b3ec0a6edd5427dd899359467922f1182894f Mon Sep 17 00:00:00 2001 From: Antony Polukhin Date: Sun, 17 Nov 2024 20:47:05 +0300 Subject: [PATCH 10/19] its alive! --- libraries/easy/include/userver/easy.hpp | 10 +++++++--- libraries/easy/samples/0_hello_world/main.cpp | 4 ++-- .../samples/0_hello_world/tests/test_basic.py | 15 +++++++++++++++ libraries/easy/src/easy.cpp | 16 ++++++++++++---- 4 files changed, 36 insertions(+), 9 deletions(-) diff --git a/libraries/easy/include/userver/easy.hpp b/libraries/easy/include/userver/easy.hpp index acbecd1ba0db..6f9cc85b96ae 100644 --- a/libraries/easy/include/userver/easy.hpp +++ b/libraries/easy/include/userver/easy.hpp @@ -56,7 +56,7 @@ class HttpBase { component_list_.Append(); } - void Schema(std::string_view schema) { schema_ = schema; } + void Schema(std::string_view schema); private: void AddHandleConfig(std::string_view path); @@ -65,7 +65,6 @@ class HttpBase { const int argc_; const char *const* argv_; std::string static_config_; - std::string schema_; components::ComponentList component_list_; }; @@ -99,7 +98,12 @@ class HttpWith final { impl::UnderlyingCallback func_; }; - HttpWith(int argc, const char *const argv[]) : impl_(argc, argv) {} + HttpWith(int argc, const char *const argv[]) : impl_(argc, argv) { + if constexpr (!std::is_void_v) { + impl_.AppendComponent(); + impl_.AddComponentsConfig(std::string{Dependency::kName} + ": {}"); + } + } ~HttpWith() { DependenciesRegistration(*this); // ADL is intentional diff --git a/libraries/easy/samples/0_hello_world/main.cpp b/libraries/easy/samples/0_hello_world/main.cpp index 63e2354b759b..76786528e91d 100644 --- a/libraries/easy/samples/0_hello_world/main.cpp +++ b/libraries/easy/samples/0_hello_world/main.cpp @@ -29,8 +29,8 @@ int main(int argc, char* argv[]) { auto res = dep.pg().Execute(storages::postgres::ClusterHostType::kSlave, "SELECT value FROM key_value_table WHERE key=$1", key); return res[0][0].As(); } else { - dep.pg().Execute(storages::postgres::ClusterHostType::kMaster, "INSERT INTO key_value_table VALUES($1, $2) ON CONFLICT DO UPDATE", key, req.GetArg("name")); - return {}; + dep.pg().Execute(storages::postgres::ClusterHostType::kMaster, "INSERT INTO key_value_table(key, value) VALUES($1, $2) ON CONFLICT (key) DO UPDATE SET value = $2", key, req.GetArg("value")); + return std::string{}; } }); } diff --git a/libraries/easy/samples/0_hello_world/tests/test_basic.py b/libraries/easy/samples/0_hello_world/tests/test_basic.py index 696228ccff0c..97d9d1735839 100644 --- a/libraries/easy/samples/0_hello_world/tests/test_basic.py +++ b/libraries/easy/samples/0_hello_world/tests/test_basic.py @@ -17,3 +17,18 @@ async def test_hi(service_client): assert response.status == 200 assert 'text/plain' in response.headers['Content-Type'] assert response.text == 'Hi, pal' + +async def test_kv(service_client): + response = await service_client.post('/kv?key=1&value=one') + assert response.status == 200 + + response = await service_client.get('/kv?key=1') + assert response.status == 200 + assert response.text == 'one' + + response = await service_client.post('/kv?key=1&value=again_1') + assert response.status == 200 + + response = await service_client.get('/kv?key=1') + assert response.status == 200 + assert response.text == 'again_1' diff --git a/libraries/easy/src/easy.cpp b/libraries/easy/src/easy.cpp index 4f6410a001d8..922c8f88efc9 100644 --- a/libraries/easy/src/easy.cpp +++ b/libraries/easy/src/easy.cpp @@ -49,7 +49,7 @@ constexpr std::string_view kConfigBase = R"~( constexpr std::string_view kConfigHandlerTemplate = R"~( {0}: path: {0} # Registering handler by URL '{0}'. - method: GET,POST # It will only reply to GET (HEAD) and POST requests. + method: GET,PUT,POST,DELETE,PATCH task_processor: main-task-processor # Run it on CPU bound task processor )~"; @@ -58,7 +58,8 @@ class EmptyDeps final : public DependenciesBase { }; const DependenciesBase& GetDeps(const components::ComponentConfig& config, const components::ComponentContext& context) { - const auto* deps = context.FindComponentOptional(); + const auto* deps = context. + FindComponentOptional(); if (deps) { return *deps; } @@ -69,6 +70,7 @@ const DependenciesBase& GetDeps(const components::ComponentConfig& config, const std::unordered_map g_http_functions_; std::optional default_content_type_; +std::string schema_; } // anonymous namespace @@ -153,14 +155,20 @@ void HttpBase::AddHandleConfig(std::string_view path) { } void HttpBase::AddComponentsConfig(std::string_view config) { - static_config_ += boost::algorithm::replace_all_copy(std::string{config}, "\n", "\n "); + auto conf = fmt::format("\n{}\n", config); + static_config_ += boost::algorithm::replace_all_copy(conf, "\n", "\n "); } +void HttpBase::Schema(std::string_view schema) { schema_ = schema; } + } // namespace impl PgDep::PgDep(const components::ComponentConfig& config, const components::ComponentContext& context) -: DependenciesBase{config, context}, pg_cluster_(context.FindComponent("postgres").GetCluster()) {} +: DependenciesBase{config, context}, pg_cluster_(context.FindComponent("postgres").GetCluster()) +{ + pg_cluster_->Execute(storages::postgres::ClusterHostType::kMaster, schema_); +} void DependenciesRegistration(HttpWith& app) { app.AddComponentsConfig(R"~( From 3fe07788b03af65574de0f488e080fc49e170e8e Mon Sep 17 00:00:00 2001 From: Antony Polukhin Date: Sun, 17 Nov 2024 22:30:27 +0300 Subject: [PATCH 11/19] cleanup --- libraries/easy/include/userver/easy.hpp | 41 +++++++++---------------- libraries/easy/src/easy.cpp | 18 ++--------- 2 files changed, 17 insertions(+), 42 deletions(-) diff --git a/libraries/easy/include/userver/easy.hpp b/libraries/easy/include/userver/easy.hpp index 6f9cc85b96ae..5f67f7946ba8 100644 --- a/libraries/easy/include/userver/easy.hpp +++ b/libraries/easy/include/userver/easy.hpp @@ -3,7 +3,6 @@ #include #include #include -#include #include #include @@ -32,11 +31,6 @@ namespace impl { using UnderlyingCallback = std::function; -struct AggressiveUnicorn; // Do not unleash - -template -using ConstDependencyRef = std::conditional_t, AggressiveUnicorn, Dependency> const&; - class HttpBase { public: HttpBase(int argc, const char *const argv[]); @@ -70,21 +64,7 @@ class HttpBase { } -template -class HttpWith; - -inline void DependenciesRegistration(HttpWith&) {} - -template -inline void DependenciesRegistration(HttpWith&) { - static_assert(sizeof(T) && false, "Define `void DependenciesRegistration(HttpWith& app)` " - "in the namespace of the your type `T` to automatically add required configurations and " - "components to the `app` for your dependences type `T`. For example:" - "void DependenciesRegistration(HttpWith& app) { app.AppendComponent(); }" - ); -} - -template +template class HttpWith final { public: class Callback { @@ -99,10 +79,8 @@ class HttpWith final { }; HttpWith(int argc, const char *const argv[]) : impl_(argc, argv) { - if constexpr (!std::is_void_v) { - impl_.AppendComponent(); - impl_.AddComponentsConfig(std::string{Dependency::kName} + ": {}"); - } + impl_.AppendComponent(); + impl_.AddComponentsConfig(std::string{Dependency::kName} + ": {}"); } ~HttpWith() { @@ -145,12 +123,23 @@ class HttpWith final { impl::HttpBase impl_; }; +template +inline void DependenciesRegistration(HttpWith&) { + static_assert(sizeof(T) && false, "Define `void DependenciesRegistration(HttpWith& app)` " + "in the namespace of the your type `T` to automatically add required configurations and " + "components to the `app` for your dependences type `T`. For example:" + "void DependenciesRegistration(HttpWith& app) { app.AppendComponent(); }" + ); +} + +inline void DependenciesRegistration(HttpWith&) {} + template template HttpWith::Callback::Callback(Function func) { if constexpr (std::is_invocable_r_v) { func_ = std::move(func); - } else if constexpr (std::is_invocable_r_v >) { + } else if constexpr (std::is_invocable_r_v) { func_ = [f = std::move(func)](const HttpRequest& req, const DependenciesBase& deps) { return f(req, static_cast(deps)); }; diff --git a/libraries/easy/src/easy.cpp b/libraries/easy/src/easy.cpp index 922c8f88efc9..21c94a7128b4 100644 --- a/libraries/easy/src/easy.cpp +++ b/libraries/easy/src/easy.cpp @@ -1,6 +1,7 @@ #include #include +#include #include #include @@ -53,21 +54,6 @@ constexpr std::string_view kConfigHandlerTemplate = R"~( task_processor: main-task-processor # Run it on CPU bound task processor )~"; -class EmptyDeps final : public DependenciesBase { - using DependenciesBase::DependenciesBase; -}; - -const DependenciesBase& GetDeps(const components::ComponentConfig& config, const components::ComponentContext& context) { - const auto* deps = context. - FindComponentOptional(); - if (deps) { - return *deps; - } - - static const EmptyDeps empty{config, context}; - return empty; -} - std::unordered_map g_http_functions_; std::optional default_content_type_; std::string schema_; @@ -83,7 +69,7 @@ class HttpBase::Handle final : public server::handlers::HttpHandlerBase { public: Handle(const components::ComponentConfig& config, const components::ComponentContext& context): HttpHandlerBase(config, context), - deps_{GetDeps(config, context)}, + deps_{context.FindComponent()}, callback_{g_http_functions_[config.Name()]} {} From a5dd59137216742fb3a780092eb0aa79485614e0 Mon Sep 17 00:00:00 2001 From: Antony Polukhin Date: Tue, 19 Nov 2024 10:36:42 +0300 Subject: [PATCH 12/19] cleanup and samples --- libraries/easy/include/userver/easy.hpp | 140 ++++++++++++------ libraries/easy/samples/0_hello_world/main.cpp | 34 +---- .../samples/0_hello_world/tests/conftest.py | 20 +-- .../samples/0_hello_world/tests/test_basic.py | 33 +---- libraries/easy/samples/1_hi/CMakeLists.txt | 6 + libraries/easy/samples/1_hi/main.cpp | 20 +++ libraries/easy/samples/1_hi/tests/conftest.py | 4 + .../easy/samples/1_hi/tests/test_basic.py | 12 ++ .../easy/samples/2_key_value/CMakeLists.txt | 6 + libraries/easy/samples/2_key_value/main.cpp | 37 +++++ .../samples/2_key_value/tests/conftest.py | 18 +++ .../samples/2_key_value/tests/test_basic.py | 14 ++ libraries/easy/samples/CMakeLists.txt | 6 + libraries/easy/src/easy.cpp | 113 +++++++------- 14 files changed, 286 insertions(+), 177 deletions(-) create mode 100644 libraries/easy/samples/1_hi/CMakeLists.txt create mode 100644 libraries/easy/samples/1_hi/main.cpp create mode 100644 libraries/easy/samples/1_hi/tests/conftest.py create mode 100644 libraries/easy/samples/1_hi/tests/test_basic.py create mode 100644 libraries/easy/samples/2_key_value/CMakeLists.txt create mode 100644 libraries/easy/samples/2_key_value/main.cpp create mode 100644 libraries/easy/samples/2_key_value/tests/conftest.py create mode 100644 libraries/easy/samples/2_key_value/tests/test_basic.py diff --git a/libraries/easy/include/userver/easy.hpp b/libraries/easy/include/userver/easy.hpp index 5f67f7946ba8..0220f96e267d 100644 --- a/libraries/easy/include/userver/easy.hpp +++ b/libraries/easy/include/userver/easy.hpp @@ -4,8 +4,8 @@ #include #include -#include #include +#include #include #include #include @@ -17,27 +17,22 @@ USERVER_NAMESPACE_BEGIN /// @brief Top namespace for `easy` library namespace easy { -using HttpRequest = server::http::HttpRequest; -using RequestContext = server::request::RequestContext; - -class DependenciesBase: public components::ComponentBase { +class DependenciesBase : public components::ComponentBase { public: static constexpr std::string_view kName = "easy-dependencies"; using components::ComponentBase::ComponentBase; virtual ~DependenciesBase(); }; -namespace impl { - -using UnderlyingCallback = std::function; +template +struct OfDependency final {}; -class HttpBase { +class HttpBase final { public: - HttpBase(int argc, const char *const argv[]); - ~HttpBase(); - + using Callback = std::function; + void DefaultContentType(http::ContentType content_type); - void Route(std::string_view path, UnderlyingCallback&& func); + void Route(std::string_view path, Callback&& func, std::initializer_list methods); void AddComponentsConfig(std::string_view config); @@ -49,51 +44,88 @@ class HttpBase { void AppendComponent() { component_list_.Append(); } - + void Schema(std::string_view schema); + void Port(std::uint16_t port); + void LogLevel(logging::Level level); + private: - void AddHandleConfig(std::string_view path); + template + friend class HttpWith; + + HttpBase(int argc, const char* const argv[]); + ~HttpBase(); class Handle; const int argc_; - const char *const* argv_; + const char* const* argv_; std::string static_config_; components::ComponentList component_list_; -}; -} + std::uint16_t port_ = 8080; + logging::Level level_ = logging::Level::kDebug; +}; template class HttpWith final { public: - class Callback { + class Callback final { public: template Callback(Function func); - auto Extract() && noexcept { return std::move(func_); } + HttpBase::Callback Extract() && noexcept { return std::move(func_); } private: - impl::UnderlyingCallback func_; + HttpBase::Callback func_; }; - HttpWith(int argc, const char *const argv[]) : impl_(argc, argv) { - impl_.AppendComponent(); - impl_.AddComponentsConfig(std::string{Dependency::kName} + ": {}"); + HttpWith(int argc, const char* const argv[]) : impl_(argc, argv) { impl_.AppendComponent(); } + ~HttpWith() { Registration(OfDependency{}, impl_); /* ADL is intentional */ } + + HttpWith& DefaultContentType(http::ContentType content_type) { + return (impl_.DefaultContentType(content_type), *this); } - ~HttpWith() { - DependenciesRegistration(*this); // ADL is intentional + HttpWith& Route( + std::string_view path, + Callback&& func, + std::initializer_list methods = + { + server::http::HttpMethod::kGet, + server::http::HttpMethod::kPost, + server::http::HttpMethod::kDelete, + server::http::HttpMethod::kPut, + server::http::HttpMethod::kPatch, + } + ) { + impl_.Route(path, std::move(func).Extract(), methods); + return *this; } - - HttpWith& DefaultContentType(http::ContentType content_type) { - impl_.DefaultContentType(content_type); + + HttpWith& Get(std::string_view path, Callback&& func) { + impl_.Route(path, std::move(func).Extract(), {server::http::HttpMethod::kGet}); return *this; } - HttpWith& Route(std::string_view path, Callback&& func) { - impl_.Route(path, std::move(func).Extract()); + HttpWith& Post(std::string_view path, Callback&& func) { + impl_.Route(path, std::move(func).Extract(), {server::http::HttpMethod::kPost}); + return *this; + } + + HttpWith& Del(std::string_view path, Callback&& func) { + impl_.Route(path, std::move(func).Extract(), {server::http::HttpMethod::kDelete}); + return *this; + } + + HttpWith& Put(std::string_view path, Callback&& func) { + impl_.Route(path, std::move(func).Extract(), {server::http::HttpMethod::kPut}); + return *this; + } + + HttpWith& Patch(std::string_view path, Callback&& func) { + impl_.Route(path, std::move(func).Extract(), {server::http::HttpMethod::kPatch}); return *this; } @@ -113,41 +145,52 @@ class HttpWith final { impl_.AppendComponent(); return *this; } - + HttpWith& Schema(std::string_view schema) { impl_.Schema(schema); return *this; } + HttpWith& Port(std::uint16_t port) { + impl_.Port(port); + return *this; + } + + HttpWith& LogLevel(logging::Level level) { + impl_.LogLevel(level); + return *this; + } + private: - impl::HttpBase impl_; + HttpBase impl_; }; -template -inline void DependenciesRegistration(HttpWith&) { - static_assert(sizeof(T) && false, "Define `void DependenciesRegistration(HttpWith& app)` " - "in the namespace of the your type `T` to automatically add required configurations and " - "components to the `app` for your dependences type `T`. For example:" - "void DependenciesRegistration(HttpWith& app) { app.AppendComponent(); }" +template +inline void Registration(T, HttpBase&&) { + static_assert( + sizeof(T) && false, + "Define `void Registration(easy::OfDependency, HttpBase& app)` " + "in the namespace of the your dependency type `T` to automatically add required configurations and " + "components to the `app`. For example:" + "void Registration(easy::OfDependency, easy::HttpBase& app) { app.AppendComponent(); }" ); } -inline void DependenciesRegistration(HttpWith&) {} +inline void Registration(OfDependency, HttpBase&) {} template template HttpWith::Callback::Callback(Function func) { - if constexpr (std::is_invocable_r_v) { - func_ = std::move(func); - } else if constexpr (std::is_invocable_r_v) { + using server::http::HttpRequest; + if constexpr (std::is_invocable_r_v) { func_ = [f = std::move(func)](const HttpRequest& req, const DependenciesBase& deps) { return f(req, static_cast(deps)); }; + } else if constexpr (std::is_invocable_r_v) { + func_ = std::move(func); } else { - static_assert(std::is_invocable_r_v); - func_ = [f = std::move(func)](const HttpRequest& req, const DependenciesBase&) { - return f(req); - }; + static_assert(std::is_invocable_r_v); + func_ = [f = std::move(func)](const HttpRequest& req, const DependenciesBase&) { return f(req); }; } } @@ -161,8 +204,7 @@ class PgDep : public DependenciesBase { storages::postgres::ClusterPtr pg_cluster_; }; -void DependenciesRegistration(HttpWith& app); - +void Registration(OfDependency, HttpBase& app); } // namespace easy diff --git a/libraries/easy/samples/0_hello_world/main.cpp b/libraries/easy/samples/0_hello_world/main.cpp index 76786528e91d..c50340d8a070 100644 --- a/libraries/easy/samples/0_hello_world/main.cpp +++ b/libraries/easy/samples/0_hello_world/main.cpp @@ -3,34 +3,10 @@ #include -constexpr std::string_view kSchema = R"~( -CREATE TABLE IF NOT EXISTS key_value_table ( - key VARCHAR PRIMARY KEY, - value VARCHAR -) -)~"; - int main(int argc, char* argv[]) { - easy::HttpWith(argc, argv) // - .Schema(kSchema) - .DefaultContentType(http::content_type::kTextPlain) - .Route("/hello", [](const easy::HttpRequest& /*req*/) { - return "Hello world"; - }) - .Route("/hello/to/{user}", [](const easy::HttpRequest& req) { - return "Hello, " + req.GetPathArg("user"); - }) - .Route("/hi", [](const easy::HttpRequest& req) { - return "Hi, " + req.GetArg("name"); - }) - .Route("/kv", [](const easy::HttpRequest& req, const easy::PgDep& dep) { - const auto& key = req.GetArg("key"); - if (req.GetMethod() == server::http::HttpMethod::kGet) { - auto res = dep.pg().Execute(storages::postgres::ClusterHostType::kSlave, "SELECT value FROM key_value_table WHERE key=$1", key); - return res[0][0].As(); - } else { - dep.pg().Execute(storages::postgres::ClusterHostType::kMaster, "INSERT INTO key_value_table(key, value) VALUES($1, $2) ON CONFLICT (key) DO UPDATE SET value = $2", key, req.GetArg("value")); - return std::string{}; - } - }); + easy::HttpWith(argc, argv) + .DefaultContentType(http::content_type::kTextPlain) + .Route("/hello", [](const server::http::HttpRequest& /*req*/) { + return "Hello world"; // Just return the string as a response body + }); } diff --git a/libraries/easy/samples/0_hello_world/tests/conftest.py b/libraries/easy/samples/0_hello_world/tests/conftest.py index 4b1bea98ca3f..87356961ffd6 100644 --- a/libraries/easy/samples/0_hello_world/tests/conftest.py +++ b/libraries/easy/samples/0_hello_world/tests/conftest.py @@ -1,18 +1,4 @@ # /// [psql prepare] -import os -import pytest -import subprocess - -from testsuite.databases.pgsql import discover - -pytest_plugins = ['pytest_userver.plugins.postgresql'] - - -@pytest.fixture(scope='session') -def pgsql_local(service_tmpdir, service_binary, pgsql_local_create): - schema_path = service_tmpdir.joinpath('schemas') - os.mkdir(schema_path) - subprocess.run([service_binary, '--dump-schema', schema_path]) - databases = discover.find_schemas('admin', [schema_path]) - return pgsql_local_create(list(databases.values())) - # /// [psql prepare] +# Use only core plugin +pytest_plugins = ["pytest_userver.plugins.core"] +# /// [psql prepare] diff --git a/libraries/easy/samples/0_hello_world/tests/test_basic.py b/libraries/easy/samples/0_hello_world/tests/test_basic.py index 97d9d1735839..c7490424d6c7 100644 --- a/libraries/easy/samples/0_hello_world/tests/test_basic.py +++ b/libraries/easy/samples/0_hello_world/tests/test_basic.py @@ -1,34 +1,7 @@ # /// [Functional test] async def test_hello_simple(service_client): - response = await service_client.get('/hello') + response = await service_client.get("/hello") assert response.status == 200 - assert 'text/plain' in response.headers['Content-Type'] - assert response.text == 'Hello world' + assert "text/plain" in response.headers["Content-Type"] + assert response.text == "Hello world" # /// [Functional test] - -async def test_hello_to(service_client): - response = await service_client.get('/hello/to/flusk') - assert response.status == 200 - assert 'text/plain' in response.headers['Content-Type'] - assert response.text == 'Hello, flusk' - -async def test_hi(service_client): - response = await service_client.get('/hi?name=pal') - assert response.status == 200 - assert 'text/plain' in response.headers['Content-Type'] - assert response.text == 'Hi, pal' - -async def test_kv(service_client): - response = await service_client.post('/kv?key=1&value=one') - assert response.status == 200 - - response = await service_client.get('/kv?key=1') - assert response.status == 200 - assert response.text == 'one' - - response = await service_client.post('/kv?key=1&value=again_1') - assert response.status == 200 - - response = await service_client.get('/kv?key=1') - assert response.status == 200 - assert response.text == 'again_1' diff --git a/libraries/easy/samples/1_hi/CMakeLists.txt b/libraries/easy/samples/1_hi/CMakeLists.txt new file mode 100644 index 000000000000..0dba282c82e0 --- /dev/null +++ b/libraries/easy/samples/1_hi/CMakeLists.txt @@ -0,0 +1,6 @@ +project(userver-easy-samples-hi CXX) + +add_executable(${PROJECT_NAME} "main.cpp") +target_link_libraries(${PROJECT_NAME} userver::easy) + +userver_testsuite_add_simple(DUMP_CONFIG True) diff --git a/libraries/easy/samples/1_hi/main.cpp b/libraries/easy/samples/1_hi/main.cpp new file mode 100644 index 000000000000..3afd0fce47fc --- /dev/null +++ b/libraries/easy/samples/1_hi/main.cpp @@ -0,0 +1,20 @@ +// Note: this is for the purposes of samples only +#include + +#include + +std::string Greet(const server::http::HttpRequest& req) { + const auto& username = req.GetPathArg("user"); + return "Hello, " + username; +} + +struct Hi { + std::string operator()(const server::http::HttpRequest& req) const { return "Hi, " + req.GetArg("name"); } +}; + +int main(int argc, char* argv[]) { + easy::HttpWith(argc, argv) + .DefaultContentType(http::content_type::kTextPlain) + .Get("/hi/{user}", &Greet) + .Get("/hi", Hi{}); +} diff --git a/libraries/easy/samples/1_hi/tests/conftest.py b/libraries/easy/samples/1_hi/tests/conftest.py new file mode 100644 index 000000000000..87356961ffd6 --- /dev/null +++ b/libraries/easy/samples/1_hi/tests/conftest.py @@ -0,0 +1,4 @@ +# /// [psql prepare] +# Use only core plugin +pytest_plugins = ["pytest_userver.plugins.core"] +# /// [psql prepare] diff --git a/libraries/easy/samples/1_hi/tests/test_basic.py b/libraries/easy/samples/1_hi/tests/test_basic.py new file mode 100644 index 000000000000..77e14f506f5f --- /dev/null +++ b/libraries/easy/samples/1_hi/tests/test_basic.py @@ -0,0 +1,12 @@ +async def test_hi_to(service_client): + response = await service_client.get("/hi/flusk") + assert response.status == 200 + assert "text/plain" in response.headers["Content-Type"] + assert response.text == "Hello, flusk" + + +async def test_hi(service_client): + response = await service_client.get("/hi?name=pal") + assert response.status == 200 + assert "text/plain" in response.headers["Content-Type"] + assert response.text == "Hi, pal" diff --git a/libraries/easy/samples/2_key_value/CMakeLists.txt b/libraries/easy/samples/2_key_value/CMakeLists.txt new file mode 100644 index 000000000000..279540941ea8 --- /dev/null +++ b/libraries/easy/samples/2_key_value/CMakeLists.txt @@ -0,0 +1,6 @@ +project(userver-easy-samples-key-value CXX) + +add_executable(${PROJECT_NAME} "main.cpp") +target_link_libraries(${PROJECT_NAME} userver::easy) + +userver_testsuite_add_simple(DUMP_CONFIG True) diff --git a/libraries/easy/samples/2_key_value/main.cpp b/libraries/easy/samples/2_key_value/main.cpp new file mode 100644 index 000000000000..cf8967422e8a --- /dev/null +++ b/libraries/easy/samples/2_key_value/main.cpp @@ -0,0 +1,37 @@ +// Note: this is for the purposes of samples only +#include + +#include + +constexpr std::string_view kSchema = R"~( +CREATE TABLE IF NOT EXISTS key_value_table ( + key VARCHAR PRIMARY KEY, + value VARCHAR +) +)~"; + +int main(int argc, char* argv[]) { + easy::HttpWith(argc, argv) + .Schema(kSchema) + .DefaultContentType(http::content_type::kTextPlain) + .Get( + "/kv", + [](const server::http::HttpRequest& req, const easy::PgDep& dep) { + auto res = dep.pg().Execute( + storages::postgres::ClusterHostType::kSlave, + "SELECT value FROM key_value_table WHERE key=$1", + req.GetArg("key") + ); + return res[0][0].As(); + } + ) + .Post("/kv", [](const server::http::HttpRequest& req, const auto& dep) { + dep.pg().Execute( + storages::postgres::ClusterHostType::kMaster, + "INSERT INTO key_value_table(key, value) VALUES($1, $2) ON CONFLICT (key) DO UPDATE SET value = $2", + req.GetArg("key"), + req.GetArg("value") + ); + return std::string{}; + }); +} diff --git a/libraries/easy/samples/2_key_value/tests/conftest.py b/libraries/easy/samples/2_key_value/tests/conftest.py new file mode 100644 index 000000000000..2a91ee35c675 --- /dev/null +++ b/libraries/easy/samples/2_key_value/tests/conftest.py @@ -0,0 +1,18 @@ +# /// [psql prepare] +import os +import pytest +import subprocess + +from testsuite.databases.pgsql import discover + +pytest_plugins = ["pytest_userver.plugins.postgresql"] + + +@pytest.fixture(scope="session") +def pgsql_local(service_tmpdir, service_binary, pgsql_local_create): + schema_path = service_tmpdir.joinpath("schemas") + os.mkdir(schema_path) + subprocess.run([service_binary, "--dump-schema", schema_path]) + databases = discover.find_schemas("admin", [schema_path]) + return pgsql_local_create(list(databases.values())) + # /// [psql prepare] diff --git a/libraries/easy/samples/2_key_value/tests/test_basic.py b/libraries/easy/samples/2_key_value/tests/test_basic.py new file mode 100644 index 000000000000..b79fcc3a2009 --- /dev/null +++ b/libraries/easy/samples/2_key_value/tests/test_basic.py @@ -0,0 +1,14 @@ +async def test_kv(service_client): + response = await service_client.post("/kv?key=1&value=one") + assert response.status == 200 + + response = await service_client.get("/kv?key=1") + assert response.status == 200 + assert response.text == "one" + + response = await service_client.post("/kv?key=1&value=again_1") + assert response.status == 200 + + response = await service_client.get("/kv?key=1") + assert response.status == 200 + assert response.text == "again_1" diff --git a/libraries/easy/samples/CMakeLists.txt b/libraries/easy/samples/CMakeLists.txt index a2b65677dfa7..5cc4652808bc 100644 --- a/libraries/easy/samples/CMakeLists.txt +++ b/libraries/easy/samples/CMakeLists.txt @@ -4,3 +4,9 @@ add_custom_target(${PROJECT_NAME}) add_subdirectory(0_hello_world) add_dependencies(${PROJECT_NAME} ${PROJECT_NAME}-hello-world) + +add_subdirectory(1_hi) +add_dependencies(${PROJECT_NAME} ${PROJECT_NAME}-hi) + +add_subdirectory(2_key_value) +add_dependencies(${PROJECT_NAME} ${PROJECT_NAME}-key-value) diff --git a/libraries/easy/src/easy.cpp b/libraries/easy/src/easy.cpp index 21c94a7128b4..e6e749d9f9cb 100644 --- a/libraries/easy/src/easy.cpp +++ b/libraries/easy/src/easy.cpp @@ -3,17 +3,17 @@ #include #include -#include #include +#include +#include +#include #include #include -#include #include +#include #include -#include #include -#include USERVER_NAMESPACE_BEGIN @@ -33,62 +33,74 @@ constexpr std::string_view kConfigBase = R"~( default_task_processor: main-task-processor # Task processor in which components start. components: # Configuring components that were registered via component_list - server: - listener: # configuring the main listening socket... - port: 8080 # ...to listen on this port and... - task_processor: main-task-processor # ...process incoming requests on this task processor. - logging: - fs-task-processor: fs-task-processor - loggers: - default: - file_path: '@stderr' - level: debug - overflow_behavior: discard # Drop logs if the system is too busy to write them down. )~"; +constexpr std::string_view kConfigServerTemplate = R"~( +server: + listener: # configuring the main listening socket... + port: {} # ...to listen on this port and... + task_processor: main-task-processor # ...process incoming requests on this task processor. +)~"; + +constexpr std::string_view kConfigLoggingTemplate = R"~( +logging: + fs-task-processor: fs-task-processor + loggers: + default: + file_path: '@stderr' + level: {} + overflow_behavior: discard # Drop logs if the system is too busy to write them down. +)~"; constexpr std::string_view kConfigHandlerTemplate = R"~( {0}: - path: {0} # Registering handler by URL '{0}'. - method: GET,PUT,POST,DELETE,PATCH + path: {1} # Registering handler by URL '{1}'. + method: {2} task_processor: main-task-processor # Run it on CPU bound task processor )~"; -std::unordered_map g_http_functions_; -std::optional default_content_type_; -std::string schema_; +struct SharedPyaload { + std::unordered_map http_functions; + std::optional default_content_type; + std::string schema; +}; -} // anonymous namespace +SharedPyaload globals{}; +} // anonymous namespace DependenciesBase::~DependenciesBase() = default; -namespace impl { - class HttpBase::Handle final : public server::handlers::HttpHandlerBase { public: - Handle(const components::ComponentConfig& config, const components::ComponentContext& context): - HttpHandlerBase(config, context), - deps_{context.FindComponent()}, - callback_{g_http_functions_[config.Name()]} - {} - - std::string HandleRequestThrow(const HttpRequest& request, RequestContext&) const override { - if (default_content_type_) { - request.GetHttpResponse().SetContentType(*default_content_type_); + Handle(const components::ComponentConfig& config, const components::ComponentContext& context) + : HttpHandlerBase(config, context), + deps_{context.FindComponent()}, + callback_{globals.http_functions.at(config.Name())} {} + + std::string HandleRequestThrow(const server::http::HttpRequest& request, server::request::RequestContext&) + const override { + if (globals.default_content_type) { + request.GetHttpResponse().SetContentType(*globals.default_content_type); } return callback_(request, deps_); } private: const DependenciesBase& deps_; - impl::UnderlyingCallback& callback_; + HttpBase::Callback& callback_; }; -HttpBase::HttpBase(int argc, const char *const argv[]) : argc_{argc}, argv_{argv}, static_config_{kConfigBase}, +HttpBase::HttpBase(int argc, const char* const argv[]) + : argc_{argc}, + argv_{argv}, + static_config_{kConfigBase}, component_list_{components::MinimalServerComponentList()} {} HttpBase::~HttpBase() { + AddComponentsConfig(fmt::format(kConfigServerTemplate, port_)); + AddComponentsConfig(fmt::format(kConfigLoggingTemplate, ToString(level_))); + namespace po = boost::program_options; po::variables_map vm; @@ -112,7 +124,7 @@ HttpBase::~HttpBase() { } if (vm.count("dump-schema")) { - std::ofstream(schema_dump + "/0_pg.sql") << schema_; + std::ofstream(schema_dump + "/0_pg.sql") << globals.schema; } if (argc_ <= 1) { @@ -126,18 +138,14 @@ HttpBase::~HttpBase() { } } -void HttpBase::DefaultContentType(http::ContentType content_type) { - default_content_type_ = content_type; -} +void HttpBase::DefaultContentType(http::ContentType content_type) { globals.default_content_type = content_type; } -void HttpBase::Route(std::string_view path, UnderlyingCallback&& func) { - g_http_functions_.emplace(path, std::move(func)); - component_list_.Append(path); - AddHandleConfig(path); -} +void HttpBase::Route(std::string_view path, Callback&& func, std::initializer_list methods) { + auto component_name = fmt::format("{}-{}", path, fmt::join(methods, ",")); -void HttpBase::AddHandleConfig(std::string_view path) { - AddComponentsConfig(fmt::format(kConfigHandlerTemplate, path)); + globals.http_functions.emplace(component_name, std::move(func)); + component_list_.Append(component_name); + AddComponentsConfig(fmt::format(kConfigHandlerTemplate, component_name, path, fmt::join(methods, ","))); } void HttpBase::AddComponentsConfig(std::string_view config) { @@ -145,18 +153,19 @@ void HttpBase::AddComponentsConfig(std::string_view config) { static_config_ += boost::algorithm::replace_all_copy(conf, "\n", "\n "); } -void HttpBase::Schema(std::string_view schema) { schema_ = schema; } - -} // namespace impl +void HttpBase::Schema(std::string_view schema) { globals.schema = schema; } + +void HttpBase::Port(std::uint16_t port) { port_ = port; } +void HttpBase::LogLevel(logging::Level level) { level_ = level; } PgDep::PgDep(const components::ComponentConfig& config, const components::ComponentContext& context) -: DependenciesBase{config, context}, pg_cluster_(context.FindComponent("postgres").GetCluster()) -{ - pg_cluster_->Execute(storages::postgres::ClusterHostType::kMaster, schema_); + : DependenciesBase{config, context}, + pg_cluster_(context.FindComponent("postgres").GetCluster()) { + pg_cluster_->Execute(storages::postgres::ClusterHostType::kMaster, globals.schema); } -void DependenciesRegistration(HttpWith& app) { +void Registration(OfDependency, HttpBase& app) { app.AddComponentsConfig(R"~( postgres: dbconnection#env: POSTGRESQL From e65f6e5df1dd5b08303a11dd082e3ae1257b0d04 Mon Sep 17 00:00:00 2001 From: Antony Polukhin Date: Tue, 19 Nov 2024 10:58:42 +0300 Subject: [PATCH 13/19] update --- libraries/easy/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/easy/CMakeLists.txt b/libraries/easy/CMakeLists.txt index 67e1813b1990..2a1b45623a5e 100644 --- a/libraries/easy/CMakeLists.txt +++ b/libraries/easy/CMakeLists.txt @@ -6,6 +6,6 @@ userver_module(easy UTEST_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/src/*_test.cpp" ) -if(USERVER_IS_THE_ROOT_PROJECT) +if(USERVER_BUILD_SAMPLES) add_subdirectory(samples) endif() From 34e570e621c77f592166101573addc846bef7a0f Mon Sep 17 00:00:00 2001 From: Antony Polukhin Date: Tue, 19 Nov 2024 11:14:13 +0300 Subject: [PATCH 14/19] tweaks --- libraries/easy/include/userver/easy.hpp | 2 ++ libraries/easy/src/easy.cpp | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/libraries/easy/include/userver/easy.hpp b/libraries/easy/include/userver/easy.hpp index 0220f96e267d..3d6e52d5c379 100644 --- a/libraries/easy/include/userver/easy.hpp +++ b/libraries/easy/include/userver/easy.hpp @@ -22,6 +22,8 @@ class DependenciesBase : public components::ComponentBase { static constexpr std::string_view kName = "easy-dependencies"; using components::ComponentBase::ComponentBase; virtual ~DependenciesBase(); + + static const std::string& GetSchema() noexcept; }; template diff --git a/libraries/easy/src/easy.cpp b/libraries/easy/src/easy.cpp index e6e749d9f9cb..dde9a301320b 100644 --- a/libraries/easy/src/easy.cpp +++ b/libraries/easy/src/easy.cpp @@ -71,6 +71,8 @@ SharedPyaload globals{}; DependenciesBase::~DependenciesBase() = default; +const std::string& DependenciesBase::GetSchema() noexcept { return globals.schema; } + class HttpBase::Handle final : public server::handlers::HttpHandlerBase { public: Handle(const components::ComponentConfig& config, const components::ComponentContext& context) @@ -162,7 +164,7 @@ void HttpBase::LogLevel(logging::Level level) { level_ = level; } PgDep::PgDep(const components::ComponentConfig& config, const components::ComponentContext& context) : DependenciesBase{config, context}, pg_cluster_(context.FindComponent("postgres").GetCluster()) { - pg_cluster_->Execute(storages::postgres::ClusterHostType::kMaster, globals.schema); + pg_cluster_->Execute(storages::postgres::ClusterHostType::kMaster, GetSchema()); } void Registration(OfDependency, HttpBase& app) { From b910a2f6300c336ea3ab9ddc58357f3dead20bf7 Mon Sep 17 00:00:00 2001 From: Antony Polukhin Date: Fri, 22 Nov 2024 10:34:58 +0300 Subject: [PATCH 15/19] more samples --- .../3_custom_dependency/CMakeLists.txt | 6 ++ .../easy/samples/3_custom_dependency/main.cpp | 64 +++++++++++++++++++ .../3_custom_dependency/tests/conftest.py | 18 ++++++ .../3_custom_dependency/tests/test_basic.py | 10 +++ libraries/easy/samples/CMakeLists.txt | 3 + libraries/easy/src/easy.cpp | 6 +- 6 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 libraries/easy/samples/3_custom_dependency/CMakeLists.txt create mode 100644 libraries/easy/samples/3_custom_dependency/main.cpp create mode 100644 libraries/easy/samples/3_custom_dependency/tests/conftest.py create mode 100644 libraries/easy/samples/3_custom_dependency/tests/test_basic.py diff --git a/libraries/easy/samples/3_custom_dependency/CMakeLists.txt b/libraries/easy/samples/3_custom_dependency/CMakeLists.txt new file mode 100644 index 000000000000..d7b47e6bf129 --- /dev/null +++ b/libraries/easy/samples/3_custom_dependency/CMakeLists.txt @@ -0,0 +1,6 @@ +project(userver-easy-samples-custom-dependency CXX) + +add_executable(${PROJECT_NAME} "main.cpp") +target_link_libraries(${PROJECT_NAME} userver::easy) + +userver_testsuite_add_simple(DUMP_CONFIG True) diff --git a/libraries/easy/samples/3_custom_dependency/main.cpp b/libraries/easy/samples/3_custom_dependency/main.cpp new file mode 100644 index 000000000000..bd719fe5be4d --- /dev/null +++ b/libraries/easy/samples/3_custom_dependency/main.cpp @@ -0,0 +1,64 @@ +// Note: this is for the purposes of samples only +#include + +#include +#include + +constexpr std::string_view kSchema = R"~( +CREATE TABLE IF NOT EXISTS events_table ( + id serial NOT NULL, + action VARCHAR PRIMARY KEY, + host_id INTEGER +) +)~"; + +class MyDependency : public easy::PgDep { +public: + MyDependency(const components::ComponentConfig& config, const components::ComponentContext& context) + : PgDep(config, context), host_id_(config["host_id"].As()) {} + + int host_id() const { return host_id_; } + + static yaml_config::Schema GetStaticConfigSchema() { + return yaml_config::MergeSchemas(R"( + type: object + description: ID of the host + additionalProperties: false + properties: + host_id: + type: integer + description: ID of the host + )"); + } + +private: + const int host_id_; +}; + +void Registration(easy::OfDependency, easy::HttpBase& app) { + Registration(easy::OfDependency{}, app); // base class initilization + + app.AddComponentsConfig(R"~( +easy-dependencies: + host_id#env: HOST_ID + host_id#fallback: 42 +)~"); +} + +template <> +inline constexpr auto components::kConfigFileMode = ConfigFileMode::kNotRequired; + +int main(int argc, char* argv[]) { + easy::HttpWith(argc, argv) + .Schema(kSchema) + .DefaultContentType(http::content_type::kTextPlain) + .Post("/log", [](const server::http::HttpRequest& req, const MyDependency& dep) { + dep.pg().Execute( + storages::postgres::ClusterHostType::kMaster, + "INSERT INTO events_table(action, host_id) VALUES($1, $2)", + req.GetArg("action"), + dep.host_id() + ); + return std::string{}; + }); +} diff --git a/libraries/easy/samples/3_custom_dependency/tests/conftest.py b/libraries/easy/samples/3_custom_dependency/tests/conftest.py new file mode 100644 index 000000000000..2a91ee35c675 --- /dev/null +++ b/libraries/easy/samples/3_custom_dependency/tests/conftest.py @@ -0,0 +1,18 @@ +# /// [psql prepare] +import os +import pytest +import subprocess + +from testsuite.databases.pgsql import discover + +pytest_plugins = ["pytest_userver.plugins.postgresql"] + + +@pytest.fixture(scope="session") +def pgsql_local(service_tmpdir, service_binary, pgsql_local_create): + schema_path = service_tmpdir.joinpath("schemas") + os.mkdir(schema_path) + subprocess.run([service_binary, "--dump-schema", schema_path]) + databases = discover.find_schemas("admin", [schema_path]) + return pgsql_local_create(list(databases.values())) + # /// [psql prepare] diff --git a/libraries/easy/samples/3_custom_dependency/tests/test_basic.py b/libraries/easy/samples/3_custom_dependency/tests/test_basic.py new file mode 100644 index 000000000000..25735a7b0f67 --- /dev/null +++ b/libraries/easy/samples/3_custom_dependency/tests/test_basic.py @@ -0,0 +1,10 @@ +async def test_log_action(service_client, pgsql): + response = await service_client.post("/log?action=test_1") + assert response.status == 200 + + cursor = pgsql["0_pg"].cursor() # '0_pg.sql' is created by easy::PgDep, so the database is '0_pg' + cursor.execute("SELECT action, host_id FROM events_table WHERE id=1") + result = cursor.fetchall() + assert len(result) == 1 + assert result[0][0] == "test_1" + assert result[0][1] == 42 diff --git a/libraries/easy/samples/CMakeLists.txt b/libraries/easy/samples/CMakeLists.txt index 5cc4652808bc..ee07b23e857a 100644 --- a/libraries/easy/samples/CMakeLists.txt +++ b/libraries/easy/samples/CMakeLists.txt @@ -10,3 +10,6 @@ add_dependencies(${PROJECT_NAME} ${PROJECT_NAME}-hi) add_subdirectory(2_key_value) add_dependencies(${PROJECT_NAME} ${PROJECT_NAME}-key-value) + +add_subdirectory(3_custom_dependency) +add_dependencies(${PROJECT_NAME} ${PROJECT_NAME}-custom-dependency) diff --git a/libraries/easy/src/easy.cpp b/libraries/easy/src/easy.cpp index dde9a301320b..d461a8c22479 100644 --- a/libraries/easy/src/easy.cpp +++ b/libraries/easy/src/easy.cpp @@ -60,9 +60,9 @@ constexpr std::string_view kConfigHandlerTemplate = R"~( )~"; struct SharedPyaload { - std::unordered_map http_functions; - std::optional default_content_type; - std::string schema; + std::unordered_map http_functions; + std::optional default_content_type; + std::string schema; }; SharedPyaload globals{}; From de0d755a6ec293f07987d1dd702739cb972191a0 Mon Sep 17 00:00:00 2001 From: Antony Polukhin Date: Fri, 29 Nov 2024 10:15:43 +0300 Subject: [PATCH 16/19] JSON example and docs --- libraries/easy/include/userver/easy.hpp | 79 ++++++++++++++++++- libraries/easy/samples/3_json/CMakeLists.txt | 24 ++++++ libraries/easy/samples/3_json/main.cpp | 44 +++++++++++ .../samples/3_json/schemas/key_value.yaml | 17 ++++ .../tests/conftest.py | 0 .../easy/samples/3_json/tests/test_basic.py | 18 +++++ .../CMakeLists.txt | 0 .../main.cpp | 0 .../4_custom_dependency/tests/conftest.py | 18 +++++ .../tests/test_basic.py | 0 libraries/easy/samples/CMakeLists.txt | 5 +- 11 files changed, 201 insertions(+), 4 deletions(-) create mode 100644 libraries/easy/samples/3_json/CMakeLists.txt create mode 100644 libraries/easy/samples/3_json/main.cpp create mode 100644 libraries/easy/samples/3_json/schemas/key_value.yaml rename libraries/easy/samples/{3_custom_dependency => 3_json}/tests/conftest.py (100%) create mode 100644 libraries/easy/samples/3_json/tests/test_basic.py rename libraries/easy/samples/{3_custom_dependency => 4_custom_dependency}/CMakeLists.txt (100%) rename libraries/easy/samples/{3_custom_dependency => 4_custom_dependency}/main.cpp (100%) create mode 100644 libraries/easy/samples/4_custom_dependency/tests/conftest.py rename libraries/easy/samples/{3_custom_dependency => 4_custom_dependency}/tests/test_basic.py (100%) diff --git a/libraries/easy/include/userver/easy.hpp b/libraries/easy/include/userver/easy.hpp index 3d6e52d5c379..91e714d66626 100644 --- a/libraries/easy/include/userver/easy.hpp +++ b/libraries/easy/include/userver/easy.hpp @@ -1,11 +1,15 @@ #pragma once +/// @file userver/easy.hpp +/// @brief Headers of an library for `easy` prototyping + #include #include #include #include #include +#include #include #include #include @@ -17,38 +21,58 @@ USERVER_NAMESPACE_BEGIN /// @brief Top namespace for `easy` library namespace easy { +/// @brief Base class for all the dependencies of the `easy` library. class DependenciesBase : public components::ComponentBase { public: static constexpr std::string_view kName = "easy-dependencies"; using components::ComponentBase::ComponentBase; virtual ~DependenciesBase(); + /// @returns the \b last schema that was provided to the easy::HttpWith or easy::HttpBase static const std::string& GetSchema() noexcept; }; +/// @brief Tag for custom Dependency registration customization. +/// +/// For a new dependency type `Dependency` a function `void Registration(OfDependency, HttpBase& app)` +/// should be provided to be able to use that dependency with the easy::HttpWith class. template struct OfDependency final {}; +/// @brief easy::HttpWith like class with erased dependencies information that should be used only in dependency +/// registration functions; use easy::HttpWith if not making a new dependency class. class HttpBase final { public: using Callback = std::function; + /// Sets the default Content-Type header for all the routes void DefaultContentType(http::ContentType content_type); + + /// Register an HTTP handler by `path` that supports the `methods` HTTP methods void Route(std::string_view path, Callback&& func, std::initializer_list methods); + /// Add a comonent config to the service config void AddComponentsConfig(std::string_view config); + /// Append a component to the component list of a service template void AppendComponent(std::string_view name) { component_list_.Append(name); } + + /// @overload template void AppendComponent() { component_list_.Append(); } + /// Stores the schema for further retrieval from DependenciesBase::GetSchema() void Schema(std::string_view schema); + + /// Set the HTTP server listen port, default is 8080. void Port(std::uint16_t port); + + /// Set the logging level for the service void LogLevel(logging::Level level); private: @@ -69,9 +93,23 @@ class HttpBase final { logging::Level level_ = logging::Level::kDebug; }; +/// @brief Class for describing the service functionality in simple declarative way that generates static configs, +/// applies schemas. +/// +/// @see @ref scripts/docs/en/userver/libraries/easy.md template class HttpWith final { public: + /// Helper class that can store any callback of the following signatures: + /// + /// * formats::json::Value(formats::json::Value, const Dependency&) + /// * formats::json::Value(formats::json::Value) + /// * formats::json::Value(const HttpRequest&, const Dependency&) + /// * std::string(const HttpRequest&, const Dependency&) + /// * formats::json::Value(const HttpRequest&) + /// * std::string(const HttpRequest&) + /// + /// If callback returns formats::json::Value then the deafult content type is set to `application/json` class Callback final { public: template @@ -86,10 +124,12 @@ class HttpWith final { HttpWith(int argc, const char* const argv[]) : impl_(argc, argv) { impl_.AppendComponent(); } ~HttpWith() { Registration(OfDependency{}, impl_); /* ADL is intentional */ } + /// @copydoc HttpBase::DefaultContentType HttpWith& DefaultContentType(http::ContentType content_type) { return (impl_.DefaultContentType(content_type), *this); } + /// @copydoc HttpBase::Route HttpWith& Route( std::string_view path, Callback&& func, @@ -106,58 +146,69 @@ class HttpWith final { return *this; } + /// Register an HTTP handler by `path` that supports the HTTP GET method. HttpWith& Get(std::string_view path, Callback&& func) { impl_.Route(path, std::move(func).Extract(), {server::http::HttpMethod::kGet}); return *this; } + /// Register an HTTP handler by `path` that supports the HTTP POST method. HttpWith& Post(std::string_view path, Callback&& func) { impl_.Route(path, std::move(func).Extract(), {server::http::HttpMethod::kPost}); return *this; } + /// Register an HTTP handler by `path` that supports the HTTP DELETE method. HttpWith& Del(std::string_view path, Callback&& func) { impl_.Route(path, std::move(func).Extract(), {server::http::HttpMethod::kDelete}); return *this; } + /// Register an HTTP handler by `path` that supports the HTTP PUT method. HttpWith& Put(std::string_view path, Callback&& func) { impl_.Route(path, std::move(func).Extract(), {server::http::HttpMethod::kPut}); return *this; } + /// Register an HTTP handler by `path` that supports the HTTP PATCH method. HttpWith& Patch(std::string_view path, Callback&& func) { impl_.Route(path, std::move(func).Extract(), {server::http::HttpMethod::kPatch}); return *this; } + /// @copydoc HttpBase::AddComponentsConfig HttpWith& AddComponentsConfig(std::string_view config) { impl_.AddComponentsConfig(config); return *this; } + /// @copydoc HttpBase::AppendComponent template HttpWith& AppendComponent(std::string_view name) { impl_.AppendComponent(name); return *this; } + /// @copydoc HttpBase::AppendComponent template HttpWith& AppendComponent() { impl_.AppendComponent(); return *this; } + /// @copydoc HttpBase::Schema HttpWith& Schema(std::string_view schema) { impl_.Schema(schema); return *this; } + /// @copydoc HttpBase::Port HttpWith& Port(std::uint16_t port) { impl_.Port(port); return *this; } + /// @copydoc HttpBase::LogLevel HttpWith& LogLevel(logging::Level level) { impl_.LogLevel(level); return *this; @@ -184,18 +235,40 @@ template template HttpWith::Callback::Callback(Function func) { using server::http::HttpRequest; - if constexpr (std::is_invocable_r_v) { + + if constexpr (std::is_invocable_r_v) { + func_ = [f = std::move(func)](const HttpRequest& req, const DependenciesBase& deps) { + req.GetHttpResponse().SetContentType(http::content_type::kApplicationJson); + return formats::json::ToString( + f(formats::json::FromString(req.RequestBody()), static_cast(deps)) + ); + }; + } else if constexpr (std::is_invocable_r_v) { + func_ = [f = std::move(func)](const HttpRequest& req, const DependenciesBase&) { + req.GetHttpResponse().SetContentType(http::content_type::kApplicationJson); + return formats::json::ToString(f(formats::json::FromString(req.RequestBody()))); + }; + } else if constexpr (std::is_invocable_r_v) { + func_ = [f = std::move(func)](const HttpRequest& req, const DependenciesBase& deps) { + req.GetHttpResponse().SetContentType(http::content_type::kApplicationJson); + return formats::json::ToString(f(req, static_cast(deps))); + }; + } else if constexpr (std::is_invocable_r_v) { func_ = [f = std::move(func)](const HttpRequest& req, const DependenciesBase& deps) { return f(req, static_cast(deps)); }; - } else if constexpr (std::is_invocable_r_v) { - func_ = std::move(func); + } else if constexpr (std::is_invocable_r_v) { + func_ = [f = std::move(func)](const HttpRequest& req, const DependenciesBase&) { + req.GetHttpResponse().SetContentType(http::content_type::kApplicationJson); + return formats::json::ToString(f(req)); + }; } else { static_assert(std::is_invocable_r_v); func_ = [f = std::move(func)](const HttpRequest& req, const DependenciesBase&) { return f(req); }; } } +/// @brief Predefined dependencies class that provides a PostgreSQL cluster client. class PgDep : public DependenciesBase { public: PgDep(const components::ComponentConfig& config, const components::ComponentContext& context); diff --git a/libraries/easy/samples/3_json/CMakeLists.txt b/libraries/easy/samples/3_json/CMakeLists.txt new file mode 100644 index 000000000000..5be1c72c727d --- /dev/null +++ b/libraries/easy/samples/3_json/CMakeLists.txt @@ -0,0 +1,24 @@ +project(userver-easy-samples-json CXX) + +file(GLOB_RECURSE SCHEMAS ${CMAKE_CURRENT_SOURCE_DIR}/schemas/*.yaml) +userver_target_generate_chaotic(${PROJECT_NAME}-chgen + ARGS + # Map '/components/schemas/*' JSONSchema types to C++ types in 'schemas' namespace + -n "/components/schemas/([^/]*)/=schemas::{0}" + -f "(.*)={0}" + # Don't call clang-format + --clang-format= + # Generate serializers for responses + --generate-serializers + OUTPUT_DIR + ${CMAKE_CURRENT_BINARY_DIR}/src + SCHEMAS + ${SCHEMAS} + RELATIVE_TO + ${CMAKE_CURRENT_SOURCE_DIR} +) + +add_executable(${PROJECT_NAME} "main.cpp") +target_link_libraries(${PROJECT_NAME} userver::easy ${PROJECT_NAME}-chgen) + +userver_testsuite_add_simple(DUMP_CONFIG True) diff --git a/libraries/easy/samples/3_json/main.cpp b/libraries/easy/samples/3_json/main.cpp new file mode 100644 index 000000000000..e93be32d3ad8 --- /dev/null +++ b/libraries/easy/samples/3_json/main.cpp @@ -0,0 +1,44 @@ +// Note: this is for the purposes of samples only +#include + +#include +#include "schemas/key_value.hpp" + +constexpr std::string_view kSchema = R"~( +CREATE TABLE IF NOT EXISTS key_value_table ( + key integer PRIMARY KEY, + value VARCHAR +) +)~"; + +int main(int argc, char* argv[]) { + easy::HttpWith(argc, argv) + .Schema(kSchema) + .Get( + "/kv", + [](formats::json::Value request_json, const easy::PgDep& dep) { + // Use generated parser for As() + auto key = request_json.As().key; + + auto res = dep.pg().Execute( + storages::postgres::ClusterHostType::kSlave, "SELECT value FROM key_value_table WHERE key=$1", key + ); + + schemas::KeyValue response{key, res[0][0].As()}; + return formats::json::ValueBuilder{response}.ExtractValue(); + } + ) + .Post("/kv", [](formats::json::Value request_json, const auto& dep) { + // Use generated parser for As() + auto key_value = request_json.As(); + + dep.pg().Execute( + storages::postgres::ClusterHostType::kMaster, + "INSERT INTO key_value_table(key, value) VALUES($1, $2) ON CONFLICT (key) DO UPDATE SET value = $2", + key_value.key, + key_value.value + ); + + return formats::json::Value{}; + }); +} diff --git a/libraries/easy/samples/3_json/schemas/key_value.yaml b/libraries/easy/samples/3_json/schemas/key_value.yaml new file mode 100644 index 000000000000..a4e3bb84433d --- /dev/null +++ b/libraries/easy/samples/3_json/schemas/key_value.yaml @@ -0,0 +1,17 @@ +# yaml +components: + schemas: + KeyRequest: + type: object + additionalProperties: false + properties: + key: + type: integer + KeyValue: + type: object + additionalProperties: false + properties: + key: + type: integer + value: + type: string diff --git a/libraries/easy/samples/3_custom_dependency/tests/conftest.py b/libraries/easy/samples/3_json/tests/conftest.py similarity index 100% rename from libraries/easy/samples/3_custom_dependency/tests/conftest.py rename to libraries/easy/samples/3_json/tests/conftest.py diff --git a/libraries/easy/samples/3_json/tests/test_basic.py b/libraries/easy/samples/3_json/tests/test_basic.py new file mode 100644 index 000000000000..1d4548f74d4c --- /dev/null +++ b/libraries/easy/samples/3_json/tests/test_basic.py @@ -0,0 +1,18 @@ +async def test_kv(service_client): + response = await service_client.post("/kv", json={"key": 1, "value": "one"}) + assert response.status == 200 + assert response.json() == None + assert "application/json" in response.headers["Content-Type"] + + response = await service_client.get("/kv", json={"key": 1}) + assert response.status == 200 + assert response.json() == {"key": 1, "value": "one"} + assert "application/json" in response.headers["Content-Type"] + + response = await service_client.post("/kv", json={"key": 1, "value": "again_1"}) + assert response.status == 200 + + response = await service_client.get("/kv", json={"key": 1}) + assert response.status == 200 + assert response.json() == {"key": 1, "value": "again_1"} + assert "application/json" in response.headers["Content-Type"] diff --git a/libraries/easy/samples/3_custom_dependency/CMakeLists.txt b/libraries/easy/samples/4_custom_dependency/CMakeLists.txt similarity index 100% rename from libraries/easy/samples/3_custom_dependency/CMakeLists.txt rename to libraries/easy/samples/4_custom_dependency/CMakeLists.txt diff --git a/libraries/easy/samples/3_custom_dependency/main.cpp b/libraries/easy/samples/4_custom_dependency/main.cpp similarity index 100% rename from libraries/easy/samples/3_custom_dependency/main.cpp rename to libraries/easy/samples/4_custom_dependency/main.cpp diff --git a/libraries/easy/samples/4_custom_dependency/tests/conftest.py b/libraries/easy/samples/4_custom_dependency/tests/conftest.py new file mode 100644 index 000000000000..2a91ee35c675 --- /dev/null +++ b/libraries/easy/samples/4_custom_dependency/tests/conftest.py @@ -0,0 +1,18 @@ +# /// [psql prepare] +import os +import pytest +import subprocess + +from testsuite.databases.pgsql import discover + +pytest_plugins = ["pytest_userver.plugins.postgresql"] + + +@pytest.fixture(scope="session") +def pgsql_local(service_tmpdir, service_binary, pgsql_local_create): + schema_path = service_tmpdir.joinpath("schemas") + os.mkdir(schema_path) + subprocess.run([service_binary, "--dump-schema", schema_path]) + databases = discover.find_schemas("admin", [schema_path]) + return pgsql_local_create(list(databases.values())) + # /// [psql prepare] diff --git a/libraries/easy/samples/3_custom_dependency/tests/test_basic.py b/libraries/easy/samples/4_custom_dependency/tests/test_basic.py similarity index 100% rename from libraries/easy/samples/3_custom_dependency/tests/test_basic.py rename to libraries/easy/samples/4_custom_dependency/tests/test_basic.py diff --git a/libraries/easy/samples/CMakeLists.txt b/libraries/easy/samples/CMakeLists.txt index ee07b23e857a..55d49fd9b942 100644 --- a/libraries/easy/samples/CMakeLists.txt +++ b/libraries/easy/samples/CMakeLists.txt @@ -11,5 +11,8 @@ add_dependencies(${PROJECT_NAME} ${PROJECT_NAME}-hi) add_subdirectory(2_key_value) add_dependencies(${PROJECT_NAME} ${PROJECT_NAME}-key-value) -add_subdirectory(3_custom_dependency) +add_subdirectory(3_json) +add_dependencies(${PROJECT_NAME} ${PROJECT_NAME}-json) + +add_subdirectory(4_custom_dependency) add_dependencies(${PROJECT_NAME} ${PROJECT_NAME}-custom-dependency) From d9f0cc62918a1dc8c3da5c2f00e55bd54d7fd40f Mon Sep 17 00:00:00 2001 From: Antony Polukhin Date: Fri, 29 Nov 2024 11:41:22 +0300 Subject: [PATCH 17/19] Better integration with core --- core/include/userver/utils/daemon_run.hpp | 18 ++++++-- core/src/utils/daemon_run.cpp | 54 +++++++++++++---------- libraries/easy/src/easy.cpp | 15 ++++--- 3 files changed, 56 insertions(+), 31 deletions(-) diff --git a/core/include/userver/utils/daemon_run.hpp b/core/include/userver/utils/daemon_run.hpp index 768d9abf5b59..4f54aa5aaeb8 100644 --- a/core/include/userver/utils/daemon_run.hpp +++ b/core/include/userver/utils/daemon_run.hpp @@ -5,22 +5,34 @@ #include + +namespace boost::program_options { +class options_description; +class variables_map; +} + USERVER_NAMESPACE_BEGIN namespace utils { -/// Parses command line arguments and calls components::Run with config file -/// from --config parameter. +/// @returns default options of DaemonMain /// -/// Other command line arguments: +/// List of options: /// * --help - show all command line arguments /// * --config CONFIG - path to config.yaml /// * --config_vars CONFIG_VARS - path to config_vars.yaml /// * --config_vars_override CONFIG_VARS - path to config_vars.override.yaml /// * --print-config-schema - print config.yaml YAML Schema /// * --print-dynamic-config-defaults - print JSON with dynamic config defaults +boost::program_options::options_description BaseRunOptions(); + +/// Parses command line arguments and calls components::Run with config file +/// from --config parameter. See BaseRunOptions() for a list of options int DaemonMain(int argc, const char* const argv[], const components::ComponentList& components_list); +/// @overload +int DaemonMain(const boost::program_options::variables_map& vm, const components::ComponentList& components_list); + } // namespace utils USERVER_NAMESPACE_END diff --git a/core/src/utils/daemon_run.cpp b/core/src/utils/daemon_run.cpp index e3e9c4d1d7b2..d023bf1ea96a 100644 --- a/core/src/utils/daemon_run.cpp +++ b/core/src/utils/daemon_run.cpp @@ -15,36 +15,38 @@ namespace utils { namespace { -std::optional ToOptional(std::string&& s) { - if (s.empty()) +template +std::optional ToOptional(const Value& val) { + if (val.empty()) return {}; else - return {std::move(s)}; + return {val.template as()}; } } // namespace -int DaemonMain(const int argc, const char* const argv[], const components::ComponentList& components_list) { - utils::impl::FinishStaticRegistration(); - +boost::program_options::options_description BaseRunOptions() { namespace po = boost::program_options; - - po::variables_map vm; po::options_description desc("Allowed options"); - std::string config_path; - std::string config_vars_path; - std::string config_vars_override_path; - // clang-format off - desc.add_options() - ("help,h", "produce this help message") - ("print-config-schema", "print config.yaml YAML Schema") - ("print-dynamic-config-defaults", "print JSON object with dynamic config defaults") - ("config,c", po::value(&config_path)->required(), "path to server config") - ("config_vars", po::value(&config_vars_path), "path to config_vars.yaml; if set, config_vars in config.yaml are ignored") - ("config_vars_override", po::value(&config_vars_override_path), "path to an additional config_vars.yaml, which overrides vars of config_vars.yaml") - ; + desc.add_options() + ("help,h", "produce this help message") + ("print-config-schema", "print config.yaml YAML Schema") + ("print-dynamic-config-defaults", "print JSON object with dynamic config defaults") + ("config_vars", po::value(), "path to config_vars.yaml; if set, config_vars in config.yaml are ignored") + ("config_vars_override", po::value(), "path to an additional config_vars.yaml, which overrides vars of config_vars.yaml") + ; // clang-format on +return desc; +} + +int DaemonMain(const int argc, const char* const argv[], const components::ComponentList& components_list) { + namespace po = boost::program_options; + po::variables_map vm; + auto desc = BaseRunOptions(); + desc.add_options() + ("config,c", po::value()->required(), "path to server config") + ; try { po::store(po::parse_command_line(argc, argv, desc), vm); @@ -59,6 +61,12 @@ int DaemonMain(const int argc, const char* const argv[], const components::Compo return 0; } + return DaemonMain(vm, components_list); +} + +int DaemonMain(const boost::program_options::variables_map& vm, const components::ComponentList& components_list) { + utils::impl::FinishStaticRegistration(); + if (vm.count("print-config-schema")) { std::cout << components::impl::GetStaticConfigSchema(components_list) << "\n"; return 0; @@ -71,9 +79,9 @@ int DaemonMain(const int argc, const char* const argv[], const components::Compo try { components::Run( - config_path, - ToOptional(std::move(config_vars_path)), - ToOptional(std::move(config_vars_override_path)), + vm["config"].as(), + ToOptional(vm["config_vars"]), + ToOptional(vm["config_vars_override"]), components_list ); return 0; diff --git a/libraries/easy/src/easy.cpp b/libraries/easy/src/easy.cpp index d461a8c22479..17ecca90e4fc 100644 --- a/libraries/easy/src/easy.cpp +++ b/libraries/easy/src/easy.cpp @@ -1,6 +1,7 @@ #include #include +#include #include #include @@ -104,9 +105,8 @@ HttpBase::~HttpBase() { AddComponentsConfig(fmt::format(kConfigLoggingTemplate, ToString(level_))); namespace po = boost::program_options; - po::variables_map vm; - po::options_description desc("Easy options"); + auto desc = utils::BaseRunOptions(); std::string config_dump; std::string schema_dump; @@ -114,12 +114,18 @@ HttpBase::~HttpBase() { desc.add_options() ("dump-config", po::value(&config_dump), "path to dump the server config") ("dump-schema", po::value(&schema_dump), "path to dump the DB schema") + ("config,c", po::value(), "path to server config") ; // clang-format on - po::store(po::command_line_parser(argc_, argv_).options(desc).allow_unregistered().run(), vm); + po::store(po::parse_command_line(argc_, argv_, desc), vm); po::notify(vm); + if (vm.count("help")) { + std::cerr << desc << '\n'; + return; + } + if (vm.count("dump-config")) { std::ofstream(config_dump) << static_config_; return; @@ -131,9 +137,8 @@ HttpBase::~HttpBase() { if (argc_ <= 1) { components::Run(components::InMemoryConfig{static_config_}, component_list_); - return; } else { - const auto ret = utils::DaemonMain(argc_, argv_, component_list_); + const auto ret = utils::DaemonMain(vm, component_list_); if (ret != 0) { std::exit(ret); } From adb286cec8d7ef9a94daae06bc0a8081c1d0b777 Mon Sep 17 00:00:00 2001 From: Antony Polukhin Date: Sun, 1 Dec 2024 17:27:18 +0300 Subject: [PATCH 18/19] review fixes --- libraries/easy/include/userver/easy.hpp | 178 ++++++++++-------- libraries/easy/samples/2_key_value/main.cpp | 2 +- .../samples/2_key_value/tests/conftest.py | 13 +- libraries/easy/samples/3_json/main.cpp | 4 +- .../easy/samples/3_json/tests/conftest.py | 17 +- .../easy/samples/4_custom_dependency/main.cpp | 73 ++++--- .../4_custom_dependency/tests/conftest.py | 28 ++- .../4_custom_dependency/tests/test_basic.py | 10 +- libraries/easy/src/easy.cpp | 124 ++++++------ 9 files changed, 261 insertions(+), 188 deletions(-) diff --git a/libraries/easy/include/userver/easy.hpp b/libraries/easy/include/userver/easy.hpp index 91e714d66626..4f1d104a0e5b 100644 --- a/libraries/easy/include/userver/easy.hpp +++ b/libraries/easy/include/userver/easy.hpp @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -21,29 +22,22 @@ USERVER_NAMESPACE_BEGIN /// @brief Top namespace for `easy` library namespace easy { -/// @brief Base class for all the dependencies of the `easy` library. +namespace impl { + class DependenciesBase : public components::ComponentBase { public: static constexpr std::string_view kName = "easy-dependencies"; using components::ComponentBase::ComponentBase; virtual ~DependenciesBase(); - - /// @returns the \b last schema that was provided to the easy::HttpWith or easy::HttpBase - static const std::string& GetSchema() noexcept; }; -/// @brief Tag for custom Dependency registration customization. -/// -/// For a new dependency type `Dependency` a function `void Registration(OfDependency, HttpBase& app)` -/// should be provided to be able to use that dependency with the easy::HttpWith class. -template -struct OfDependency final {}; +} // namespace impl /// @brief easy::HttpWith like class with erased dependencies information that should be used only in dependency /// registration functions; use easy::HttpWith if not making a new dependency class. class HttpBase final { public: - using Callback = std::function; + using Callback = std::function; /// Sets the default Content-Type header for all the routes void DefaultContentType(http::ContentType content_type); @@ -51,23 +45,33 @@ class HttpBase final { /// Register an HTTP handler by `path` that supports the `methods` HTTP methods void Route(std::string_view path, Callback&& func, std::initializer_list methods); - /// Add a comonent config to the service config - void AddComponentsConfig(std::string_view config); - /// Append a component to the component list of a service template - void AppendComponent(std::string_view name) { + bool TryAddComponent(std::string_view name, std::string_view config) { + if (component_list_.Contains(name)) { + return false; + } + component_list_.Append(name); + AddComponentConfig(name, config); + return true; } - /// @overload template - void AppendComponent() { - component_list_.Append(); + bool TryAddComponent(std::string_view name) { + if (component_list_.Contains(name)) { + return false; + } + + component_list_.Append(name); + return true; } - /// Stores the schema for further retrieval from DependenciesBase::GetSchema() - void Schema(std::string_view schema); + /// Stores the schema for further retrieval from GetDbSchema() + void DbSchema(std::string_view schema); + + /// @returns the \b last schema that was provided to the easy::HttpWith or easy::HttpBase + static const std::string& GetDbSchema() noexcept; /// Set the HTTP server listen port, default is 8080. void Port(std::uint16_t port); @@ -79,6 +83,8 @@ class HttpBase final { template friend class HttpWith; + void AddComponentConfig(std::string_view name, std::string_view config); + HttpBase(int argc, const char* const argv[]); ~HttpBase(); @@ -93,13 +99,27 @@ class HttpBase final { logging::Level level_ = logging::Level::kDebug; }; +/// Class that combines dependencies passed to HttpWith into a single type, that is passed to callbacks. +/// +/// @see @ref scripts/docs/en/userver/libraries/easy.md +template +class Dependencies final : public impl::DependenciesBase, public Dependency... { +public: + Dependencies(const components::ComponentConfig& config, const components::ComponentContext& context) + : DependenciesBase{config, context}, Dependency{context}... {} + + static void RegisterOn(HttpBase& app) { (Dependency::RegisterOn(app), ...); } +}; + /// @brief Class for describing the service functionality in simple declarative way that generates static configs, /// applies schemas. /// /// @see @ref scripts/docs/en/userver/libraries/easy.md -template +template > class HttpWith final { public: + using Dependency = std::conditional_t, Deps, Dependencies>; + /// Helper class that can store any callback of the following signatures: /// /// * formats::json::Value(formats::json::Value, const Dependency&) @@ -121,8 +141,10 @@ class HttpWith final { HttpBase::Callback func_; }; - HttpWith(int argc, const char* const argv[]) : impl_(argc, argv) { impl_.AppendComponent(); } - ~HttpWith() { Registration(OfDependency{}, impl_); /* ADL is intentional */ } + HttpWith(int argc, const char* const argv[]) : impl_(argc, argv) { + impl_.TryAddComponent(Dependency::kName); + } + ~HttpWith() { Dependency::RegisterOn(impl_); } /// @copydoc HttpBase::DefaultContentType HttpWith& DefaultContentType(http::ContentType content_type) { @@ -176,29 +198,9 @@ class HttpWith final { return *this; } - /// @copydoc HttpBase::AddComponentsConfig - HttpWith& AddComponentsConfig(std::string_view config) { - impl_.AddComponentsConfig(config); - return *this; - } - - /// @copydoc HttpBase::AppendComponent - template - HttpWith& AppendComponent(std::string_view name) { - impl_.AppendComponent(name); - return *this; - } - - /// @copydoc HttpBase::AppendComponent - template - HttpWith& AppendComponent() { - impl_.AppendComponent(); - return *this; - } - /// @copydoc HttpBase::Schema - HttpWith& Schema(std::string_view schema) { - impl_.Schema(schema); + HttpWith& DbSchema(std::string_view schema) { + impl_.DbSchema(schema); return *this; } @@ -218,75 +220,87 @@ class HttpWith final { HttpBase impl_; }; -template -inline void Registration(T, HttpBase&&) { - static_assert( - sizeof(T) && false, - "Define `void Registration(easy::OfDependency, HttpBase& app)` " - "in the namespace of the your dependency type `T` to automatically add required configurations and " - "components to the `app`. For example:" - "void Registration(easy::OfDependency, easy::HttpBase& app) { app.AppendComponent(); }" - ); -} - -inline void Registration(OfDependency, HttpBase&) {} - -template +template template -HttpWith::Callback::Callback(Function func) { +HttpWith::Callback::Callback(Function func) { using server::http::HttpRequest; - if constexpr (std::is_invocable_r_v) { - func_ = [f = std::move(func)](const HttpRequest& req, const DependenciesBase& deps) { + constexpr unsigned kMatches = + (std::is_invocable_r_v << 0) | + (std::is_invocable_r_v << 1) | + (std::is_invocable_r_v << 2) | + (std::is_invocable_r_v << 3) | + (std::is_invocable_r_v << 4) | + (std::is_invocable_r_v << 5); + static_assert( + kMatches, + "Failed to find a matching signature. See the easy::HttpWith::Callback docs for info on " + "supported signatures" + ); + constexpr bool has_single_match = ((kMatches & (kMatches - 1)) == 0); + static_assert( + has_single_match, + "Found more than one matching signature, probably due to `auto` usage in parameters. See " + "the easy::HttpWith::Callback docs for info on supported signatures" + ); + + if constexpr (kMatches & 1) { + func_ = [f = std::move(func)](const HttpRequest& req, const impl::DependenciesBase& deps) { req.GetHttpResponse().SetContentType(http::content_type::kApplicationJson); return formats::json::ToString( f(formats::json::FromString(req.RequestBody()), static_cast(deps)) ); }; - } else if constexpr (std::is_invocable_r_v) { - func_ = [f = std::move(func)](const HttpRequest& req, const DependenciesBase&) { + } else if constexpr (kMatches & 2) { + func_ = [f = std::move(func)](const HttpRequest& req, const impl::DependenciesBase&) { req.GetHttpResponse().SetContentType(http::content_type::kApplicationJson); return formats::json::ToString(f(formats::json::FromString(req.RequestBody()))); }; - } else if constexpr (std::is_invocable_r_v) { - func_ = [f = std::move(func)](const HttpRequest& req, const DependenciesBase& deps) { + } else if constexpr (kMatches & 4) { + func_ = [f = std::move(func)](const HttpRequest& req, const impl::DependenciesBase& deps) { req.GetHttpResponse().SetContentType(http::content_type::kApplicationJson); return formats::json::ToString(f(req, static_cast(deps))); }; - } else if constexpr (std::is_invocable_r_v) { - func_ = [f = std::move(func)](const HttpRequest& req, const DependenciesBase& deps) { + } else if constexpr (kMatches & 8) { + func_ = [f = std::move(func)](const HttpRequest& req, const impl::DependenciesBase& deps) { return f(req, static_cast(deps)); }; - } else if constexpr (std::is_invocable_r_v) { - func_ = [f = std::move(func)](const HttpRequest& req, const DependenciesBase&) { + } else if constexpr (kMatches & 16) { + func_ = [f = std::move(func)](const HttpRequest& req, const impl::DependenciesBase&) { req.GetHttpResponse().SetContentType(http::content_type::kApplicationJson); return formats::json::ToString(f(req)); }; } else { - static_assert(std::is_invocable_r_v); - func_ = [f = std::move(func)](const HttpRequest& req, const DependenciesBase&) { return f(req); }; + static_assert(kMatches & 32); + func_ = [f = std::move(func)](const HttpRequest& req, const impl::DependenciesBase&) { return f(req); }; } } -/// @brief Predefined dependencies class that provides a PostgreSQL cluster client. -class PgDep : public DependenciesBase { +/// @brief Dependency class that provides a PostgreSQL cluster client. +class PgDep { public: - PgDep(const components::ComponentConfig& config, const components::ComponentContext& context); - + explicit PgDep(const components::ComponentContext& context); storages::postgres::Cluster& pg() const noexcept { return *pg_cluster_; } + static void RegisterOn(HttpBase& app); private: storages::postgres::ClusterPtr pg_cluster_; }; -void Registration(OfDependency, HttpBase& app); +/// @brief Dependency class that provides a Http client. +class HttpDep { +public: + explicit HttpDep(const components::ComponentContext& context); + clients::http::Client& http() { return http_; } + static void RegisterOn(HttpBase& app); -} // namespace easy +private: + clients::http::Client& http_; +}; -template <> -inline constexpr auto components::kConfigFileMode = ConfigFileMode::kNotRequired; +} // namespace easy -template <> -inline constexpr auto components::kConfigFileMode = ConfigFileMode::kNotRequired; +template +inline constexpr auto components::kConfigFileMode> = ConfigFileMode::kNotRequired; USERVER_NAMESPACE_END diff --git a/libraries/easy/samples/2_key_value/main.cpp b/libraries/easy/samples/2_key_value/main.cpp index cf8967422e8a..309678a5d2a5 100644 --- a/libraries/easy/samples/2_key_value/main.cpp +++ b/libraries/easy/samples/2_key_value/main.cpp @@ -12,7 +12,7 @@ CREATE TABLE IF NOT EXISTS key_value_table ( int main(int argc, char* argv[]) { easy::HttpWith(argc, argv) - .Schema(kSchema) + .DbSchema(kSchema) .DefaultContentType(http::content_type::kTextPlain) .Get( "/kv", diff --git a/libraries/easy/samples/2_key_value/tests/conftest.py b/libraries/easy/samples/2_key_value/tests/conftest.py index 2a91ee35c675..21a32cff6580 100644 --- a/libraries/easy/samples/2_key_value/tests/conftest.py +++ b/libraries/easy/samples/2_key_value/tests/conftest.py @@ -9,10 +9,15 @@ @pytest.fixture(scope="session") -def pgsql_local(service_tmpdir, service_binary, pgsql_local_create): - schema_path = service_tmpdir.joinpath("schemas") - os.mkdir(schema_path) - subprocess.run([service_binary, "--dump-schema", schema_path]) +def schema_path(service_binary, service_tmpdir): + path = service_tmpdir.joinpath("schemas") + os.mkdir(path) + subprocess.run([service_binary, "--dump-db-schema", path / "0_pg.sql"]) + return path + + +@pytest.fixture(scope="session") +def pgsql_local(schema_path, pgsql_local_create): databases = discover.find_schemas("admin", [schema_path]) return pgsql_local_create(list(databases.values())) # /// [psql prepare] diff --git a/libraries/easy/samples/3_json/main.cpp b/libraries/easy/samples/3_json/main.cpp index e93be32d3ad8..155fe88d97cf 100644 --- a/libraries/easy/samples/3_json/main.cpp +++ b/libraries/easy/samples/3_json/main.cpp @@ -13,7 +13,7 @@ CREATE TABLE IF NOT EXISTS key_value_table ( int main(int argc, char* argv[]) { easy::HttpWith(argc, argv) - .Schema(kSchema) + .DbSchema(kSchema) .Get( "/kv", [](formats::json::Value request_json, const easy::PgDep& dep) { @@ -28,7 +28,7 @@ int main(int argc, char* argv[]) { return formats::json::ValueBuilder{response}.ExtractValue(); } ) - .Post("/kv", [](formats::json::Value request_json, const auto& dep) { + .Post("/kv", [](formats::json::Value request_json, easy::PgDep dep) { // Use generated parser for As() auto key_value = request_json.As(); diff --git a/libraries/easy/samples/3_json/tests/conftest.py b/libraries/easy/samples/3_json/tests/conftest.py index 2a91ee35c675..c927a72c0ffe 100644 --- a/libraries/easy/samples/3_json/tests/conftest.py +++ b/libraries/easy/samples/3_json/tests/conftest.py @@ -1,4 +1,4 @@ -# /// [psql prepare] +# /// [pgsql prepare] import os import pytest import subprocess @@ -9,10 +9,15 @@ @pytest.fixture(scope="session") -def pgsql_local(service_tmpdir, service_binary, pgsql_local_create): - schema_path = service_tmpdir.joinpath("schemas") - os.mkdir(schema_path) - subprocess.run([service_binary, "--dump-schema", schema_path]) +def schema_path(service_binary, service_tmpdir): + path = service_tmpdir.joinpath("schemas") + os.mkdir(path) + subprocess.run([service_binary, "--dump-db-schema", path / "0_pg.sql"]) + return path + + +@pytest.fixture(scope="session") +def pgsql_local(schema_path, pgsql_local_create): databases = discover.find_schemas("admin", [schema_path]) return pgsql_local_create(list(databases.values())) - # /// [psql prepare] + # /// [pgsql prepare] diff --git a/libraries/easy/samples/4_custom_dependency/main.cpp b/libraries/easy/samples/4_custom_dependency/main.cpp index bd719fe5be4d..70c4a8c64bb3 100644 --- a/libraries/easy/samples/4_custom_dependency/main.cpp +++ b/libraries/easy/samples/4_custom_dependency/main.cpp @@ -4,61 +4,72 @@ #include #include +#include +#include + constexpr std::string_view kSchema = R"~( CREATE TABLE IF NOT EXISTS events_table ( id serial NOT NULL, - action VARCHAR PRIMARY KEY, - host_id INTEGER + action VARCHAR PRIMARY KEY ) )~"; -class MyDependency : public easy::PgDep { +class ActionClient : public components::ComponentBase { public: - MyDependency(const components::ComponentConfig& config, const components::ComponentContext& context) - : PgDep(config, context), host_id_(config["host_id"].As()) {} + static constexpr std::string_view kName = "action-client"; + + ActionClient(const components::ComponentConfig& config, const components::ComponentContext& context) + : ComponentBase{config, context}, + service_url_(config["service-url"].As()), + http_client_(context.FindComponent().GetHttpClient()) {} - int host_id() const { return host_id_; } + auto CreateHttpRequest(std::string action) const { + return http_client_.CreateRequest().url(service_url_).post().data(std::move(action)).perform(); + } static yaml_config::Schema GetStaticConfigSchema() { - return yaml_config::MergeSchemas(R"( + return yaml_config::MergeSchemas(R"( type: object - description: ID of the host + description: My dependencies schema additionalProperties: false properties: - host_id: - type: integer - description: ID of the host + service-url: + type: string + description: URL of the service to send the actions to )"); } private: - const int host_id_; + const std::string service_url_; + clients::http::Client& http_client_; }; -void Registration(easy::OfDependency, easy::HttpBase& app) { - Registration(easy::OfDependency{}, app); // base class initilization +class ActionDep { +public: + explicit ActionDep(const components::ComponentContext& config) : component_{config.FindComponent()} {} + auto CreateActionRequest(std::string action) const { return component_.CreateHttpRequest(std::move(action)); } - app.AddComponentsConfig(R"~( -easy-dependencies: - host_id#env: HOST_ID - host_id#fallback: 42 -)~"); -} +protected: + static void RegisterOn(easy::HttpBase& app) { + app.TryAddComponent(ActionClient::kName, "service-url: http://some-service.example/v1/action"); + easy::HttpDep::RegisterOn(app); + } -template <> -inline constexpr auto components::kConfigFileMode = ConfigFileMode::kNotRequired; +private: + ActionClient& component_; +}; int main(int argc, char* argv[]) { - easy::HttpWith(argc, argv) - .Schema(kSchema) + using Deps = easy::Dependencies; + + easy::HttpWith(argc, argv) + .DbSchema(kSchema) .DefaultContentType(http::content_type::kTextPlain) - .Post("/log", [](const server::http::HttpRequest& req, const MyDependency& dep) { - dep.pg().Execute( - storages::postgres::ClusterHostType::kMaster, - "INSERT INTO events_table(action, host_id) VALUES($1, $2)", - req.GetArg("action"), - dep.host_id() + .Post("/log", [](const server::http::HttpRequest& req, const Deps& deps) { + const auto& action = req.GetArg("action"); + deps.pg().Execute( + storages::postgres::ClusterHostType::kMaster, "INSERT INTO events_table(action) VALUES($1)", action ); - return std::string{}; + return deps.CreateActionRequest(action)->body(); }); } diff --git a/libraries/easy/samples/4_custom_dependency/tests/conftest.py b/libraries/easy/samples/4_custom_dependency/tests/conftest.py index 2a91ee35c675..31c20c170a3c 100644 --- a/libraries/easy/samples/4_custom_dependency/tests/conftest.py +++ b/libraries/easy/samples/4_custom_dependency/tests/conftest.py @@ -1,4 +1,4 @@ -# /// [psql prepare] +# /// [pgsql prepare] import os import pytest import subprocess @@ -7,12 +7,28 @@ pytest_plugins = ["pytest_userver.plugins.postgresql"] +USERVER_CONFIG_HOOKS = ["userver_actions_service"] + + +@pytest.fixture(scope="session") +def userver_actions_service(mockserver_info): + def do_patch(config_yaml, config_vars): + components = config_yaml["components_manager"]["components"] + components["action-client"]["service-url"] = mockserver_info.url("/v1/action") + + return do_patch + + +@pytest.fixture(scope="session") +def schema_path(service_binary, service_tmpdir): + path = service_tmpdir.joinpath("schemas") + os.mkdir(path) + subprocess.run([service_binary, "--dump-db-schema", path / "0_pg.sql"]) + return path + @pytest.fixture(scope="session") -def pgsql_local(service_tmpdir, service_binary, pgsql_local_create): - schema_path = service_tmpdir.joinpath("schemas") - os.mkdir(schema_path) - subprocess.run([service_binary, "--dump-schema", schema_path]) +def pgsql_local(schema_path, pgsql_local_create): databases = discover.find_schemas("admin", [schema_path]) return pgsql_local_create(list(databases.values())) - # /// [psql prepare] + # /// [pgsql prepare] diff --git a/libraries/easy/samples/4_custom_dependency/tests/test_basic.py b/libraries/easy/samples/4_custom_dependency/tests/test_basic.py index 25735a7b0f67..6e599154d046 100644 --- a/libraries/easy/samples/4_custom_dependency/tests/test_basic.py +++ b/libraries/easy/samples/4_custom_dependency/tests/test_basic.py @@ -1,10 +1,14 @@ -async def test_log_action(service_client, pgsql): +async def test_log_action(service_client, pgsql, mockserver): + @mockserver.handler("/v1/action") + def _mock(request): + assert request.get_data() == b"test_1", f"Actual data is {request.get_data()}" + return mockserver.make_response() + response = await service_client.post("/log?action=test_1") assert response.status == 200 cursor = pgsql["0_pg"].cursor() # '0_pg.sql' is created by easy::PgDep, so the database is '0_pg' - cursor.execute("SELECT action, host_id FROM events_table WHERE id=1") + cursor.execute("SELECT action FROM events_table WHERE id=1") result = cursor.fetchall() assert len(result) == 1 assert result[0][0] == "test_1" - assert result[0][1] == 42 diff --git a/libraries/easy/src/easy.cpp b/libraries/easy/src/easy.cpp index 17ecca90e4fc..1f61bdf1fa51 100644 --- a/libraries/easy/src/easy.cpp +++ b/libraries/easy/src/easy.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -33,52 +34,51 @@ constexpr std::string_view kConfigBase = R"~( default_task_processor: main-task-processor # Task processor in which components start. - components: # Configuring components that were registered via component_list -)~"; + components: # Configuring components that were registered via component_list)~"; constexpr std::string_view kConfigServerTemplate = R"~( -server: - listener: # configuring the main listening socket... - port: {} # ...to listen on this port and... - task_processor: main-task-processor # ...process incoming requests on this task processor. + server: + listener: # configuring the main listening socket... + port: {} # ...to listen on this port and... + task_processor: main-task-processor # ...process incoming requests on this task processor. )~"; constexpr std::string_view kConfigLoggingTemplate = R"~( -logging: - fs-task-processor: fs-task-processor - loggers: - default: - file_path: '@stderr' - level: {} - overflow_behavior: discard # Drop logs if the system is too busy to write them down. + logging: + fs-task-processor: fs-task-processor + loggers: + default: + file_path: '@stderr' + level: {} + overflow_behavior: discard # Drop logs if the system is too busy to write them down. )~"; -constexpr std::string_view kConfigHandlerTemplate = R"~( -{0}: - path: {1} # Registering handler by URL '{1}'. - method: {2} - task_processor: main-task-processor # Run it on CPU bound task processor -)~"; +constexpr std::string_view kConfigHandlerTemplate{ + "path: {0} # Registering handler by URL '{0}'.\n" + "method: {1}\n" + "task_processor: main-task-processor # Run it on CPU bound task processor\n"}; struct SharedPyaload { std::unordered_map http_functions; std::optional default_content_type; - std::string schema; + std::string db_schema; }; SharedPyaload globals{}; } // anonymous namespace +namespace impl { + DependenciesBase::~DependenciesBase() = default; -const std::string& DependenciesBase::GetSchema() noexcept { return globals.schema; } +} // namespace impl class HttpBase::Handle final : public server::handlers::HttpHandlerBase { public: Handle(const components::ComponentConfig& config, const components::ComponentContext& context) : HttpHandlerBase(config, context), - deps_{context.FindComponent()}, + deps_{context.FindComponent()}, callback_{globals.http_functions.at(config.Name())} {} std::string HandleRequestThrow(const server::http::HttpRequest& request, server::request::RequestContext&) @@ -90,7 +90,7 @@ class HttpBase::Handle final : public server::handlers::HttpHandlerBase { } private: - const DependenciesBase& deps_; + const impl::DependenciesBase& deps_; HttpBase::Callback& callback_; }; @@ -101,8 +101,8 @@ HttpBase::HttpBase(int argc, const char* const argv[]) component_list_{components::MinimalServerComponentList()} {} HttpBase::~HttpBase() { - AddComponentsConfig(fmt::format(kConfigServerTemplate, port_)); - AddComponentsConfig(fmt::format(kConfigLoggingTemplate, ToString(level_))); + static_config_.append(fmt::format(kConfigServerTemplate, port_)); + static_config_.append(fmt::format(kConfigLoggingTemplate, ToString(level_))); namespace po = boost::program_options; po::variables_map vm; @@ -113,7 +113,7 @@ HttpBase::~HttpBase() { // clang-format off desc.add_options() ("dump-config", po::value(&config_dump), "path to dump the server config") - ("dump-schema", po::value(&schema_dump), "path to dump the DB schema") + ("dump-db-schema", po::value(&schema_dump), "path to dump the DB schema") ("config,c", po::value(), "path to server config") ; // clang-format on @@ -131,8 +131,8 @@ HttpBase::~HttpBase() { return; } - if (vm.count("dump-schema")) { - std::ofstream(schema_dump + "/0_pg.sql") << globals.schema; + if (vm.count("dump-db-schema")) { + std::ofstream(schema_dump) << globals.db_schema; } if (argc_ <= 1) { @@ -152,43 +152,61 @@ void HttpBase::Route(std::string_view path, Callback&& func, std::initializer_li globals.http_functions.emplace(component_name, std::move(func)); component_list_.Append(component_name); - AddComponentsConfig(fmt::format(kConfigHandlerTemplate, component_name, path, fmt::join(methods, ","))); + AddComponentConfig(component_name, fmt::format(kConfigHandlerTemplate, path, fmt::join(methods, ","))); } -void HttpBase::AddComponentsConfig(std::string_view config) { - auto conf = fmt::format("\n{}\n", config); - static_config_ += boost::algorithm::replace_all_copy(conf, "\n", "\n "); +void HttpBase::AddComponentConfig(std::string_view component, std::string_view config) { + static_config_ += fmt::format("\n {}:", component); + if (config.empty()) { + static_config_ += " {}\n"; + } else { + if (config.back() == '\n') { + config = std::string_view{config.data(), config.size() - 1}; + } + static_config_ += boost::algorithm::replace_all_copy("\n" + std::string{config}, "\n", "\n "); + static_config_ += '\n'; + } } -void HttpBase::Schema(std::string_view schema) { globals.schema = schema; } +void HttpBase::DbSchema(std::string_view schema) { globals.db_schema = schema; } + +const std::string& HttpBase::GetDbSchema() noexcept { return globals.db_schema; } void HttpBase::Port(std::uint16_t port) { port_ = port; } void HttpBase::LogLevel(logging::Level level) { level_ = level; } -PgDep::PgDep(const components::ComponentConfig& config, const components::ComponentContext& context) - : DependenciesBase{config, context}, - pg_cluster_(context.FindComponent("postgres").GetCluster()) { - pg_cluster_->Execute(storages::postgres::ClusterHostType::kMaster, GetSchema()); +PgDep::PgDep(const components::ComponentContext& context) + : pg_cluster_(context.FindComponent("postgres").GetCluster()) { + pg_cluster_->Execute(storages::postgres::ClusterHostType::kMaster, HttpBase::GetDbSchema()); } -void Registration(OfDependency, HttpBase& app) { - app.AddComponentsConfig(R"~( -postgres: - dbconnection#env: POSTGRESQL - dbconnection#fallback: 'postgresql://testsuite@localhost:15433/postgres' - blocking_task_processor: fs-task-processor - dns_resolver: async - -testsuite-support: - -dns-client: - fs-task-processor: fs-task-processor -)~"); +void PgDep::RegisterOn(HttpBase& app) { + app.TryAddComponent( + "postgres", + "dbconnection#env: POSTGRESQL\n" + "dbconnection#fallback: 'postgresql://testsuite@localhost:15433/postgres'\n" + "blocking_task_processor: fs-task-processor\n" + "dns_resolver: async\n" + ); + + app.TryAddComponent(components::TestsuiteSupport::kName, ""); + app.TryAddComponent( + clients::dns::Component::kName, "fs-task-processor: fs-task-processor" + ); +} - app.AppendComponent("postgres"); - app.AppendComponent(); - app.AppendComponent(); +HttpDep::HttpDep(const components::ComponentContext& context) + : http_(context.FindComponent().GetHttpClient()) {} + +void HttpDep::RegisterOn(easy::HttpBase& app) { + app.TryAddComponent( + components::HttpClient::kName, + "pool-statistics-disable: false\n" + "thread-name-prefix: http-client\n" + "threads: 2\n" + "fs-task-processor: fs-task-processor\n" + ); } } // namespace easy From 564d49a200e743d2a563d6ae6432b141e84d32a1 Mon Sep 17 00:00:00 2001 From: Antony Polukhin Date: Sun, 1 Dec 2024 17:32:36 +0300 Subject: [PATCH 19/19] minimal docs --- scripts/docs/en/userver/libraries/easy.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/scripts/docs/en/userver/libraries/easy.md b/scripts/docs/en/userver/libraries/easy.md index d623532a1626..39e2f2104ab8 100644 --- a/scripts/docs/en/userver/libraries/easy.md +++ b/scripts/docs/en/userver/libraries/easy.md @@ -1,3 +1,11 @@ ## Easy - library for single file prototyping -TODO: +**Quality:** @ref QUALITY_TIERS "Silver Tier". + +Library for easy prototyping. Service functionality is described in code in a +short and declarative way. Static configs and database schemas are applied +automatically. + +Migration of a service on easy library to a more functional +[pg_service_template](https://github.com/userver-framework/pg_service_template) +is straightforward.