diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2440e38 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +build/ +.vscode/ +.vs/ +gen/ +*.user +.cache +**__pycache__ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..e69de29 diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..1e851a0 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,111 @@ +# This file is a part of RPCXX project + +#[[ +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +]] + + +cmake_minimum_required(VERSION 3.16) + +project(rpcxx VERSION 1.0 LANGUAGES CXX C) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_EXTENSIONS OFF) + +option(BUILD_SHARED_LIBS "Build libs as shared" OFF) +option(RPCXX_WITH_CODEGEN "Compile Code Generator" ON) +option(RPCXX_TEST_SANITIZERS "Enable sanitizers" ON) +option(RPCXX_TEST_RPS "Build QT5 based RPS test" OFF) +option(RPCXX_WITH_TESTS "Build tests" OFF) + +include(cmake/RPCXXCodegen.cmake) +include(cmake/CPM.cmake) + +CPMAddPackage("gh:p-ranav/argparse@3.0") +CPMAddPackage("gh:fmtlib/fmt#11.0.2") +CPMAddPackage("gh:cyanidle/describe@1.0") + +add_library(rpcxx-warnings INTERFACE) +add_library(rpcxx-options INTERFACE) +add_library(rpcxx-headers INTERFACE) +target_include_directories(rpcxx-headers INTERFACE + $ + $) + +if(CMAKE_COMPILER_IS_GNUCXX) + target_compile_options(rpcxx-warnings INTERFACE -Wall -Wextra) +endif() + +add_library(rpcxx-future STATIC src/future.cpp) +target_link_libraries(rpcxx-future PRIVATE rpcxx-options rpcxx-warnings) +target_link_libraries(rpcxx-future PUBLIC rpcxx-headers) + +file(GLOB JSON_VIEW_SOURCES CONFIGURE_DEPENDS src/json_view/*.cpp) +add_library(rpcxx-json STATIC ${JSON_VIEW_SOURCES}) +target_link_libraries(rpcxx-json PRIVATE rpcxx-options rpcxx-warnings) +target_link_libraries(rpcxx-json PUBLIC rpcxx-headers describe) + +file(GLOB RPCXX_SOURCES CONFIGURE_DEPENDS src/rpcxx/*.cpp) +add_library(rpcxx STATIC ${RPCXX_SOURCES}) +add_library(rpcxx::rpcxx ALIAS rpcxx) + +target_link_libraries(rpcxx PRIVATE rpcxx-options rpcxx-warnings) +target_link_libraries(rpcxx PUBLIC rpcxx-json rpcxx-headers rpcxx-future) + +install(TARGETS rpcxx OPTIONAL) + +# Do not clone submodules +cmake_policy(SET CMP0097 NEW) +CPMAddPackage(NAME rapidjson + GITHUB_REPOSITORY Tencent/rapidjson + VERSION 1.1.1 + GIT_TAG 7c73dd7 + GIT_SUBMODULES "" + DOWNLOAD_ONLY YES +) + +target_include_directories(rpcxx-json PRIVATE ${rapidjson_SOURCE_DIR}/include) + +if(RPCXX_WITH_TESTS) + CPMAddPackage("gh:doctest/doctest@2.4.11") + CPMAddPackage( + NAME benchmark + GITHUB_REPOSITORY google/benchmark + VERSION 1.8.3 + OPTIONS "BENCHMARK_ENABLE_TESTING OFF" + GIT_SHALLOW YES + EXCLUDE_FROM_ALL YES + ) +endif() + +if(WIN32) + target_link_libraries(rpcxx-json PRIVATE ws2_32) + target_link_libraries(rpcxx PRIVATE ws2_32) +endif() + +if(RPCXX_WITH_TESTS) + include(CTest) + enable_testing() + add_subdirectory(test) +endif() +if(RPCXX_WITH_CODEGEN) + add_subdirectory(codegen) +endif() diff --git a/LICENCE.txt b/LICENCE.txt new file mode 100644 index 0000000..619d98e --- /dev/null +++ b/LICENCE.txt @@ -0,0 +1,45 @@ +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +Copyright 2024 "НЕОЛАНТ Сервис", "НЕОЛАНТ Калиниград", Алексей Доронин, Анастасия Луговец, Дмитрий Дьяконов + +Данная лицензия разрешает лицам, получившим копию данного программного обеспечения и +сопутствующей документации (в дальнейшем именуемыми «Программное Обеспечение»), +безвозмездно использовать Программное Обеспечение без ограничений, +включая неограниченное право на использование, +копирование, изменение, добавление, публикацию, распространение, +сублицензирование и/или продажу копий Программного Обеспечения, +также как и лицам, которым предоставляется данное Программное Обеспечение, +при соблюдении следующих условий: + +Указанное выше уведомление об авторском праве и данные условия +должны быть включены во все копии или значимые части данного Программного Обеспечения. + +ДАННОЕ ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ ПРЕДОСТАВЛЯЕТСЯ «КАК ЕСТЬ», +БЕЗ КАКИХ-ЛИБО ГАРАНТИЙ, ЯВНО ВЫРАЖЕННЫХ ИЛИ ПОДРАЗУМЕВАЕМЫХ, +ВКЛЮЧАЯ, НО НЕ ОГРАНИЧИВАЯСЬ ГАРАНТИЯМИ ТОВАРНОЙ ПРИГОДНОСТИ, +СООТВЕТСТВИЯ ПО ЕГО КОНКРЕТНОМУ НАЗНАЧЕНИЮ И ОТСУТСТВИЯ НАРУШЕНИЙ ПРАВ. +НИ В КАКОМ СЛУЧАЕ АВТОРЫ ИЛИ ПРАВООБЛАДАТЕЛИ НЕ НЕСУТ +ОТВЕТСТВЕННОСТИ ПО ИСКАМ О ВОЗМЕЩЕНИИ УЩЕРБА, УБЫТКОВ ИЛИ ДРУГИХ ТРЕБОВАНИЙ +ПО ДЕЙСТВУЮЩИМ КОНТРАКТАМ, ДЕЛИКТАМ ИЛИ ИНОМУ, ВОЗНИКШИМ ИЗ, +ИМЕЮЩИМ ПРИЧИНОЙ ИЛИ СВЯЗАННЫМ С ПРОГРАММНЫМ ОБЕСПЕЧЕНИЕМ ИЛИ ИСПОЛЬЗОВАНИЕМ +ПРОГРАММНОГО ОБЕСПЕЧЕНИЯ ИЛИ ИНЫМИ ДЕЙСТВИЯМИ С ПРОГРАММНЫМ ОБЕСПЕЧЕНИЕМ. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..0159487 --- /dev/null +++ b/README.md @@ -0,0 +1,100 @@ +# RPCXX - Transport-agnostic (JsonRPC 2.0) cpp library + +```cpp +rpcxx::Server server; +rpcxx::Transport transport; + +server->SetTransport(&transport); + +transport.OnReply([](rpcxx::JsonView reply){ + MySendString(reply.Dump()); +}); +OnMyRequest([&](string_view msg){ + transport.Receive(rpcxx::Json::From(msg).View()); +}); + +// params as array +server.Method("add", [](int a, int b){ + return a + b; +}); +// Receive params as object +server.Method("named_sub", [](size_t a, size_t b){ + return a - b +}, rpcxx::NamesMap("a", "b")); + +myEventLoop.run(); +``` + +## JsonView +* Makes a structured View into the object to be serialized. +* Support great customizability with DESCRIBE attributes +* Uses Arenas for allocating the DOM - perfect memory model for RPC requests +* Arenas make freeing deep DOMs very fast +* Arenas make JsonViews trivially-copyable +* Arenas provide great cache-locality +```cpp +struct My { + string field; +}; +DESCRIBE(My, &_::field) + +My obj{"VeryLongString.............. etc."}; +jv::DefaultArena alloc; +jv::JsonView::From(obj, alloc); +// No copies. +// 16 bytes bump-allocated for single JsonPair (Json object of size 1) +// jv::DefaultArena has an extra size N stack buffer +``` + +## Codegen - Generate Server and Client stubs! + +* Custom DSL for describing RPC and Structures. +* Based on LUA 5.4+. +* Supports: cpp, go(limited) +* Can be built and immedieately used in single CMAKE build. +* Can be used for generating Json config boilerplate + +```lua +namespace "my" + +config = include("config.lua") + +CompressionAlgo = Alias(config.CompressionAlgo) + +SessionResult = struct() { + id = string, + version = string, +} + +Locale = enum() { + "ru_RU" +} + +L10n = struct() { + errors = Locale("ru_RU"), +} + +SessionFeatures = struct() { + rpc_compression = { + enabled = bool(false), + threshhold = u64(1024), + algo = CompressionAlgo(CompressionAlgo.gzip) + }, + localization = Optional(L10n), +} + +SessionParams = struct() { + reconnectId = Optional(string), + features = Optional(SessionFeatures) +} + +CloseParams = struct() { + allow_reconnect = Optional(bool), + reason = Optional(string), +} + +methods("Server_Name") { + initialize_session = async {#SessionParams} >> SessionResult, + close_session = {#CloseParams}, +} +``` \ No newline at end of file diff --git a/cmake/CPM.cmake b/cmake/CPM.cmake new file mode 100644 index 0000000..9e76fd5 --- /dev/null +++ b/cmake/CPM.cmake @@ -0,0 +1,1275 @@ +# CPM.cmake - CMake's missing package manager +# =========================================== +# See https://github.com/cpm-cmake/CPM.cmake for usage and update instructions. +# +# MIT License +# ----------- +#[[ + Copyright (c) 2019-2023 Lars Melchior and contributors + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +]] + +cmake_minimum_required(VERSION 3.14 FATAL_ERROR) + +# Initialize logging prefix +if(NOT CPM_INDENT) + set(CPM_INDENT + "CPM:" + CACHE INTERNAL "" + ) +endif() + +if(NOT COMMAND cpm_message) + function(cpm_message) + message(${ARGV}) + endfunction() +endif() + +set(CURRENT_CPM_VERSION 0.40.2) + +get_filename_component(CPM_CURRENT_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}" REALPATH) +if(CPM_DIRECTORY) + if(NOT CPM_DIRECTORY STREQUAL CPM_CURRENT_DIRECTORY) + if(CPM_VERSION VERSION_LESS CURRENT_CPM_VERSION) + message( + AUTHOR_WARNING + "${CPM_INDENT} \ +A dependency is using a more recent CPM version (${CURRENT_CPM_VERSION}) than the current project (${CPM_VERSION}). \ +It is recommended to upgrade CPM to the most recent version. \ +See https://github.com/cpm-cmake/CPM.cmake for more information." + ) + endif() + if(${CMAKE_VERSION} VERSION_LESS "3.17.0") + include(FetchContent) + endif() + return() + endif() + + get_property( + CPM_INITIALIZED GLOBAL "" + PROPERTY CPM_INITIALIZED + SET + ) + if(CPM_INITIALIZED) + return() + endif() +endif() + +if(CURRENT_CPM_VERSION MATCHES "development-version") + message( + WARNING "${CPM_INDENT} Your project is using an unstable development version of CPM.cmake. \ +Please update to a recent release if possible. \ +See https://github.com/cpm-cmake/CPM.cmake for details." + ) +endif() + +set_property(GLOBAL PROPERTY CPM_INITIALIZED true) + +macro(cpm_set_policies) + # the policy allows us to change options without caching + cmake_policy(SET CMP0077 NEW) + set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) + + # the policy allows us to change set(CACHE) without caching + if(POLICY CMP0126) + cmake_policy(SET CMP0126 NEW) + set(CMAKE_POLICY_DEFAULT_CMP0126 NEW) + endif() + + # The policy uses the download time for timestamp, instead of the timestamp in the archive. This + # allows for proper rebuilds when a projects url changes + if(POLICY CMP0135) + cmake_policy(SET CMP0135 NEW) + set(CMAKE_POLICY_DEFAULT_CMP0135 NEW) + endif() + + # treat relative git repository paths as being relative to the parent project's remote + if(POLICY CMP0150) + cmake_policy(SET CMP0150 NEW) + set(CMAKE_POLICY_DEFAULT_CMP0150 NEW) + endif() +endmacro() +cpm_set_policies() + +option(CPM_USE_LOCAL_PACKAGES "Always try to use `find_package` to get dependencies" + $ENV{CPM_USE_LOCAL_PACKAGES} +) +option(CPM_LOCAL_PACKAGES_ONLY "Only use `find_package` to get dependencies" + $ENV{CPM_LOCAL_PACKAGES_ONLY} +) +option(CPM_DOWNLOAD_ALL "Always download dependencies from source" $ENV{CPM_DOWNLOAD_ALL}) +option(CPM_DONT_UPDATE_MODULE_PATH "Don't update the module path to allow using find_package" + $ENV{CPM_DONT_UPDATE_MODULE_PATH} +) +option(CPM_DONT_CREATE_PACKAGE_LOCK "Don't create a package lock file in the binary path" + $ENV{CPM_DONT_CREATE_PACKAGE_LOCK} +) +option(CPM_INCLUDE_ALL_IN_PACKAGE_LOCK + "Add all packages added through CPM.cmake to the package lock" + $ENV{CPM_INCLUDE_ALL_IN_PACKAGE_LOCK} +) +option(CPM_USE_NAMED_CACHE_DIRECTORIES + "Use additional directory of package name in cache on the most nested level." + $ENV{CPM_USE_NAMED_CACHE_DIRECTORIES} +) + +set(CPM_VERSION + ${CURRENT_CPM_VERSION} + CACHE INTERNAL "" +) +set(CPM_DIRECTORY + ${CPM_CURRENT_DIRECTORY} + CACHE INTERNAL "" +) +set(CPM_FILE + ${CMAKE_CURRENT_LIST_FILE} + CACHE INTERNAL "" +) +set(CPM_PACKAGES + "" + CACHE INTERNAL "" +) +set(CPM_DRY_RUN + OFF + CACHE INTERNAL "Don't download or configure dependencies (for testing)" +) + +if(DEFINED ENV{CPM_SOURCE_CACHE}) + set(CPM_SOURCE_CACHE_DEFAULT $ENV{CPM_SOURCE_CACHE}) +else() + set(CPM_SOURCE_CACHE_DEFAULT OFF) +endif() + +set(CPM_SOURCE_CACHE + ${CPM_SOURCE_CACHE_DEFAULT} + CACHE PATH "Directory to download CPM dependencies" +) + +if(NOT CPM_DONT_UPDATE_MODULE_PATH) + set(CPM_MODULE_PATH + "${CMAKE_BINARY_DIR}/CPM_modules" + CACHE INTERNAL "" + ) + # remove old modules + file(REMOVE_RECURSE ${CPM_MODULE_PATH}) + file(MAKE_DIRECTORY ${CPM_MODULE_PATH}) + # locally added CPM modules should override global packages + set(CMAKE_MODULE_PATH "${CPM_MODULE_PATH};${CMAKE_MODULE_PATH}") +endif() + +if(NOT CPM_DONT_CREATE_PACKAGE_LOCK) + set(CPM_PACKAGE_LOCK_FILE + "${CMAKE_BINARY_DIR}/cpm-package-lock.cmake" + CACHE INTERNAL "" + ) + file(WRITE ${CPM_PACKAGE_LOCK_FILE} + "# CPM Package Lock\n# This file should be committed to version control\n\n" + ) +endif() + +include(FetchContent) + +# Try to infer package name from git repository uri (path or url) +function(cpm_package_name_from_git_uri URI RESULT) + if("${URI}" MATCHES "([^/:]+)/?.git/?$") + set(${RESULT} + ${CMAKE_MATCH_1} + PARENT_SCOPE + ) + else() + unset(${RESULT} PARENT_SCOPE) + endif() +endfunction() + +# Try to infer package name and version from a url +function(cpm_package_name_and_ver_from_url url outName outVer) + if(url MATCHES "[/\\?]([a-zA-Z0-9_\\.-]+)\\.(tar|tar\\.gz|tar\\.bz2|zip|ZIP)(\\?|/|$)") + # We matched an archive + set(filename "${CMAKE_MATCH_1}") + + if(filename MATCHES "([a-zA-Z0-9_\\.-]+)[_-]v?(([0-9]+\\.)*[0-9]+[a-zA-Z0-9]*)") + # We matched - (ie foo-1.2.3) + set(${outName} + "${CMAKE_MATCH_1}" + PARENT_SCOPE + ) + set(${outVer} + "${CMAKE_MATCH_2}" + PARENT_SCOPE + ) + elseif(filename MATCHES "(([0-9]+\\.)+[0-9]+[a-zA-Z0-9]*)") + # We couldn't find a name, but we found a version + # + # In many cases (which we don't handle here) the url would look something like + # `irrelevant/ACTUAL_PACKAGE_NAME/irrelevant/1.2.3.zip`. In such a case we can't possibly + # distinguish the package name from the irrelevant bits. Moreover if we try to match the + # package name from the filename, we'd get bogus at best. + unset(${outName} PARENT_SCOPE) + set(${outVer} + "${CMAKE_MATCH_1}" + PARENT_SCOPE + ) + else() + # Boldly assume that the file name is the package name. + # + # Yes, something like `irrelevant/ACTUAL_NAME/irrelevant/download.zip` will ruin our day, but + # such cases should be quite rare. No popular service does this... we think. + set(${outName} + "${filename}" + PARENT_SCOPE + ) + unset(${outVer} PARENT_SCOPE) + endif() + else() + # No ideas yet what to do with non-archives + unset(${outName} PARENT_SCOPE) + unset(${outVer} PARENT_SCOPE) + endif() +endfunction() + +function(cpm_find_package NAME VERSION) + string(REPLACE " " ";" EXTRA_ARGS "${ARGN}") + find_package(${NAME} ${VERSION} ${EXTRA_ARGS} QUIET) + if(${CPM_ARGS_NAME}_FOUND) + if(DEFINED ${CPM_ARGS_NAME}_VERSION) + set(VERSION ${${CPM_ARGS_NAME}_VERSION}) + endif() + cpm_message(STATUS "${CPM_INDENT} Using local package ${CPM_ARGS_NAME}@${VERSION}") + CPMRegisterPackage(${CPM_ARGS_NAME} "${VERSION}") + set(CPM_PACKAGE_FOUND + YES + PARENT_SCOPE + ) + else() + set(CPM_PACKAGE_FOUND + NO + PARENT_SCOPE + ) + endif() +endfunction() + +# Create a custom FindXXX.cmake module for a CPM package This prevents `find_package(NAME)` from +# finding the system library +function(cpm_create_module_file Name) + if(NOT CPM_DONT_UPDATE_MODULE_PATH) + # erase any previous modules + file(WRITE ${CPM_MODULE_PATH}/Find${Name}.cmake + "include(\"${CPM_FILE}\")\n${ARGN}\nset(${Name}_FOUND TRUE)" + ) + endif() +endfunction() + +# Find a package locally or fallback to CPMAddPackage +function(CPMFindPackage) + set(oneValueArgs NAME VERSION GIT_TAG FIND_PACKAGE_ARGUMENTS) + + cmake_parse_arguments(CPM_ARGS "" "${oneValueArgs}" "" ${ARGN}) + + if(NOT DEFINED CPM_ARGS_VERSION) + if(DEFINED CPM_ARGS_GIT_TAG) + cpm_get_version_from_git_tag("${CPM_ARGS_GIT_TAG}" CPM_ARGS_VERSION) + endif() + endif() + + set(downloadPackage ${CPM_DOWNLOAD_ALL}) + if(DEFINED CPM_DOWNLOAD_${CPM_ARGS_NAME}) + set(downloadPackage ${CPM_DOWNLOAD_${CPM_ARGS_NAME}}) + elseif(DEFINED ENV{CPM_DOWNLOAD_${CPM_ARGS_NAME}}) + set(downloadPackage $ENV{CPM_DOWNLOAD_${CPM_ARGS_NAME}}) + endif() + if(downloadPackage) + CPMAddPackage(${ARGN}) + cpm_export_variables(${CPM_ARGS_NAME}) + return() + endif() + + cpm_find_package(${CPM_ARGS_NAME} "${CPM_ARGS_VERSION}" ${CPM_ARGS_FIND_PACKAGE_ARGUMENTS}) + + if(NOT CPM_PACKAGE_FOUND) + CPMAddPackage(${ARGN}) + cpm_export_variables(${CPM_ARGS_NAME}) + endif() + +endfunction() + +# checks if a package has been added before +function(cpm_check_if_package_already_added CPM_ARGS_NAME CPM_ARGS_VERSION) + if("${CPM_ARGS_NAME}" IN_LIST CPM_PACKAGES) + CPMGetPackageVersion(${CPM_ARGS_NAME} CPM_PACKAGE_VERSION) + if("${CPM_PACKAGE_VERSION}" VERSION_LESS "${CPM_ARGS_VERSION}") + message( + WARNING + "${CPM_INDENT} Requires a newer version of ${CPM_ARGS_NAME} (${CPM_ARGS_VERSION}) than currently included (${CPM_PACKAGE_VERSION})." + ) + endif() + cpm_get_fetch_properties(${CPM_ARGS_NAME}) + set(${CPM_ARGS_NAME}_ADDED NO) + set(CPM_PACKAGE_ALREADY_ADDED + YES + PARENT_SCOPE + ) + cpm_export_variables(${CPM_ARGS_NAME}) + else() + set(CPM_PACKAGE_ALREADY_ADDED + NO + PARENT_SCOPE + ) + endif() +endfunction() + +# Parse the argument of CPMAddPackage in case a single one was provided and convert it to a list of +# arguments which can then be parsed idiomatically. For example gh:foo/bar@1.2.3 will be converted +# to: GITHUB_REPOSITORY;foo/bar;VERSION;1.2.3 +function(cpm_parse_add_package_single_arg arg outArgs) + # Look for a scheme + if("${arg}" MATCHES "^([a-zA-Z]+):(.+)$") + string(TOLOWER "${CMAKE_MATCH_1}" scheme) + set(uri "${CMAKE_MATCH_2}") + + # Check for CPM-specific schemes + if(scheme STREQUAL "gh") + set(out "GITHUB_REPOSITORY;${uri}") + set(packageType "git") + elseif(scheme STREQUAL "gl") + set(out "GITLAB_REPOSITORY;${uri}") + set(packageType "git") + elseif(scheme STREQUAL "bb") + set(out "BITBUCKET_REPOSITORY;${uri}") + set(packageType "git") + # A CPM-specific scheme was not found. Looks like this is a generic URL so try to determine + # type + elseif(arg MATCHES ".git/?(@|#|$)") + set(out "GIT_REPOSITORY;${arg}") + set(packageType "git") + else() + # Fall back to a URL + set(out "URL;${arg}") + set(packageType "archive") + + # We could also check for SVN since FetchContent supports it, but SVN is so rare these days. + # We just won't bother with the additional complexity it will induce in this function. SVN is + # done by multi-arg + endif() + else() + if(arg MATCHES ".git/?(@|#|$)") + set(out "GIT_REPOSITORY;${arg}") + set(packageType "git") + else() + # Give up + message(FATAL_ERROR "${CPM_INDENT} Can't determine package type of '${arg}'") + endif() + endif() + + # For all packages we interpret @... as version. Only replace the last occurrence. Thus URIs + # containing '@' can be used + string(REGEX REPLACE "@([^@]+)$" ";VERSION;\\1" out "${out}") + + # Parse the rest according to package type + if(packageType STREQUAL "git") + # For git repos we interpret #... as a tag or branch or commit hash + string(REGEX REPLACE "#([^#]+)$" ";GIT_TAG;\\1" out "${out}") + elseif(packageType STREQUAL "archive") + # For archives we interpret #... as a URL hash. + string(REGEX REPLACE "#([^#]+)$" ";URL_HASH;\\1" out "${out}") + # We don't try to parse the version if it's not provided explicitly. cpm_get_version_from_url + # should do this at a later point + else() + # We should never get here. This is an assertion and hitting it means there's a problem with the + # code above. A packageType was set, but not handled by this if-else. + message(FATAL_ERROR "${CPM_INDENT} Unsupported package type '${packageType}' of '${arg}'") + endif() + + set(${outArgs} + ${out} + PARENT_SCOPE + ) +endfunction() + +# Check that the working directory for a git repo is clean +function(cpm_check_git_working_dir_is_clean repoPath gitTag isClean) + + find_package(Git REQUIRED) + + if(NOT GIT_EXECUTABLE) + # No git executable, assume directory is clean + set(${isClean} + TRUE + PARENT_SCOPE + ) + return() + endif() + + # check for uncommitted changes + execute_process( + COMMAND ${GIT_EXECUTABLE} status --porcelain + RESULT_VARIABLE resultGitStatus + OUTPUT_VARIABLE repoStatus + OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET + WORKING_DIRECTORY ${repoPath} + ) + if(resultGitStatus) + # not supposed to happen, assume clean anyway + message(WARNING "${CPM_INDENT} Calling git status on folder ${repoPath} failed") + set(${isClean} + TRUE + PARENT_SCOPE + ) + return() + endif() + + if(NOT "${repoStatus}" STREQUAL "") + set(${isClean} + FALSE + PARENT_SCOPE + ) + return() + endif() + + # check for committed changes + execute_process( + COMMAND ${GIT_EXECUTABLE} diff -s --exit-code ${gitTag} + RESULT_VARIABLE resultGitDiff + OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_QUIET + WORKING_DIRECTORY ${repoPath} + ) + + if(${resultGitDiff} EQUAL 0) + set(${isClean} + TRUE + PARENT_SCOPE + ) + else() + set(${isClean} + FALSE + PARENT_SCOPE + ) + endif() + +endfunction() + +# Add PATCH_COMMAND to CPM_ARGS_UNPARSED_ARGUMENTS. This method consumes a list of files in ARGN +# then generates a `PATCH_COMMAND` appropriate for `ExternalProject_Add()`. This command is appended +# to the parent scope's `CPM_ARGS_UNPARSED_ARGUMENTS`. +function(cpm_add_patches) + # Return if no patch files are supplied. + if(NOT ARGN) + return() + endif() + + # Find the patch program. + find_program(PATCH_EXECUTABLE patch) + if(WIN32 AND NOT PATCH_EXECUTABLE) + # The Windows git executable is distributed with patch.exe. Find the path to the executable, if + # it exists, then search `../usr/bin` and `../../usr/bin` for patch.exe. + find_package(Git QUIET) + if(GIT_EXECUTABLE) + get_filename_component(extra_search_path ${GIT_EXECUTABLE} DIRECTORY) + get_filename_component(extra_search_path_1up ${extra_search_path} DIRECTORY) + get_filename_component(extra_search_path_2up ${extra_search_path_1up} DIRECTORY) + find_program( + PATCH_EXECUTABLE patch HINTS "${extra_search_path_1up}/usr/bin" + "${extra_search_path_2up}/usr/bin" + ) + if(NOT PATCH_EXECUTABLE) + cpm_message(WARNING "Last resort: Will use `git apply` as PATCH_EXECUTABLE") + set(PATCH_EXECUTABLE ${GIT_EXECUTABLE} apply) + endif() + endif() + endif() + if(NOT PATCH_EXECUTABLE) + message(FATAL_ERROR "Couldn't find `patch` executable to use with PATCHES keyword.") + endif() + + # Create a temporary + set(temp_list ${CPM_ARGS_UNPARSED_ARGUMENTS}) + + # Ensure each file exists (or error out) and add it to the list. + set(first_item True) + foreach(PATCH_FILE ${ARGN}) + # Make sure the patch file exists, if we can't find it, try again in the current directory. + if(NOT EXISTS "${PATCH_FILE}") + if(NOT EXISTS "${CMAKE_CURRENT_LIST_DIR}/${PATCH_FILE}") + message(FATAL_ERROR "Couldn't find patch file: '${PATCH_FILE}'") + endif() + set(PATCH_FILE "${CMAKE_CURRENT_LIST_DIR}/${PATCH_FILE}") + endif() + + # Convert to absolute path for use with patch file command. + get_filename_component(PATCH_FILE "${PATCH_FILE}" ABSOLUTE) + + # The first patch entry must be preceded by "PATCH_COMMAND" while the following items are + # preceded by "&&". + if(first_item) + set(first_item False) + list(APPEND temp_list "PATCH_COMMAND") + else() + list(APPEND temp_list "&&") + endif() + # Add the patch command to the list (try reverse patch first and then normal patch) + list(APPEND temp_list "${PATCH_EXECUTABLE}" "-R" "-p1" "<" "${PATCH_FILE}") + list(APPEND temp_list "&&" "${PATCH_EXECUTABLE}" "-p1" "<" "${PATCH_FILE}") + list(APPEND temp_list "||" "${PATCH_EXECUTABLE}" "-p1" "<" "${PATCH_FILE}") + endforeach() + + # Move temp out into parent scope. + set(CPM_ARGS_UNPARSED_ARGUMENTS + ${temp_list} + PARENT_SCOPE + ) + +endfunction() + +# method to overwrite internal FetchContent properties, to allow using CPM.cmake to overload +# FetchContent calls. As these are internal cmake properties, this method should be used carefully +# and may need modification in future CMake versions. Source: +# https://github.com/Kitware/CMake/blob/dc3d0b5a0a7d26d43d6cfeb511e224533b5d188f/Modules/FetchContent.cmake#L1152 +function(cpm_override_fetchcontent contentName) + cmake_parse_arguments(PARSE_ARGV 1 arg "" "SOURCE_DIR;BINARY_DIR" "") + if(NOT "${arg_UNPARSED_ARGUMENTS}" STREQUAL "") + message(FATAL_ERROR "${CPM_INDENT} Unsupported arguments: ${arg_UNPARSED_ARGUMENTS}") + endif() + + string(TOLOWER ${contentName} contentNameLower) + set(prefix "_FetchContent_${contentNameLower}") + + set(propertyName "${prefix}_sourceDir") + define_property( + GLOBAL + PROPERTY ${propertyName} + BRIEF_DOCS "Internal implementation detail of FetchContent_Populate()" + FULL_DOCS "Details used by FetchContent_Populate() for ${contentName}" + ) + set_property(GLOBAL PROPERTY ${propertyName} "${arg_SOURCE_DIR}") + + set(propertyName "${prefix}_binaryDir") + define_property( + GLOBAL + PROPERTY ${propertyName} + BRIEF_DOCS "Internal implementation detail of FetchContent_Populate()" + FULL_DOCS "Details used by FetchContent_Populate() for ${contentName}" + ) + set_property(GLOBAL PROPERTY ${propertyName} "${arg_BINARY_DIR}") + + set(propertyName "${prefix}_populated") + define_property( + GLOBAL + PROPERTY ${propertyName} + BRIEF_DOCS "Internal implementation detail of FetchContent_Populate()" + FULL_DOCS "Details used by FetchContent_Populate() for ${contentName}" + ) + set_property(GLOBAL PROPERTY ${propertyName} TRUE) +endfunction() + +# Download and add a package from source +function(CPMAddPackage) + cpm_set_policies() + + list(LENGTH ARGN argnLength) + if(argnLength EQUAL 1) + cpm_parse_add_package_single_arg("${ARGN}" ARGN) + + # The shorthand syntax implies EXCLUDE_FROM_ALL and SYSTEM + set(ARGN "${ARGN};EXCLUDE_FROM_ALL;YES;SYSTEM;YES;") + endif() + + set(oneValueArgs + NAME + FORCE + VERSION + GIT_TAG + DOWNLOAD_ONLY + GITHUB_REPOSITORY + GITLAB_REPOSITORY + BITBUCKET_REPOSITORY + GIT_REPOSITORY + SOURCE_DIR + FIND_PACKAGE_ARGUMENTS + NO_CACHE + SYSTEM + GIT_SHALLOW + EXCLUDE_FROM_ALL + SOURCE_SUBDIR + CUSTOM_CACHE_KEY + ) + + set(multiValueArgs URL OPTIONS DOWNLOAD_COMMAND PATCHES) + + cmake_parse_arguments(CPM_ARGS "" "${oneValueArgs}" "${multiValueArgs}" "${ARGN}") + + # Set default values for arguments + + if(NOT DEFINED CPM_ARGS_VERSION) + if(DEFINED CPM_ARGS_GIT_TAG) + cpm_get_version_from_git_tag("${CPM_ARGS_GIT_TAG}" CPM_ARGS_VERSION) + endif() + endif() + + if(CPM_ARGS_DOWNLOAD_ONLY) + set(DOWNLOAD_ONLY ${CPM_ARGS_DOWNLOAD_ONLY}) + else() + set(DOWNLOAD_ONLY NO) + endif() + + if(DEFINED CPM_ARGS_GITHUB_REPOSITORY) + set(CPM_ARGS_GIT_REPOSITORY "https://github.com/${CPM_ARGS_GITHUB_REPOSITORY}.git") + elseif(DEFINED CPM_ARGS_GITLAB_REPOSITORY) + set(CPM_ARGS_GIT_REPOSITORY "https://gitlab.com/${CPM_ARGS_GITLAB_REPOSITORY}.git") + elseif(DEFINED CPM_ARGS_BITBUCKET_REPOSITORY) + set(CPM_ARGS_GIT_REPOSITORY "https://bitbucket.org/${CPM_ARGS_BITBUCKET_REPOSITORY}.git") + endif() + + if(DEFINED CPM_ARGS_GIT_REPOSITORY) + list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS GIT_REPOSITORY ${CPM_ARGS_GIT_REPOSITORY}) + if(NOT DEFINED CPM_ARGS_GIT_TAG) + set(CPM_ARGS_GIT_TAG v${CPM_ARGS_VERSION}) + endif() + + # If a name wasn't provided, try to infer it from the git repo + if(NOT DEFINED CPM_ARGS_NAME) + cpm_package_name_from_git_uri(${CPM_ARGS_GIT_REPOSITORY} CPM_ARGS_NAME) + endif() + endif() + + set(CPM_SKIP_FETCH FALSE) + + if(DEFINED CPM_ARGS_GIT_TAG) + list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS GIT_TAG ${CPM_ARGS_GIT_TAG}) + # If GIT_SHALLOW is explicitly specified, honor the value. + if(DEFINED CPM_ARGS_GIT_SHALLOW) + list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS GIT_SHALLOW ${CPM_ARGS_GIT_SHALLOW}) + endif() + endif() + + if(DEFINED CPM_ARGS_URL) + # If a name or version aren't provided, try to infer them from the URL + list(GET CPM_ARGS_URL 0 firstUrl) + cpm_package_name_and_ver_from_url(${firstUrl} nameFromUrl verFromUrl) + # If we fail to obtain name and version from the first URL, we could try other URLs if any. + # However multiple URLs are expected to be quite rare, so for now we won't bother. + + # If the caller provided their own name and version, they trump the inferred ones. + if(NOT DEFINED CPM_ARGS_NAME) + set(CPM_ARGS_NAME ${nameFromUrl}) + endif() + if(NOT DEFINED CPM_ARGS_VERSION) + set(CPM_ARGS_VERSION ${verFromUrl}) + endif() + + list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS URL "${CPM_ARGS_URL}") + endif() + + # Check for required arguments + + if(NOT DEFINED CPM_ARGS_NAME) + message( + FATAL_ERROR + "${CPM_INDENT} 'NAME' was not provided and couldn't be automatically inferred for package added with arguments: '${ARGN}'" + ) + endif() + + # Check if package has been added before + cpm_check_if_package_already_added(${CPM_ARGS_NAME} "${CPM_ARGS_VERSION}") + if(CPM_PACKAGE_ALREADY_ADDED) + cpm_export_variables(${CPM_ARGS_NAME}) + return() + endif() + + # Check for manual overrides + if(NOT CPM_ARGS_FORCE AND NOT "${CPM_${CPM_ARGS_NAME}_SOURCE}" STREQUAL "") + set(PACKAGE_SOURCE ${CPM_${CPM_ARGS_NAME}_SOURCE}) + set(CPM_${CPM_ARGS_NAME}_SOURCE "") + CPMAddPackage( + NAME "${CPM_ARGS_NAME}" + SOURCE_DIR "${PACKAGE_SOURCE}" + EXCLUDE_FROM_ALL "${CPM_ARGS_EXCLUDE_FROM_ALL}" + SYSTEM "${CPM_ARGS_SYSTEM}" + PATCHES "${CPM_ARGS_PATCHES}" + OPTIONS "${CPM_ARGS_OPTIONS}" + SOURCE_SUBDIR "${CPM_ARGS_SOURCE_SUBDIR}" + DOWNLOAD_ONLY "${DOWNLOAD_ONLY}" + FORCE True + ) + cpm_export_variables(${CPM_ARGS_NAME}) + return() + endif() + + # Check for available declaration + if(NOT CPM_ARGS_FORCE AND NOT "${CPM_DECLARATION_${CPM_ARGS_NAME}}" STREQUAL "") + set(declaration ${CPM_DECLARATION_${CPM_ARGS_NAME}}) + set(CPM_DECLARATION_${CPM_ARGS_NAME} "") + CPMAddPackage(${declaration}) + cpm_export_variables(${CPM_ARGS_NAME}) + # checking again to ensure version and option compatibility + cpm_check_if_package_already_added(${CPM_ARGS_NAME} "${CPM_ARGS_VERSION}") + return() + endif() + + if(NOT CPM_ARGS_FORCE) + if(CPM_USE_LOCAL_PACKAGES OR CPM_LOCAL_PACKAGES_ONLY) + cpm_find_package(${CPM_ARGS_NAME} "${CPM_ARGS_VERSION}" ${CPM_ARGS_FIND_PACKAGE_ARGUMENTS}) + + if(CPM_PACKAGE_FOUND) + cpm_export_variables(${CPM_ARGS_NAME}) + return() + endif() + + if(CPM_LOCAL_PACKAGES_ONLY) + message( + SEND_ERROR + "${CPM_INDENT} ${CPM_ARGS_NAME} not found via find_package(${CPM_ARGS_NAME} ${CPM_ARGS_VERSION})" + ) + endif() + endif() + endif() + + CPMRegisterPackage("${CPM_ARGS_NAME}" "${CPM_ARGS_VERSION}") + + if(DEFINED CPM_ARGS_GIT_TAG) + set(PACKAGE_INFO "${CPM_ARGS_GIT_TAG}") + elseif(DEFINED CPM_ARGS_SOURCE_DIR) + set(PACKAGE_INFO "${CPM_ARGS_SOURCE_DIR}") + else() + set(PACKAGE_INFO "${CPM_ARGS_VERSION}") + endif() + + if(DEFINED FETCHCONTENT_BASE_DIR) + # respect user's FETCHCONTENT_BASE_DIR if set + set(CPM_FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR}) + else() + set(CPM_FETCHCONTENT_BASE_DIR ${CMAKE_BINARY_DIR}/_deps) + endif() + + cpm_add_patches(${CPM_ARGS_PATCHES}) + + if(DEFINED CPM_ARGS_DOWNLOAD_COMMAND) + list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS DOWNLOAD_COMMAND ${CPM_ARGS_DOWNLOAD_COMMAND}) + elseif(DEFINED CPM_ARGS_SOURCE_DIR) + list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS SOURCE_DIR ${CPM_ARGS_SOURCE_DIR}) + if(NOT IS_ABSOLUTE ${CPM_ARGS_SOURCE_DIR}) + # Expand `CPM_ARGS_SOURCE_DIR` relative path. This is important because EXISTS doesn't work + # for relative paths. + get_filename_component( + source_directory ${CPM_ARGS_SOURCE_DIR} REALPATH BASE_DIR ${CMAKE_CURRENT_BINARY_DIR} + ) + else() + set(source_directory ${CPM_ARGS_SOURCE_DIR}) + endif() + if(NOT EXISTS ${source_directory}) + string(TOLOWER ${CPM_ARGS_NAME} lower_case_name) + # remove timestamps so CMake will re-download the dependency + file(REMOVE_RECURSE "${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-subbuild") + endif() + elseif(CPM_SOURCE_CACHE AND NOT CPM_ARGS_NO_CACHE) + string(TOLOWER ${CPM_ARGS_NAME} lower_case_name) + set(origin_parameters ${CPM_ARGS_UNPARSED_ARGUMENTS}) + list(SORT origin_parameters) + if(CPM_ARGS_CUSTOM_CACHE_KEY) + # Application set a custom unique directory name + set(download_directory ${CPM_SOURCE_CACHE}/${lower_case_name}/${CPM_ARGS_CUSTOM_CACHE_KEY}) + elseif(CPM_USE_NAMED_CACHE_DIRECTORIES) + string(SHA1 origin_hash "${origin_parameters};NEW_CACHE_STRUCTURE_TAG") + set(download_directory ${CPM_SOURCE_CACHE}/${lower_case_name}/${origin_hash}/${CPM_ARGS_NAME}) + else() + string(SHA1 origin_hash "${origin_parameters}") + set(download_directory ${CPM_SOURCE_CACHE}/${lower_case_name}/${origin_hash}) + endif() + # Expand `download_directory` relative path. This is important because EXISTS doesn't work for + # relative paths. + get_filename_component(download_directory ${download_directory} ABSOLUTE) + list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS SOURCE_DIR ${download_directory}) + + if(CPM_SOURCE_CACHE) + file(LOCK ${download_directory}/../cmake.lock) + endif() + + if(EXISTS ${download_directory}) + if(CPM_SOURCE_CACHE) + file(LOCK ${download_directory}/../cmake.lock RELEASE) + endif() + + cpm_store_fetch_properties( + ${CPM_ARGS_NAME} "${download_directory}" + "${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-build" + ) + cpm_get_fetch_properties("${CPM_ARGS_NAME}") + + if(DEFINED CPM_ARGS_GIT_TAG AND NOT (PATCH_COMMAND IN_LIST CPM_ARGS_UNPARSED_ARGUMENTS)) + # warn if cache has been changed since checkout + cpm_check_git_working_dir_is_clean(${download_directory} ${CPM_ARGS_GIT_TAG} IS_CLEAN) + if(NOT ${IS_CLEAN}) + message( + WARNING "${CPM_INDENT} Cache for ${CPM_ARGS_NAME} (${download_directory}) is dirty" + ) + endif() + endif() + + cpm_add_subdirectory( + "${CPM_ARGS_NAME}" + "${DOWNLOAD_ONLY}" + "${${CPM_ARGS_NAME}_SOURCE_DIR}/${CPM_ARGS_SOURCE_SUBDIR}" + "${${CPM_ARGS_NAME}_BINARY_DIR}" + "${CPM_ARGS_EXCLUDE_FROM_ALL}" + "${CPM_ARGS_SYSTEM}" + "${CPM_ARGS_OPTIONS}" + ) + set(PACKAGE_INFO "${PACKAGE_INFO} at ${download_directory}") + + # As the source dir is already cached/populated, we override the call to FetchContent. + set(CPM_SKIP_FETCH TRUE) + cpm_override_fetchcontent( + "${lower_case_name}" SOURCE_DIR "${${CPM_ARGS_NAME}_SOURCE_DIR}/${CPM_ARGS_SOURCE_SUBDIR}" + BINARY_DIR "${${CPM_ARGS_NAME}_BINARY_DIR}" + ) + + else() + # Enable shallow clone when GIT_TAG is not a commit hash. Our guess may not be accurate, but + # it should guarantee no commit hash get mis-detected. + if(NOT DEFINED CPM_ARGS_GIT_SHALLOW) + cpm_is_git_tag_commit_hash("${CPM_ARGS_GIT_TAG}" IS_HASH) + if(NOT ${IS_HASH}) + list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS GIT_SHALLOW TRUE) + endif() + endif() + + # remove timestamps so CMake will re-download the dependency + file(REMOVE_RECURSE ${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-subbuild) + set(PACKAGE_INFO "${PACKAGE_INFO} to ${download_directory}") + endif() + endif() + + cpm_create_module_file(${CPM_ARGS_NAME} "CPMAddPackage(\"${ARGN}\")") + + if(CPM_PACKAGE_LOCK_ENABLED) + if((CPM_ARGS_VERSION AND NOT CPM_ARGS_SOURCE_DIR) OR CPM_INCLUDE_ALL_IN_PACKAGE_LOCK) + cpm_add_to_package_lock(${CPM_ARGS_NAME} "${ARGN}") + elseif(CPM_ARGS_SOURCE_DIR) + cpm_add_comment_to_package_lock(${CPM_ARGS_NAME} "local directory") + else() + cpm_add_comment_to_package_lock(${CPM_ARGS_NAME} "${ARGN}") + endif() + endif() + + cpm_message( + STATUS "${CPM_INDENT} Adding package ${CPM_ARGS_NAME}@${CPM_ARGS_VERSION} (${PACKAGE_INFO})" + ) + + if(NOT CPM_SKIP_FETCH) + # CMake 3.28 added EXCLUDE, SYSTEM (3.25), and SOURCE_SUBDIR (3.18) to FetchContent_Declare. + # Calling FetchContent_MakeAvailable will then internally forward these options to + # add_subdirectory. Up until these changes, we had to call FetchContent_Populate and + # add_subdirectory separately, which is no longer necessary and has been deprecated as of 3.30. + set(fetchContentDeclareExtraArgs "") + if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.28.0") + if(${CPM_ARGS_EXCLUDE_FROM_ALL}) + list(APPEND fetchContentDeclareExtraArgs EXCLUDE_FROM_ALL) + endif() + if(${CPM_ARGS_SYSTEM}) + list(APPEND fetchContentDeclareExtraArgs SYSTEM) + endif() + if(DEFINED CPM_ARGS_SOURCE_SUBDIR) + list(APPEND fetchContentDeclareExtraArgs SOURCE_SUBDIR ${CPM_ARGS_SOURCE_SUBDIR}) + endif() + # For CMake version <3.28 OPTIONS are parsed in cpm_add_subdirectory + if(CPM_ARGS_OPTIONS AND NOT DOWNLOAD_ONLY) + foreach(OPTION ${CPM_ARGS_OPTIONS}) + cpm_parse_option("${OPTION}") + set(${OPTION_KEY} "${OPTION_VALUE}") + endforeach() + endif() + endif() + cpm_declare_fetch( + "${CPM_ARGS_NAME}" ${fetchContentDeclareExtraArgs} "${CPM_ARGS_UNPARSED_ARGUMENTS}" + ) + + cpm_fetch_package("${CPM_ARGS_NAME}" ${DOWNLOAD_ONLY} populated ${CPM_ARGS_UNPARSED_ARGUMENTS}) + if(CPM_SOURCE_CACHE AND download_directory) + file(LOCK ${download_directory}/../cmake.lock RELEASE) + endif() + if(${populated} AND ${CMAKE_VERSION} VERSION_LESS "3.28.0") + cpm_add_subdirectory( + "${CPM_ARGS_NAME}" + "${DOWNLOAD_ONLY}" + "${${CPM_ARGS_NAME}_SOURCE_DIR}/${CPM_ARGS_SOURCE_SUBDIR}" + "${${CPM_ARGS_NAME}_BINARY_DIR}" + "${CPM_ARGS_EXCLUDE_FROM_ALL}" + "${CPM_ARGS_SYSTEM}" + "${CPM_ARGS_OPTIONS}" + ) + endif() + cpm_get_fetch_properties("${CPM_ARGS_NAME}") + endif() + + set(${CPM_ARGS_NAME}_ADDED YES) + cpm_export_variables("${CPM_ARGS_NAME}") +endfunction() + +# Fetch a previously declared package +macro(CPMGetPackage Name) + if(DEFINED "CPM_DECLARATION_${Name}") + CPMAddPackage(NAME ${Name}) + else() + message(SEND_ERROR "${CPM_INDENT} Cannot retrieve package ${Name}: no declaration available") + endif() +endmacro() + +# export variables available to the caller to the parent scope expects ${CPM_ARGS_NAME} to be set +macro(cpm_export_variables name) + set(${name}_SOURCE_DIR + "${${name}_SOURCE_DIR}" + PARENT_SCOPE + ) + set(${name}_BINARY_DIR + "${${name}_BINARY_DIR}" + PARENT_SCOPE + ) + set(${name}_ADDED + "${${name}_ADDED}" + PARENT_SCOPE + ) + set(CPM_LAST_PACKAGE_NAME + "${name}" + PARENT_SCOPE + ) +endmacro() + +# declares a package, so that any call to CPMAddPackage for the package name will use these +# arguments instead. Previous declarations will not be overridden. +macro(CPMDeclarePackage Name) + if(NOT DEFINED "CPM_DECLARATION_${Name}") + set("CPM_DECLARATION_${Name}" "${ARGN}") + endif() +endmacro() + +function(cpm_add_to_package_lock Name) + if(NOT CPM_DONT_CREATE_PACKAGE_LOCK) + cpm_prettify_package_arguments(PRETTY_ARGN false ${ARGN}) + file(APPEND ${CPM_PACKAGE_LOCK_FILE} "# ${Name}\nCPMDeclarePackage(${Name}\n${PRETTY_ARGN})\n") + endif() +endfunction() + +function(cpm_add_comment_to_package_lock Name) + if(NOT CPM_DONT_CREATE_PACKAGE_LOCK) + cpm_prettify_package_arguments(PRETTY_ARGN true ${ARGN}) + file(APPEND ${CPM_PACKAGE_LOCK_FILE} + "# ${Name} (unversioned)\n# CPMDeclarePackage(${Name}\n${PRETTY_ARGN}#)\n" + ) + endif() +endfunction() + +# includes the package lock file if it exists and creates a target `cpm-update-package-lock` to +# update it +macro(CPMUsePackageLock file) + if(NOT CPM_DONT_CREATE_PACKAGE_LOCK) + get_filename_component(CPM_ABSOLUTE_PACKAGE_LOCK_PATH ${file} ABSOLUTE) + if(EXISTS ${CPM_ABSOLUTE_PACKAGE_LOCK_PATH}) + include(${CPM_ABSOLUTE_PACKAGE_LOCK_PATH}) + endif() + if(NOT TARGET cpm-update-package-lock) + add_custom_target( + cpm-update-package-lock COMMAND ${CMAKE_COMMAND} -E copy ${CPM_PACKAGE_LOCK_FILE} + ${CPM_ABSOLUTE_PACKAGE_LOCK_PATH} + ) + endif() + set(CPM_PACKAGE_LOCK_ENABLED true) + endif() +endmacro() + +# registers a package that has been added to CPM +function(CPMRegisterPackage PACKAGE VERSION) + list(APPEND CPM_PACKAGES ${PACKAGE}) + set(CPM_PACKAGES + ${CPM_PACKAGES} + CACHE INTERNAL "" + ) + set("CPM_PACKAGE_${PACKAGE}_VERSION" + ${VERSION} + CACHE INTERNAL "" + ) +endfunction() + +# retrieve the current version of the package to ${OUTPUT} +function(CPMGetPackageVersion PACKAGE OUTPUT) + set(${OUTPUT} + "${CPM_PACKAGE_${PACKAGE}_VERSION}" + PARENT_SCOPE + ) +endfunction() + +# declares a package in FetchContent_Declare +function(cpm_declare_fetch PACKAGE) + if(${CPM_DRY_RUN}) + cpm_message(STATUS "${CPM_INDENT} Package not declared (dry run)") + return() + endif() + + FetchContent_Declare(${PACKAGE} ${ARGN}) +endfunction() + +# returns properties for a package previously defined by cpm_declare_fetch +function(cpm_get_fetch_properties PACKAGE) + if(${CPM_DRY_RUN}) + return() + endif() + + set(${PACKAGE}_SOURCE_DIR + "${CPM_PACKAGE_${PACKAGE}_SOURCE_DIR}" + PARENT_SCOPE + ) + set(${PACKAGE}_BINARY_DIR + "${CPM_PACKAGE_${PACKAGE}_BINARY_DIR}" + PARENT_SCOPE + ) +endfunction() + +function(cpm_store_fetch_properties PACKAGE source_dir binary_dir) + if(${CPM_DRY_RUN}) + return() + endif() + + set(CPM_PACKAGE_${PACKAGE}_SOURCE_DIR + "${source_dir}" + CACHE INTERNAL "" + ) + set(CPM_PACKAGE_${PACKAGE}_BINARY_DIR + "${binary_dir}" + CACHE INTERNAL "" + ) +endfunction() + +# adds a package as a subdirectory if viable, according to provided options +function( + cpm_add_subdirectory + PACKAGE + DOWNLOAD_ONLY + SOURCE_DIR + BINARY_DIR + EXCLUDE + SYSTEM + OPTIONS +) + + if(NOT DOWNLOAD_ONLY AND EXISTS ${SOURCE_DIR}/CMakeLists.txt) + set(addSubdirectoryExtraArgs "") + if(EXCLUDE) + list(APPEND addSubdirectoryExtraArgs EXCLUDE_FROM_ALL) + endif() + if("${SYSTEM}" AND "${CMAKE_VERSION}" VERSION_GREATER_EQUAL "3.25") + # https://cmake.org/cmake/help/latest/prop_dir/SYSTEM.html#prop_dir:SYSTEM + list(APPEND addSubdirectoryExtraArgs SYSTEM) + endif() + if(OPTIONS) + foreach(OPTION ${OPTIONS}) + cpm_parse_option("${OPTION}") + set(${OPTION_KEY} "${OPTION_VALUE}") + endforeach() + endif() + set(CPM_OLD_INDENT "${CPM_INDENT}") + set(CPM_INDENT "${CPM_INDENT} ${PACKAGE}:") + add_subdirectory(${SOURCE_DIR} ${BINARY_DIR} ${addSubdirectoryExtraArgs}) + set(CPM_INDENT "${CPM_OLD_INDENT}") + endif() +endfunction() + +# downloads a previously declared package via FetchContent and exports the variables +# `${PACKAGE}_SOURCE_DIR` and `${PACKAGE}_BINARY_DIR` to the parent scope +function(cpm_fetch_package PACKAGE DOWNLOAD_ONLY populated) + set(${populated} + FALSE + PARENT_SCOPE + ) + if(${CPM_DRY_RUN}) + cpm_message(STATUS "${CPM_INDENT} Package ${PACKAGE} not fetched (dry run)") + return() + endif() + + FetchContent_GetProperties(${PACKAGE}) + + string(TOLOWER "${PACKAGE}" lower_case_name) + + if(NOT ${lower_case_name}_POPULATED) + if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.28.0") + if(DOWNLOAD_ONLY) + # MakeAvailable will call add_subdirectory internally which is not what we want when + # DOWNLOAD_ONLY is set. Populate will only download the dependency without adding it to the + # build + FetchContent_Populate( + ${PACKAGE} + SOURCE_DIR "${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-src" + BINARY_DIR "${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-build" + SUBBUILD_DIR "${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-subbuild" + ${ARGN} + ) + else() + FetchContent_MakeAvailable(${PACKAGE}) + endif() + else() + FetchContent_Populate(${PACKAGE}) + endif() + set(${populated} + TRUE + PARENT_SCOPE + ) + endif() + + cpm_store_fetch_properties( + ${CPM_ARGS_NAME} ${${lower_case_name}_SOURCE_DIR} ${${lower_case_name}_BINARY_DIR} + ) + + set(${PACKAGE}_SOURCE_DIR + ${${lower_case_name}_SOURCE_DIR} + PARENT_SCOPE + ) + set(${PACKAGE}_BINARY_DIR + ${${lower_case_name}_BINARY_DIR} + PARENT_SCOPE + ) +endfunction() + +# splits a package option +function(cpm_parse_option OPTION) + string(REGEX MATCH "^[^ ]+" OPTION_KEY "${OPTION}") + string(LENGTH "${OPTION}" OPTION_LENGTH) + string(LENGTH "${OPTION_KEY}" OPTION_KEY_LENGTH) + if(OPTION_KEY_LENGTH STREQUAL OPTION_LENGTH) + # no value for key provided, assume user wants to set option to "ON" + set(OPTION_VALUE "ON") + else() + math(EXPR OPTION_KEY_LENGTH "${OPTION_KEY_LENGTH}+1") + string(SUBSTRING "${OPTION}" "${OPTION_KEY_LENGTH}" "-1" OPTION_VALUE) + endif() + set(OPTION_KEY + "${OPTION_KEY}" + PARENT_SCOPE + ) + set(OPTION_VALUE + "${OPTION_VALUE}" + PARENT_SCOPE + ) +endfunction() + +# guesses the package version from a git tag +function(cpm_get_version_from_git_tag GIT_TAG RESULT) + string(LENGTH ${GIT_TAG} length) + if(length EQUAL 40) + # GIT_TAG is probably a git hash + set(${RESULT} + 0 + PARENT_SCOPE + ) + else() + string(REGEX MATCH "v?([0123456789.]*).*" _ ${GIT_TAG}) + set(${RESULT} + ${CMAKE_MATCH_1} + PARENT_SCOPE + ) + endif() +endfunction() + +# guesses if the git tag is a commit hash or an actual tag or a branch name. +function(cpm_is_git_tag_commit_hash GIT_TAG RESULT) + string(LENGTH "${GIT_TAG}" length) + # full hash has 40 characters, and short hash has at least 7 characters. + if(length LESS 7 OR length GREATER 40) + set(${RESULT} + 0 + PARENT_SCOPE + ) + else() + if(${GIT_TAG} MATCHES "^[a-fA-F0-9]+$") + set(${RESULT} + 1 + PARENT_SCOPE + ) + else() + set(${RESULT} + 0 + PARENT_SCOPE + ) + endif() + endif() +endfunction() + +function(cpm_prettify_package_arguments OUT_VAR IS_IN_COMMENT) + set(oneValueArgs + NAME + FORCE + VERSION + GIT_TAG + DOWNLOAD_ONLY + GITHUB_REPOSITORY + GITLAB_REPOSITORY + BITBUCKET_REPOSITORY + GIT_REPOSITORY + SOURCE_DIR + FIND_PACKAGE_ARGUMENTS + NO_CACHE + SYSTEM + GIT_SHALLOW + EXCLUDE_FROM_ALL + SOURCE_SUBDIR + ) + set(multiValueArgs URL OPTIONS DOWNLOAD_COMMAND) + cmake_parse_arguments(CPM_ARGS "" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + foreach(oneArgName ${oneValueArgs}) + if(DEFINED CPM_ARGS_${oneArgName}) + if(${IS_IN_COMMENT}) + string(APPEND PRETTY_OUT_VAR "#") + endif() + if(${oneArgName} STREQUAL "SOURCE_DIR") + string(REPLACE ${CMAKE_SOURCE_DIR} "\${CMAKE_SOURCE_DIR}" CPM_ARGS_${oneArgName} + ${CPM_ARGS_${oneArgName}} + ) + endif() + string(APPEND PRETTY_OUT_VAR " ${oneArgName} ${CPM_ARGS_${oneArgName}}\n") + endif() + endforeach() + foreach(multiArgName ${multiValueArgs}) + if(DEFINED CPM_ARGS_${multiArgName}) + if(${IS_IN_COMMENT}) + string(APPEND PRETTY_OUT_VAR "#") + endif() + string(APPEND PRETTY_OUT_VAR " ${multiArgName}\n") + foreach(singleOption ${CPM_ARGS_${multiArgName}}) + if(${IS_IN_COMMENT}) + string(APPEND PRETTY_OUT_VAR "#") + endif() + string(APPEND PRETTY_OUT_VAR " \"${singleOption}\"\n") + endforeach() + endif() + endforeach() + + if(NOT "${CPM_ARGS_UNPARSED_ARGUMENTS}" STREQUAL "") + if(${IS_IN_COMMENT}) + string(APPEND PRETTY_OUT_VAR "#") + endif() + string(APPEND PRETTY_OUT_VAR " ") + foreach(CPM_ARGS_UNPARSED_ARGUMENT ${CPM_ARGS_UNPARSED_ARGUMENTS}) + string(APPEND PRETTY_OUT_VAR " ${CPM_ARGS_UNPARSED_ARGUMENT}") + endforeach() + string(APPEND PRETTY_OUT_VAR "\n") + endif() + + set(${OUT_VAR} + ${PRETTY_OUT_VAR} + PARENT_SCOPE + ) + +endfunction() \ No newline at end of file diff --git a/cmake/RPCXXCodegen.cmake b/cmake/RPCXXCodegen.cmake new file mode 100644 index 0000000..42a8438 --- /dev/null +++ b/cmake/RPCXXCodegen.cmake @@ -0,0 +1,174 @@ +# This file is a part of RPCXX project + +#[[ +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +]] + + +include_guard() +macro(_codegen_find_exec) + if (NOT RPCXX_CODE_GENERATOR) + if (TARGET rpcxx-codegen) + set(_if_codegen_target rpcxx-codegen) + set(RPCXX_CODE_GENERATOR "$") + else() + find_program(RPCXX_CODE_GENERATOR NAMES rpcxx-codegen REQUIRED) + endif() + endif() +endmacro() + +function(_codegen_scan_includes spec out visited) + macro(append_uniq list item) + list(FIND ${list} ${item} _found) + if (_found EQUAL -1) + list(APPEND ${list} ${item}) + endif() + endmacro() + + get_filename_component(base_dir ${spec} DIRECTORY) + set(${out} "" PARENT_SCOPE) + if (NOT EXISTS ${spec}) + return() + endif() + file(READ ${spec} body) + string(REGEX MATCHALL "include\\w*\(\\w*[^)\\W]+\\w*\)" includes "${body}") + foreach(inc ${includes}) + string(REGEX MATCH "\"(.*)\"" path ${inc}) + set(path ${base_dir}/${CMAKE_MATCH_1}) + list(FIND visited ${path} found) + if (NOT found EQUAL -1) + continue() + endif() + _codegen_scan_includes(${path} _nested "${visited}") + list(APPEND visited ${path}) + foreach (nest ${_nested}) + append_uniq(visited ${nest}) + endforeach() + endforeach() + set(${out} "${visited}" PARENT_SCOPE) +endfunction() + +function(rpcxx_codegen_go ARG_SPEC) + _codegen_find_exec() + set(options) + set(oneValueArgs PREFIX DIR TARGET) + set(multiValueArgs) + cmake_parse_arguments(ARG + "${options}" + "${oneValueArgs}" + "${multiValueArgs}" + ${ARGN}) + + if (NOT ARG_SPEC) + message(FATAL_ERROR "SPEC (path to .json) argument required") + endif() + if (NOT ARG_DIR) + message(FATAL_ERROR "DIR argument required") + endif() + if (NOT ARG_PREFIX) + message(FATAL_ERROR "PREFIX argument required") + endif() + if (NOT ARG_TARGET) + message(FATAL_ERROR "TARGET argument required") + endif() + + _codegen_scan_includes(${ARG_SPEC} _scanned_deps "") + message(STATUS "Codegen for ${ARG_TARGET}: depends: [${ARG_SPEC};${_scanned_deps}]") + + set(marker ${CMAKE_CURRENT_BINARY_DIR}/${ARG_TARGET}.marker) + add_custom_command( + OUTPUT ${marker} + COMMAND ${RPCXX_CODE_GENERATOR} + ARGS ${ARG_SPEC} + --output-dir ${ARG_DIR} + --lang go + --marker ${marker} + -o pkg_prefix=${ARG_PREFIX} + ${kwargs} + DEPENDS ${ARG_SPEC} ${_if_codegen_target} ${_scanned_deps} + COMMENT "Rpcxx Codegen (GO): ${ARG_SPEC} => ${ARG_DIR}" + ) + add_custom_target(${ARG_TARGET} DEPENDS ${marker}) +endfunction() + +function(rpcxx_codegen ARG_SPEC) + _codegen_find_exec() + set(options NO_SERVER NO_CLIENT DESCRIBE_SERVER) + set(oneValueArgs PREFIX TARGET) + set(multiValueArgs) + cmake_parse_arguments(ARG + "${options}" + "${oneValueArgs}" + "${multiValueArgs}" + ${ARGN}) + if (NOT ARG_SPEC) + message(FATAL_ERROR "SPEC (path to .json) argument required") + endif() + if (NOT IS_ABSOLUTE ARG_SPEC) + set(ARG_SPEC ${CMAKE_CURRENT_LIST_DIR}/${ARG_SPEC}) + endif() + if (NOT EXISTS ${ARG_SPEC}) + message(FATAL_ERROR "Spec file ('${ARG_SPEC}') does not exist") + endif() + if (NOT ARG_TARGET) + message(FATAL_ERROR "TARGET argument is required.") + endif() + if(ARG_DESCRIBE_SERVER) + list(APPEND kwargs --describe-server) + endif() + if(ARG_NO_SERVER) + list(APPEND kwargs --no-server) + endif() + if(ARG_NO_CLIENT) + list(APPEND kwargs --no-client) + endif() + + _codegen_scan_includes(${ARG_SPEC} _scanned_deps "") + message(STATUS "Codegen for ${ARG_TARGET}: depends: [${ARG_SPEC};${_scanned_deps}]") + + get_filename_component(output_stem ${ARG_SPEC} NAME_WLE) + set(prefix ${CMAKE_CURRENT_BINARY_DIR}/${ARG_PREFIX}) + + if (NOT EXISTS ${prefix}) + file(MAKE_DIRECTORY ${prefix}) + endif() + + set(output_stem ${prefix}/${output_stem}) + set(output ${output_stem}.hpp) + set(byprod ${output_stem}.private.hpp) + set_property(SOURCE ${output} PROPERTY GENERATED 1) + + add_custom_command( + OUTPUT ${output} + BYPRODUCTS ${byprod} + COMMAND ${RPCXX_CODE_GENERATOR} + ARGS ${ARG_SPEC} --output-dir ${prefix} ${kwargs} + DEPENDS ${ARG_SPEC} ${_if_codegen_target} ${_scanned_deps} + COMMENT "Rpcxx Codegen: ${ARG_SPEC} => ${output}" + ) + add_custom_target(${ARG_TARGET}_gen DEPENDS ${output}) + + add_library(${ARG_TARGET} INTERFACE) + target_link_libraries(${ARG_TARGET} INTERFACE rpcxx::rpcxx) + target_include_directories(${ARG_TARGET} INTERFACE ${CMAKE_CURRENT_BINARY_DIR}) + + add_dependencies(${ARG_TARGET} ${ARG_TARGET}_gen) +endfunction() diff --git a/codegen/CMakeLists.txt b/codegen/CMakeLists.txt new file mode 100644 index 0000000..c04a267 --- /dev/null +++ b/codegen/CMakeLists.txt @@ -0,0 +1,67 @@ +# This file is a part of RPCXX project + +#[[ +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +]] + + +add_executable(rpcxx-codegen + src/codegen.cpp + src/cppgen.cpp + src/gogen.cpp + src/populate.cpp +) +target_link_libraries(rpcxx-codegen PRIVATE + rpcxx-options rpcxx-warnings fmt argparse rpcxx-json +) +if (CMAKE_COMPILER_IS_GNUCC AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 7.0 AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 9.0) + target_link_libraries(rpcxx-codegen PRIVATE stdc++fs) +endif() +target_include_directories(rpcxx-codegen PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/include +) +install(TARGETS rpcxx-codegen RUNTIME DESTINATION bin EXCLUDE_FROM_ALL) +install(FILES ../cmake/RPCXXCodegen.cmake DESTINATION ${CMAKE_ROOT}/Modules/ EXCLUDE_FROM_ALL) + +CPMAddPackage(NAME lua + URL https://www.lua.org/ftp/lua-5.4.7.tar.gz + URL_HASH SHA256=9fbf5e28ef86c69858f6d3d34eccc32e911c1a28b4120ff3e84aaa70cfbf1e30 + VERSION 5.4.7 + DOWNLOAD_ONLY YES +) + +set(LUA_SRC_FILES + lapi.c lcode.c lctype.c ldebug.c ldo.c + ldump.c lfunc.c lgc.c llex.c lmem.c + lobject.c lopcodes.c lparser.c lstate.c + lstring.c ltable.c ltm.c lundump.c + lvm.c lzio.c lauxlib.c lbaselib.c lcorolib.c + ldblib.c liolib.c lmathlib.c loadlib.c loslib.c lstrlib.c + ltablib.c lutf8lib.c linit.c +) + +list(TRANSFORM LUA_SRC_FILES PREPEND ${lua_SOURCE_DIR}/src/) +add_library(codegen-lua STATIC ${LUA_SRC_FILES}) +target_include_directories(codegen-lua PUBLIC $) +target_link_libraries(rpcxx-codegen PRIVATE codegen-lua) +if (UNIX AND NOT MSYS) + target_compile_definitions(codegen-lua PRIVATE LUA_USE_POSIX) +endif() diff --git a/codegen/include/codegen.hpp b/codegen/include/codegen.hpp new file mode 100644 index 0000000..0734115 --- /dev/null +++ b/codegen/include/codegen.hpp @@ -0,0 +1,314 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#ifndef GEN_COMMON_HPP +#define GEN_COMMON_HPP + +#include +#include +#include +#include +#include +#include +#include +#include + +struct lua_State; + +namespace rpcxx::gen { + +using std::vector; +using std::string_view; +using std::string; + +template struct overloaded : Fs... {using Fs::operator()...;}; +template overloaded(Fs...) -> overloaded; + +template +decltype(auto) Visit(Var&& v, Fs&&...fs) { + return std::visit(overloaded{std::forward(fs)...}, std::forward(v)); +} + +template struct [[nodiscard]] defer { + defer(Fn&& f) : f(std::forward(f)) {} + ~defer() {f();} + Fn f; +}; + +template +std::runtime_error Err(Fmt&& fmt, Args&&...args) { + return std::runtime_error(fmt::format(std::forward(fmt), std::forward(args)...)); +} + +constexpr auto OneTab = " "; +namespace fs = std::filesystem; + +enum Targets { + TargetNone = 0, + TargetTypes = 1 << 0, + TargetClient = 1 << 1, + TargetServer = 1 << 2, + TargetAll = TargetTypes | TargetClient | TargetServer, +}; + +enum class Lang { + cpp, go +}; + +struct Namespace { + string sourceFile; + string name; //"." as separators + string part = {}; + int depth = 0; + + constexpr auto rank() const noexcept { + return std::tie(sourceFile, name, part, depth); + } + constexpr bool operator<(const Namespace& o) const noexcept { + return rank() < o.rank(); + } + constexpr bool operator==(const Namespace& o) const noexcept { + return rank() == o.rank(); + } + constexpr bool operator!=(const Namespace& o) const noexcept { + return rank() != o.rank(); + } +}; + +struct TypeBase +{ + Namespace ns = {}; + string name; +}; + +struct TypeVariant; + +using Type = TypeVariant*; + +using TypeMap = std::map>; + +struct Builtin { + enum Kind : short { + Invalid = 0, Bool, Int8, Uint8, Int16, Uint16, + Int32, Uint32, Int64, Uint64, Float, Double, String, + String_View, Binary, Binary_View, Json, Json_View, Void, + }; + Kind kind; +}; + +struct Enum : TypeBase { + struct Value { + string name; + std::optional number; + }; + vector values; +}; + +struct Struct : TypeBase { + struct Field { + string name; + Type type = nullptr; + size_t sz = 0; + }; + vector fields; +}; + +struct Array : TypeBase { + Type item; +}; + +struct Alias : TypeBase { + Type item; +}; + +struct Map : TypeBase { + Type item; +}; + +struct Optional : TypeBase { + Type item; +}; + +namespace def { + +struct Variant; +using Value = Variant*; + +struct Nil {}; + +struct Int { + long long value; +}; + +struct Num { + double value; +}; + +struct String { + string_view value; +}; + +struct Bool { + bool value; +}; + +struct Table { + std::map value; +}; + +struct Array { + std::vector value; +}; + +struct Variant : public std::variant { + using variant::variant; + variant& AsVariant() noexcept { + return *this; + } + const variant& AsVariant() const noexcept { + return *this; + } +}; + +} + +struct WithDefault : TypeBase { + Type item; + def::Value value; +}; + +struct TypeVariant : public std::variant +{ + using variant::variant; + variant& AsVariant() noexcept { + return *this; + } + const variant& AsVariant() const noexcept { + return *this; + } + TypeBase* Base() noexcept { + return Visit( + AsVariant(), + [](Builtin) -> TypeBase* { + return nullptr; + }, [](TypeBase& b){ + return &b; + }); + } +}; + +template +bool is(U* t) { + return std::holds_alternative(*t); +} + +template +T* as(U* t) { + return std::get_if(t); +} + +struct ParamsArray : vector { + using vector::vector; +}; + +struct ParamsNamed : TypeMap { + using TypeMap::TypeMap; +}; + +struct ParamsPack { + Type item; + size_t size() const {return 1;} +}; + +using Params = std::variant; + +struct Notify +{ + string service; + string name; + Params params; +}; + +struct Method : Notify +{ + Type returns; + uint32_t timeout = 10000; + bool async = false; +}; + +struct AST +{ + std::map> builtins; + std::map>> savedTypes; + vector types; + vector notify; + vector methods; + vector attrs; +}; + +struct GenParams +{ + Lang lang = Lang::cpp; + Targets targets = TargetAll; + vector extraIncludes; + Namespace main; + bool describeServer = false; +}; + +struct GoOpts { + string pkgPrefix; +}; + +struct CppOpts { + bool sourceFile = false; +}; + +struct FormatContext { + GenParams& params; + AST& ast; + argparse::ArgumentParser& prog; + void* opts = nullptr; +}; + +inline TypeBase* GetBase(Type t) { + return Visit(t->AsVariant(), [](Builtin) -> TypeBase* { + return nullptr; + }, [](auto& other) -> TypeBase* { + return &other; + }); +} + +inline Namespace* GetNs(Type t) { + return Visit(t->AsVariant(), [](Builtin) -> Namespace* { + return nullptr; + }, [](auto& other)-> Namespace* { + return &other.ns; + }); +} + +void PopulateFromFrontend(lua_State* L, FormatContext& ctx); + +} //gen + +#endif //GEN_COMMON_HPP diff --git a/codegen/include/cpp/clientgen.hpp b/codegen/include/cpp/clientgen.hpp new file mode 100644 index 0000000..c2de769 --- /dev/null +++ b/codegen/include/cpp/clientgen.hpp @@ -0,0 +1,173 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#include "cppgen.hpp" +#include + +namespace rpcxx::gen::cpp::client +{ + +constexpr auto format_inline = R"EOF( +struct {client_name} : public rpcxx::Client +{{ + using rpcxx::Client::Client; + {client_name}* as_{client_name}() noexcept {{return this;}} +{methods}{notifications} +}}; +)EOF"; + +constexpr auto format_source = R"EOF( +{methods}{notifications})EOF"; + +constexpr auto method_inline = FMT_COMPILE(R"EOF( + rpcxx::Future<{return_type}> {method_name}({args_with_types}millis __timeout = {timeout});)EOF"); + +constexpr auto method_source = FMT_COMPILE(R"EOF( +inline rpcxx::Future<{return_type}> {client_name}::{method_name}({args_with_types}millis __timeout) {{ + return Request{if_pack}<{return_type}>(rpcxx::Method{{"{method_name}", __timeout}}{args}); +}})EOF"); + +constexpr auto notify_inline = FMT_COMPILE(R"EOF( + void {method_name}({args_with_types});)EOF"); + +constexpr auto notify_source = FMT_COMPILE(R"EOF( +inline void {client_name}::{method_name}({args_with_types}) {{ + return Notify{if_pack}("{method_name}"{args}); +}})EOF"); + +static bool sameService(string_view name, string_view service) { + return name == service.substr(0, service.size() - strlen("_Client")); +} + +static string formatArgs(const Notify& n) +{ + return Visit(n.params, + [&](const ParamsPack&) { + return string{",args"}; + }, + [&](const ParamsArray& positinal) { + string result; + for (auto count = 0u; count < positinal.size(); ++count) { + result += fmt::format(",{}{}", "arg", count); + } + return result; + }, + [&](const ParamsNamed& named) { + string result; + for (auto& [name, _]: named) { + result += fmt::format(",rpcxx::Arg(\"{}\", {})", name, name); + } + return result; + }); +} + +static string sig(Params const& params, bool method) { + auto sz = Visit(params, [](auto& p){return p.size();}); + return client::FormatSignature(params) + (method && sz ? ", " : ""); +} + +static string formatMethods(FormatContext& ctx, string_view name) +{ + auto& opts = *static_cast(ctx.opts); + string result; + for (auto& m: ctx.ast.methods) { + if (!sameService(m.service, name)) continue; + auto formatter = [&](auto fmt){ + return fmt::format( + fmt, + fmt::arg("if_pack", std::holds_alternative(m.params) ? "Pack" : ""), + fmt::arg("client_name", name), + fmt::arg("return_type", PrintType(m.returns)), + fmt::arg("method_name", m.name), + fmt::arg("args", formatArgs(m)), + fmt::arg("timeout", m.timeout), + fmt::arg("args_with_types", sig(m.params, true)) + ); + }; + if (opts.sourceFile) { + result += formatter(method_source); + } else { + result += formatter(method_inline); + } + } + return result; +} + +static string formatNotifications(FormatContext& ctx, string_view name) +{ + auto& opts = *static_cast(ctx.opts); + string result; + for (auto& n: ctx.ast.notify) { + if (!sameService(n.service, name)) continue; + auto formatter = [&](auto fmt){ + return fmt::format( + fmt, + fmt::arg("if_pack", std::holds_alternative(n.params) ? "Pack" : ""), + fmt::arg("client_name", name), + fmt::arg("method_name", n.name), + fmt::arg("args", formatArgs(n)), + fmt::arg("args_with_types", sig(n.params, false)) + ); + }; + if (opts.sourceFile) { + result += formatter(notify_source); + } else { + result += formatter(notify_inline); + } + } + return result; +} + +static string formatOne(string_view name, FormatContext& ctx) { + auto& opts = *static_cast(ctx.opts); + return fmt::format( + opts.sourceFile ? format_source : format_inline, + fmt::arg("client_name", name), + fmt::arg("methods", formatMethods(ctx, name)), + fmt::arg("notifications", formatNotifications(ctx, name)) + ); +} + +string Format(FormatContext& ctx) +{ + if (!(ctx.params.targets & TargetClient)) return ""; + if (ctx.ast.methods.empty() && ctx.ast.notify.empty()) { + return ""; + } + string result; + std::set names; + for (auto& m : ctx.ast.methods) { + names.insert(m.service); + } + for (auto& n : ctx.ast.notify) { + names.insert(n.service); + } + for (auto name: names) { + result += formatOne(string{name} + "_Client", ctx); + } + return result; +} + +} + diff --git a/codegen/include/cpp/servergen.hpp b/codegen/include/cpp/servergen.hpp new file mode 100644 index 0000000..e780e75 --- /dev/null +++ b/codegen/include/cpp/servergen.hpp @@ -0,0 +1,187 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#include "cppgen.hpp" +#include + +namespace rpcxx::gen::cpp::server +{ + +constexpr auto format_inline = R"EOF( +struct {server_name} : public rpcxx::Server +{{ + {server_name}(); + {server_name}* as_{server_name}() noexcept {{return this;}} +{methods} +}};{describe} +)EOF"; + +constexpr auto format_source = R"EOF( +inline {server_name}::{server_name}() : Server() {{{register_methods} +}} +)EOF"; + +constexpr auto method_fmt = FMT_COMPILE(R"EOF( + virtual {return_type} {method_name}({args_with_types}) = 0;)EOF"); + +constexpr auto register_method = FMT_COMPILE(R"EOF( + {method_or_notify}<&{server_name}::{method_name}>("{method_name}"{names_mapping});)EOF"); + +static string generateNamesMap(const ParamsNamed& params) +{ + string map; + bool first = true; + for (auto& [name, _]: params) { + if (first) { + first = false; + } else { + map += ", "; + } + map += '"'+string{name}+'"'; + } + return ", rpcxx::NamesMap("+map+')'; +} + +template +static string generateSingleRegister(const Notify& method, string_view server) +{ + auto do_format = [&](const string& names) { + return fmt::format(register_method, + fmt::arg("method_name", method.name), + fmt::arg("server_name", server), + fmt::arg("names_mapping", names), + fmt::arg("method_or_notify", isMethod ? "Method" : "Notify") + ); + }; + if (auto named = std::get_if(&method.params)) { + return do_format(generateNamesMap(*named)); + } else if (auto pack = std::get_if(&method.params)) { + return do_format(fmt::format(", rpcxx::PackParams<{}>()", PrintType(pack->item))); + } else { + return do_format(""); + } +} + +static bool sameService(string_view name, string_view service) { + return name == service.substr(0, service.size() - strlen("_Server")); +} + +static string formatMethodRegister(AST& ast, string_view server) +{ + string registerAll; + for (auto& m: ast.methods) { + if (!sameService(m.service, server)) continue; + registerAll += generateSingleRegister(m, server); + } + for (auto& n: ast.notify) { + if (!sameService(n.service, server)) continue; + registerAll += generateSingleRegister(n, server); + } + return registerAll; +} + +static string formatMethods(AST& ast, string_view server) +{ + string methods; + for (auto& m: ast.methods) { + if (!sameService(m.service, server)) continue; + auto ret = PrintType(m.returns); + methods += fmt::format( + method_fmt, + fmt::arg("return_type", m.async ? fmt::format("rpcxx::Future<{}>", ret) : ret), + fmt::arg("method_name", m.name), + fmt::arg("args_with_types", server::FormatSignature(m.params)) + ); + } + for (auto& n: ast.notify) { + if (!sameService(n.service, server)) continue; + methods += fmt::format( + method_fmt, + fmt::arg("return_type", "void"), + fmt::arg("method_name", n.name), + fmt::arg("args_with_types", server::FormatSignature(n.params)) + ); + } + return methods; +} + +static string printMethods(AST& ast, string_view server) { + string res; + unsigned dontSplit = 0; + auto add = [&](auto& name) { + res += ",&_::"+string{name}; + if (++dontSplit == 5) { + dontSplit = 0; + res+='\n'; + } + }; + for (auto& n: ast.notify) { + if (!sameService(n.service, server)) continue; + add(n.name); + } + for (auto& m: ast.methods) { + if (!sameService(m.service, server)) continue; + add(m.name); + } + return res; +} + +static string formatOne(string_view name, FormatContext& ctx) { + auto gen_desc = [&]{ + return fmt::format( + FMT_COMPILE("\nDESCRIBE({}::{}{})"), + ToNamespace(ctx.params.main.name), name, printMethods(ctx.ast, name)); + }; + auto& opts = *static_cast(ctx.opts); + return fmt::format( + opts.sourceFile ? format_source : format_inline, + fmt::arg("server_name", name), + fmt::arg("register_methods", opts.sourceFile ? formatMethodRegister(ctx.ast, name) : ""), + fmt::arg("methods", opts.sourceFile ? "" : formatMethods(ctx.ast, name)), + fmt::arg("describe", ctx.params.describeServer ? gen_desc() : "") + ); +} + +string Format(FormatContext& ctx) +{ + if (!(ctx.params.targets & TargetServer)) return ""; + if (ctx.ast.methods.empty() && ctx.ast.notify.empty()) { + return ""; + } + string result; + std::set names; + for (auto& m : ctx.ast.methods) { + names.insert(m.service); + } + for (auto& n : ctx.ast.notify) { + names.insert(n.service); + } + for (auto name: names) { + result += formatOne(string(name) + "_Server", ctx); + } + return result; +} + +} + diff --git a/codegen/include/cpp/typegen.hpp b/codegen/include/cpp/typegen.hpp new file mode 100644 index 0000000..b410454 --- /dev/null +++ b/codegen/include/cpp/typegen.hpp @@ -0,0 +1,405 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#include "cppgen.hpp" +#include +#include + +namespace rpcxx::gen::cpp::types { + +constexpr auto struct_fmt = FMT_COMPILE(R"EOF( +struct {type_name} {{{fields} +}}; +DESCRIBE({ns}::{type_name}{field_names}) +)EOF"); + +constexpr auto enum_fmt = FMT_COMPILE(R"EOF( +enum class {type_name} {{{fields} +}}; +DESCRIBE({ns}::{type_name}{field_names}) +)EOF"); + +constexpr auto field = FMT_COMPILE(R"EOF( + {type} {name} = {{{default}}};)EOF"); + +constexpr auto alias = FMT_COMPILE(R"EOF( +using {type_name} = {aliased_type}; +)EOF"); + +static string_view rawName(Type t) { + return Visit(t->AsVariant(), [](Builtin) -> string_view{ + return {}; + }, [](auto const& other)-> string_view { + return other.name; + }); +} + +static string defaultFromTrivial(def::Value v) { + return Visit( + v->AsVariant(), + [](def::String& s) -> string { + return '"' + string{s.value} + '"'; + }, + [](def::Int& s) -> string { + return std::to_string(s.value); + }, + [](def::Num& s) -> string { + return std::to_string(s.value); + }, + [](def::Bool& s) -> string { + return s.value ? "true" : "false"; + }, + [](auto&) -> string { + return {}; + } + ); +} + +static string defaultFromRaw(Type t, def::Value v); + +static string defaultFromArray(Array& t, def::Value adef) { + vector items; + auto src = as(adef); + if (!src) throw Err("Table expected for default value"); + for (auto& v: src->value) { + items.push_back(defaultFromRaw(t.item, v)); + } + return fmt::format("{}", fmt::join(items, ", ")); +} + +static string defaultFromTable(Map& t, def::Value adef) { + string result; + auto src = as(adef); + if (!src) throw Err("Table expected for default value"); + for (auto& [k, v]: src->value) { + result += "{"; + result += k; + result += ", "; + result += defaultFromRaw(t.item, v); + result += "}"; + result += ", "; + } + return result; +} + +static string defaultFromEnum(Enum& e, def::Value adef) { + auto src = as(adef); + if (!src) throw Err("String expected for default value"); + auto type = TypeVariant{e}; //micro - kostyl + return PrintType(&type) + "::" + string{src->value}; +} + +static string defaultFromStruct(Struct& t, def::Value adef) { + string result; + auto src = as(adef); + if (!src) throw Err("Table expected for default value"); + vector> unordered; + for (auto& pair: src->value) { + auto nested = std::find_if(t.fields.begin(), t.fields.end(), [&](Struct::Field& f){ + return f.name == pair.first; + }); + if (nested == t.fields.end()) { + throw Err("Could not find field ({}) for default value in struct", pair.first); + } + string nestedDef = defaultFromRaw(nested->type, pair.second); + unordered.push_back({string{pair.first}, std::move(nestedDef)}); + } + for (auto& f: t.fields) { + auto toUse = std::find_if(unordered.begin(), unordered.end(), [&](auto& pair){ + return f.name == pair.first; + }); + if (toUse != unordered.end()) { + result += PrintType(f.type) + "{" + toUse->second + "},"; + } else { + result += "{},"; + } + } + return result; +} + +static string defaultFromRaw(Type t, def::Value adef) try { + return Visit( + t->AsVariant(), + [&](Map& v) -> string { + return defaultFromTable(v, adef); + }, + [&](Array& v) -> string { + return defaultFromArray(v, adef); + }, + [&](Struct& v) -> string { + return defaultFromStruct(v, adef); + }, + [&](Enum& v) -> string { + return defaultFromEnum(v, adef); + }, + [&](auto&) -> string { + return defaultFromTrivial(adef); + } + ); +} catch (std::exception& e) { + throw Err("{}\n =>\tGetting default for '{}'", e.what(), PrintType(t)); +} + +static string getDefault(Type t) { + return Visit(t->AsVariant(), + [&](WithDefault& d) -> string { + return defaultFromRaw(d.item, d.value); + }, + [&](Optional& o) -> string { + return getDefault(o.item); + }, + [&](Alias& a) -> string { + return getDefault(a.item); + }, + [](auto&) -> string { + return {}; + }); +} + +static string formatSingleType(FormatContext& ctx, Type t) +{ + return Visit(t->AsVariant(), + [&](const Alias& a) { + return fmt::format( + alias, + fmt::arg("type_name", a.name), + fmt::arg("aliased_type", PrintType(a.item)) + ); + }, + [&](const Enum& a) { + string fields; + string field_names; + unsigned count = 0; + for (auto& v: a.values) { + if (auto& n = v.number) { + fields += fmt::format(FMT_COMPILE("\n {} = {},"), v.name, *n); + } else { + fields += fmt::format(FMT_COMPILE("\n {},"), v.name); + } + field_names += ','; + if (++count % 3 == 0) { + field_names += '\n'; + field_names += string(12, ' '); + } + field_names += "_::"+v.name; + } + return fmt::format( + enum_fmt, + fmt::arg("ns", ToNamespace(a.ns.name)), + fmt::arg("type_name", rawName(t)), + fmt::arg("fields", fields), + fmt::arg("field_names", field_names) + ); + }, + [&](Struct const& s) { + string fields; + string field_names; + unsigned count = 0; + for (auto& it: s.fields) { + auto& subName = it.name; + auto& subType = it.type; + fields += fmt::format( + field, + fmt::arg("type", PrintType(subType)), + fmt::arg("name", subName), + fmt::arg("default", getDefault(subType)) + ); + field_names += ','; + if (++count % 3 == 0) { + field_names += '\n'; + field_names += string(12, ' '); + } + field_names += "&_::"+string{subName}; + } + return fmt::format( + struct_fmt, + fmt::arg("ns", ToNamespace(s.ns.name)), + fmt::arg("type_name", rawName(t)), + fmt::arg("fields", fields), + fmt::arg("field_names", field_names) + ); + }, + [](const auto&) -> string { + throw Err("Unformattable type passed to cpp formatting"); + }); +} + +using DependecyDepth = uint32_t; + +static DependecyDepth CalcDepth(Type t) { + return Visit(t->AsVariant(), + [](Struct s){ + uint32_t max = 1; + for (auto& f: s.fields) { + if (auto curr = CalcDepth(f.type) + 1; curr > max) { + max = curr; + } + } + return max; + }, + [](Builtin) -> uint32_t{ + return 0; + }, + [](Enum) -> uint32_t{ + return 0; + }, + [](auto&& other) { + return CalcDepth(other.item) + 1; + }); +} + +using DepPair = std::pair; + +static bool CompareDeps(const DepPair& lhs, const DepPair& rhs) { + auto lns = GetNs(lhs.second); + auto rns = GetNs(rhs.second); + return std::tie(lhs.first, *lns) < std::tie(rhs.first, *rns); +} + +static size_t getSizeof(Builtin b) { + switch (b.kind) { + case Builtin::Bool: return sizeof(bool); + case Builtin::Int8: + case Builtin::Uint8: return sizeof(int8_t); + case Builtin::Int16: + case Builtin::Uint16: return sizeof(int16_t); + case Builtin::Int32: + case Builtin::Uint32: return sizeof(int32_t); + case Builtin::Int64: + case Builtin::Uint64: return sizeof(int64_t); + case Builtin::Float: return sizeof(float); + case Builtin::Double: return sizeof(double); + case Builtin::Binary: return sizeof(float); + case Builtin::String: return sizeof(std::string); + case Builtin::String_View: return sizeof(std::string_view); + case Builtin::Json: return sizeof(std::string_view) * 2; + case Builtin::Json_View: return sizeof(std::string_view); + default: return sizeof(void*); + } +} + +static size_t getSizeof(Type t) { + return Visit(t->AsVariant(), + [](Builtin b){ + return getSizeof(b); + }, + [](Enum&){ + return sizeof(unsigned); + }, + [](Struct& s){ + size_t sz = 0; + for (auto& f: s.fields) { + if (!f.sz) { + f.sz = getSizeof(f.type); + } + sz += f.sz; + } + return sz; + }, + [](Alias const& a){ + return getSizeof(a.item); + }, + [](WithDefault const& d){ + return getSizeof(d.item); + }, + [](Array const&){ + return sizeof(vector); + }, + [](Map const&){ + return sizeof(std::map); + }, + [](Optional const& o){ + return getSizeof(o.item); + }); +} + +static void reorderMembers(Struct& t) { + for (auto& f: t.fields) { + f.sz = getSizeof(f.type); + if (auto s = std::get_if(f.type)) { + reorderMembers(*s); + } + } + std::sort(t.fields.begin(), t.fields.end(), [](Struct::Field& lhs, Struct::Field& rhs){ + return std::make_tuple(-lhs.sz, std::cref(lhs.name)) < std::make_tuple(-rhs.sz, std::cref(rhs.name)); + }); +} + +std::string Format(FormatContext& ctx) +{ + auto& opts = *static_cast(ctx.opts); + if (opts.sourceFile) return ""; + if (!(ctx.params.targets & TargetTypes)) + return ""; + std::vector byDepth; + for (auto& t: ctx.ast.types) { + auto* asStruct = std::get_if(&t->AsVariant()); + if (asStruct) { + reorderMembers(*asStruct); + } + if (asStruct || is(t) || is(t)) { + byDepth.push_back({CalcDepth(t), t}); + } + } + std::sort(byDepth.begin(), byDepth.end(), CompareDeps); + string result; + Namespace lastns; + string guardEnd; + auto end_ns = [&]{ + if (lastns.name.empty()) { + return; + } + result += "} //namespace " + ToNamespace(lastns.name) + "\n"; + result += guardEnd; + result += "\n"; + }; + auto start_ns = [&](Namespace const& ns) { + if (lastns != ns) { + end_ns(); + auto [gstart, gend] = cpp::MakeGuard(ns); + result += gstart; + result += fmt::format("\nnamespace {} {{", ToNamespace(ns.name)); + lastns = ns; + guardEnd = std::move(gend); + } + }; + for (auto& [_, type]: byDepth) { + try { + auto base = GetBase(type); + if (!base) { + throw Err("Invalid type passed"); + } + auto curr = base->ns; + curr.part = base->name; + start_ns(curr); + result += formatSingleType(ctx, type); + } catch (std::exception& exc) { + throw Err("{}\n =>\tGenerating code for type '{}'", exc.what(), PrintType(type)); + } + } + end_ns(); + return result; +} + +} diff --git a/codegen/include/cppgen.hpp b/codegen/include/cppgen.hpp new file mode 100644 index 0000000..1ca35e8 --- /dev/null +++ b/codegen/include/cppgen.hpp @@ -0,0 +1,60 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#ifndef GEN_CPPGEN_HPP +#define GEN_CPPGEN_HPP + +#include "codegen.hpp" + +namespace rpcxx::gen::cpp { + +struct Guard { + string begin; + string end; +}; + +Guard MakeGuard(string_view file, string_view ns, string_view part = {}); +Guard MakeGuard(Namespace const& ns); +string ToNamespace(string_view raw, string sep = "::"); +string PrintType(Type t); + +namespace server { +string FormatSignature(Params const& params); +string Format(FormatContext& ctx); +} + +namespace client { +string FormatSignature(Params const& params); +string Format(FormatContext& ctx); +} + +namespace types { +string Format(FormatContext& ctx, vector& attrs); +} + +string Format(FormatContext& ctx); + +} + +#endif //GEN_CPPGEN_HPP diff --git a/codegen/include/gogen.hpp b/codegen/include/gogen.hpp new file mode 100644 index 0000000..31a41a7 --- /dev/null +++ b/codegen/include/gogen.hpp @@ -0,0 +1,36 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#ifndef GEN_GOGEN_HPP +#define GEN_GOGEN_HPP +#include "codegen.hpp" + +namespace rpcxx::gen::go { + +using Writer = std::function; +void Format(FormatContext& ctx, Writer const& writer); + +} + +#endif //GEN_GOGEN_HPP diff --git a/codegen/src/codegen.cpp b/codegen/src/codegen.cpp new file mode 100644 index 0000000..d78116b --- /dev/null +++ b/codegen/src/codegen.cpp @@ -0,0 +1,232 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#include +#include +#include +#include +#include +#include +#include "codegen.hpp" +#include "codegen.lua.h" +#include "cppgen.hpp" +#include "gogen.hpp" +extern "C" { +#include "lua.h" +#include "lualib.h" +#include "lauxlib.h" +} + +using namespace rpcxx; +using namespace gen; + +template +Type makeBuiltin(AST& ast, string_view name) { + static TypeVariant v{Builtin{val}}; + return ast.builtins[name] = &v; +}; + +static void populateBuiltins(AST& ast) { + ast.builtins["nothing"] = ast.builtins["noreturn"] = makeBuiltin(ast, "void"); + makeBuiltin(ast, "binary"); + makeBuiltin(ast, "binary_view"); + makeBuiltin(ast, "json"); + makeBuiltin(ast, "json_view"); + ast.builtins["bool"] = makeBuiltin(ast, "boolean"); + makeBuiltin(ast, "string_view"); + ast.builtins["str"] = makeBuiltin(ast, "string"); + ast.builtins["u64"] = makeBuiltin(ast, "uint64"); + ast.builtins["uint"] = ast.builtins["u32"] = makeBuiltin(ast, "uint32"); + ast.builtins["u16"] = makeBuiltin(ast, "uint16"); + ast.builtins["u8"] = makeBuiltin(ast, "uint8"); + ast.builtins["i64"] = makeBuiltin(ast, "int64"); + ast.builtins["int"] = ast.builtins["i32"] = makeBuiltin(ast, "int32"); + ast.builtins["i16"] = makeBuiltin(ast, "int16"); + ast.builtins["i8"] = makeBuiltin(ast, "int8"); + ast.builtins["f64"] = ast.builtins["number"] = makeBuiltin(ast, "double"); + ast.builtins["f32"] = makeBuiltin(ast, "float"); +} + + +static void parseCli(argparse::ArgumentParser& cli, GenParams& params, int argc, char* argv[]) { + cli.add_argument("spec") + .required() + .help("target .json file"); + cli.add_argument("--name", "-n") + .default_value("") + .help("name of the main file. default: [stem of spec file]"); + cli.add_argument("--output-dir", "-d") + .default_value(".") + .help("output generated files to"); + cli.add_argument("--describe-server") + .implicit_value(true) + .default_value(false) + .help("generate DESCRIBE() for server methods"); + cli.add_argument("--marker", "-m") + .help("marker file, that gets touched on generation (to be used in build systems)"); + cli.add_argument("--no-client") + .implicit_value(true) + .default_value(false) + .help("omit client-related codegen"); + cli.add_argument("--no-server") + .implicit_value(true) + .default_value(false) + .help("omit server-related codegen"); + cli.add_argument("--opt", "-o") + .append() + .default_value>({}) + .help("lang-specific options"); + cli.add_argument("--lang") + .default_value("cpp") + .action([&](auto lang){ + if (lang == "cpp") { + params.lang = Lang::cpp; + } else if(lang == "go") { + params.lang = Lang::go; + } else { + throw Err("Invalid lang param: {}", lang); + } + }) + .help("target language"); + cli.add_argument("--stdout") + .help("print result to stdout instead of a file") + .default_value(false) + .implicit_value(true); + cli.add_description( + "Generates a client and server stub headers based on lua based DSL spec.\n" + "Currently supported languages: [cpp]" + ); + try { + cli.parse_args(argc, argv); + } catch (const std::runtime_error& err) { + fmt::println(stderr, "Error: {}", err.what()); + std::cerr << cli; + std::exit(1); + } +} + +//! Most of memory in this program intentionally leaks +/// We just alloc on heap +/// + disable Lua GC to allow direct refs into VM`s string_views +int main(int argc, char *argv[]) try +{ + lua_State* L = luaL_newstate(); + lua_gc(L, LUA_GCSTOP); //we do not use gc + defer close{[L]{ + lua_close(L); + }}; + argparse::ArgumentParser cli("rpccxx-codegen"); + GenParams params; + parseCli(cli, params, argc, argv); + fs::path specfile = cli.get("spec"); + std::filesystem::current_path(specfile.parent_path()); + params.main.sourceFile = string{specfile.string()}; + AST ast; + populateBuiltins(ast); + FormatContext ctx{params, ast, cli}; + PopulateFromFrontend(L, ctx); + params.describeServer = cli["describe-server"] == true; + auto mainOut = cli.get("name"); + if (mainOut.empty()) { + mainOut = specfile.stem().string(); + } + if (cli["no-client"] == true) { + params.targets = Targets(params.targets & ~TargetClient); + } + if (cli["no-server"] == true) { + params.targets = Targets(params.targets & ~TargetServer); + } + fs::path dir = cli.get("d"); + auto comment = [&]{ + switch (params.lang) { + case Lang::go: + case Lang::cpp: return "//"; + } + return ""; + }(); + auto isStdOut = cli["--stdout"] == true; + auto writeOutput = [&](fs::path file, string_view res){ + file = dir/std::move(file); + if (isStdOut) { + fmt::print(stdout, "{} ===> {}\n{}\n\n", comment, file.string(), res); + } else { + if (std::filesystem::is_directory(file)) { + throw Err("Output file '{}' is a directory", file.string()); + } + auto fileDir = file.parent_path(); + if (!fs::exists(fileDir)){ + if (!fs::create_directories(fileDir)) { + throw Err("Could not create directories for: {}", fileDir.string()); + } + } + std::ofstream out(file, std::ofstream::trunc); + if (!out.is_open()) { + throw Err("Could not open: {}", file.string()); + } + out << res; + } + }; + auto langOpts = cli.get>("opt"); + auto iterOpts = [&](auto fn){ + for (string_view o: langOpts) { + auto pos = o.find_first_of('='); + if (pos == string_view::npos || pos == o.size()) { + throw Err("incorrect 'opt' format: expected key=val"); + } + fn(o.substr(0, pos), o.substr(pos + 1)); + } + }; + if (params.lang == Lang::cpp) { + CppOpts cppOpts; + auto was = ctx.params.targets; + ctx.opts = &cppOpts; + ctx.params.targets = Targets(was & TargetTypes); + writeOutput(mainOut+".private.hpp", cpp::Format(ctx)); + ctx.params.targets = Targets(was & ~TargetTypes); + ctx.params.extraIncludes = {mainOut+".private.hpp"}; + for (auto& f: ctx.ast.attrs) { + ctx.params.extraIncludes.push_back(f+".hpp"); + } + writeOutput(mainOut+".hpp", cpp::Format(ctx)); + } else if (params.lang == Lang::go) { + GoOpts opts; + iterOpts([&](string_view key, string_view val){ + if (key == "pkg_prefix") { + opts.pkgPrefix = string{val}; + } + }); + ctx.opts = &opts; + go::Format(ctx, writeOutput); + } else { + throw Err("unsupported lang"); + } + if (auto marker = cli.present("marker")) { + auto m = std::filesystem::path(*marker); + std::filesystem::create_directories(m.parent_path()); + std::ofstream(m, std::ios_base::trunc | std::ios_base::out).write("1", 1); + } +} catch (std::exception& exc) { + fmt::println(stderr, "Exception: Traceback: \n =>\t{}", exc.what()); + return -1; +} diff --git a/codegen/src/codegen.lua.h b/codegen/src/codegen.lua.h new file mode 100644 index 0000000..b5627b6 --- /dev/null +++ b/codegen/src/codegen.lua.h @@ -0,0 +1,582 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +inline const char* codegen_script = R"(local make_type +local check_ns + +args = args or {} + +local function make_value(t, ...) + if #{...} == 0 then return t end + if t.__value__ then + error(tostring(t)..'is already a value!') + end + if not t.__check__ then + error("Type '"..tostring(t).."' does not support default values") + end + local res = make_type(nil, t) + res.__subtype__ = "default" + res.__value__ = t:__check__(...) + return res +end + +local function make_pack(t) + if t.__subtype__ ~= "struct" then + error(tostring(t).." is not a Struct. Pack (#) operation is not allowed") + end + if t.__is_pack__ then + error(tostring(t).." is already a Pack. Pack (#) operation is not allowed") + end + local copy = {} + for k, v in pairs(t) do + copy[k] = v + end + copy.__is_pack__ = true + return copy +end + +local function make_signature(sig, ret) + if sig.__return__ then + error(sig.__name__.." already has a return type: ".. + sig.__return__.__name__.." cannot apply: "..ret.__name__) + end + sig.__return__ = ret + return sig +end + +local strict_mode = 0 + +local builtins = setmetatable({}, { + __index = function(t, k) + return strict_mode > 0 and error("Could not find: "..k) or rawget(t, k) + end +}) + +local function new_state() + return { + types = setmetatable({}, {__index = builtins}), + methods = {}, + notify = {}, + attrs = {}, + depth = 0, + source = __current_file__ or "root", + ns = nil, + } +end + +local state = new_state() +local namespaces = {} + +setmetatable(_G, { + __newindex = function (t, k, v) + if type(v) == "table" and v.__is_type__ then + if v.__subtype__ == "builtin" then + v.__name__ = k + builtins[k] = v + return + end + v:__resolve_name__({k}) + if state.types[v.__name__] then + error("Cannot redeclare type: "..tostring(v)) + end + state.types[v.__name__] = v + else + rawset(t, k, v) + end + end, + __index = function (t, k) + return state.types[k] + end +}) + +local function print_type(t) + return "'"..tostring(t.__subtype__).." "..tostring(t.__ns__ or '').."."..tostring(t.__name__).."'" +end + +local _defaultedCounter = 0 +local function defaultedCount() + _defaultedCounter = _defaultedCounter + 1 + return tostring(_defaultedCounter) +end + +local function resolve_name(t, path) + check_ns() + path = path or {} + if t.__ns__ == nil then + t.__ns__ = state.ns + end + local sub = t.__subtype__ + if sub == "builtin" then + return + end + if not t.__name__ then + t.__name__ = table.concat(path, "__") + end + if sub == "struct" then + for k, v in pairs(t.__fields__) do + resolve_name(v, {table.unpack(path), k}, t) + end + elseif sub == "enum" then + return + else + assert(t.__next__, "Missing wrapped type in sub: "..sub) + resolve_name(t.__next__, path) + end +end + +make_type = function(name, _next, check_val, extra) + local meta = { + __tostring = print_type, + __call = make_value, + __len = make_pack, + __shr = make_signature, + } + if extra then + for k, v in pairs(extra) do + meta[k] = v + end + end + local t = setmetatable({ + __name__ = name, + __is_type__ = true, + __next__ = _next, + __attrs__ = state.attrs, + __resolve_name__ = resolve_name, + __source__ = state.source, + __ns__ = state.ns, + __ns_depth__ = state.depth, + }, meta) + if check_val then + t.__check__ = check_val + elseif _next then + t.__check__ = _next.__check__ + end + return t +end + +local function resolve_type(v, name) + if type(v) == "table" then + if v.__is_type__ then + return v + else + return struct()(v) + end + else + error("Field '"..name.."' => type expected, but got: "..tostring(v)) + end +end + +local function make_builtin(check_val) + local result = make_type(nil, nil, check_val) + result.__subtype__ = "builtin"; + return result +end + +local function init_num(min, max) + return function (t, ...) + assert(#{...} == 1, "Number expects only one param") + local res = ... + assert(type(res) == "number", "Expected a number as default param") + assert(res >= min and res <= max, + res.." => does not fit into ["..min.." - "..max.."] for type: "..tostring(t)) + return res + end +end + +local function typed(name) + return function (t, ...) + assert(#{...} == 1, tostring(t)..": expects only one param") + assert(type(...) == name, tostring(t)..": ".."Expected '"..name.."', got: "..tostring(...)) + return ... + end +end + +local populate = function () + local function integer(signed, bits) + if signed then + return init_num(-(2^(bits-1)), 2^(bits-1)-1) + else + return init_num(0, 2^bits-1) + end + end + local s = {} + local u = {} + for _, v in ipairs{8, 16, 32, 64} do + s[v] = integer(true, v) + u[v] = integer(false, v) + end + return s, u +end +local signed, unsigned = populate() +local function is_priv(key) + return key:sub(1, 2) == "__" +end + +local function parse_signature(name, sig) + assert(type(sig) == "table", "Signatures should be an array/table of types") + local isNamed = false + local isPack = false + for k, v in pairs(sig) do + if type(k) == "number" and isNamed then + error(name..": Invalid signature, mixing named and positionals not allowed") + end + if type(k) == "string" then + if is_priv(k) then + goto continue + end + isNamed = true + end + assert(type(v) == "table", "Signature contains non type") + assert(v.__is_type__, "Signature contains non type") + if v.__is_pack__ then + assert(#sig == 1, "#Pack allowed as the only type in signature") + assert(not isNamed, "#Pack is not allowed to have named params") + isPack = true + end + v:__resolve_name__({name, "arg", tostring(k)}) + ::continue:: + end + if sig.__return__ then + sig.__return__:__resolve_name__({name, "ret"}) + end + return { + named = isNamed, + async = sig.__async__ or false, + params = sig, + pack = isPack, + returns = sig.__return__ or void + } +end + +local function resolve_inc(was, path) + if __resolve_inc__ then + return __resolve_inc__(was, path) + else + return path + end +end + +local function enter_file(file) + local was = state + state = new_state() + state.source = resolve_inc(was.source, file) + state.depth = was.depth + 1 + state.ns = nil + return was +end + +local function exit_into(was) + check_ns() + namespaces[state.ns] = state + local catched = state.types + state = namespaces[was.ns] + return catched +end + +local function exit_strict() + if strict_mode > 0 then + strict_mode = strict_mode - 1 + end +end + +local function in_strict(func) + strict_mode = strict_mode + 1 + return function(...) + local res = func(...) + exit_strict() + return res + end +end + +check_ns = function() + assert(state.ns ~= nil, "use 'namespace ' directive first") +end + +-- User facing definitions + +void = make_builtin() +noreturn = make_builtin() +nothing = make_builtin() + +binary = make_builtin() +binary_view = make_builtin() + +int8 = make_builtin(signed[8]) +i8 = make_builtin(signed[8]) + +uint8 = make_builtin(unsigned[8]) +u8 = make_builtin(unsigned[8]) + +int16 = make_builtin(signed[16]) +i16 = make_builtin(signed[16]) + +uint16 = make_builtin(unsigned[16]) +u16 = make_builtin(unsigned[16]) + +json = make_builtin() +json_view = make_builtin() + +boolean = make_builtin(typed("boolean")) +bool = make_builtin(typed("boolean")) + +int = make_builtin(signed[32]) +int32 = make_builtin(signed[32]) +i32 = make_builtin(signed[32]) + +uint32 = make_builtin(unsigned[32]) +u32 = make_builtin(unsigned[32]) +uint = make_builtin(unsigned[32]) + +int64 = make_builtin(signed[64]) +i64 = make_builtin(signed[64]) + +uint64 = make_builtin(unsigned[64]) +u64 = make_builtin(unsigned[64]) + +f32 = make_builtin(typed("number")) +float = make_builtin(typed("number")) + +double = make_builtin(typed("number")) +number = make_builtin(typed("number")) +f64 = make_builtin(typed("number")) + +str = make_builtin(typed("string")) +string = make_builtin(typed("string")) +string.__name__ = "string" +string_view = make_builtin(typed("string")) + +local function check_ident(id) + return id --TODO: check that id is valid alphanum identifier +end + +local function init_opts(opts) + if type(opts) == "string" then + return {name = opts} + else + return opts or {} + end +end + +local function _check_struct(table) + if type(table) == "table" and not table.__is_type__ then + return struct()(table) + else + return table + end +end + +-- User facing functions + +function methods(opts) + opts = init_opts(opts) + return in_strict(function(values) + for method, sig in pairs(values) do + assert(type(method) == "string") + local parsed = parse_signature(method, sig) + parsed.service = opts.name or "RPC" + parsed.name = check_ident(method) + table.insert(state.methods, parsed) + end + end) +end + +function notify(opts) + opts = init_opts(opts) + return in_strict(function(values) + for notif, sig in pairs(values) do + assert(type(notif) == "string") + local parsed = parse_signature(notif, sig) + assert(parsed.returns.__name__ == "void", notif..": Notifications cannot have return types") + parsed.service = opts.name or "RPC" + parsed.name = check_ident(notif) + table.insert(state.notify, parsed) + end + end) +end + +function struct(opts) + check_ns() + opts = init_opts(opts) + return in_strict(function(shape) + local res = make_type(opts.name, nil, function(s, shape) + assert(type(shape) == "table", "struct default value should be table") + local res = {} + for k, v in pairs(s.__fields__) do + if shape[k] then + if not v.__check__ then + error("Field of struct: "..k.." ("..tostring(v)..") does not support default values") + end + res[k] = v:__check__(shape[k]) + end + end + return res + end) + assert(type(shape) == "table", '{field = t, ...} expected for struct()') + res.__subtype__ = "struct" + res.__fields__ = {} + for k, v in pairs(shape) do + if not is_priv(k) then + res.__fields__[k] = resolve_type(v, k) + end + end + return res + end) +end + +function enum(opts) + check_ns() + opts = init_opts(opts) + return in_strict(function(values) + assert(type(values) == "table", '{name, name2, ...} expected for enum()') + local res = make_type(opts.name, nil, function(s, v) + assert(type(v) == "string", "enum default value should be a string") + for i, f in ipairs(s.__fields__) do + if f == v then + return v + end + end + error(v.." is not a valid member of "..tostring(s)) + end) + res.__subtype__ = "enum" + res.__fields__ = values + for k, v in pairs(values) do + if type(k) == "number" then + assert(type(k) == "number", "Enum keys should be integers") + assert(type(v) == "string", "Enum values should be strings") + else + assert(type(k) == "string", "Strict Enum keys should be strings") + assert(type(v) == "number", "Strict Enum values should be integers") + end + res[k] = v + end + return res + end) +end + +function routes(opts) + check_ns() + opts = init_opts(opts) + return in_strict(function(...) + -- annotation only + end) +end + +function async(signature) + assert(type(signature) == "table", "async can be used only with signatures: {t, t}") + signature.__async__ = true + return signature +end + +function include(path) + local was = enter_file(path) + dofile(state.source) + return exit_into(was) +end + +function Optional(_next) + assert(_next, 'Could not find type for Optional()') + _next = _check_struct(_next) + local res = make_type(nil, _next) + res.__subtype__ = "opt" + return res +end + +function Array(_next) + assert(_next, 'Could not find type for Array()') + _next = _check_struct(_next) + local check = function(t, arr) + local res = {} + for i, v in pairs(arr) do + assert(type(i) == "number", "default value for Array() must be an array") + res[i] = _next:__check__(v) + end + return res + end + local res = make_type(nil, _next, check) + res.__subtype__ = "arr" + return res +end + +function Map(_next) + assert(_next, 'Could not find type for Map()') + _next = _check_struct(_next) + local check = function(t, obj) + local res = {} + for k, v in pairs(obj) do + assert(type(k) == "string", "default value for Map() must be a table") + res[k] = _next:__check__(v) + end + return res + end + local res = make_type(nil, _next, check) + res.__subtype__ = "map" + return res +end + +function Alias(_next) + assert(_next, 'Could not find type for Alias()') + _next = _check_struct(_next) + local res = make_type(nil, _next) + res.__subtype__ = "alias" + return res +end + +function syntax(s) + assert(s == "rpcxx", "Unsupported syntax: "..s) +end + +function namespace(ns) + assert(ns ~= "__root__", "name '__root__' is reserved") + assert(type(ns) == "string", "namespace should be a string") + assert(state.ns == nil, "namespace was already set to: "..tostring(state.ns)) + state.ns = ns + namespaces[state.ns] = state + for _, v in pairs(state.types) do + v.__ns__ = ns + end + if namespaces.__root__ == nil then + namespaces.__root__ = state + end +end + +function attributes(file) + if type(file) == "string" then + table.insert(state.attrs, file) + elseif type(file) == "table" then + for _, v in pairs(file) do + table.insert(state.attrs, v) + end + else + error("attributes should be a filepath or list of paths") + end + for _, v in pairs(state.types) do + v.__attrs__ = state.attrs + end +end + +return namespaces + + + +)"; diff --git a/codegen/src/cppgen.cpp b/codegen/src/cppgen.cpp new file mode 100644 index 0000000..7415474 --- /dev/null +++ b/codegen/src/cppgen.cpp @@ -0,0 +1,223 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#include "cpp/clientgen.hpp" +#include "cpp/servergen.hpp" +#include "cpp/typegen.hpp" +#include "fmt/compile.h" +#include + +namespace rpcxx::gen::cpp +{ + +static string builtin(Builtin builtin) +{ + using namespace std::string_literals; + switch (builtin.kind) { + case Builtin::Json: return "rpcxx::Json"s; + case Builtin::Json_View: return "rpcxx::JsonView"s; + case Builtin::Bool: return "bool"s; + case Builtin::Int8: return "int8_t"s; + case Builtin::Uint8: return "uint8_t"s; + case Builtin::Int16: return "int16_t"s; + case Builtin::Uint16: return "uint16_t"s; + case Builtin::Int32: return "int32_t"s; + case Builtin::Uint32: return "uint32_t"s; + case Builtin::Int64: return "int64_t"s; + case Builtin::Uint64: return "uint64_t"s; + case Builtin::Float: return "float"s; + case Builtin::Double: return "double"s; + case Builtin::String: return "std::string"s; + case Builtin::String_View: return "std::string_view"s; + case Builtin::Binary: return "rpcxx::Binary"s; + case Builtin::Binary_View: return "rpcxx::BinaryView"s; + case Builtin::Void: return "void"s; + default: throw Err("NonBuiltin passed into Builtin Handler"); + } +} + +string PrintType(Type t) { + return Visit(t->AsVariant(), + [](Builtin b) -> string { + return builtin(b); + }, + [](Optional& o) -> string { + return fmt::format(FMT_COMPILE("std::optional<{}>"), PrintType(o.item)); + }, + [](Map& m) -> string { + return fmt::format(FMT_COMPILE("rpcxx::Map<{}>"), PrintType(m.item)); + }, + [](Array& a) -> string { + return fmt::format(FMT_COMPILE("std::vector<{}>"), PrintType(a.item)); + }, + [](WithDefault& a) -> string { + return PrintType(a.item); + }, + [](auto& v) -> string { + return fmt::format(FMT_COMPILE("{}::{}"), ToNamespace(v.ns.name), v.name); + } + ); +} + +constexpr auto common_template = FMT_COMPILE(R"EOF(// THIS FILE IS GENERATED. Do not modify. Generated from: {source_file} +{guard_start} +{extra_includes} +#include +{type_gen} +{namespace_start} +{server_gen} +{client_gen} + +{server_gen_source} +{client_gen_source} +{namespace_end} +{guard_end} +)EOF"); + +Guard MakeGuard(string_view file, string_view ns, string_view part) { + auto fns = ToNamespace(file, "_"); + auto nsns = ToNamespace(ns, "_"); + return { + fmt::format(FMT_COMPILE("#ifndef _{0}_G_{1}_P_{2}\n#define _{0}_G_{1}_P_{2}"), nsns, fns, part), + fmt::format(FMT_COMPILE("#endif //_{}_G_{}_P_{}"), nsns, fns, part) + }; +} + +Guard MakeGuard(Namespace const& ns) { + return MakeGuard(ns.sourceFile, ns.name, ns.part); +} + +string ToNamespace(string_view file, string sep) { + string result; + for (auto ch: file) { + if (!std::isalnum(ch) && ch != '_') { + result += sep; + } else { + result += ch; + } + } + return result; +} + +static bool isTrivial(Type t) { + return Visit( + t->AsVariant(), + [](Builtin){ + return true; + }, + [](Enum&){ + return true; + }, + [](Alias& a) { + return isTrivial(a.item); + }, + [](Optional& o) { + return isTrivial(o.item); + }, + [](auto&){ + return false; + } + ); +} + +static string doFormat(Params const& params, bool needCref) { + auto ifCref = [&](Type t) { + return needCref && !isTrivial(t) ? " const&" : ""; + }; + return Visit(params, + [&](ParamsNamed const& named) -> string { + string result; + int idx = 0; + for (auto& [k, v]: named) { + result += fmt::format(FMT_COMPILE("{}{}{} {}"), idx ? ", " : "", PrintType(v), ifCref(v), k); + idx++; + } + return result; + }, + [&](ParamsArray const& arr) -> string { + string result; + int idx = 0; + for (auto& v: arr) { + result += fmt::format(FMT_COMPILE("{}{}{} arg{}"), idx ? ", " : "", PrintType(v), ifCref(v), idx); + idx++; + } + return result; + }, + [&](ParamsPack const& pack) -> string { + return fmt::format(FMT_COMPILE("{}{} args"), + PrintType(pack.item), + ifCref(pack.item)); + } + ); +} + +string server::FormatSignature(Params const& params) { + return doFormat(params, false); +} + +string client::FormatSignature(Params const& params) { + return doFormat(params, true); +} + +string Format(FormatContext& ctx) +{ + auto& opts = *static_cast(ctx.opts); + auto mainns = ToNamespace(ctx.params.main.name); + string extraIncludes = "\n"; + for (auto& ns: ctx.params.extraIncludes) { + extraIncludes += fmt::format(FMT_COMPILE("#include \"{}\"\n"), ns); + } + bool genNamespace = !mainns.empty(); + string gstart, gend; + if (ctx.params.targets != Targets::TargetTypes) { + auto gs = MakeGuard(ctx.params.main); + gstart = std::move(gs.begin); + gend = std::move(gs.end); + } else { + genNamespace = false; + } + auto types = types::Format(ctx); + opts.sourceFile = false; + auto clientHeader = client::Format(ctx); + auto serverHeader = server::Format(ctx); + opts.sourceFile = true; + auto clientSrc = client::Format(ctx); + auto serverSrc = server::Format(ctx); + return fmt::format( + common_template, + fmt::arg("extra_includes", extraIncludes), + fmt::arg("type_gen", types), + fmt::arg("client_gen", clientHeader), + fmt::arg("server_gen", serverHeader), + fmt::arg("guard_start", gstart), + fmt::arg("namespace_start", genNamespace ? fmt::format("namespace {} \n{{", mainns) : ""), + fmt::arg("namespace_end", genNamespace ? fmt::format("}} //namespace {}", mainns) : ""), + fmt::arg("client_gen_source", clientSrc), + fmt::arg("server_gen_source", serverSrc), + fmt::arg("guard_end", "\n"+gend), + fmt::arg("source_file", ctx.prog.get("spec")) + ); +} + +} diff --git a/codegen/src/gogen.cpp b/codegen/src/gogen.cpp new file mode 100644 index 0000000..cc89f39 --- /dev/null +++ b/codegen/src/gogen.cpp @@ -0,0 +1,167 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#include "gogen.hpp" +#include + +using namespace rpcxx::gen; + +static string builtin(Builtin builtin) +{ + using namespace std::string_literals; + switch (builtin.kind) { + case Builtin::Json: return "any"s; + case Builtin::Json_View: return "any"s; + case Builtin::Bool: return "bool"s; + case Builtin::Int8: return "int8"s; + case Builtin::Uint8: return "uint8"s; + case Builtin::Int16: return "int16"s; + case Builtin::Uint16: return "uint16"s; + case Builtin::Int32: return "int32"s; + case Builtin::Uint32: return "uint32"s; + case Builtin::Int64: return "int64"s; + case Builtin::Uint64: return "uint64"s; + case Builtin::Float: return "float32"s; + case Builtin::Double: return "float64"s; + case Builtin::String: return "string"s; + case Builtin::String_View: return "string"s; + case Builtin::Binary: return "any"s; + case Builtin::Binary_View: return "any"s; + case Builtin::Void: return ""s; + default: throw Err("NonBuiltin passed into Builtin Handler"); + } +} + +static string PrintType(Type t) { + return Visit( + t->AsVariant(), + [](Builtin b) -> string { + return builtin(b); + }, + [](Optional& o) -> string { + return fmt::format(FMT_COMPILE("*{}"), PrintType(o.item)); + }, + [](Map& m) -> string { + return fmt::format(FMT_COMPILE("map[string]{}"), PrintType(m.item)); + }, + [](Array& a) -> string { + return fmt::format(FMT_COMPILE("[]{}"), PrintType(a.item)); + }, + [](WithDefault& a) -> string { + return PrintType(a.item); + }, + [](TypeBase& v) -> string { + return fmt::format("{}{}", v.ns.depth ? v.ns.name + "." : "", v.name); + } + ); +} + +constexpr auto Tab = " "; + +static string pascalCase(string_view src) { + string res(src); + if (res.size()) { + res[0] = std::toupper(res[0]); + } + size_t in = 0; + size_t out = 0; + for(;in < res.size(); ++in) { + auto ch = res[in]; + if (ch == '_' && in != res.size() - 1) { + res[out++] = std::toupper(res[++in]); + } else { + res[out++] = ch; + } + } + res.resize(out); + return res; +} + +static string formatFields(Struct& s) { + string res; + size_t maxNameLen = 0; + size_t maxTypeLen = 0; + vector pascalNames; + vector types; + std::sort(s.fields.begin(), s.fields.end(), [](Struct::Field& l, Struct::Field& r){ + return l.name < r.name; + }); + for (auto& f: s.fields) { + auto& n = pascalNames.emplace_back(pascalCase(f.name)); + if (n.size() > maxNameLen) { + maxNameLen = n.size(); + } + auto& t = types.emplace_back(PrintType(f.type)); + if (t.size() > maxTypeLen) { + maxTypeLen = t.size(); + } + } + size_t idx = 0; + for (auto& f: s.fields) { + auto i = idx++; + string attrs; + // maybe used + res += fmt::format( + FMT_COMPILE("\n{}{:<{}} {:<{}} `json:\"{}{}\"`"), + Tab, + pascalNames[i], maxNameLen, + types[i], maxTypeLen, + f.name, attrs); + } + return res; +} + +void go::Format(FormatContext &ctx, const Writer &writer) +{ + assert(ctx.opts); + GoOpts& opts = *static_cast(ctx.opts); //not used yet, will be for imports + std::map> byNs; + for (auto& t: ctx.ast.types) { + if (is(t) || is(t)) { + throw Err("Enums or aliases not supported yet"); + } + if (auto* str = std::get_if(&t->AsVariant())) { + byNs[str->ns].push_back(t); + } + } + for (auto& [ns, types]: byNs) { + std::sort(types.begin(), types.end(), [](Type l, Type r){ + return l->Base()->name < r->Base()->name; + }); + auto dotPos = ns.name.find_last_of('.'); + auto pkg = ns.name.substr(dotPos + 1); + string file; + file += fmt::format("package {}\n\n", pkg); + for (auto t: types) { + if (auto* str = std::get_if(&t->AsVariant())) { + file += fmt::format( + FMT_COMPILE("type {} struct {{{}\n}}\n\n"), + str->name, formatFields(*str)); + } + } + auto pref = ns.name; + std::replace(pref.begin(), pref.end(), '.', '/'); + writer(fs::path(pref) / (pkg+".gen.go"), file); + } +} diff --git a/codegen/src/populate.cpp b/codegen/src/populate.cpp new file mode 100644 index 0000000..5a0eb36 --- /dev/null +++ b/codegen/src/populate.cpp @@ -0,0 +1,483 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#include +#include +#include +#include +#include +#include +#include "codegen.hpp" +#include "codegen.lua.h" +#include "cppgen.hpp" +#include "gogen.hpp" +extern "C" { +#include "lua.h" +#include "lualib.h" +#include "lauxlib.h" +} + +using namespace rpcxx; +using namespace gen; + +template +static void iterateTable(lua_State* L, Fn&& f) { + if (lua_type(L, -1) != LUA_TTABLE) { + throw Err("Table expected"); + } + lua_pushnil(L); + while(lua_next(L, -2)) { + auto was = lua_gettop(L); + bool contin = true; + if constexpr (std::is_invocable_r_v) { + f(); + } else { + contin = f(); + } + auto now = lua_gettop(L); + if (was != now) { + throw Err("After calling an iterator function =>" + " stack is not of the same size (was: {} != now: {})", + was, now); + } + if (!contin) { + lua_pop(L, 2); + return; + } + lua_pop(L, 1); + } +} + +template +static void iterateTableConsume(lua_State* L, Fn&& f) { + iterateTable(L, f); + lua_pop(L, 1); +} + +static string_view getSV(lua_State* L, int idx = -1) { + size_t sz; + if (lua_type(L, idx) != LUA_TSTRING) { + throw Err("Expected string, got: {}", luaL_typename(L, idx)); + } + auto msg = luaL_checklstring(L, idx, &sz); + return {msg, sz}; +} + +static string_view popSV(lua_State* L) { + auto res = getSV(L, -1); + lua_pop(L, 1); + return res; +} + +static bool test(lua_State* L, const char* name) { + lua_getfield(L, -1, name); + bool result = false; + if (!lua_isnil(L, -1) && lua_toboolean(L, -1)) { + result = true; + } + lua_pop(L, 1); + return result; +} + +static Type resolveType(lua_State* L, string_view tname, FormatContext& ctx); + +static Type resolveNext(lua_State* L, FormatContext& ctx) { + lua_getfield(L, -1, "__next__"); + lua_getfield(L, -1, "__name__"); + auto name = popSV(L); + auto found = resolveType(L, name, ctx); + lua_pop(L, 1); + return found; +} + +static Namespace extractNs(lua_State* L) { + lua_getfield(L, -1, "__ns_depth__"); + lua_getfield(L, -2, "__ns__"); + lua_getfield(L, -3, "__source__"); + Namespace ns; + if (!lua_isnil(L, -1) && !lua_isnil(L, -2) && !lua_isnil(L, -3)) { + ns = Namespace{string{getSV(L, -1)}, string{getSV(L, -2)}, string{}, int(lua_tointeger(L, -3))}; + } else { + throw Err("Invalid namespace received"); + } + lua_pop(L, 3); + return ns; +} + +static Type registerIfNeeded(Type t, FormatContext& ctx) { + ctx.ast.types.push_back(t); + auto base = GetBase(t); + if (!base) { + throw Err("attempt to register builtin"); + } + return ctx.ast.savedTypes[base->ns][base->name] = t; +} + +static Type tryLookup(string_view name, Namespace& ns, FormatContext& ctx) { + auto n = ctx.ast.savedTypes.find(ns); + if (n == ctx.ast.savedTypes.end()) { + return nullptr; + } + auto t = n->second.find(name); + if (t == n->second.end()) { + return nullptr; + } + return t->second; +} + +static void populateAttrs(lua_State* L, vector& attrs) { + if (lua_type(L, -1) != LUA_TTABLE) { + lua_pop(L, 1); + return; + } + iterateTableConsume(L, [&]{ + auto attr = string{getSV(L)}; + if (std::find(attrs.begin(), attrs.end(), attr) == attrs.end()) { + attrs.push_back(std::move(attr)); + } + }); +} + +static def::Value parseDefault(lua_State* L); + +static def::Value parseArr(lua_State* L) { + def::Array res; + iterateTable(L, [&]{ + auto i = lua_tointeger(L, -2); + if (i < 1) throw Err("Negative index in default-array"); + auto idx = size_t(i - 1); + if (res.value.size() <= idx) { + res.value.resize(idx + 1); + } + lua_pushvalue(L, -1); + res.value[idx] = parseDefault(L); + }); + size_t idx = 0; + for (auto& v: res.value) { + if (!v) { + throw Err("Index in default-array #{} not populated", idx); + } + idx++; + } + return new def::Variant{std::move(res)}; +} + +static def::Value parseTable(lua_State* L) { + def::Table res; + iterateTable(L, [&]{ + auto key = getSV(L, -2); + lua_pushvalue(L, -1); + res.value[string{key}] = parseDefault(L); + }); + return new def::Variant{std::move(res)}; +} + +static def::Value parseDefault(lua_State* L) { + using namespace def; + defer clean([&]{ + lua_pop(L, 1); + }); + auto t = lua_type(L, -1); + switch (t) { + case LUA_TNIL: return new Variant{Nil{}}; + case LUA_TSTRING: return new Variant{String{getSV(L, -1)}}; + case LUA_TBOOLEAN: return new Variant{Bool{bool(lua_toboolean(L, -1))}}; + case LUA_TNUMBER: { + if (lua_isinteger(L, -1)) { + return new Variant{Int{lua_tointeger(L, -1)}}; + } else { + return new Variant{Num{lua_tonumber(L, -1)}}; + } + } + case LUA_TTABLE: { + auto allInts = true; + iterateTable(L, [&]{ + if (!lua_isinteger(L, -2)) { + allInts = false; + } + if (!allInts && lua_type(L, -2) != LUA_TSTRING) { + throw Err("Non-string/int keys are not supported in tables"); + } + }); + return allInts ? parseArr(L) : parseTable(L); + } + default: + throw Err("Unsupported type for default: {}", lua_typename(L, t)); + } + return nullptr; +} + +static Type resolveType(lua_State* L, string_view tname, FormatContext& ctx) try { + if (!test(L, "__is_type__")) { + throw Err("Type expected: {}", tname); + } + if (auto it = ctx.ast.builtins.find(tname); it != ctx.ast.builtins.end()) { + return it->second; + } + lua_getfield(L, -1, "__subtype__"); + auto sub = popSV(L); + auto ns = extractNs(L); + if (auto found = tryLookup(tname, ns, ctx)){ + return found; + } + lua_getfield(L, -1, "__attrs__"); + populateAttrs(L, ctx.ast.attrs); + if (sub == "builtin") { + throw Err("Unhandled builtin type: {}", tname); + } else if (sub == "alias") { + Alias result; + result.ns = ns; + result.name = tname; + result.item = resolveNext(L, ctx); + return registerIfNeeded(new TypeVariant{result}, ctx); + } else if (sub == "arr") { + Array result; + result.ns = ns; + result.name = tname; + result.item = resolveNext(L, ctx); + return registerIfNeeded(new TypeVariant{result}, ctx); + } else if (sub == "opt") { + Optional result; + result.ns = ns; + result.name = tname; + result.item = resolveNext(L, ctx); + return registerIfNeeded(new TypeVariant{result}, ctx); + } else if (sub == "map") { + Map result; + result.ns = ns; + result.name = tname; + result.item = resolveNext(L, ctx); + return registerIfNeeded(new TypeVariant{result}, ctx); + } else if (sub == "enum") { + Enum result; + result.ns = ns; + result.name = tname; + lua_getfield(L, -1, "__fields__"); + iterateTableConsume(L, [&]{ + Enum::Value curr; + if (lua_type(L, -2) == LUA_TSTRING) { + curr.name = string{getSV(L, -2)}; + if (!lua_isinteger(L, -1)) { + throw Err("Expected integer in enum {}: value: {}", tname, curr.name); + } + curr.number = lua_tointeger(L, -1); + } else { + curr.name = string{getSV(L, -1)}; + } + result.values.push_back(std::move(curr)); + }); + std::sort(result.values.begin(), result.values.end(), [](auto& lhs, auto& rhs){ + return lhs.name < rhs.name; + }); + return registerIfNeeded(new TypeVariant{result}, ctx); + } else if (sub == "default") { + WithDefault result; + result.ns = ns; + result.name = tname; + result.item = resolveNext(L, ctx); + lua_getfield(L, -1, "__value__"); + result.value = parseDefault(L); + return registerIfNeeded(new TypeVariant{result}, ctx); + } else if (sub == "struct") { + Struct result; + result.ns = ns; + result.name = tname; + lua_getfield(L, -1, "__fields__"); + iterateTableConsume(L, [&]{ + auto subname = getSV(L, -2); + lua_getfield(L, -1, "__name__"); + auto subtname = popSV(L); + auto found = resolveType(L, subtname, ctx); + if (!found) { + throw Err("Error resolving: {}.{}", tname, subname); + } + result.fields.push_back({string{subname}, found}); + }); + return registerIfNeeded(new TypeVariant{result}, ctx); + } else { + throw Err("{}: unknown subtype: {}", tname, sub); + } +} catch (std::exception& e) { + throw Err("{}\n =>\tWhile resolving type: '{}'", e.what(), tname); +} + +static void doResolveNotify(Notify& result, lua_State* L, FormatContext& ctx) { + auto ispack = test(L, "pack"); + auto isnamed = test(L, "named"); + lua_getfield(L, -1, "service"); + result.service = string{popSV(L)}; + lua_getfield(L, -1, "name"); + result.name = string{popSV(L)}; + auto* pack = ispack ? &result.params.emplace() : nullptr; + auto* arr = !ispack && !isnamed ? &result.params.emplace() : nullptr; + auto* named = !ispack && isnamed ? &result.params.emplace() : nullptr; + lua_getfield(L, -1, "params"); + iterateTableConsume(L, [&]{ + if (lua_type(L, -2) == LUA_TSTRING) { + auto key = getSV(L, -2); + if (key.substr(0, 2) == "__") { + return true; + } + } + if (lua_type(L, -1) != LUA_TTABLE) { + return true; + } + lua_getfield(L, -1, "__name__"); + auto pname = popSV(L); + auto par = resolveType(L, pname, ctx); + if (pack) { + pack->item = par; + } else if (arr) { + auto idx = unsigned(lua_tointeger(L, -2) - 1); + if (arr->size() <= idx) { + arr->resize(idx + 1); + } + (*arr)[idx] = par; + } else { + assert(named); + auto key = getSV(L, -2); + (*named)[string{key}] = par; + } + return true; + }); +} + +static void resolveMethod(lua_State* L, FormatContext& ctx) { + Method& result = ctx.ast.methods.emplace_back(); + doResolveNotify(result, L, ctx); + result.async = test(L, "async"); + lua_getfield(L, -1, "returns"); + lua_getfield(L, -1, "__name__"); + auto retname = popSV(L); + result.returns = resolveType(L, retname, ctx); + lua_pop(L, 1); +} + +static void resolveNotify(lua_State* L, FormatContext& ctx) { + Notify& result = ctx.ast.notify.emplace_back(); + doResolveNotify(result, L, ctx); +} + +static int msghandler(lua_State* L) { + luaL_Buffer b; + lua_Debug ar; + luaL_buffinit(L, &b); + auto msg = getSV(L, 1); + msg = msg.substr(msg.find(": ") + 2); + luaL_addlstring(&b, msg.data(), msg.size()); + int level = 1; + while (lua_getstack(L, level++, &ar)) { + lua_getinfo(L, "Sln", &ar); + // if (strcmp(ar.source, "") == 0) { + // continue; + // } + if (ar.currentline <= 0) { + continue; + } else { + lua_pushfstring(L, "\n =>\t%s:%d", ar.short_src, ar.currentline); + luaL_addvalue(&b); + } + } + luaL_pushresult(&b); + return 1; +} + +static void parseOneNamespace(lua_State* L, FormatContext& ctx) { + lua_getfield(L, -1, "types"); + iterateTableConsume(L, [&]{ + resolveType(L, getSV(L, -2), ctx); + }); + lua_getfield(L, -1, "methods"); + iterateTableConsume(L, [&]{ + resolveMethod(L, ctx); + }); + lua_getfield(L, -1, "notify"); + iterateTableConsume(L, [&]{ + resolveNotify(L, ctx); + }); +} + +template +int Protected(lua_State* L) noexcept { + try { + return f(L); + } catch (std::exception& e) { + lua_pushstring(L, e.what()); + } + lua_error(L); + std::abort(); +} + +static int resolve_inc(lua_State* L) { + auto was = fs::path{getSV(L, 1)}; + auto wanted = fs::path{getSV(L, 2)}; + if (wanted.is_absolute()) { + lua_pushvalue(L, 2); + return 1; + } + auto rel = was.parent_path()/wanted; + if (fs::exists(rel)) { + lua_pushstring(L, rel.string().c_str()); + return 1; + } + throw Err("Could not include: {} => {} does not exist", + wanted.string(), rel.string()); +} + +static void initEnv(lua_State* L, FormatContext& ctx) { + lua_pushlstring(L, ctx.params.main.sourceFile.data(), ctx.params.main.sourceFile.size()); + lua_setglobal(L, "__current_file__"); + lua_register(L, "__resolve_inc__", Protected); + if (!lua_checkstack(L, 300)) { + throw Err("Could not reserve lua stack"); + } +} + +void gen::PopulateFromFrontend(lua_State* L, FormatContext& ctx) { + lua_pushcfunction(L, msghandler); + auto msgh = lua_gettop(L); + luaL_openlibs(L); + initEnv(L, ctx); + if (luaL_loadbufferx(L, codegen_script, strlen(codegen_script), "", "t") != LUA_OK) { + throw Err("Could not load init-script: {}", getSV(L)); + } + if (lua_pcall(L, 0, LUA_MULTRET, 0) != LUA_OK) { + throw Err("Error running init-script: {}", getSV(L)); + } + int all_ns = luaL_ref(L, LUA_REGISTRYINDEX); + auto spec = ctx.prog.get("spec"); + if (luaL_loadfile(L, spec.c_str()) != LUA_OK) { + throw Err("Could not load spec file: {} => {}", spec, getSV(L)); + } + if (lua_pcall(L, 0, 0, msgh) != LUA_OK) { + throw Err("{}", getSV(L)); + } + lua_rawgeti(L, LUA_REGISTRYINDEX, all_ns); + lua_getfield(L, -1, "__root__"); + parseOneNamespace(L, ctx); + lua_getfield(L, -1, "ns"); + auto res = string{getSV(L)}; + lua_pop(L, 2); + ctx.params.main.name = res; +} diff --git a/include/future/cancel_token.hpp b/include/future/cancel_token.hpp new file mode 100644 index 0000000..bb4dd72 --- /dev/null +++ b/include/future/cancel_token.hpp @@ -0,0 +1,82 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#ifndef FUT_CANCEL_HPP +#define FUT_CANCEL_HPP + +#include "multi_future.hpp" + +namespace fut +{ + +struct Cancel { + std::string reason; +}; + +struct CancelSignal { + CancelSignal(MultiFuture sig = {}) : sig(std::move(sig)) {} + bool IsValid() const noexcept { + return sig.IsValid(); + } + template + void OnCancel(Fn f) { + OnCancel(nullptr, std::move(f)); + } + template + void OnCancel(rc::Strong exec, Fn f) { + sig.AtLast(exec, [MV(f)](auto res) mutable { + try { + [[maybe_unused]] auto c = res.get(); + if constexpr (std::is_invocable_v) { + f(); + } else { + f(std::move(c)); + } + } catch (...) {} + }); + } +private: + MultiFuture sig; +}; + +struct CancelController { + CancelController() { + fut = prom.GetFuture(); + } + void operator()(std::string reason) { + if (prom.IsValid()) { + prom(Cancel{std::move(reason)}); + } + } + CancelSignal Signal() { + return fut; + } +private: + Promise prom; + fut::MultiFuture fut; +}; + +} + +#endif //FUT_CANCEL_HPP diff --git a/include/future/executor.hpp b/include/future/executor.hpp new file mode 100644 index 0000000..ff5fb20 --- /dev/null +++ b/include/future/executor.hpp @@ -0,0 +1,63 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#ifndef FUT_EXECUTOR_HPP +#define FUT_EXECUTOR_HPP + +#include "move_func.hpp" +#include "rc/rc.hpp" + +namespace fut { + +struct Executor : rc::SingleVirtualBase { + using Job = MoveFunc; + enum Status : int32_t { + Defer, // (maybe) will be called from another thread + Cancel, // will not be called + Done, // called + }; + virtual Status Execute(Job job) noexcept = 0; + virtual ~Executor() = default; +}; + +struct StoppableExecutor final : fut::Executor { + StoppableExecutor() noexcept = default; + void Stop() noexcept { + dead = true; + } + Status Execute(Job job) noexcept override { + if (dead.load(std::memory_order_acquire)) { + return Cancel; + } + job(); + return Done; + } +protected: + std::atomic_bool dead = false; +}; + + +} //fut + +#endif //FUT_EXECUTOR_HPP diff --git a/include/future/future.hpp b/include/future/future.hpp new file mode 100644 index 0000000..f3fa5a3 --- /dev/null +++ b/include/future/future.hpp @@ -0,0 +1,569 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#ifndef FUT_FUTURE_HPP +#define FUT_FUTURE_HPP + +#include +#include +#include +#include "executor.hpp" +#include "rc/rc.hpp" + +#define MV(x) x=std::move(x) + +namespace fut +{ + +struct FutureError : std::logic_error { + using std::logic_error::logic_error; +}; + +template struct Future; +template struct is_future : std::false_type {}; +template struct is_future> : std::true_type {}; + +struct Base { + friend void AddRef(Base* d) noexcept { + d->_refs.fetch_add(1, std::memory_order_acq_rel); + } + friend void Unref(Base* d) noexcept { + if (d->_refs.fetch_sub(1, std::memory_order_acq_rel) == 1) { + d->deleter(d); + } + } + enum Flags : short { + fullfilled = 1 << 0, + has_val = 1 << 1, + future_taken = 1 << 2, + in_continue = 1 << 3, + }; + using Notify = void(*)(Base* self, bool call); + using Deleter = void(*)(Base* self); + + Base(Deleter deleter) noexcept : deleter(deleter) {} + Base(Base&&) = delete; + ~Base(); + + template static void DeleterFor(Base* s); + + Deleter deleter = nullptr; + rc::Strong exec = nullptr; + rc::Strong chain = nullptr; + std::atomic notify{nullptr}; + void* ctx = nullptr; + std::exception_ptr exc = nullptr; + std::atomic flags = 0; + std::atomic promises = 0; + std::atomic _refs{0}; +}; + +#ifdef _MSC_VER +#pragma warning( push ) +#pragma warning( disable: 4324 ) +#endif + +template +struct Data final : Base { + Data() noexcept : Base(DeleterFor) {} + alignas(T) char buff[sizeof(T)]; + T* data() noexcept { + assert(flags & has_val); + return std::launder(reinterpret_cast(buff)); + } + void set_value(T&& v) noexcept { + [[maybe_unused]] auto was = flags.fetch_or(has_val, std::memory_order_release); + assert(!(was & has_val)); + new (data()) T{std::move(v)}; + } + ~Data() { + if (flags & has_val) { + data()->~T(); + } + } +}; + +#ifdef _MSC_VER +#pragma warning( pop ) +#endif + +template<> struct Data final : Base { + Data() noexcept : Base(DeleterFor) {} + void* data() noexcept { + return reinterpret_cast(1); + } +}; + +template +using StatePtr = rc::Strong>; + +template +struct Result { + Result(T* res) noexcept : res(res) {} + Result(std::exception_ptr exc) noexcept : exc(std::move(exc)) {} + //Result(Result const&) = delete; + //Result(Result&&) = delete; + + std::exception_ptr get_exception() const& noexcept { + return res ? std::exception_ptr{} : exc; + } + [[nodiscard]] std::exception_ptr get_exception() && noexcept { + return res ? std::exception_ptr{} : std::move(exc); + } + explicit operator bool() const noexcept { + return res; + } + T* get_ptr() noexcept { + return res; + } + [[nodiscard]] T get() { + if (meta_Likely(res)) { + if constexpr (std::is_void_v) return; + else return std::move(*std::exchange(res, nullptr)); + } else if (exc) { + std::rethrow_exception(std::move(exc)); + } else { + throw std::runtime_error("get() already called"); + } + } +protected: + T* res = {}; + std::exception_ptr exc; +}; + +template +struct Future; +template +struct Promise; + +namespace d { +void continueChain(rc::Strong data, bool once = false) noexcept; +template struct strip_fut {using type = T;}; +template struct strip_fut> {using type = T;}; +template struct GetRet { + using type = std::invoke_result_t; + using strip = typename strip_fut::type; +}; +template struct GetRet { + using type = std::invoke_result_t; + using strip = typename strip_fut::type; +}; +template +void notifyImpl(Base* self, bool call) noexcept; +template +void notifyLastImpl(Base* self, bool call) noexcept; +template +void notifyTryImpl(Base* self, bool call) noexcept; +} //detail + +template +struct [[nodiscard]] Future { + using value_type = T; + + template + using IfValidTryOrLast = std::enable_if_t>>; + template + using IfValidCatch = std::enable_if_t>; + template + using IfValidThen = d::GetRet; + + Future(StatePtr state = nullptr) noexcept : state(state) {} + + Future(const Future&) = delete; + Future& operator=(const Future&) = delete; + Future(Future&&) noexcept = default; + Future& operator=(Future&&) noexcept = default; + + template + static Future FromFunction(Fn f) { + Promise prom; + auto fut = prom.GetFuture(); + f(std::move(prom)); + return fut; + } + StatePtr TakeState() noexcept { + return std::move(state); + } + Data* PeekState() noexcept { + return state.get(); + } + bool IsValid() const noexcept { + return bool(state); + } + template> + auto Then(rc::Strong exec, Fn f) { + Data& data = check(); + using Ret = d::GetRet; + rc::Strong chain = new Data; + data.chain = chain; + data.exec = exec; + data.ctx = new Fn{std::move(f)}; + data.notify.store(d::notifyImpl, std::memory_order_release); + d::continueChain(TakeState()); + return Future(chain); + } + template> + auto ThenSync(Fn f) { + return Then(nullptr, std::move(f)); + } + template> + auto Try(rc::Strong exec, Fn f) { + Data& data = check(); + using Ret = d::GetRet, Fn>; + rc::Strong chain = new Data; + data.chain = chain; + data.exec = exec; + data.ctx = new Fn{std::move(f)}; + data.notify.store(d::notifyTryImpl, std::memory_order_release); + d::continueChain(TakeState()); + return Future(chain); + } + template> + auto TrySync(Fn f) { + return Try(nullptr, std::move(f)); + } + template> + void AtLast(rc::Strong exec, Fn f) { + Data& data = check(); + data.exec = exec; + data.ctx = new Fn{std::move(f)}; + data.notify.store(d::notifyLastImpl, std::memory_order_release); + d::continueChain(TakeState()); + } + template> + void AtLastSync(Fn f) { + AtLast(nullptr, std::move(f)); + } + template> + void Catch(rc::Strong exec, Fn f) { + return AtLast(exec, [MV(f)](Result ok) mutable { + try {(void)ok.get();} catch (std::exception& e) { + f(e); + } + }); + } + template> + void CatchSync(Fn f) { + Catch(nullptr, std::move(f)); + } +protected: + Data& check() { + if (!PeekState()) { + throw FutureError("Invalid Future"); + } + Data& data = *PeekState(); + // TODO: ?? checks? + if (data.notify.load(std::memory_order_acquire)) throw FutureError("Then() Called Twice"); + return data; + } + + StatePtr state; +}; + +template +struct [[nodiscard]] SharedPromise { + template + using if_exception = std::enable_if_t> == is, bool>; + + SharedPromise(StatePtr ptr = new Data) noexcept : state(ptr) { + ref(); + } + bool IsValid() const noexcept { + return state && !(state->flags & Base::fullfilled); + } + bool operator()(Result res) const { + if (auto ptr = res.get_ptr()) { + if constexpr (std::is_void_v) { + return (*this)(); + } else { + return (*this)(std::move(*ptr)); + } + } else { + return (*this)(std::move(res).get_exception()); + } + } + bool operator()(std::exception_ptr exc) const { + if (!state) throw FutureError("Invalid Promise"); + Data& data = *state; + auto was = data.flags.fetch_or(Base::fullfilled, std::memory_order_acq_rel); + data.exc = std::move(exc); + d::continueChain(state.get()); + return !(was & Base::fullfilled); + } + template = 1> + bool operator()(E exc) const { + return (*this)(std::make_exception_ptr(std::move(exc))); + } + bool operator()() const { + static_assert(std::is_void_v); + if (!state) throw FutureError("Invalid Promise"); + Data& data = *state; + auto was = data.flags.fetch_or(Base::fullfilled, std::memory_order_acq_rel); + d::continueChain(state.get()); + return !(was & Base::fullfilled); + } + template = 1> + bool operator()(U && value) const { + if (!state) throw FutureError("Invalid Promise"); + Data& data = *state; + auto was = data.flags.fetch_or(Base::fullfilled | Base::has_val, + std::memory_order_acq_rel); + if (was & Base::fullfilled) return false; + assert(!(was & Base::has_val)); + new (data.data()) T{std::forward(value)}; + d::continueChain(state.get()); + return true; + } + SharedPromise(SharedPromise const & p) noexcept { + state = p.state; + ref(); + } + SharedPromise(SharedPromise&& p) noexcept : + state(std::move(p.state)) + {} + Future GetFuture() { + if (!state) throw FutureError("Invalid Promise"); + Data& data = *state; + auto was = data.flags.fetch_or(Base::future_taken, std::memory_order_acq_rel); + if (was & Base::future_taken) throw FutureError("Future already taken"); + return {state}; + } + SharedPromise& operator=(SharedPromise const & p) noexcept { + if (state.data != p.state.data) { + deref(); + state = p.state; + ref(); + } + return *this; + } + SharedPromise& operator=(SharedPromise&& p) noexcept { + std::swap(state, p.state); + return *this; + } + ~SharedPromise() { + deref(); + } +protected: + void ref() noexcept { + if (Data* d = state.get()) { + d->promises.fetch_add(1, std::memory_order_release); + } + } + void deref() noexcept { + if (Data* d = state.get()) { + if (d->promises.fetch_sub(1, std::memory_order_acquire) == 1) { + auto f = d->flags.load(std::memory_order_acquire); + if (!(f & Base::fullfilled)) { + (*this)(FutureError("Broken Promise")); + } + } + } + } + StatePtr state; +}; + +template +struct [[nodiscard]] Promise : SharedPromise { + Promise(StatePtr ptr = new Data) noexcept : SharedPromise(std::move(ptr)) {} + using SharedPromise::operator(); + using SharedPromise::SharedPromise; + using SharedPromise::operator=; + Promise(const Promise&) = delete; + Promise(Promise&&) noexcept = default; + Promise& operator=(Promise&&) noexcept = default; +}; + +template +Future Rejected(std::exception_ptr exc) { + Promise prom; + prom(std::move(exc)); + return prom.GetFuture(); +} + +template +Future Rejected(E v) { + Promise prom; + prom(std::move(v)); + return prom.GetFuture(); +} + +Future Resolved(); + +template +Future Resolved(T value) { + Promise prom; + prom(std::move(value)); + return prom.GetFuture(); +} + +namespace d { + +inline void fullfill(Base* d) { + [[maybe_unused]] auto was = d->flags.fetch_or(Base::fullfilled); + assert(!(was & Base::fullfilled)); +} + +template +inline Result getRes(Base* d) noexcept { + return d->exc ? Result(d->exc) : Result(static_cast*>(d)->data()); +} + +inline bool needContinue(Base* d) noexcept { + return !(d->flags.load(std::memory_order_acquire) & Base::in_continue); +} + +template +void notifyForward(Base* _self, bool call) { + auto* self = static_cast*>(_self); + auto* chain = static_cast*>(self->chain.get()); + if (call) { + assert(chain); + assert(self->flags & Base::fullfilled); + if (self->exc) { + chain->exc = std::move(self->exc); + } else { + if constexpr (!std::is_void_v) { + chain->set_value(std::move(*self->data())); + } + } + fullfill(chain); + if (needContinue(self)) { + continueChain(chain); + } + } +} + +template +void setResult(rc::Strong& chain, Fn& fn, Result res) noexcept try { + using ret = GetRet>, Fn>; + using type = typename ret::type; + using strip = typename ret::strip; + constexpr auto is_future_returned = !std::is_same_v; + [[maybe_unused]] Data* next = static_cast*>(chain.get()); + if constexpr (unpack) { + if (!res) { + chain->exc = std::move(res).get_exception(); + fullfill(chain.get()); + return; + } + } + if constexpr (is_future_returned) { + // attach received future as parent + Future fut(nullptr); + if constexpr (unpack && std::is_void_v) { + fut = fn(); + } else if constexpr (unpack) { + fut = fn(res.get()); + } else { + fut = fn(std::move(res)); + } + Data* parent = fut.PeekState(); + parent->chain = chain; + parent->notify.store(notifyForward, std::memory_order_release); + continueChain(parent, true); //once. if set -> sets chain -> we continue it + } else if constexpr (!std::is_void_v) { + // next future result set from f() + if constexpr (unpack && std::is_void_v) { + next->set_value(fn()); + } else if constexpr (unpack) { + next->set_value(fn(res.get())); + } else { + next->set_value(fn(std::move(res))); + } + fullfill(next); + } else { + // next future is void + if constexpr (unpack && std::is_void_v) { + fn(); + } else if constexpr (unpack) { + fn(res.get()); + } else { + fn(std::move(res)); + } + fullfill(next); + } +} catch (...) { + chain->exc = std::current_exception(); + fullfill(chain.get()); +} + +[[noreturn]] void onLastExc(); + +} + +template +void d::notifyLastImpl(Base* _self, bool call) noexcept { + Data* self = static_cast*>(_self); + Fn* fn = static_cast(self->ctx); + if (call) { + assert(self->flags & Base::fullfilled); + try { + // last handler should not throw + if (self->exc) { + (void)(*fn)(Result(self->exc)); + } else { + (void)(*fn)(Result(static_cast*>(self)->data())); + } + } catch (...) { + onLastExc(); + } + } + delete fn; +} + +template +void d::notifyImpl(Base* self, bool call) noexcept { + Fn* fn = static_cast(self->ctx); + if (call) { + assert(self->flags & Base::fullfilled); + assert(self->chain); + setResult(self->chain, *fn, getRes(self)); + if (needContinue(self)) { + continueChain(self->chain); + } + } + delete fn; +} + +template +void d::notifyTryImpl(Base* self, bool call) noexcept { + Fn* fn = static_cast(self->ctx); + if (call) { + assert(self->flags & Base::fullfilled); + assert(self->chain); + setResult(self->chain, *fn, getRes(self)); + if (needContinue(self)) { + continueChain(self->chain); + } + } + delete fn; +} + +template void Base::DeleterFor(Base* s) { + delete static_cast*>(s); +} + +} //fut + +#endif // FUT_FUTURE_HPP diff --git a/include/future/gather.hpp b/include/future/gather.hpp new file mode 100644 index 0000000..fcdbb9b --- /dev/null +++ b/include/future/gather.hpp @@ -0,0 +1,159 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#ifndef FUT_GATHER_HPP +#define FUT_GATHER_HPP +#pragma once + +#include "future.hpp" +#include +#include +#include + +namespace fut +{ + +namespace detail +{ + +template +using return_t = std::conditional_t<(std::is_void_v && ...), void, std::tuple...>>; + +template +struct GatherCtx : rc::DefaultBase { + std::recursive_mutex mut; + non_void_t> results {}; + size_t doneCount = {}; + Promise> setter {}; +}; + +template +using SharedGatherCtx = rc::Strong>; + +template +void handleSingleFut(SharedGatherCtx ctx, Future fut) +{ + fut.AtLastSync([ctx](auto res){ + std::lock_guard lock(ctx->mut); + if(!ctx->setter.IsValid()) { + return; + } + if (auto&& err = res.get_exception()) { + ctx->setter(std::move(err)); + } else { + if constexpr (!std::is_void_v) { + std::get(ctx->results) = res.get(); + } + if (++ctx->doneCount == sizeof...(Args)) { + if constexpr (std::is_same_vresults), meta::empty>) { + ctx->setter(); + } else { + ctx->setter(std::move(ctx->results)); + } + } + } + }); +} + +template +void callGatherHandlers(SharedGatherCtx ctx, + std::index_sequence, + Future...futs) +{ + (handleSingleFut(ctx, std::move(futs)), ...); +} + +} + +template +Future> GatherTuple(Future...futs) +{ + static_assert(sizeof...(Args), "Empty Promise List"); + using Ctx = detail::GatherCtx; + auto ctx = rc::Strong(new Ctx); + auto gathered = ctx->setter.GetFuture(); + callGatherHandlers(std::move(ctx), std::index_sequence_for{}, std::move(futs)...); + return gathered; +} + +template +auto Gather(Iter iter, Sent end) +{ + using futT = typename Iter::value_type; + using T = typename futT::value_type; + if constexpr (!std::is_void_v) { + static_assert(std::is_default_constructible_v); + } + using promT = std::conditional_t, void, std::vector>; + using resultsT = std::conditional_t, empty, std::vector>; + if (iter == end) { + if constexpr (!std::is_void_v) + return fut::Resolved(promT{}); + else + return fut::Resolved(); + } + struct Ctx { + std::recursive_mutex mut; + resultsT results; + Promise prom; + size_t left; + }; + auto ctx = std::make_shared(); + ctx->left = size_t(std::distance(iter, end)); + if constexpr (!std::is_void_v) { + ctx->results.resize(ctx->left); + } + auto final = ctx->prom.GetFuture(); + size_t idx = 0; + for (;iter != end; ++iter) { + auto curr = idx++; + (*iter).AtLastSync([=](auto res){ + std::lock_guard lock(ctx->mut); + if (!ctx->prom.IsValid()) + return; + if (res) { + if constexpr (!std::is_void_v) + ctx->results[curr] = res.get(); + if (!--ctx->left) { + if constexpr (!std::is_void_v) + ctx->prom(std::move(ctx->results)); + else + ctx->prom(); + } + } else { + ctx->prom(std::move(res).get_exception()); + } + }); + } + return final; +} + +template +auto Gather(Range range) { + return Gather(std::begin(range), std::end(range)); +} + +} //fut + +#endif //FUT_GATHER_HPP diff --git a/include/future/move_func.hpp b/include/future/move_func.hpp new file mode 100644 index 0000000..7a8e771 --- /dev/null +++ b/include/future/move_func.hpp @@ -0,0 +1,168 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#ifndef FUT_CALL_ONCE_HPP +#define FUT_CALL_ONCE_HPP + +#include +#include +#include +#include +#include +#include "meta/meta.hpp" + +namespace fut { + +using namespace meta; + +struct InvalidMoveFuncCall : public std::exception { + const char* what() const noexcept { + return "Invalid MoveFunc Call"; + } +}; + +template +struct FuncSig { + using R = Ret; + using A = meta::TypeList; +}; + +namespace detail { + +constexpr size_t SOO = sizeof(void*) * 3; +enum Op { destroy, move,}; + +union Storage { + alignas(std::max_align_t) char _small[SOO]; + void* _big; +}; + +template +inline Fn* cast(detail::Storage* s) noexcept { + return sizeof(Fn) > detail::SOO ? static_cast(s->_big) : std::launder(reinterpret_cast(s->_small)); +} + +template +Ret _invoke(Storage* s, Args...args) { + auto& fn = *cast(s); + return fn(std::forward(args)...); +} + +template +void _manager(Op op, detail::Storage* s, void* o) noexcept { + constexpr auto isBig = sizeof(Fn) > detail::SOO; + switch (op) { + case destroy: { + if constexpr (isBig) delete cast(s); + else cast(s)->~Fn(); + break; + } + case move: { + auto other = static_cast(o); + if constexpr (isBig) { + s->_big = std::exchange(other->_big, nullptr); + } else { + new (s->_small) Fn(std::move(*cast(other))); + cast(other)->~Fn(); + } + break; + } + } +} +} + +template class MoveFunc; + +template +class MoveFunc +{ + mutable detail::Storage stor; + void(*manager)(detail::Op, detail::Storage*, void*) noexcept = nullptr; + Ret(*call)(detail::Storage*, Args...) = nullptr; +public: + using Sig = FuncSig; + MoveFunc() noexcept = default; + + template + static constexpr bool _valid = std::is_move_constructible_v && std::is_invocable_v; + + template>> + MoveFunc(Fn f) : + manager(detail::_manager), + call(detail::_invoke) + { + if constexpr (!std::is_void_v) { + static_assert(std::is_convertible_v, Ret>); + } + if constexpr (sizeof(Fn) > detail::SOO) { + stor._big = new Fn(std::move(f)); + } else { + new (stor._small) Fn(std::move(f)); + } + } + explicit operator bool() const noexcept { + return manager; + } + meta_alwaysInline Ret operator()(Args...a) const { + if (!manager) { + throw InvalidMoveFuncCall(); + } + return call(const_cast(&stor), std::forward(a)...); + } + MoveFunc(MoveFunc&& oth) noexcept { + moveIn(oth); + } + MoveFunc& operator=(MoveFunc&& oth) noexcept { + // cannot just swap => cannot swap small storage for + // non-trivially movable types (pinned-like) + if (this != &oth) { + deref(); + moveIn(oth); + } + return *this; + } + ~MoveFunc() { + deref(); + } +private: + void deref() noexcept { + if (manager) { + manager(detail::destroy, &stor, nullptr); + } + } + void moveIn(MoveFunc& oth) noexcept { + call = std::exchange(oth.call, nullptr); + manager = std::exchange(oth.manager, nullptr); + if (manager) { + manager(detail::move, &stor, &oth.stor); + } + } +}; + +template +MoveFunc(Ret(*)(Args...)) -> MoveFunc; + +} //fut + +#endif // FUT_CALL_ONCE_HPP diff --git a/include/future/multi_future.hpp b/include/future/multi_future.hpp new file mode 100644 index 0000000..2a5f8a1 --- /dev/null +++ b/include/future/multi_future.hpp @@ -0,0 +1,114 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#ifndef FUT_MULTI_FUT_HPP +#define FUT_MULTI_FUT_HPP + +#include "future.hpp" +#include + +namespace fut { + +template +struct MultiState : rc::DefaultBase { + bool done = false; + alignas(T) char buff[sizeof(T)]; + std::exception_ptr exc; + std::vector> proms; + ~MultiState() { + if (done && !exc) { + std::launder(reinterpret_cast(buff))->~T(); + } + } +}; + +template<> +struct MultiState : rc::DefaultBase { + bool done = false; + std::exception_ptr exc; + std::vector> proms; +}; + +template +struct MultiFuture { + MultiFuture() noexcept = default; + MultiFuture(Future fut) : state(new MultiState) { + fut.AtLastSync([state = state](auto res) noexcept { + state->done = true; + if (!res) { + state->exc = std::move(res).get_exception(); + } else { + if constexpr (!std::is_void_v) { + new (state->buff) T{res.get()}; + } + } + for (auto& p: state->proms) { + tryRes(*state, p); + } + }); + } + bool IsValid() const noexcept { + return bool(state); + } + template + auto Then(rc::Strong exec, Fn&& f) { + return GetFuture().Then(exec, std::forward(f)); + } + template + auto Try(rc::Strong exec, Fn&& f) { + return GetFuture().Try(exec, std::forward(f)); + } + template + auto AtLast(rc::Strong exec, Fn&& f) { + return GetFuture().AtLast(exec, std::forward(f)); + } + Future GetFuture() { + return prom().GetFuture(); + } +protected: + auto& prom() { + if (!state) throw FutureError("Invalid Multi Future"); + auto& p = state->proms.emplace_back(); + tryRes(*state, p); + return p; + } + static void tryRes(MultiState& state, Promise& p) { + if (!state.done) return; + if (state.exc) { + p(state.exc); + } else { + if constexpr (std::is_void_v) { + p(); + } else { + auto* data = std::launder(reinterpret_cast(state.buff)); + p(*data); + } + } + } + rc::Strong> state; +}; + +} + +#endif //FUT_MULTI_FUT_HPP diff --git a/include/future/signal.hpp b/include/future/signal.hpp new file mode 100644 index 0000000..8dcba94 --- /dev/null +++ b/include/future/signal.hpp @@ -0,0 +1,75 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#ifndef FUT_SIGNAL_HPP +#define FUT_SIGNAL_HPP + +#include "executor.hpp" +#include +#include + +namespace fut { + +struct unit{}; + +template +struct Signal { + Signal() { + impl = new Impl; + } + bool Invoke(T value = {}) noexcept { + std::lock_guard lock(mut); + if (!impl->func) return false; + if (exec) { + exec->Execute([impl = impl, value = std::move(value)]() mutable { + impl->func(std::move(value)); + }); + } else { + impl->func(std::move(value)); + } + return true; + } + + void operator()(rc::Strong _exec, fut::MoveFunc cb) { + std::lock_guard lock(mut); + std::swap(cb, impl->func); + std::swap(_exec, this->exec); + } + void operator()(fut::MoveFunc cb) { + std::lock_guard lock(mut); + std::swap(cb, impl->func); + this->exec = nullptr; + } +protected: + struct Impl : rc::DefaultBase { + fut::MoveFunc func; + }; + rc::Strong exec; + rc::Strong impl; + std::recursive_mutex mut; +}; + +} + +#endif //FUT_SIGNAL_HPP diff --git a/include/future/to_std_fut.hpp b/include/future/to_std_fut.hpp new file mode 100644 index 0000000..34c877d --- /dev/null +++ b/include/future/to_std_fut.hpp @@ -0,0 +1,53 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#ifndef FUT_TO_STD_FUT +#define FUT_TO_STD_FUT + +#include +#include "future.hpp" + +namespace fut +{ + +template +std::future ToStdFuture(Future fut) { + auto prom = std::promise(); + auto f = prom.get_future(); + fut.AtLastSync([p=std::move(prom)](Result res) mutable { + if (auto err = std::move(res).get_exception()) { + p.set_exception(std::move(err)); + } else { + if constexpr(std::is_void_v) p.set_value(); + else p.set_value(res.get()); + } + }); + return f; +} + +} + +#endif //FUT_TO_STD_FUT + + diff --git a/include/json_view/algo.hpp b/include/json_view/algo.hpp new file mode 100644 index 0000000..713888a --- /dev/null +++ b/include/json_view/algo.hpp @@ -0,0 +1,109 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#pragma once +#include +#ifndef JV_ALGOS_HPP +#define JV_ALGOS_HPP + +#include "json_view.hpp" +#include "pointer.hpp" +#include + +namespace jv { + +template +auto SortedInsert(Range& rng, T&& v, Comp cmp, Eq eq) +{ + auto end = rng.end(); + auto pos = std::lower_bound(rng.begin(), end, v, cmp); + if (pos == end) { + rng.push_back(std::forward(v)); + return rng.end() - 1; + } else if (eq(*pos, v)) { + *pos = std::forward(v); + return pos; + } else { + return rng.insert(pos, std::forward(v)); + } +} + +template +auto SortedInsertJson(Range& rng, JsonPair pair) { + return SortedInsert(rng, pair, KeyLess{}, KeyEq{}); +} + +template +auto LowerBoundJson(Iter beg, Iter end, JsonPair const& pair) { + return std::lower_bound(beg, end, pair, KeyLess{}); +} + +template +auto LowerBoundJson(Range const& range, JsonPair const& pair) { + return std::lower_bound(std::begin(range), std::end(range), pair, KeyLess{}); +} + +// returns new size +inline unsigned SortedInsertJson(JsonPair* storage, unsigned size, JsonPair const& entry, size_t cap = SIZE_MAX) { + (void)cap; + auto end = storage + size; + auto pos = LowerBoundJson(storage, end, entry); + if (pos == end) { + assert(size < cap); + storage[size++] = entry; + } else if (meta_Unlikely(pos->key == entry.key)) { + pos->value = entry.value; + } else { + assert(size < cap); + size++; + auto diff = size_t(end - pos); + memmove(pos + 1, pos, sizeof(*pos) * diff); + *pos = entry; + } + return size; +} + +JsonView Flatten(JsonView src, Arena& alloc, unsigned depth = JV_DEFAULT_DEPTH); + +enum CopyFlags { + NoCopyStrings = 1, + NoCopyBinary = 2, +}; + +JsonView Copy(JsonView src, Arena& alloc, unsigned depth = JV_DEFAULT_DEPTH, unsigned flags = 0); + +constexpr auto DEFAULT_MARGIN = std::numeric_limits::epsilon() * 10; +bool DeepEqual(JsonView lhs, JsonView rhs, unsigned depth = JV_DEFAULT_DEPTH, double margin = DEFAULT_MARGIN); + +inline bool operator==(JsonView lhs, JsonView rhs) { + return DeepEqual(lhs, rhs); +} + +inline bool operator!=(JsonView lhs, JsonView rhs) { + return !DeepEqual(lhs, rhs); +} + +} + +#endif //JV_ALGOS_HPP diff --git a/include/json_view/alloc.hpp b/include/json_view/alloc.hpp new file mode 100644 index 0000000..07252e7 --- /dev/null +++ b/include/json_view/alloc.hpp @@ -0,0 +1,207 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#ifndef JV_ALLOC_HPP +#define JV_ALLOC_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include "meta/compiler_macros.hpp" + +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wnon-virtual-dtor" +#define jv_arena_attrs __attribute__((__returns_nonnull__,__alloc_size__(2),__alloc_align__(3))) +#else +#define jv_arena_attrs +#endif + +namespace jv +{ + +struct Arena { + static constexpr auto max_align = alignof(std::max_align_t); + meta_alwaysInline + void* operator()(size_t sz, size_t align = max_align) + jv_arena_attrs + { + return ::operator new(sz, Allocate(sz, align)); + } + meta_alwaysInline + void* Allocate(size_t sz, size_t align = max_align) + jv_arena_attrs + { + auto res = DoAllocate(sz, align); + if (meta_Unlikely(!res)) throw std::bad_alloc{}; + return res; + } +protected: + virtual void* DoAllocate(size_t sz, size_t align) = 0; +}; + +struct NullArena final : Arena { + void* DoAllocate(size_t, size_t) override { + throw std::bad_alloc{}; + } +}; + +namespace detail { +template +struct stackBuff { + stackBuff() = default; + stackBuff(stackBuff&&) = delete; + alignas(std::max_align_t) char buff[sz]; +}; +template<> +struct stackBuff { + static constexpr char* buff = nullptr; +}; +struct arena { + arena() = default; + arena(arena const&) = delete; + arena(arena && o) noexcept { + moveIn(o); + } + arena& operator=(arena && o) noexcept { + if (this != &o) { + clear(); + moveIn(o); + } + return *this; + } + void moveIn(arena& o) noexcept { + buffptr = std::exchange(o.buffptr, nullptr); + space = std::exchange(o.space, 0); + blockSize = o.blockSize; + allocs = std::exchange(o.allocs, std::forward_list{}); + } + ~arena() { + clear(); + } + void newBlock() { + buffptr = allocs.emplace_front(::operator new(blockSize, std::align_val_t(Arena::max_align))); + space = blockSize; + } + void clear() { + for (auto a: allocs) { + ::operator delete(a, std::align_val_t(Arena::max_align)); + } + allocs.clear(); + buffptr = nullptr; + space = 0; + } + void* doAlloc(size_t bytes, size_t align) { + if (meta_Unlikely(bytes > blockSize)) { + return allocs.emplace_front(::operator new(bytes, std::align_val_t(Arena::max_align))); + } + if (meta_Unlikely(!std::align(align, bytes, buffptr, space))) { + newBlock(); + } + space -= bytes; + return std::exchange(buffptr, static_cast(buffptr) + bytes); + } + + void* buffptr{}; + size_t space{}; + size_t blockSize{}; + std::forward_list allocs; +}; +} //detail + +template +struct DefaultArena final : Arena, + protected detail::stackBuff, + protected detail::arena +{ + DefaultArena(size_t blockSize = 4096) noexcept { + SetBlockSize(blockSize); + Clear(); + } + void SetBlockSize(size_t sz) { + this->blockSize = sz; + } + void Clear() { + arena::clear(); + this->buffptr = this->buff; + this->space = onStack; + } +protected: + void* DoAllocate(size_t bytes, size_t align) final { + return detail::arena::doAlloc(bytes, align); + } +}; + +template +struct arena_allocator { + Arena* a = nullptr; + using value_type = T; + arena_allocator(Arena& alloc) noexcept : a(&alloc) {} + template + arena_allocator(arena_allocator const& other) : a(other.a) {} + meta_alwaysInline T* allocate(std::size_t n) { + return static_cast(a->Allocate(sizeof(T) * n, alignof(T))); + } + void deallocate(T*, std::size_t) noexcept {} + template + bool operator==(arena_allocator const& other) const noexcept { + return a == other.a; + } +}; + +template +using ArenaVector = std::vector>; + +struct ArenaString : public ArenaVector { + using ArenaVector::ArenaVector; + using ArenaVector::operator=; + + operator std::string_view() const noexcept { + return {data(), size()}; + } + + ArenaString(std::string_view part, arena_allocator alloc) : ArenaVector(alloc) { + reserve(part.size() + 1); + Append(part); + } + + void Append(std::string_view part) { + auto was = size(); + resize(was + part.size()); + ::memcpy(data() + was, part.data(), part.size()); + } +}; + +} //jv + +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif + +#endif //JV_ALLOC_HPP diff --git a/include/json_view/data.hpp b/include/json_view/data.hpp new file mode 100644 index 0000000..b72f8b9 --- /dev/null +++ b/include/json_view/data.hpp @@ -0,0 +1,90 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#ifndef JV_DATA_HPP +#define JV_DATA_HPP + +#include + +namespace jv +{ + +struct JsonView; +struct JsonPair; + +enum Type : int16_t { + t_null = 0, + t_binary = 1 << 0, + t_boolean = 1 << 1, + t_number = 1 << 2, + t_string = 1 << 3, + t_signed = 1 << 4, + t_unsigned = 1 << 5, + t_array = 1 << 6, + t_object = 1 << 7, + t_discarded = 1 << 8, + t_custom = 1 << 9, + + t_float = t_number, + t_any_integer = t_signed | t_unsigned, + t_any_number = t_number | t_signed | t_unsigned, +}; + +enum Flags : short { + f_none = 0, + f_sorted = 1 << 0, //is object + keys are sorted and unique +}; + +constexpr Type operator|(Type l, Type r) noexcept { + return Type(int(l) | int(r)); +} +constexpr Type& operator|=(Type& l, Type r) noexcept { + return l = Type(int(l) | int(r)); +} +constexpr Flags operator|(Flags l, Flags r) noexcept { + return Flags(int(l) | int(r)); +} +constexpr Flags& operator|=(Flags& l, Flags r) noexcept { + return l = Flags(int(l) | int(r)); +} + +struct Data { + Type type = {}; + Flags flags = {}; + unsigned size; + union { + const char* string; + const void* binary; + void* custom; + const JsonView* array; + const JsonPair* object; + double number; + uint64_t uinteger; + int64_t integer; + bool boolean; + } d; +}; + +} +#endif // JV_DATA_HPP diff --git a/include/json_view/dump.hpp b/include/json_view/dump.hpp new file mode 100644 index 0000000..23322c7 --- /dev/null +++ b/include/json_view/dump.hpp @@ -0,0 +1,107 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#ifndef JV_DUMP_JSON_HPP +#define JV_DUMP_JSON_HPP +#include "json_view/json.hpp" +#pragma once + +#include "membuff/membuff.hpp" +#include "json_view.hpp" + +namespace jv +{ + +struct DumpOptions { + bool pretty = false; + unsigned maxDepth = JV_DEFAULT_DEPTH; + char indentChar = ' '; + unsigned indent = 4; +}; + +void DumpJsonInto(membuff::Out& out, JsonView json, DumpOptions opts = {}); + +inline std::string DumpJson(JsonView j, DumpOptions opts = {}) { + membuff::StringOut buff; + DumpJsonInto(buff, j, opts); + return buff.Consume(); +} + +void DumpMsgPackInto(membuff::Out& out, JsonView json, DumpOptions opts = {}); + +inline std::string DumpMsgPack(JsonView j, DumpOptions opts = {}) { + membuff::StringOut buff; + DumpMsgPackInto(buff, j, opts); + return buff.Consume(); +} + +template +JsonView DumpStruct(Arena& alloc) { + if constexpr (describe::is_described_enum_v) { + return describe::Get().name; + } else if constexpr (describe::is_described_struct_v) { + constexpr auto desc = describe::Get(); + constexpr size_t fields = desc.fields_count; + auto result = MakeObjectOf(fields, alloc); + desc.for_each_field([&](auto f){ + using f_t = typename decltype(f)::type; + auto& curr = result[desc.index_of(f)]; + curr.key = f.name; + curr.value = DumpStruct(alloc); + }); + return JsonView(result, fields); + } else if constexpr (is_optional::value) { + return DumpStruct(alloc); + } else if constexpr (std::is_convertible_v) { + return "string"; + } else if constexpr (is_assoc_container_v) { + auto result = MakeObjectOf(1, alloc); + *result = {"", DumpStruct(alloc)}; + return JsonView(result, 1); + } else if constexpr (is_index_container_v) { + auto result = MakeObjectOf(1, alloc); + *result = {"[index]", DumpStruct(alloc)}; + return JsonView(result, 1); + } + else if constexpr (std::is_same_v) return "uint8"; + else if constexpr (std::is_same_v) return "int8"; + else if constexpr (std::is_same_v) return "uint16"; + else if constexpr (std::is_same_v) return "int16"; + else if constexpr (std::is_same_v) return "uint32"; + else if constexpr (std::is_same_v) return "int32"; + else if constexpr (std::is_same_v) return "uint64"; + else if constexpr (std::is_same_v) return "int64"; + else if constexpr (std::is_same_v) return "float"; + else if constexpr (std::is_same_v) return "double"; + else if constexpr (std::is_same_v) return "bool"; + else if constexpr (std::is_same_v) return "json"; + else if constexpr (std::is_same_v) return "json"; + else if constexpr (std::is_same_v) return "json"; + else { + static_assert(always_false, "Unsupported type in DumpStruct()"); + } +} +} //jv + +#endif //JV_DUMP_JSON_HPP diff --git a/include/json_view/json.hpp b/include/json_view/json.hpp new file mode 100644 index 0000000..b8e3414 --- /dev/null +++ b/include/json_view/json.hpp @@ -0,0 +1,680 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#pragma once +#ifndef JV_JSON_HPP +#define JV_JSON_HPP + +#include "json_view/parse.hpp" +#include "json_view.hpp" +#include "algo.hpp" +#include "pointer.hpp" +#include + + +namespace jv { + +template +struct JsonConfigTraits { + static constexpr bool ObjectSorted = false; +}; + +struct JsonConfig; + +struct JsonConfig { + template + using Allocator = std::allocator; + template + using Object = std::map>; + template + using Array = std::vector; + using String = std::string; + using Binary = std::vector; +}; + +template<> +struct JsonConfigTraits { + static constexpr bool ObjectSorted = true; +}; + +template +struct BasicMutJson { + template + using Alloc = typename Config::template Allocator; + struct Value; + using Array = typename Config::template Array; + using Object = typename Config::template Object; + using Key_t = typename Object::key_type; + using String = typename Config::String; + using Binary = typename Config::Binary; + + struct Value { + Type type = t_null; + union { + Array* arr = {}; + Object* obj; + String* str; + Binary* bin; + double number; + int64_t integer; + uint64_t uinteger; + bool boolean; + }; + }; + BasicMutJson() noexcept = default; + Type GetType() const noexcept { + return data.type; + } + BasicMutJson(double number) noexcept { + data.type = t_number; + data.number = number; + } + template && !std::is_same_v, int> = 1> + BasicMutJson(T num) noexcept { + if constexpr (std::is_signed_v) { + data.type = t_signed; + data.integer = int64_t(num); + } else { + data.type = t_unsigned; + data.uinteger = uint64_t(num); + } + } + BasicMutJson(Array&& v) noexcept : BasicMutJson(t_array) { + GetArray() = std::move(v); + } + BasicMutJson(Object&& v) noexcept : BasicMutJson(t_object) { + GetObject() = std::move(v); + } + BasicMutJson(String&& v) noexcept : BasicMutJson(t_string) { + GetString() = std::move(v); + } + BasicMutJson(Binary&& v) noexcept : BasicMutJson(t_binary) { + GetBinary() = std::move(v); + } + BasicMutJson(Array const& v, unsigned depth = JV_DEFAULT_DEPTH) noexcept : BasicMutJson(t_array) { + DepthError::Check(depth--); + auto& out = GetArray(); + out.reserve(v.size()); + for (auto& i: v) { + copy(out.emlace_back(), i, depth); + } + } + BasicMutJson(Object const& v, unsigned depth = JV_DEFAULT_DEPTH) noexcept : BasicMutJson(t_object) { + DepthError::Check(depth--); + auto& out = GetObject(); + auto hint = out.begin(); + for (auto& it: v) { + hint = out.emplace_hint(hint, Key_t{it.first}, BasicMutJson(it.second, depth)); + } + } + BasicMutJson(String const& v) noexcept : BasicMutJson(t_string) { + GetString() = v; + } + BasicMutJson(Binary const& v) noexcept : BasicMutJson(t_binary) { + GetBinary() = v; + } + BasicMutJson& operator[](string_view key) { + if (Is(t_null)) { + *this = {t_object}; + } + AssertType(t_object); + return (*data.obj)[Key_t{key}]; + } + BasicMutJson& operator[](unsigned idx) { + AssertType(t_array); + return (*data.arr).at(idx); + } + BasicMutJson(string_view str) : BasicMutJson(t_string) { + (*data.str) = String(str); + } + BasicMutJson(const char* str, int64_t len = -1) : BasicMutJson(t_string) { + (*data.str) = String(str, len == -1 ? strlen(str) : size_t(len)); + } + BasicMutJson(std::nullptr_t) noexcept : BasicMutJson(t_null) {} + explicit BasicMutJson(bool boolean) noexcept { + data.type = t_boolean; + data.number = boolean; + } + bool& GetBool(TraceFrame const& frame = {}) { + AssertType(t_boolean, frame); + return data.boolean; + } + bool GetBool(TraceFrame const& frame = {}) const { + AssertType(t_boolean, frame); + return data.boolean; + } + int64_t& GetInt(TraceFrame const& frame = {}) { + AssertType(t_signed, frame); + return data.integer; + } + int64_t GetInt(TraceFrame const& frame = {}) const { + AssertType(t_signed, frame); + return data.integer; + } + uint64_t& GetUint(TraceFrame const& frame = {}) { + AssertType(t_unsigned, frame); + return data.uinteger; + } + uint64_t GetUint(TraceFrame const& frame = {}) const { + AssertType(t_unsigned, frame); + return data.uinteger; + } + String& GetString(TraceFrame const& frame = {}) { + AssertType(t_string, frame); + return *data.str; + } + const String& GetString(TraceFrame const& frame = {}) const { + AssertType(t_string, frame); + return *data.str; + } + Binary& GetBinary(TraceFrame const& frame = {}) { + AssertType(t_binary, frame); + return *data.bin; + } + const Binary& GetBinary(TraceFrame const& frame = {}) const { + AssertType(t_binary, frame); + return *data.bin; + } + Array& GetArray(TraceFrame const& frame = {}) { + AssertType(t_array, frame); + return *data.arr; + } + const Array& GetArray(TraceFrame const& frame = {}) const { + AssertType(t_array, frame); + return *data.arr; + } + Object& GetObject(TraceFrame const& frame = {}) { + AssertType(t_object, frame); + return *data.obj; + } + const Object& GetObject(TraceFrame const& frame = {}) const { + AssertType(t_object, frame); + return *data.obj; + } + BasicMutJson(Type t) { + switch (t) { + case t_array: + init(t, data.arr); + break; + case t_object: + init(t, data.obj); + break; + case t_string: + init(t, data.str); + break; + case t_binary: + init(t, data.bin); + break; + default: { + data = {}; + data.type = t; + } + } + } + BasicMutJson(BasicMutJson&& o) noexcept { + data = o.data; + o.data.type = t_null; + } + bool Is(Type t) const noexcept { + return t ? data.type & t : data.type == t; + } + BasicMutJson& operator=(BasicMutJson&& o) noexcept { + if (this != &o) { + DefaultArena arena; + Destroy(arena); + data = o.data; + o.data.type = t_null; + } + return *this; + } + BasicMutJson Copy(unsigned depth = JV_DEFAULT_DEPTH) const { + return BasicMutJson{*this, depth}; + } + BasicMutJson& Assign(JsonPointer ptr, TraceFrame const& frame = {}); + void Destroy(Arena& alloc); + ~BasicMutJson() noexcept(false) { + DefaultArena arena; + Destroy(arena); + } + void AssertType(Type wanted, const TraceFrame &frame = {}) const { + bool ok = Is(wanted); + if (meta_Unlikely(!ok)) { + TypeMissmatch exc(frame); + exc.wanted = wanted; + exc.was = data.type; + throw exc; + } + } + Value DataUnsafe() const noexcept { + return data; + } + Value TakeUnsafe() noexcept { + return std::exchange(data, Value{}); + } + explicit BasicMutJson(Value v) noexcept : data(v) {} + explicit BasicMutJson(JsonView source, unsigned depth = JV_DEFAULT_DEPTH) { + copy(*this, source, depth); + } + [[nodiscard]] + JsonView View(Arena& alloc, unsigned depth = JV_DEFAULT_DEPTH, bool sorted = true) const; +protected: + template + void init(Type t, T*& o) { + Alloc ownAlloc; + using Traits = std::allocator_traits>; + o = Traits::allocate(ownAlloc, 1); + if constexpr (std::is_nothrow_constructible_v) { + Traits::construct(ownAlloc, o); + } else { + try {Traits::construct(ownAlloc, o);} + catch (...) { + Traits::deallocate(ownAlloc, o, 1); + o = nullptr; + throw; + } + } + data.type = t; + } + template + void disposeOf(T* o) noexcept { + assert(o); + Alloc ownAlloc; + using Traits = std::allocator_traits>; + Traits::destroy(ownAlloc, o); + Traits::deallocate(ownAlloc, o, 1); + } + static void copy(BasicMutJson& to, JsonView src, unsigned depth); + static void copy(Array& to, Array const& from, unsigned depth); + static void copy(Object& to, Object const& from, unsigned depth); + static void copy(BasicMutJson& to, BasicMutJson const& from, unsigned depth); + BasicMutJson(const BasicMutJson& other, unsigned depth) { + copy(*this, other, depth); + } + Value data; +}; + +//mutable and persistent version of json +using MutableJson = BasicMutJson<>; + +//persistent but immutable version of json +struct Json { + Json() noexcept = default; + explicit Json(JsonView source, unsigned depth = JV_DEFAULT_DEPTH) : + alloc(512) + { + view = Copy(source, alloc, depth); + } + Json(const Json& o) { + view = Copy(o.view, alloc); + } + Json& operator=(const Json& o) { + if (this != &o) { + view = Copy(o.view, alloc); + } + return *this; + } + const JsonView operator[](unsigned idx) { + return view[idx]; + } + const JsonView operator[](string_view key) { + return view[key]; + } + template [[nodiscard]] + static Json From(T const& object) { + Json res; + res.view = JsonView::From(object, res.alloc); + return res; + } + [[nodiscard]] + static Json ParseFile(std::filesystem::path path, unsigned depth = JV_DEFAULT_DEPTH) { + Json res; + res.view = ParseJsonFile(path, res.alloc, {depth}); + return res; + } + [[nodiscard]] + static Json Parse(string_view json, unsigned depth = JV_DEFAULT_DEPTH) { + Json res; + res.view = ParseJson(json, res.alloc, {depth}); + return res; + } + [[nodiscard]] + static Json FromMsgPack(string_view msgpack, unsigned depth = JV_DEFAULT_DEPTH) { + Json res; + res.view = ParseMsgPack(msgpack, res.alloc, {depth}); + return res; + } + template [[nodiscard]] + static Json FromInit(Fn&& f) { + Json res; + res.view = JsonView(f(res.alloc)); + return res; + } + const JsonView* operator->() const noexcept { + return &view; + } + Json(Json&&) noexcept = default; + Json& operator=(Json&&) noexcept = default; + JsonView View() const noexcept { + return view; + } +protected: + JsonView view; + DefaultArena<0> alloc; +}; + +template +void MergePatch(BasicMutJson& target, JsonView patch, unsigned depth = JV_DEFAULT_DEPTH) +{ + DepthError::Check(depth--); + if (patch.Is(t_object)) { + if (!target.Is(t_object)) { + target = {t_object}; + } + auto& result = *target.DataUnsafe().obj; + for (auto& pair: patch.Object()) { + if (pair.value.Is(t_null)) { + if (auto it = result.find(pair.key); it != result.end()) { + result.erase(it); + } + } else if (auto it = result.find(pair.key); it != result.end()) { + MergePatch(it->second, pair.value, depth); + } else { + auto pos = result.emplace_hint(it, pair.key, BasicMutJson(pair.value, depth)); + MergePatch(pos->second, pair.value, depth); + } + } + } else { + target = BasicMutJson{patch, depth}; + } +} + +template +void Unflatten(BasicMutJson& result, JsonView flat, TraceFrame const& frame = {}, unsigned depth = JV_DEFAULT_DEPTH) { + for (auto [k, v]: flat.Object()) { + DefaultArena alloc; + auto ptr = JsonPointer::FromString(k, alloc); + if (ptr.size > depth) { + throw DepthError{}; + } + result.Assign(ptr, frame) = BasicMutJson(v, depth - ptr.size); + } +} + +template +struct Convert> { + static JsonView DoIntoJson(BasicMutJson const& value, Arena& alloc) { + return value.View(alloc); + } + static void DoFromJson(BasicMutJson& out, JsonView json, TraceFrame const&) { + out = BasicMutJson{json}; + } +}; + +template<> +struct Convert { + static JsonView DoIntoJson(Json const& value, Arena&) { + return value.View(); + } + static void DoFromJson(Json& out, JsonView json, TraceFrame const&) { + out = Json{json}; + } +}; + +template +JsonView BasicMutJson::View(Arena& alloc, unsigned depth, bool sorted) const { + DepthError::Check(depth--); + switch (data.type) { + case t_number: { + return data.number; + } + case t_signed: { + return data.integer; + } + case t_unsigned: { + return data.uinteger; + } + case t_boolean: { + return JsonView(data.boolean); + } + case t_array: { + auto sz = unsigned(data.arr->size()); + auto arr = MakeArrayOf(sz, alloc); + for (auto i = 0u; i < sz; ++i) { + arr[i] = (*data.arr)[i].View(alloc, depth); + } + return JsonView(arr, sz); + } + case t_object: { + constexpr bool already_sorted = JsonConfigTraits::ObjectSorted; + auto sz = unsigned(data.obj->size()); + auto obj = MakeObjectOf(sz, alloc); + unsigned count = 0; + if (sorted && !already_sorted) { + for (auto& it: *data.obj) { + JsonPair curr; + curr.key = string_view(it.first); + curr.value = it.second.View(alloc, depth); + count = SortedInsertJson(obj, count, curr, sz); + } + Data res{t_object, f_sorted, count, {}}; + res.d.object = obj; + return JsonView(res); + } else { + for (auto& it: *data.obj) { + JsonPair& curr = obj[count++]; + curr.key = string_view(it.first); + curr.value = it.second.View(alloc, depth); + } + Data res{t_object, already_sorted ? f_sorted : f_none, sz, {}}; + res.d.object = obj; + return JsonView(res); + } + } + case t_string: { + return string_view(*data.str); + } + case t_binary: { + return JsonView::Binary(string_view(data.bin->data(), data.bin->size())); + } + default: return {}; + } +} + +template +void BasicMutJson::copy(BasicMutJson &to, JsonView src, unsigned int depth) { + DepthError::Check(depth--); + to = {src.GetType()}; + switch (src.GetType()) { + case t_array: { + to.data.arr->resize(src.GetUnsafe().size); + unsigned idx = 0; + for (auto i: src.Array(false)) { + copy((*to.data.arr)[idx++], i, depth); + } + break; + } + case t_object: { + auto hint = to.data.obj->begin(); + for (auto [k, v]: src.Object(false)) { + hint = to.data.obj->emplace_hint(hint, k, BasicMutJson(v, depth)); + } + break; + } + case t_string: { + *to.data.str = String{src.GetStringUnsafe()}; + break; + } + case t_binary: { + auto b = src.GetBinaryUnsafe(); + to.data.bin->resize(src.GetUnsafe().size); + memcpy(to.data.bin->data(), b.data(), b.size()); + break; + } + case t_number: { + to.data.number = src.GetUnsafe().d.number; + break; + } + case t_signed: { + to.data.integer = src.GetUnsafe().d.integer; + break; + } + case t_unsigned: { + to.data.uinteger = src.GetUnsafe().d.uinteger; + break; + } + case t_boolean: { + to.data.boolean = src.GetUnsafe().d.boolean; + break; + } + default: + to.data = {}; + } +} + +template +void BasicMutJson::copy(Array &to, const Array &from, unsigned int depth) { + to.reserve(from.size()); + for (auto& i: from) { + to.emplace_back(BasicMutJson(i, depth)); + } +} + +template +void BasicMutJson::copy(Object &to, const Object &from, unsigned int depth) { + auto hint = to.begin(); + for (auto& it: from) { + hint = to.emplace_hint(hint, it.first, BasicMutJson(it.second, depth)); + } +} + +template +void BasicMutJson::copy(BasicMutJson &to, const BasicMutJson &from, unsigned int depth) { + DepthError::Check(depth--); + to = BasicMutJson{from.data.type}; + switch (from.data.type) { + case t_array: { + copy(*to.data.arr, *from.data.arr, depth); + break; + } + case t_object: { + copy(*to.data.obj, *from.data.obj, depth); + break; + } + case t_string: { + *to.data.str = *from.data.str; + break; + } + case t_binary: { + *to.data.str = *from.data.str; + break; + } + default: { + to.data = from.data; + break; + } + } +} + +template +void BasicMutJson::Destroy(Arena& alloc) try { + ArenaVector stack(alloc); + stack.push_back(data); + data = {}; + while(!stack.empty()) { + auto it = stack.back(); + stack.pop_back(); + switch(it.type) { + case t_array: { + for (auto& nit: *it.arr) { + stack.push_back(nit.TakeUnsafe()); + } + it.arr->clear(); + disposeOf(it.arr); + break; + } + case t_object: { + for (auto& nit: *it.obj) { + stack.push_back(nit.second.TakeUnsafe()); + } + it.obj->clear(); + disposeOf(it.obj); + break; + } + case t_binary: { + disposeOf(it.bin); + break; + } + case t_string: { + disposeOf(it.str); + break; + } + default: break; + } + } +} catch (...) { + fputs(" ### POSSIBLE LEAK DETECTED while deleting Json!\n", stderr); + throw; +} + +template +auto BasicMutJson::Assign(JsonPointer ptr, TraceFrame const& frame) -> BasicMutJson& { + BasicMutJson* curr = this; + unsigned idx = 0; + for (auto part: ptr) { + try { + part.Visit( + [&](string_view key){ + if (curr->Is(t_null)) { + *curr = BasicMutJson(t_object); + } + auto& obj = curr->GetObject(); + auto found = obj.find(key); + if (found != obj.end()) { + curr = &found->second; + } else { + curr = &obj[Key_t{key}]; + } + }, + [&](unsigned idx){ + if (curr->Is(t_null)) { + *curr = BasicMutJson(t_array); + } + auto& arr = curr->GetArray(); + if (arr.size() <= idx) { + arr.resize(idx + 1); + } + curr = &arr[idx]; + }); + } catch (JsonException& e) { + e.trace = frame.PrintTrace() + ptr.SubPtr(0, idx).Join('.'); + throw; + } + idx++; + } + return *curr; +} + +} + +#endif // JV_JSON_HPP diff --git a/include/json_view/json_view.hpp b/include/json_view/json_view.hpp new file mode 100644 index 0000000..288f5b6 --- /dev/null +++ b/include/json_view/json_view.hpp @@ -0,0 +1,1055 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#ifndef JV_JSON_VIEW_HPP +#define JV_JSON_VIEW_HPP + +#include "data.hpp" +#include "describe/describe.hpp" +#include "json_view/alloc.hpp" +#include "trace_frame.hpp" +#include "meta/meta.hpp" +#include +#include +#include +#include +#include + +namespace jv +{ + +#ifndef JV_DEFAULT_DEPTH +#define JV_DEFAULT_DEPTH 300 +#endif + +using namespace meta; +using std::string_view; + +//! Attributes + +//! Convert structs as tuples of thier fields +struct StructAsTuple {}; + +struct FieldIndexBase {}; +//! explicit field index (by default it is DESCRIBE() order) +template +struct FieldIndex : FieldIndexBase { + static constexpr unsigned value = idx; +}; +//! Convert enums not into strings, but into integers +struct EnumAsInteger {}; +//! Inherit to mark all fields as skippable +struct SkipMissing : describe::Attrs {}; +//! Mark field as required (it is by default) +struct Required {}; +//! Subclass will be used as a validator for fields +struct FieldValidator {static void validate(...) = delete;}; +//! Subclass will be used as a validator for classes +struct ClassValidator {static void validate(...) = delete;}; +//! Helper for shorter code. Can be inherited or used as an attr. Will call T::validate(U) +template +struct ValidatedWith : FieldValidator, ClassValidator { + using GetAttrs = describe::Attrs>; + template static void validate(U& val) { T::validate(val); } +}; +struct EnumFallbackBase {}; +//! Do not throw exceptions on enum conversion errors, rather return fallback +template +struct EnumFallback : EnumFallbackBase { + static constexpr auto value = val; +}; + +using std::string_view; + +struct JsonView; +struct JsonPointer; + +template using if_integral = std::enable_if_t && !std::is_same_v, int>; +template using if_floating_point = std::enable_if_t, int>; +template using if_string_like = std::enable_if_t, int>; +template using if_struct = std::enable_if_t, int>; +template using if_enum = std::enable_if_t, int>; +template using if_vector_like = std::enable_if_t && !std::is_constructible_v, int>; +template using if_map_like = std::enable_if_t, int>; + +template struct Convert; + +struct JsonView +{ + using value_type = JsonView; + JsonView(Data d) noexcept : data(d) {} + JsonView(std::nullptr_t = {}) noexcept { + data.type = t_null; + } + explicit JsonView(bool val) noexcept { + data.d.boolean = val; + data.type = t_boolean; + } + JsonView(const char* str, int64_t len = -1) noexcept { + if (len == -1) { + len = unsigned(strlen(str)); + } + data.size = unsigned(len); + data.d.string = str; + data.type = t_string; + } + JsonView(string_view str) noexcept { + assert(str.size() <= (std::numeric_limits::max)()); + data.size = unsigned(str.size()); + data.d.string = str.data(); + data.type = t_string; + } + template = 1> + JsonView(T val) noexcept { + if constexpr (std::is_signed_v) { + data.d.integer = static_cast(val); + data.type = t_signed; + } else { + data.d.uinteger = static_cast(val); + data.type = t_unsigned; + } + } + template = 1> + JsonView(T val) noexcept { + data.d.number = static_cast(val); + data.type = t_number; + } + explicit JsonView(const JsonView* array, unsigned size) noexcept { + data.size = size; + data.d.array = array; + data.type = t_array; + } + template + JsonView(const JsonView(&arr)[size]) noexcept : + JsonView(arr, size) + {} + explicit JsonView(const JsonPair* object, unsigned size) noexcept { + data.size = size; + data.d.object = object; + data.type = t_object; + } + template + JsonView(const JsonPair(&obj)[size]) noexcept : + JsonView(obj, size) + {} + static JsonView Custom(void* data, unsigned size = 0) noexcept { + Data res; + res.size = size; + res.d.custom = data; + res.type = t_custom; + return res; + } + static JsonView Binary(string_view data) noexcept { + Data res; + res.size = unsigned(data.size()); + res.d.binary = data.data(); + res.type = t_binary; + return res; + } + static JsonView Discarded(string_view why = {}) noexcept { + JsonView res = {why}; + res.data.type = t_discarded; + return res; + } + bool Is(Type t) const noexcept { + return t ? (data.type & t) : !data.type; + } + struct AsObj; + struct AsArr; + AsObj Object(bool check = true) const; + AsArr Array(bool check = true) const; + JsonView WithFlagsUnsafe(Flags flags) noexcept { + data.flags = flags; + return *this; + } + template + void GetTo(T& out, TraceFrame const& frame = {}) const { + Convert::DoFromJson(out, *this, frame); + } + template + static JsonView From(T const& object, Arena& alloc) { + return Convert::DoIntoJson(object, alloc); + } + template + std::decay_t Get(TraceFrame const& frame = {}) const { + std::decay_t res; + GetTo(res, frame); + return res; + } + string_view GetString(TraceFrame const& frame = {}) const { + AssertType(t_string, frame); + return GetStringUnsafe(); + } + string_view GetBinary(TraceFrame const& frame = {}) const { + AssertType(t_binary, frame); + return GetBinaryUnsafe(); + } + string_view GetStringUnsafe() const { + return string_view{data.d.string, data.size}; + } + string_view GetBinaryUnsafe() const { + return {static_cast(data.d.binary), data.size}; + } + string_view GetDiscardReason() const { + AssertType(t_discarded); + return {data.d.string, data.size}; + } + const JsonView* Find(const JsonPointer& ptr, TraceFrame const& frame = {}) const; + const JsonPair* Find(string_view key, TraceFrame const& frame = {}) const; + const JsonView* FindVal(string_view key, TraceFrame const& frame = {}) const; + const JsonView At(string_view key, TraceFrame const& frame = {}) const; + const JsonView operator[](string_view key) const {return At(key, {});} + const JsonView operator[](unsigned idx) const {return At(idx, {});} + const JsonView At(unsigned idx, TraceFrame const& frame = {}) const { + if (auto res = Find(idx, frame)) { + return *res; + } else { + throwIndexError(idx, frame); + } + } + const JsonView *Find(unsigned idx, TraceFrame const& frame = {}) const { + AssertType(t_array, frame); + if (meta_Unlikely(data.size <= idx)) + return nullptr; + return data.d.array + idx; + } + template + U Value(const T& key, const U& adefault = {}, TraceFrame const& frame = {}) const { + AssertType(t_object, frame); + if (const JsonView* res = FindVal(key)) { + return res->Get(TraceFrame(key, frame)); + } else { + return U{adefault}; + } + } + template + U Value(unsigned idx, const U& adefault = {}, TraceFrame const& frame = {}) const { + AssertType(t_array); + if (data.size > idx) { + return data.d.array[idx].Get(TraceFrame(idx, frame)); + } else { + return U{adefault}; + } + } + template + string_view Value(const T& key, const char* adefault) const { + return Value(key, string_view{adefault}); + } + unsigned Size() const { + constexpr Type sized = t_array | t_object | t_string | t_binary; + AssertType(sized); + return data.size; + } + string_view GetTypeName() const noexcept; + Type GetType() const noexcept {return data.type;} + Flags GetFlags() const noexcept {return data.flags;} + bool HasFlag(Flags f) const noexcept {return data.flags & f;} + void AssertType(Type wanted, TraceFrame const& frame = {}) const; + static string_view PrintType(Type t) noexcept; + std::string Dump(bool pretty = false) const; + std::string DumpMsgPack() const; + [[noreturn]] void throwMissmatch(Type wanted, TraceFrame const& frame = {}) const; + [[noreturn]] void throwKeyError(string_view key, TraceFrame const& frame = {}) const; + [[noreturn]] void throwIndexError(unsigned key, TraceFrame const& frame = {}) const; + [[noreturn]] void throwIntRangeError(int64_t min, uint64_t max, TraceFrame const& frame = {}) const; + const Data& GetUnsafe() const noexcept { + return data; + } +protected: + Data data; +}; + +inline JsonView EmptyObject() { + return JsonView{static_cast(nullptr), 0}; +} + +inline JsonView EmptyArray() { + return JsonView{static_cast(nullptr), 0}; +} + +struct JsonPair { + string_view key; + JsonView value; +}; + +struct JsonView::AsObj { + AsObj(JsonView j, bool check = true) : j(j) { + if (check) j.AssertType(t_object); + } + unsigned size() const noexcept { + return j.GetUnsafe().size; + } + const JsonPair *begin() const noexcept { + return j.GetUnsafe().d.object; + } + const JsonPair* end() const noexcept { + return j.GetUnsafe().d.object + j.GetUnsafe().size; + } +protected: + JsonView j; +}; + +struct JsonView::AsArr { + AsArr(JsonView j, bool check = true) : j(j) { + if (check) j.AssertType(t_array); + } + unsigned size() const noexcept { + return j.GetUnsafe().size; + } + const JsonView *begin() const noexcept { + return j.GetUnsafe().d.array; + } + const JsonView* end() const noexcept { + return j.GetUnsafe().d.array + j.GetUnsafe().size; + } +protected: + JsonView j; +}; + +inline typename JsonView::AsObj JsonView::Object(bool check) const { + return {*this, check}; +} + +inline typename JsonView::AsArr JsonView::Array(bool check) const { + return {*this, check}; +} + +[[nodiscard]] +inline std::string_view CopyString(std::string_view src, Arena& alloc) { + if (src.empty()) return ""; + auto ptr = alloc(src.size(), alignof(char)); + if (meta_Unlikely(!ptr)) { + throw std::bad_alloc{}; + } + memcpy(ptr, src.data(), src.size()); + return {static_cast(ptr), src.size()}; +} + +[[nodiscard]] +inline JsonView* MakeArrayOf(unsigned count, Arena& alloc) { + if (!count) return nullptr; + auto res = static_cast(alloc(sizeof(JsonView) * count, alignof(JsonView))); + if (meta_Unlikely(!res)) { + throw std::bad_alloc{}; + } + return res; +} + +[[nodiscard]] +inline JsonPair* MakeObjectOf(unsigned count, Arena& alloc) { + if (!count) return nullptr; + auto res = static_cast(alloc(sizeof(JsonPair) * count, alignof(JsonPair))); + if (meta_Unlikely(!res)) { + throw std::bad_alloc{}; + } + return res; +} + +namespace detail { +inline const JsonPair* sortedFind(const JsonPair* object, unsigned len, string_view key) { + auto first = object; + while (len > 0) { + auto half = len >> 1; + auto middle = first + half; + if (middle->key < key) { + first = middle; + ++first; + len = len - half - 1; + } else { + if (middle->key == key) { + return middle; + } + len = half; + } + } + return nullptr; +} +} + +inline const JsonPair* JsonView::Find(string_view key, TraceFrame const& frame) const { + AssertType(t_object, frame); + if (HasFlag(f_sorted)) { + return detail::sortedFind(data.d.object, data.size, key); + } else { + for (auto i = 0u; i < data.size; ++i) { + if (data.d.object[i].key == key) { + return data.d.object + i; + } + } + } + return nullptr; +} + +inline const JsonView* JsonView::FindVal(string_view key, TraceFrame const& frame) const { + if (auto res = Find(key, frame)) { + return &res->value; + } else { + return nullptr; + } +} + +inline const JsonView JsonView::At(string_view key, TraceFrame const& frame) const { + if (auto res = Find(key, frame)) { + return res->value; + } else { + throwKeyError(key, frame); + } +} + +struct KeyLess { + bool operator()(const JsonPair& lhs, const JsonPair& rhs) const noexcept { + return lhs.key < rhs.key; + } +}; + +struct KeyEq { + bool operator()(const JsonPair& lhs, const JsonPair& rhs) const noexcept { + return lhs.key == rhs.key; + } +}; + +inline string_view JsonView::GetTypeName() const noexcept { + return PrintType(data.type); +} + + +inline void JsonView::AssertType(Type wanted, const TraceFrame &frame) const { + bool ok = Is(wanted); + if (meta_Unlikely(!ok)) { + throwMissmatch(wanted, frame); + } +} + +inline string_view JsonView::PrintType(Type t) noexcept { + switch (t) { + case t_array: return std::string_view("array"); + case t_string: return std::string_view("string"); + case t_object: return std::string_view("object"); + case t_null: return std::string_view("null"); + case t_signed: return std::string_view("signed"); + case t_boolean: return std::string_view("boolean"); + case t_unsigned: return std::string_view("unsigned"); + case t_binary: return std::string_view("binary"); + case t_discarded: return std::string_view("discarded"); + case t_number: return std::string_view("number"); + case t_custom: return std::string_view("custom"); + default: return std::string_view(""); + } +} + +struct DepthError : std::exception { + DepthError() noexcept {} + const char* what() const noexcept { + return "Json is too deep"; + } + meta_alwaysInline static void Check(unsigned depth) { + if (meta_Unlikely(!depth)) { + throw DepthError{}; + } + } +}; + +struct JsonException : std::exception +{ + JsonException(TraceFrame const& frame = {}); + JsonException(JsonPointer const& ptr); + void SetTrace(JsonPointer const& ptr); + void SetTrace(TraceFrame const& frame); + std::string trace; +protected: + mutable std::string msg; +}; + +struct ForeignError : JsonException +{ + using JsonException::JsonException; + ForeignError(std::string msg, TraceFrame const& frame = {}); + ForeignError(std::string msg, JsonPointer const& ptr); + std::exception_ptr nested; + const char* what() const noexcept override; +}; + +struct KeyError : JsonException +{ + using JsonException::JsonException; + std::string missing; + const char* what() const noexcept override; +}; + +struct IndexError : JsonException +{ + using JsonException::JsonException; + unsigned wanted; + unsigned actualSize; + const char* what() const noexcept override; +}; + +struct TypeMissmatch : JsonException +{ + using JsonException::JsonException; + Type wanted; + Type was; + const char* what() const noexcept override; +}; + +struct IntRangeError : JsonException +{ + using JsonException::JsonException; + bool isUnsigned = false; + union { + int64_t i; + uint64_t u = 0; + } was; + int64_t min = 0; + uint64_t max = 0; + const char* what() const noexcept override; +}; + +namespace detail { + +template +struct is_lossless { + static constexpr auto same_sign = std::is_signed_v == std::is_signed_v; + static constexpr auto value = same_sign && sizeof(To) >= sizeof(From); +}; + +template +To intChecked(JsonView j, FromT our, TraceFrame const& frame) noexcept(is_lossless::value); + +} //detail + +/// Conversions +template struct Convert { + static JsonView DoIntoJson(T const& value, Arena& ctx) { + return IntoJson(value, ctx); + } + static void DoFromJson(T& out, JsonView json, TraceFrame const& frame) { + FromJson(out, json, frame); + } +}; + +inline JsonView IntoJson(bool value, Arena&) { + return JsonView{value}; +} + +inline void FromJson(bool& value, JsonView json, TraceFrame const& frame) { + json.AssertType(t_boolean, frame); + value = json.GetUnsafe().d.boolean; +} + +template = 1> +JsonView IntoJson(T value, Alloc&) { + return JsonView{value}; +} + +template = 1> +void FromJson(T& value, JsonView json, TraceFrame const& frame) { + switch(json.GetType()) { + case t_signed: { + value = detail::intChecked(json, json.GetUnsafe().d.integer, frame); + break; + } + case t_unsigned: { + value = detail::intChecked(json, json.GetUnsafe().d.uinteger, frame); + break; + } + default: { + json.throwMissmatch(t_signed | t_unsigned, frame); + } + } +} + +template = 1> +JsonView IntoJson(T value, Arena&) { + return JsonView{value}; +} + +template = 1> +void FromJson(T& value, JsonView json, TraceFrame const& frame) { + switch(json.GetType()) { + case t_signed: { + value = static_cast(json.GetUnsafe().d.integer); + break; + } + case t_unsigned: { + value = static_cast(json.GetUnsafe().d.uinteger); + break; + } + case t_number: { + value = static_cast(json.GetUnsafe().d.number); + break; + } + default: { + json.throwMissmatch(t_signed | t_unsigned | t_number, frame); + } + } +} + +template = 1> +JsonView IntoJson(T const& value, Arena&) { + return JsonView{value}; +} + +template = 1> +void FromJson(T& value, JsonView json, TraceFrame const& frame) { + json.AssertType(t_string, frame); + value = static_cast>(json.GetStringUnsafe()); +} + +namespace detail { + +template +void impl_into(JsonView* arr, const Tuple& object, Arena& ctx, std::index_sequence) { + ((void)(arr[Is] = JsonView::From(std::get(object), ctx)), ...); +} +template +void impl_from(const JsonView* source, Tuple& out, TraceFrame const& frame, std::index_sequence) { + ((void)(source[Is].GetTo(std::get(out), TraceFrame(Is, frame))), ...); +} + +} + +template +JsonView IntoJson(std::tuple const& value, Arena& ctx) { + constexpr unsigned count = sizeof...(Ts); + auto arr = static_cast(ctx(sizeof(JsonView) * count)); + detail::impl_into(arr, value, ctx, std::make_index_sequence()); + return JsonView{arr, count}; +} + +template +void FromJson(std::tuple& value, JsonView json, TraceFrame const& frame) { + constexpr unsigned count = sizeof...(Ts); + json.AssertType(t_array, frame); + if (json.GetUnsafe().size <= count) { + json.throwIndexError(count, frame); + } + detail::impl_from(json.GetUnsafe().d.array, value, frame, std::make_index_sequence()); +} + +template +JsonView IntoJson(std::optional const& value, Arena& ctx) { + return value ? JsonView::From(*value, ctx) : JsonView(nullptr); +} + +template +void FromJson(std::optional& out, JsonView json, TraceFrame const& frame) { + if (json.Is(t_null)) { + out.reset(); + } else { + json.GetTo(out.emplace(), frame); + } +} + +template +struct is_optional : std::false_type {}; +template +struct is_optional> : std::true_type {}; + +namespace detail { +template void deserializeFieldsSorted(T& obj, JsonView json, TraceFrame const& frame); +template void deserializeFields(T& obj, JsonView json, TraceFrame const& frame); +template void deserializeAsTuple(T& obj, JsonView json, TraceFrame const& frame); +template JsonView serializeAsTuple(T const& value, Arena& alloc); +template void runValidator(T& output, TraceFrame const& next); +} + +template = 1> +void FromJson(T& out, JsonView json, TraceFrame const& frame) { + if constexpr (describe::has_attr_v) { + json.AssertType(t_array, frame); + detail::deserializeAsTuple(out, json, frame); + } else if (json.HasFlag(f_sorted)) { + json.AssertType(t_object, frame); + detail::deserializeFieldsSorted(out, json, frame); + } else { + json.AssertType(t_object, frame); + detail::deserializeFields(out, json, frame); + } + using validator = describe::extract_attr_t; + detail::runValidator(out, TraceFrame(describe::Get().name, frame)); +} + +template = 1> +JsonView IntoJson(T const& value, Arena& ctx) { + if constexpr (describe::has_attr_v) { + return detail::serializeAsTuple(value, ctx); + } else { + constexpr auto desc = describe::Get(); + constexpr auto size = desc.fields_count; + auto obj = MakeObjectOf(size, ctx); + unsigned count = 0; + desc.for_each_field([&](auto field){ + auto entry = JsonPair{field.name, JsonView::From(field.get(value), ctx)}; + count = SortedInsertJson(obj, count, entry, size); + }); + Data result; + result.type = t_object; + result.size = count; + result.d.object = obj; + result.flags = f_sorted; + return JsonView(result); + } +} + +template = 1> +JsonView IntoJson(T const& value, Arena&) { + if constexpr (describe::has_attr_v) { + return JsonView(std::underlying_type_t(value)); + } else { + string_view name; + using fallback = describe::extract_attr_t; + if (!describe::enum_to_name(value, name)) { + if constexpr (std::is_void_v) { + throw std::runtime_error( + "invalid enum value for '" + std::string{describe::Get().name} + + "': "+std::to_string(std::underlying_type_t(value))); + } else { + (void)describe::enum_to_name(T(fallback::value), name); + } + } + return name; + } +} + +template = 1> +void FromJson(T& out, JsonView json, TraceFrame const& frame) { + if constexpr (describe::has_attr_v) { + auto asUnder = json.Get>(frame); + // maybe later: if validate integer attr + // even later: consider fallback is integer validation fails + //bool ok = false; + //describe::Get().for_each_field([&](auto f){ + // if (!ok && asUnder == f.value) { + // ok = true; + // } + //}); + //if (!ok) { + // auto msg = "invalid integer for enum '"+std::string{describe::Get().name} +"': "+std::to_string(asUnder); + // throw ForeignError(std::move(msg), frame); + //} + out = T(asUnder); + } else { + auto name = json.Get(frame); + using fallback = describe::extract_attr_t; + if (!describe::name_to_enum(name, out)) { + if constexpr (std::is_void_v) { + auto msg = "invalid string for enum '" + + std::string{describe::Get().name} + + "': " + std::string{name}; + throw ForeignError(std::move(msg), frame); + } else { + out = fallback::value; + } + } + } +} + +template<> struct Convert { + static JsonView DoIntoJson(JsonView value, Arena&) { + return value; + } + static void DoFromJson(JsonView& out, JsonView json, TraceFrame const&) { + out = json; + } +}; + +template = 1> +JsonView IntoJson(T const& value, Arena& ctx) { + auto arr = MakeArrayOf(unsigned(value.size()), ctx); + unsigned count = 0; + for (auto& v: value) { + arr[count++] = JsonView::From(v, ctx); + } + return JsonView(arr, count); +} + +template = 1> +void FromJson(T& out, JsonView json, TraceFrame const& frame) { + json.AssertType(t_array, frame); + out.clear(); + unsigned count = 0; + for (JsonView i: json.Array(false)) { + i.GetTo(out.emplace_back(), TraceFrame(count++, frame)); + } +} + +template = 1> +JsonView IntoJson(T const& value, Arena& ctx) { + auto obj = MakeObjectOf(unsigned(value.size()), ctx); + unsigned count = 0; + for (auto& [k, v]: value) { + auto& current = obj[count++]; + current.key = string_view{k}; + current.value = JsonView::From(v, ctx); + } + return JsonView(obj, count); +} + +template = 1> +void FromJson(T& out, JsonView json, TraceFrame const& frame) { + json.AssertType(t_object, frame); + out.clear(); + for (auto [k, v]: json.Object(false)) { + v.GetTo(out[typename T::key_type{k}], TraceFrame(k, frame)); + } +} + +template +JsonView IntoJson(std::pair const& value, Arena& ctx) { + auto arr = MakeArrayOf(2, ctx); + arr[0] = JsonView::From(value.first, ctx); + arr[1] = JsonView::From(value.second, ctx); + return JsonView(arr, 2); +} + +template +void FromJson(std::pair& out, JsonView json, TraceFrame const& frame) { + json.AssertType(t_array, frame); + if (auto sz = json.GetUnsafe().size; sz < 2) { + json.throwIndexError(2, frame); + } + json.GetUnsafe().d.array[0].GetTo(out.first, TraceFrame(0, frame)); + json.GetUnsafe().d.array[1].GetTo(out.second, TraceFrame(1, frame)); +} + +template>> +struct StaticJsonView { + StaticJsonView(T const& obj = {}) { + unsigned idx = 0; + NullArena null; + desc.for_each_field([&](auto f){ + auto& field = f.get(obj); + auto& curr = storage[idx++]; + curr.key = f.name; + if constexpr (std::is_constructible_v) { + curr.value = JsonView(field); + } else { + // TODO: recursively get needed storage to statically allocate + curr.value = JsonView::From(field, null); + } + }); + } + JsonView View() const noexcept { + return {storage}; + } +protected: + static constexpr auto desc = describe::Get(); + JsonPair storage[desc.fields_count]; +}; + +namespace detail { +struct fieldHelper { + string_view name; + bool hit = {}; + bool required = {}; +}; + +template +constexpr bool isRequired() { + constexpr bool isRequired = describe::has_attr_v; + constexpr bool skip = describe::has_attr_v + || describe::has_attr_v + || is_optional::value; + return isRequired || !skip; +} + +template +constexpr auto prepFields() { + constexpr auto desc = describe::Get(); + std::array res = {}; + size_t idx = 0; + desc.for_each_field([&](auto field){ + auto curr = res[idx++]; + curr.name = field.name; + curr.hit = false; + curr.required = isRequired(); + }); + return res; +} + +template +void runValidator(T& output, TraceFrame const& next) { + if constexpr (!std::is_void_v) { + try { + Validator::validate(output); + } catch (...) { + ForeignError exc(next); + exc.nested = std::current_exception(); + throw exc; + } + } +} + +inline JsonView tupleGet(bool required, unsigned idx, const JsonView* arr, unsigned sz, TraceFrame const& frame) { + if (sz <= idx) { + if (required) { + IndexError err(frame); + err.actualSize = sz; + err.wanted = idx; + throw err; + } else { + return JsonView(); + } + } + return arr[idx]; +} + +template +void deserializeAsTuple(T& obj, JsonView json, TraceFrame const& frame) { + constexpr auto desc = describe::Get(); + unsigned count = 0; + auto arr = json.GetUnsafe().d.array; + auto sz = json.GetUnsafe().size; + desc.for_each_field([&](auto f){ + using F = decltype(f); + using Idx = describe::extract_attr_t; + unsigned index; + if constexpr (!std::is_void_v) index = Idx::value; + else index = count; + auto& output = f.get(obj); + auto src = tupleGet(isRequired(), index, arr, sz, frame); + TraceFrame fieldFrame(f.name, frame); + src.GetTo(output, fieldFrame); + using validator = describe::extract_attr_t; + runValidator(output, fieldFrame); + count++; + }); +} + +template +constexpr unsigned getIdxFor() { + using Explicit = describe::extract_attr_t; + if constexpr (std::is_void_v) { + return 0; + } else { + return Explicit::value; + } +} + +template +constexpr unsigned maxIdxFor(describe::Description) { + constexpr auto simple = sizeof...(Fields); + constexpr std::array fromAttr{getIdxFor()...}; + for (auto i: fromAttr) { + if (i > simple) { + return i; + } + } + return simple; +} + +template +JsonView serializeAsTuple(const T &value, Arena &alloc) +{ + constexpr auto desc = describe::Get(); + constexpr auto total = maxIdxFor(desc); + auto arr = MakeArrayOf(total, alloc); + unsigned count = 0; + desc.for_each_field([&](auto f){ + using F = decltype(f); + constexpr auto manual = getIdxFor(); + auto idx = manual ? manual : count; + arr[idx] = JsonView::From(f.get(value), alloc); + count++; + }); + return JsonView(arr, total); +} + +template +void deserializeFields(T& obj, JsonView json, TraceFrame const& frame) { + constexpr auto desc = describe::Get(); + constexpr auto helpers = prepFields(); + auto thisRun = helpers; + for (auto pair: json.Object()) { + unsigned count = 0; + desc.for_each_field([&](auto field){ + if (pair.key == field.name) { + thisRun[count++].hit = true; + auto& output = field.get(obj); + TraceFrame next(field.name, frame); + pair.value.GetTo(output, next); + using validator = describe::extract_attr_t; + runValidator(output, next); + } + }); + } + for (auto& field: thisRun) { + if (field.required && !field.hit) { + json.throwKeyError(field.name, frame); + } + } +} + +template +void deserializeFieldsSorted(T& obj, JsonView json, TraceFrame const& frame) { + constexpr auto desc = describe::Get(); + desc.for_each_field([&](auto field){ + using F = decltype(field); + auto& output = field.get(obj); + auto next = TraceFrame(field.name, frame); + if constexpr (isRequired()) { + json.At(field.name, frame).GetTo(output, next); + } else { + if (auto f = json.FindVal(field.name, frame)) { + f->GetTo(output, next); + } + } + using validator = describe::extract_attr_t; + runValidator(output, next); + }); +} + +template +inline To intChecked(JsonView j, FromT our, TraceFrame const& frame) noexcept(is_lossless::value) +{ + (void)j; + using is_lossless = is_lossless; + if constexpr (!is_lossless::value) { + constexpr auto _min = (std::numeric_limits::min)(); + constexpr auto _max = (std::numeric_limits::max)(); + auto fail = [&]{ + j.throwIntRangeError(_min, _max, frame); + }; + if constexpr (is_lossless::same_sign) { + if (meta_Unlikely(our < _min || _max < our)) fail(); + } else if constexpr (std::is_signed_v) { //TO is unsigned + if constexpr ((std::numeric_limits::max)() > _max) { + if (meta_Unlikely(our < 0 || our > _max)) fail(); + } else { + if (meta_Unlikely(our < 0)) fail(); + } + } else /*signed To*/ { + if (meta_Unlikely(our > FromT(_max))) fail(); + } + } + return static_cast(our); +} + +} //detail + + +} //jv + +#endif // JV_JSON_VIEW_HPP diff --git a/include/json_view/parse.hpp b/include/json_view/parse.hpp new file mode 100644 index 0000000..f3603aa --- /dev/null +++ b/include/json_view/parse.hpp @@ -0,0 +1,70 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#ifndef JV_JSON_PARSER_HPP +#define JV_JSON_PARSER_HPP +#pragma once + +#include "json_view.hpp" +#include +#include "membuff/membuff.hpp" +#include "alloc.hpp" + +namespace jv +{ + +struct ParsingError : public std::runtime_error { + using std::runtime_error::runtime_error; + size_t position = 0; +}; + +struct ParseSettings { + ParseSettings() = default; + unsigned maxDepth = JV_DEFAULT_DEPTH; + bool sorted = true; +}; + +struct [[nodiscard]] ParseResult { + JsonView result; + size_t consumed; + operator JsonView() const noexcept { + return result; + } +}; + + +JsonView ParseJson(std::istream& data, Arena& alloc, ParseSettings params = {}); +JsonView ParseJson(membuff::In& data, Arena& alloc, ParseSettings params = {}); +JsonView ParseJsonInPlace(char* buff, size_t len, Arena& alloc, ParseSettings params = {}); +JsonView ParseJsonFile(std::filesystem::path const& file, Arena& alloc, ParseSettings params = {}); +JsonView ParseJson(string_view json, Arena& alloc, ParseSettings params = {}); + +ParseResult ParseMsgPack(string_view data, Arena& alloc, ParseSettings params = {}); +ParseResult ParseMsgPackInPlace(string_view data, Arena& alloc, ParseSettings params = {}); +ParseResult ParseMsgPackInPlace(const void* data, size_t size, Arena& alloc, ParseSettings params = {}); +ParseResult ParseMsgPack(membuff::In& reader, Arena& alloc, ParseSettings params = {}); + +} + +#endif //JV_JSON_PARSER_HPP diff --git a/include/json_view/pointer.hpp b/include/json_view/pointer.hpp new file mode 100644 index 0000000..81813e6 --- /dev/null +++ b/include/json_view/pointer.hpp @@ -0,0 +1,138 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#ifndef JV_JSON_ALGORITHM_HPP +#define JV_JSON_ALGORITHM_HPP + +#include "json_view.hpp" +#include "membuff/membuff.hpp" +#include + +namespace jv { + +struct JsonKey { + constexpr JsonKey() noexcept = default; + constexpr JsonKey(JsonKey const&) noexcept = default; + constexpr JsonKey(JsonKey &&) noexcept = default; + constexpr JsonKey& operator=(JsonKey const&) noexcept = default; + constexpr JsonKey& operator=(JsonKey &&) noexcept = default; + constexpr JsonKey(string_view key) noexcept : + size(unsigned(key.size())), + string(key.data()) + { + string = string ? string : ""; + } + constexpr JsonKey(unsigned idx) noexcept : + size(idx) + {} + constexpr JsonKey(const char* key) noexcept : + JsonKey(string_view{key}) + { + } + template + constexpr decltype(auto) Visit(Fn&& f) const { + if (string) { + return f(string_view{string, size}); + } else { + return f(size); + } + } + template + constexpr decltype(auto) Visit(KFn&& key, IFn&& id) const { + if (string) { + return key(string_view{string, size}); + } else { + return id(size); + } + } +protected: + //use padding here? + unsigned size{}; + const char* string{}; +}; + +struct JsonPointer { + const JsonKey* keys = {}; + unsigned size = {}; + constexpr JsonPointer() noexcept = default; + constexpr JsonPointer(const JsonKey* keys, unsigned size) noexcept : + keys(keys), size(size) + {} + template + constexpr JsonPointer(const JsonKey (&keys)[N]) noexcept : + keys(keys), size(N) + {} + static constexpr unsigned npos = (std::numeric_limits::max)(); + static JsonPointer FromString(std::string_view ptr, Arena& alloc, char sep); + static JsonPointer FromString(std::string_view ptr, Arena& alloc); + void JoinInto(membuff::Out& out, char sep = '/', bool asUri = false) const; + std::string Join(char sep = '/', bool asUri = false) const; + constexpr JsonPointer SubPtr(unsigned begin, unsigned len = npos) const { + if (size <= begin) { + throw std::out_of_range{"JsonPointer SubPtr(): " + + std::to_string(begin) + + " >= " + std::to_string(size)}; + } + auto left = size - begin; + return {keys + begin, len > left ? left : len}; + } + constexpr const JsonKey* begin() const noexcept {return keys;} + constexpr const JsonKey* end() const noexcept {return keys + size;} + constexpr const JsonKey* cbegin() const noexcept {return keys;} + constexpr const JsonKey* cend() const noexcept {return keys + size;} +}; + +namespace detail { +template +void doDeepIterate(JsonView view, K& keys, F& cb, unsigned depth) +{ + DepthError::Check(depth--); + if (view.Is(t_object)) { + for (auto [k, v]: view.Object()) { + keys.emplace_back(k); + doDeepIterate(v, keys, cb, depth); + keys.pop_back(); + } + } else if (view.Is(t_array)) { + unsigned idx = 0; + for (auto v: view.Array()) { + keys.emplace_back(idx++); + doDeepIterate(v, keys, cb, depth); + keys.pop_back(); + } + } else { + cb(JsonPointer{keys.data(), unsigned(keys.size())}, view); + } +} +} + +template>> +void DeepIterate(JsonView view, Arena& alloc, F&&f, unsigned depth = JV_DEFAULT_DEPTH) { + ArenaVector keys(alloc); + detail::doDeepIterate(view, keys, f, depth); +} + +} //jv + +#endif // JV_JSON_ALGORITHM_HPP diff --git a/include/json_view/trace_frame.hpp b/include/json_view/trace_frame.hpp new file mode 100644 index 0000000..532a32d --- /dev/null +++ b/include/json_view/trace_frame.hpp @@ -0,0 +1,100 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#ifndef JV_TRACE_FRAME_HPP +#define JV_TRACE_FRAME_HPP + +#include +#include +#include "meta/meta.hpp" + +namespace jv +{ + +struct TraceFrame { + constexpr TraceFrame() noexcept {} + TraceFrame(TraceFrame&&) = delete; + constexpr TraceFrame(unsigned idx, TraceFrame const& prev) noexcept : + prev(&prev), size(idx), str(nullptr) + {} + constexpr TraceFrame(std::string_view key, TraceFrame const& prev) noexcept : + prev(&prev), size(unsigned(key.size())), str(key.data()) + { + str = str ? str : ""; + } + template void Walk(F&& f) const { + auto node = prepareWalk(); + while(node) { + if(node->str) f(std::string_view{node->str, node->size}); + else f(node->size); + node = node->next; + } + } + constexpr void SetIndex(unsigned _idx) noexcept { + size = _idx; + str = nullptr; + } + constexpr void SetKey(std::string_view key) noexcept { + size = unsigned(key.size()); + str = key.data(); + } + std::string PrintTrace() const + { + std::string result; + Walk(meta::overloaded{ + [&](std::string_view key){ + result += '.'; + result += key; + }, + [&](unsigned idx){ + result += std::string_view(".["); + result += std::to_string(idx); + result += ']'; + } + }); + return result; + } +private: + const TraceFrame* prepareWalk() const { + if (!prev) + return nullptr; + const TraceFrame* parent = prev; + const TraceFrame* current = this; + while(parent) { + parent->next = current; + current = parent; + parent = parent->prev; + } + return current->next; + } + + const TraceFrame* prev{}; + mutable const TraceFrame* next{}; + unsigned size{}; + const char* str{}; +}; + +} + +#endif // JV_TRACE_FRAME_HPP diff --git a/include/membuff/membuff.hpp b/include/membuff/membuff.hpp new file mode 100644 index 0000000..8113eec --- /dev/null +++ b/include/membuff/membuff.hpp @@ -0,0 +1,260 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#ifndef MEMBUFF_HPP +#define MEMBUFF_HPP + +#include +#include +#include +#include +#include +#include "meta/compiler_macros.hpp" +#include + +// These classes are used for half-virtual buffer implementations +// Api is absolutely minimal - just one virt method to be implemented +// membuff::Out::Grow(amount) -> try to allocate more space (or flush previous) +// membuff::In::Refill(amount) -> try to get more data to read from buffer + +namespace membuff +{ + +inline constexpr size_t NoHint = 0; + +struct In +{ + const char* buffer = {}; + size_t ptr = {}; + size_t capacity = {}; + long LastError = {}; + + size_t Available() const noexcept; + [[nodiscard]] + char ReadByte(size_t growAmount = NoHint); + size_t Read(char* buff, size_t size, size_t growAmount = NoHint); + size_t Read(void* buff, size_t size, size_t growAmount = NoHint); + virtual size_t TryTotalLeft() {return 0;} + virtual void Refill(size_t amountHint = NoHint) = 0; + virtual ~In() = default; +}; + +struct Out +{ + char* buffer = {}; + size_t ptr = {}; + size_t capacity = {}; + long LastError = {}; + + size_t SpaceLeft() const noexcept; + char* Current() noexcept; + void Write(const char* data, size_t size, size_t growAmount = NoHint); + void Write(const void* data, size_t size, size_t growAmount = NoHint); + void Write(std::string_view data, size_t growAmount = NoHint); + void Write(char byte, size_t growAmount = NoHint); + void Write(uint8_t byte, size_t growAmount = NoHint) { + return Write(char(byte), growAmount); + } + virtual void Grow(size_t amountHint) = 0; + virtual ~Out() = default; +}; + +template +struct StringOut final: Out +{ + using size_type = typename String::size_type; + [[nodiscard]] String Consume() noexcept { + out.resize(size_type(ptr)); + capacity = ptr = 0; + return std::move(out); + } + void Grow(size_t amount) override { + out.resize(out.size() + size_type(amount)); + buffer = reinterpret_cast(out.data()); + capacity = size_t(out.size()); + } + template + StringOut(size_t startSize = 512, Args&&...a) : out(std::forward(a)...) { + init(startSize); + } +protected: + void init(size_t startSize) { + out.resize(size_type(startSize)); + buffer = reinterpret_cast(out.data()); + capacity = size_t(out.size()); + } + String out; +}; + +template +struct FuncOut final : Out { + FuncOut(Fn f) : f(std::move(f)) { + capacity = buff; + ptr = 0; + buffer = stor; + } + void Flush() { + f(static_cast(stor), ptr); + ptr = 0; + } +protected: + void Grow(size_t) override { + Flush(); + } + char stor[buff]; + Fn f; +}; + +template FuncOut(Fn) -> FuncOut<1024, Fn>; + +struct IStreamIn final : In { + char buff[2048]; + std::istream& stream; + IStreamIn(std::istream& stream) : stream(stream) { + ptr = 0; + buffer = buff; + capacity = 0; + Refill(NoHint); + } + void Refill(size_t) override { + stream.read(buff, sizeof(buff)); + auto read = stream.gcount(); + if (read > 0) { + capacity = size_t(read); + } else { + LastError = long(read); + capacity = 0; + } + } +}; + +inline size_t Out::SpaceLeft() const noexcept { + return capacity - ptr; +} + +inline char *Out::Current() noexcept { + return buffer + ptr; +} + +inline void Out::Write(const char *data, size_t size, size_t growAmount) +{ + if (ptr + size > capacity) { + do { + Grow(growAmount ? growAmount : capacity); + if (meta_Unlikely(LastError)) { + return; + } + auto left = capacity - ptr; + auto min = std::min meta_NO_MACRO (size, left); + if (meta_Likely(min)) { + ::memcpy(buffer + ptr, data, min); + } + size -= min; + ptr += min; + data += min; + } while (size); + } else { + if (meta_Likely(size)) { + ::memcpy(buffer + ptr, data, size); + } + ptr += size; + } +} + +inline void Out::Write(const void *data, size_t size, size_t growAmount) +{ + Write(static_cast(data), size, growAmount); +} + +inline void Out::Write(std::string_view data, size_t growAmount) +{ + return Write(data.data(), data.size(), growAmount); +} + +inline void Out::Write(char byte, size_t growAmount) { + LastError = 0; + if (meta_Unlikely(ptr >= capacity)) { + Grow(growAmount ? growAmount : capacity); + if (meta_Unlikely(LastError)) { + return; + } + } + buffer[ptr++] = byte; +} + +inline size_t In::Available() const noexcept +{ + return capacity - ptr; +} + +inline char In::ReadByte(size_t growAmount) +{ + LastError = 0; + if (meta_Unlikely(ptr >= capacity)) { + Refill(growAmount ? growAmount : capacity); + if (!capacity) { + return {}; + } + } + char res = buffer[ptr++]; + return res; +} + +inline size_t In::Read(char *buff, size_t size, size_t growAmount) +{ + auto start = buff; + if (ptr + size > capacity) { + do { + if (auto av = Available()) { + ::memcpy(buff, buffer + ptr, av); + buff += av; + } + ptr = 0; + Refill(growAmount ? growAmount : capacity); + if (!capacity) { + return size_t(buff - start); + } + auto left = capacity - ptr; + auto min = (std::min)(size, left); + ::memcpy(buff, buffer + ptr, min); + size -= min; + ptr += min; + buff += min; + } while (size); + return size_t(buff - start); + } else { + ::memcpy(buff, buffer + ptr, size); + ptr += size; + return size; + } +} + +inline size_t In::Read(void *buff, size_t size, size_t growAmount) +{ + return Read(static_cast(buff), size, growAmount); +} + +} //jv + +#endif //MEMBUFF_HPP diff --git a/include/meta/compiler_macros.hpp b/include/meta/compiler_macros.hpp new file mode 100644 index 0000000..fa63533 --- /dev/null +++ b/include/meta/compiler_macros.hpp @@ -0,0 +1,57 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#ifndef META_COMP_MACROS_H +#define META_COMP_MACROS_H + +#include +#ifdef __GNUC__ +#define meta_Likely(x) __builtin_expect(!!(x), 1) +#define meta_Unlikely(x) __builtin_expect(!!(x), 0) +#define meta_attr_PURE __attribute__((pure)) +#else +#define meta_Likely(x) (x) +#define meta_Unlikely(x) (x) +#define meta_attr_PURE +#endif + +#ifdef __GNUC__ // GCC, Clang, ICC +#define meta_alwaysInline __attribute__((always_inline)) +#define meta_Unreachable() do{assert(false && "unreachable"); __builtin_unreachable();}while(0) +#define meta_Restrict __restrict__ +#elif defined(_MSC_VER) // MSVC +#define meta_Restrict +#define meta_alwaysInline __forceinline +#define meta_Unreachable() do{assert(false && "unreachable"); __assume(false);}while(0) +#else +#define meta_Restrict +#define meta_alwaysInline +#define meta_Unreachable() do{assert(false && "unreachable");}while(0) +#endif + +// disable min() max() on windows +#define meta_NO_MACRO + + +#endif //META_COMP_MACROS_H diff --git a/include/meta/meta.hpp b/include/meta/meta.hpp new file mode 100644 index 0000000..8afbe49 --- /dev/null +++ b/include/meta/meta.hpp @@ -0,0 +1,154 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#ifndef META_META_HPP +#define META_META_HPP + +#include "compiler_macros.hpp" +#include +#include +#include +#include +#include + +namespace meta { + +struct never{}; + +template struct TypeList { + static constexpr auto size = sizeof...(Args); + static constexpr auto idxs() noexcept { + return std::make_index_sequence(); + } + template + constexpr bool operator==(T) const { + return std::is_same_v; + } + template constexpr bool operator!=(T) const { + return !std::is_same_v; + } + template static constexpr bool Contains() { + return (std::is_same_v || ...); + } +}; +template +constexpr TypeList PopFrontType(TypeList) {return {};} +template struct HeadTypeOf { + using type = never; +}; +template struct HeadTypeOf> { + using type = Head; +}; +template using HeadTypeOf_t = typename HeadTypeOf::type; + +template constexpr bool always_false = false; + +template struct is_assoc_container : std::false_type{}; +template struct is_index_container : std::false_type{}; + +namespace det { +template +using check_range_t = + std::void_t().begin()), decltype(std::declval().end())>; +} + +//! @warning NOT a fully-compliant container detector +template struct is_index_container().clear()), + decltype(std::declval().push_back(std::declval())), + decltype(std::declval()[std::declval()]), + det::check_range_t +>> : std::true_type{}; + +//! @warning NOT a fully-compliant container detector +template struct is_assoc_container().clear()), + decltype(std::declval()[std::declval()]), + det::check_range_t +>> : std::true_type{}; + +//! @warning NOT a fully-compliant container detector +template constexpr bool is_assoc_container_v = is_assoc_container::value; +//! @warning NOT a fully-compliant container detector +template constexpr bool is_index_container_v = is_index_container::value; + +template struct is_optional : std::false_type {}; +template struct is_optional> : std::true_type {}; +template constexpr bool is_optional_v = is_optional::value; + +template struct overloaded : Ts... {using Ts::operator()...;}; +template overloaded(Ts...) -> overloaded; + +template +struct [[nodiscard]] defer { + defer(Fn f) noexcept(std::is_nothrow_move_constructible_v) : fn(std::move(f)) {} + ~defer() noexcept(std::is_nothrow_invocable_v) {fn();} + Fn fn; +}; +template defer(Fn) -> defer; + +struct empty {}; +template struct non_void { + using type = T; +}; +template<> struct non_void { + using type = empty; +}; +template using non_void_t = typename non_void::type; + +template struct RipFunc; + +template struct RipFunc { + using Ret = R; + using Args = TypeList; + static constexpr auto ArgCount = sizeof...(A); + static constexpr bool IsMethod = false; +}; +template +struct RipFunc : RipFunc +{}; +template +struct RipFunc : RipFunc +{}; +template +struct RipFunc : RipFunc { + using Cls = C; + static constexpr bool IsMethod = true; +}; +template +struct RipFunc : RipFunc {}; +template struct RipFunc>> : + RipFunc +{}; +template using FuncRet_t = typename RipFunc::Ret; +template using FuncArgs_t = typename RipFunc::Args; +template constexpr auto FuncArgCount_v = RipFunc::ArgCount; + +} + +#endif //META_META_HPP diff --git a/include/meta/visit.hpp b/include/meta/visit.hpp new file mode 100644 index 0000000..2ebbcc2 --- /dev/null +++ b/include/meta/visit.hpp @@ -0,0 +1,40 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#ifndef META_VISIT_HPP +#define META_VISIT_HPP + +#include +#include "meta.hpp" + +namespace meta { + +template +constexpr decltype(auto) Visit(Var&& v, Fs&&...fs) { + return std::visit(overloaded{std::forward(fs)...}, std::forward(v)); +} + +} + +#endif //META_VISIT_HPP diff --git a/include/rc/rc.hpp b/include/rc/rc.hpp new file mode 100644 index 0000000..8d36c8c --- /dev/null +++ b/include/rc/rc.hpp @@ -0,0 +1,193 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#ifndef RC_HPP +#define RC_HPP + +#include +#include +#include + +namespace rc +{ + +template +struct Strong { + template + using if_compatible = std::enable_if_t, int>; + + Strong(T* data = nullptr) noexcept : data(data) { + if (data) AddRef(data); + } + Strong(const Strong& o) noexcept : data(o.data) { + if (data) AddRef(data); + } + explicit operator bool() const noexcept { + return data; + } + T* get() const noexcept { + return data; + } + T& operator*() const noexcept { + return *data; + } + T* Release() noexcept { + return std::exchange(data, nullptr); + } + template = 1> + Strong(Strong const& other) : Strong(other.data) {} + template = 1> + Strong(Strong && other) : data(std::exchange(other.data, nullptr)) {} + Strong(Strong&& o) noexcept : data(std::exchange(o.data, nullptr)) {} + Strong& operator=(const Strong& o) noexcept { + if (this != &o) { + if(data) Unref(data); + data = o.data; + if(data) AddRef(data); + } + return *this; + } + T* operator->() const noexcept { + return data; + } + Strong& operator=(Strong&& o) noexcept { + std::swap(data, o.data); + return *this; + } + ~Strong() { + if(data) Unref(data); + } +protected: + template friend struct Strong; + T* data; +}; + +struct DefaultBase { + friend void AddRef(DefaultBase* d) noexcept { + d->_refs.fetch_add(1, std::memory_order_acq_rel); + } + template + friend void Unref(T* d) noexcept { + if (d->_refs.fetch_sub(1, std::memory_order_acq_rel) == 1) { + delete d; + } + } +protected: + std::atomic _refs{0}; + std::atomic_bool _sync{false}; +}; + +struct WeakBlock : DefaultBase { + std::atomic data; + using DefaultBase::_sync; +}; + +//! Should not be multi-inherited! (multiple separate ref-counts to same object will appear) +struct SingleVirtualBase : public DefaultBase { + virtual ~SingleVirtualBase() = default; +}; + +struct VirtualBase : public virtual SingleVirtualBase {}; + +inline static void _lock(std::atomic_bool& lock) { + auto unlocked = false; + while(!lock.compare_exchange_strong(unlocked, true, std::memory_order_acq_rel)); +} + +inline static void _unlock(std::atomic_bool& lock) { + lock.store(false, std::memory_order_release); +} + +struct WeakableVirtual : VirtualBase { + friend void Unref(WeakableVirtual* d) noexcept { + _lock(d->_block->_sync); + if (d->_refs.fetch_sub(1, std::memory_order_acq_rel) == 1) { + if (d->_block) { + d->_block->data.store(nullptr, std::memory_order_release); + } + delete d; + } + _unlock(d->_block->_sync); + } + template + friend Strong GetWeak(T* d, int* offset) { + _make(d->_block, d->_sync, reinterpret_cast(d), offset); + return d->_block; + } + ~WeakableVirtual() override { + if (_block) { + _block->data.store(nullptr, std::memory_order_release); + } + } +protected: + static void _make(Strong& block, std::atomic_bool &lock, char* d, int* offset) { + _lock(lock); + if (!block) { + try {block = new WeakBlock;} + catch (...) { + _unlock(lock); + throw; + } + block->data.store(d, std::memory_order_release); + } + _unlock(lock); + if (offset) { + *offset = int(d - block->data.load(std::memory_order_acquire)); + } + } + Strong _block; +}; + +template +struct Weak { + template + using if_compatible = std::enable_if_t, int>; + + Weak() noexcept = default; + Weak(std::nullptr_t) noexcept {}; + + template = 1> + Weak(U* obj) : block(obj ? GetWeak(static_cast(obj), &offset) : nullptr) {} + template = 1> + Weak(Strong const& obj) : Weak(obj.get()) {} + + T* peek() const noexcept { + if (!block) return nullptr; + return reinterpret_cast(block->data.load(std::memory_order_acquire) + offset); + } + Strong lock() const noexcept { + if (!block) return nullptr; + _lock(block->_sync); + Strong r = reinterpret_cast(block->data.load(std::memory_order_acquire) + offset); + _unlock(block->_sync); + return r; + } +private: + int offset{}; + Strong block; +}; + +} + +#endif //RC_HPP diff --git a/include/rpcxx/client.hpp b/include/rpcxx/client.hpp new file mode 100644 index 0000000..f0ce53f --- /dev/null +++ b/include/rpcxx/client.hpp @@ -0,0 +1,126 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#ifndef RPCXX_CLIENT_HPP +#define RPCXX_CLIENT_HPP + +#include "common.hpp" +#include "protocol.hpp" +#include "transport.hpp" +#include "rpcxx/utils.hpp" + +namespace rpcxx +{ + +struct ClientTransportMissing : public std::exception { + const char* what() const noexcept override { + return "Client Tranport Not Set"; + } +}; + +struct Client +{ + using millis = rpcxx::millis; + Client(rc::Weak t = nullptr); + virtual ~Client(); + Client(const Client&) = delete; + Client(Client&&) = delete; + + struct [[nodiscard]] BatchGuard; + + BatchGuard StartBatch(); + rc::Weak GetTransport() noexcept; + rc::Weak SetTransport(rc::Weak t) noexcept; + template + Future RequestPack(Method method, const Args& pack) { + DefaultArena arena; + return RequestRaw(method, JsonView::From(pack, arena)); + } + template + void NotifyPack(string_view method, const Args& pack) { + DefaultArena arena; + return NotifyRaw(method, JsonView::From(pack, arena)); + } + template + Future RequestRaw(Method method, JsonView params) { + auto cb = Promise(); + auto result = cb.GetFuture().ThenSync([](JsonView res) -> Ret { + TraceFrame root; + auto frame = TraceFrame("(rpc.result)", root); + if constexpr (std::is_void_v) { + res.AssertType(t_null, frame); + } else { + return res.Get(frame); + } + }); + sendRequest(std::move(cb), method, params); + return result; + } + template + Future Request(Method method, const Args&...args) { + IntoParams wrap(args...); + return RequestRaw(method, wrap.Result); + } + void NotifyRaw(string_view method, JsonView params); + template + void Notify(string_view method, const Args&...args) { + IntoParams wrap(args...); + NotifyRaw(method, wrap.Result); + } + void SetPrefix(string prefix); +private: + void sendRequest(Promise cb, Method method, JsonView params); + void batchDone(); + IClientTransport& tr(); + + bool batchActive = false; + Batch currentBatch; + rc::Weak transport = nullptr; + string prefix = ""; +}; + +struct Client::BatchGuard { + BatchGuard(Client& cli) : cli(cli) {} + void Finish() { + if (std::exchange(valid, false)) + cli.batchDone(); + } + ~BatchGuard() noexcept(false) { + Finish(); + } +private: + Client& cli; + bool valid = true; +}; + +inline Client::BatchGuard Client::StartBatch() { + if (meta_Unlikely(std::exchange(batchActive, true))) { + throw std::runtime_error("Cannot StartBatch() while one is active"); + } + return {*this}; +} + +} //rpcxx + +#endif // RPCXX_CLIENT_HPP diff --git a/include/rpcxx/common.hpp b/include/rpcxx/common.hpp new file mode 100644 index 0000000..64e47df --- /dev/null +++ b/include/rpcxx/common.hpp @@ -0,0 +1,112 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#ifndef RPCXX_COMMON_HPP +#define RPCXX_COMMON_HPP + +#include +#include +#include +#include +#include +#include +#include + +#include "meta/meta.hpp" +#include "future/future.hpp" +#include "json_view/json_view.hpp" +#include "json_view/json.hpp" + +namespace rpcxx +{ + +using namespace meta; +using namespace fut; +using namespace jv; + +using std::optional; +using std::string; +using std::string_view; + +enum class ErrorCode : int64_t { + parse = -32700, // Parse error Invalid JSON was received by the server. + invalid_request = -32600, // Invalid Request The JSON sent is not a valid Request object. + method_not_found = -32601, // Method not found The method does not exist / is not available. + invalid_params = -32602, // Invalid params Invalid method parameter(s). + internal = -32603, //Internal error Internal JSON-RPC error. + server = -32099, //-32000 to -32099 Server error Reserved for implementation-defined server-errors. + server_end = -32001, //sentinel value for server implementation-defined errors +}; + +DESCRIBE_ATTRS(ErrorCode, EnumAsInteger) + +constexpr inline string_view PrintCode(ErrorCode code) { + using namespace std::string_view_literals; + switch (code) { + case ErrorCode::parse: return "Parse Error"; + case ErrorCode::invalid_request: return "Invalid Request"; + case ErrorCode::method_not_found: return "Method not found"; + case ErrorCode::invalid_params: return "Invalid Params"; + case ErrorCode::internal: return "Internal Error"; + default: return "User Defined"; + } +} + +using OptJson = std::optional; +using OptJsonView = std::optional; +template, + typename Alloc = std::allocator>> +using Map = std::map; + +enum class Protocol { + //see JSONRPC 2.0 spec + json_v2_compliant = 0, + // same as *_compliant, but without ("jsonrpc": "2.0") and "method" => "m", "params" => "p", etc + json_v2_minified = 1, +}; + +constexpr inline string_view PrintProto(Protocol proto) { + switch (proto) { + case Protocol::json_v2_compliant: return "json_v2_compliant"; + case Protocol::json_v2_minified: return "json_v2_minified"; + default: return ""; + } +} + +} //rpcxx + +template<> +struct jv::Convert { + using T = rpcxx::ErrorCode; + using underlying = std::underlying_type_t; + template + static JsonView DoIntoJson(const T& object, Alloc&) { + return JsonView(underlying(object)); + } + static void DoFromJson(T& out, JsonView json, TraceFrame const& frame) { + out = T(json.Get(frame)); + } +}; + +#endif //RPCXX_COMMON_HPP diff --git a/include/rpcxx/context.hpp b/include/rpcxx/context.hpp new file mode 100644 index 0000000..693cfcb --- /dev/null +++ b/include/rpcxx/context.hpp @@ -0,0 +1,50 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#ifndef RPCXX_CONTEXT_HPP +#define RPCXX_CONTEXT_HPP + +#include "common.hpp" +#include + +namespace rpcxx { + +struct Context : rc::DefaultBase { + Context(); + void CloneFrom(Context const& other); + std::any& SetValue(string_view name); + std::any* Value(string_view name); + template + T* Value(string_view name) { + return std::any_cast(Value(name)); + } +protected: + std::map> data; +}; + +using ContextPtr = rc::Strong; + +} + +#endif //RPCXX_CONTEXT_HPP diff --git a/include/rpcxx/exception.hpp b/include/rpcxx/exception.hpp new file mode 100644 index 0000000..4e7dd8e --- /dev/null +++ b/include/rpcxx/exception.hpp @@ -0,0 +1,58 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#ifndef RPCXX_EXCEPTION_HPP +#define RPCXX_EXCEPTION_HPP + +#include +#include +#include "common.hpp" +#include "describe/describe.hpp" + +namespace rpcxx +{ + +struct RpcException : public std::exception +{ + RpcException() noexcept = default; + RpcException(std::string_view msg, + ErrorCode code = ErrorCode::internal, + std::optional data = {}) : + code(code), + message(msg), + data(std::move(data)) + {} + ErrorCode code; + std::string message; + std::optional data; + const char* what() const noexcept override { + return message.c_str(); + } +}; + +DESCRIBE(RpcException, &_::code, &_::message, &_::data) + +} + +#endif //RPCXX_EXCEPTION_HPP diff --git a/include/rpcxx/handler.hpp b/include/rpcxx/handler.hpp new file mode 100644 index 0000000..78b2c7d --- /dev/null +++ b/include/rpcxx/handler.hpp @@ -0,0 +1,65 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#ifndef RPCXX_HANDLER_HPP +#define RPCXX_HANDLER_HPP + +#include "utils.hpp" +#include "context.hpp" +#include "protocol.hpp" + +namespace rpcxx { + +struct Request { + Arena &alloc; + Method method {}; + ContextPtr context {}; + JsonView params {}; +}; + +// Route is a json pointer +struct IHandler : rc::WeakableVirtual { + IHandler(); + template + void SetTransport(T* tr) {tr->SetHandler(this);} + rc::Weak GetRoute(string_view route); + void SetRoute(string_view route, rc::Weak handler); + void Handle(Request& request, Promise cb) noexcept; + void HandleNotify(Request& request); + virtual ~IHandler(); + IHandler(IHandler&&) = delete; +protected: + virtual void OnForward(string_view route, Request& req, Promise& cb) noexcept; + virtual void OnForwardNotify(string_view route, Request& req); + + virtual void DoHandle(Request& request, Promise cb) noexcept = 0; + virtual void DoHandleNotify(Request& request) = 0; +private: + struct Impl; + FastPimpl d; +}; + +} + +#endif //RPCXX_HANDLER_HPP diff --git a/include/rpcxx/protocol.hpp b/include/rpcxx/protocol.hpp new file mode 100644 index 0000000..4782428 --- /dev/null +++ b/include/rpcxx/protocol.hpp @@ -0,0 +1,202 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#ifndef RPCXX_PROTO_HPP +#define RPCXX_PROTO_HPP + +#include "common.hpp" +#include "rpcxx/exception.hpp" + +namespace rpcxx { + +template +struct Fields { + static constexpr string_view Id = "id"; + static constexpr string_view Method = proto == Protocol::json_v2_compliant ? "method" : "m"; + static constexpr string_view Params = proto == Protocol::json_v2_compliant ? "params" : "p"; + static constexpr string_view Result = proto == Protocol::json_v2_compliant ? "result" : "r"; + static constexpr string_view Error = proto == Protocol::json_v2_compliant ? "error" : "e"; +}; + +using millis = uint32_t; +constexpr auto NoTimeout = (std::numeric_limits::max)(); +constexpr auto Compliant = std::integral_constant{}; +constexpr auto Minified = std::integral_constant{}; + +template +struct NamedArg { + string_view name; + const T& value; +}; + +template struct is_named_arg : std::false_type {}; +template struct is_named_arg> : std::true_type {}; + +template +NamedArg Arg(string_view name, const T& ref) noexcept {return {name, ref};} + +struct Method { + Method() = default; + + explicit Method(string_view str, millis timeout) : + name(str), timeout(timeout) + {} + + string_view name; + millis timeout = NoTimeout; +}; + +template +struct Formatter { + using Fs = Fields; + Formatter() = default; + JsonView MakeRequest(JsonView id, string_view method, JsonView params) noexcept { + body = {{ + {Fs::Id, id}, + {Fs::Method, method}, + {Fs::Params, params}, + }}; + if constexpr (proto == Compliant) { + body[3] = {"jsonrpc", "2.0"}; + } + return JsonView(body.data(), proto == Compliant ? 4 : 3); + } + JsonView MakeNotify(string_view method, JsonView params) noexcept { + body = {{ + {Fs::Method, method}, + {Fs::Params, params}, + }}; + if constexpr (proto == Compliant) { + body[2] = {"jsonrpc", "2.0"}; + } + return JsonView(body.data(), proto == Compliant ? 3 : 2); + } + JsonView MakeResponce(JsonView id, JsonView resp) noexcept { + body = {{ + {Fs::Id, id}, + {Fs::Result, resp}, + }}; + if constexpr (proto == Compliant) { + body[2] = {"jsonrpc", "2.0"}; + } + return JsonView(body.data(), proto == Compliant ? 3 : 2); + } + JsonView MakeError(JsonView id, RpcException const& exception) noexcept { + errWrapper = {exception}; + body = {{ + {Fs::Id, id}, + {Fs::Error, errWrapper.View()}, + }}; + if constexpr (proto == Compliant) { + body[2] = {"jsonrpc", "2.0"}; + } + return JsonView(body.data(), proto == Compliant ? 3 : 2); + } +private: + StaticJsonView errWrapper; + std::array body; +}; + +struct UnpackedRequest { + string_view method; + JsonView params; +}; + +template +UnpackedRequest UnpackSingleRequest(JsonView req) { + JsonView* method = req.FindVal(Fields::Method); + if (meta_Unlikely(!method)) { + throw RpcException{"Missing 'method' field", ErrorCode::invalid_request}; + } + if (meta_Unlikely(!method->Is(jv::t_string))) { + JsonPair errobj[] = {{"was_type", method->GetTypeName()}}; + throw RpcException{"'method' field is not a string", ErrorCode::invalid_request, Json(errobj)}; + } + if (proto == Protocol::json_v2_compliant) { + if (auto tag = req.FindVal("jsonrpc"); !tag || !DeepEqual(*tag, "2.0")){ + throw RpcException{"'jsonrpc' field missing or != '2.0'", ErrorCode::invalid_request}; + } + } + auto foundParams = req.FindVal(Fields::Params); + return {method->GetStringUnsafe(), foundParams ? *foundParams : EmptyArray()}; +} + +template +JsonView UnpackSingleResponce(JsonView resp) { + if (meta_Unlikely(!resp.Is(jv::t_object))) { + jv::JsonPair data[]{{"was_type", resp.GetTypeName()}}; + throw RpcException("non-object responce", ErrorCode::parse, Json(data)); + } else if (JsonView* res = resp.FindVal(Fields::Result)) { + return *res; + } else if (JsonView* err = resp.FindVal(Fields::Error)) { + TraceFrame root; + throw err->Get({"(rpc.error)", root}); + } else { + throw RpcException("Missing 'result' or 'error' field", ErrorCode::parse); + } +} + +namespace det { + +template +static void populateArr(std::index_sequence, JsonView* arr, Arena& ctx, const Args&...args) { + ((void)(arr[Is] = JsonView::From(args, ctx)), ...); +} +template +static void populateObj(std::index_sequence, JsonPair* obj, Arena& ctx, const Args&...args) { + ((void)(obj[Is].key = args.name), ...); + ((void)(obj[Is].value = JsonView::From(args.value, ctx)), ...); +} + +} + +template +struct IntoParams { + static constexpr auto count = sizeof...(Args); + static constexpr auto namedCount = ((is_named_arg::value * 1) + ... + 0); + static_assert(!namedCount || namedCount == count, + "all arguments must be named OR all unnamed"); + JsonView Result; + IntoParams(const Args&...args) { + if constexpr (!count) { + Result = EmptyArray(); + } else if constexpr (namedCount) { + det::populateObj(std::make_index_sequence(), obj, alloc, args...); + Result = JsonView(obj, count); + } else { + det::populateArr(std::make_index_sequence(), arr, alloc, args...); + Result = JsonView(arr, count); + } + } +private: + union { + JsonView arr[count + 1]; + JsonPair obj[count + 1]; + }; + DefaultArena<512> alloc; +}; + +} + +#endif //RPCXX_PROTO_HPP diff --git a/include/rpcxx/rpcxx.hpp b/include/rpcxx/rpcxx.hpp new file mode 100644 index 0000000..92a85f3 --- /dev/null +++ b/include/rpcxx/rpcxx.hpp @@ -0,0 +1,37 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#ifndef RPCXX_HPP +#define RPCXX_HPP +#pragma once + +#include "client.hpp" +#include "server.hpp" +#include "future/gather.hpp" +#include "exception.hpp" +#include "json_view/dump.hpp" +#include "json_view/parse.hpp" + + +#endif //RPCXX_HPP diff --git a/include/rpcxx/server.hpp b/include/rpcxx/server.hpp new file mode 100644 index 0000000..f65bee5 --- /dev/null +++ b/include/rpcxx/server.hpp @@ -0,0 +1,314 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#pragma once +#ifndef RPCXX_SERVER_HPP +#define RPCXX_SERVER_HPP + +#include "rpcxx/utils.hpp" +#include "common.hpp" +#include "protocol.hpp" +#include "transport.hpp" +#include "meta/visit.hpp" +#include "handler.hpp" +#include + +namespace rpcxx +{ + +template +std::array NamesMap(Args&&...names) { + return {std::string{std::forward(names)}...}; +} + +template +struct PackParams { + static_assert(describe::is_described_v && !std::is_enum_v); + using type = T; +}; + +namespace detail { +template T get(JsonView json, unsigned idx, TraceFrame& frame); +template T get(JsonView json, string_view key, TraceFrame& frame); +template struct is_pack : std::false_type {}; +template struct is_pack> : std::true_type {}; +template struct is_pack> : std::true_type {}; +} + +struct MiddlewareContext { + string_view route; + Request& request; + + MiddlewareContext(Request& req) noexcept : request(req) {} + MiddlewareContext(MiddlewareContext const&) = delete; +}; + +struct ExceptionContext { + string_view route; + string_view method; + ContextPtr context; + std::exception* exception; + + ExceptionContext() noexcept = default; + ExceptionContext(ExceptionContext const&) = delete; +}; + +struct Server : IHandler +{ + using RouteMiddleware = MoveFunc; + using Middleware = MoveFunc; + //! return new to override for further handlers and client + using ExceptionHandler = MoveFunc; + using RouteExceptionHandler = MoveFunc; + + using Fallback = MoveFunc; + using NoNames = const int*; + + Server(); + Server(const Server&) = delete; + Server(Server&&) = delete; + virtual ~Server(); + + bool IsMethodRegistered(std::string_view method) const; + std::vector RegisteredMethods() const; + void Unregister(std::string_view method); + + void AddMiddleware(Middleware mw); + void AddRouteMiddleware(RouteMiddleware mw); + + ContextPtr CurrentContext(); + + template + void AddRouteExceptionHandler(Fn f) { + if constexpr (std::is_void_v>) { + doAddRouteExceptionHandler([MV(f)](string_view route, ExceptionContext& ectx) mutable { + f(route, ectx); + return nullptr; + }); + } else { + doAddRouteExceptionHandler(std::move(f)); + } + } + template + void AddExceptionHandler(Fn f) { + if constexpr (std::is_void_v>) { + doAddExceptionHandler([MV(f)](ExceptionContext& ectx) mutable { + f(ectx); + return nullptr; + }); + } else { + doAddExceptionHandler(std::move(f)); + } + } + + void SetFallback(Fallback handler); + + template + void Method(string_view method, Fn handler, Names names = {}) { + doRegisterMethod>(string{method}, std::move(handler), names, FuncArgs_t{}); + } + template + void Notify(string_view method, Fn handler, Names names = {}) { + doRegisterNotify(string{method}, std::move(handler), names, FuncArgs_t{}); + } +protected: + struct CallCtx { + Request& req; + Arena& alloc; + Promise& cb; + + bool IsMethodCall() const noexcept {return cb.IsValid();} + }; + template + void Notify(string_view method, Ret (User::*handler)(Args...), Names names = {}) { + Notify(method, [this, handler](Args...a) { + (static_cast(this)->*handler)(std::forward(a)...); + }, names); + } + template + void Notify(string_view method, Names names = {}) { + implCall(method, names, ::meta::FuncArgs_t{}); + } + template + void Method(string_view method, Names names = {}) { + implCall(method, names, ::meta::FuncArgs_t{}); + } + template + void Method(string_view method, Ret (User::*handler)(Args...), Names names = {}) { + Method(method, [this, handler](Args...a) -> Ret { + return (static_cast(this)->*handler)(std::forward(a)...); + }, names); + } + virtual fut::Executor* GetExecutor() const noexcept; +private: + void doAddRouteExceptionHandler(RouteExceptionHandler h); + void doAddExceptionHandler(ExceptionHandler h); + virtual void OnForward(string_view route, Request& req, Promise& cb) noexcept override final; + virtual void OnForwardNotify(string_view route, Request& req) override final; + void DoHandleNotify(Request& req) final; + void DoHandle(Request& req, Promise cb) noexcept override final; + + template + void implCall(string_view method, Names names, TypeList) { + using cls = typename ::meta::RipFunc::Cls; + auto impl = [this](Args...a) { + return (static_cast(this)->*handler)(std::forward(a)...); + }; + if constexpr (ismethod) { + Method(method, impl, names); + } else { + Notify(method, impl, names); + } + } + + template + void doRegisterNotify(std::string method, Fn handler, Names names, TypeList) + { + registerCall(method, [=, MV(names), MV(handler)](CallCtx& ctx){ + constexpr auto args = TypeList{}; + validateRequest(names, sizeof...(Args), ctx, true); + doCall(handler, ctx.req.params, names, args, args.idxs()); + }); + } + + template + struct Wrap { + string method; + Server* self; + Promise cb; + ContextPtr ctx; + void operator()(Result result) noexcept try { + if constexpr (std::is_void_v) { + result.get(); + cb(JsonView(nullptr)); + } else { + DefaultArena<512> alloc; + cb(JsonView::From(result.get(), alloc)); + } + } catch (std::exception& e) { + auto over = self->excHandlers("", method, std::move(ctx), e); + cb(over ? std::move(over) : std::current_exception()); + } + }; + + template + void doRegisterMethod(std::string method, Fn handler, Names names, TypeList) + { + if constexpr (fut::is_future::value) { + registerCall(method, [=, MV(names), MV(handler)](CallCtx& ctx){ + constexpr auto args = TypeList{}; + validateRequest(names, sizeof...(Args), ctx); + doCall(handler, ctx.req.params, names, args, args.idxs()) + .AtLast(GetExecutor(), Wrap{ + method, this, std::move(ctx.cb), ctx.req.context + }); + }); + } else { + registerCall(method, [=, MV(names), MV(handler)](CallCtx& ctx){ + constexpr auto args = TypeList{}; + validateRequest(names, args.size, ctx); + if constexpr (std::is_void_v) { + doCall(handler, ctx.req.params, names, args, args.idxs()); + ctx.cb(nullptr); + } else { + auto ret = doCall(handler, ctx.req.params, names, args, args.idxs()); + ctx.cb(JsonView::From(ret, ctx.alloc)); + } + }); + } + } + template + static auto doCall(Fn& fn, JsonView params, Names& names, TypeList, std::index_sequence) + { + TraceFrame root; + TraceFrame frame("", root); + (void)params; + if constexpr (std::is_same_v) { + return fn(detail::get>(params, Is, frame)...); + } else if constexpr (detail::is_pack::value) { + static_assert(sizeof...(Args) == 1); + static_assert(std::is_same_v..., typename Names::type>); + return fn(params.Get(frame)); + } else { + static constexpr auto namesCount = sizeof(Names) / sizeof(typename Names::value_type); + static_assert(sizeof...(Args) == namesCount, "Names Map must be the same size as Args Count"); + return fn(detail::get>(params, names[Is], frame)...); + } + } + using Call = MoveFunc; + void validateRequest(NoNames, unsigned nargs, CallCtx& ctx, bool notif = false) { + validateRequest(static_cast(nullptr), nargs, ctx, notif); + } + void validateRequest(const std::string* names, unsigned int nargs, CallCtx& ctx, bool notif = false); + template + void validateRequest(const std::array& n, unsigned, CallCtx& ctx, bool notif = false) { + validateRequest(n.data(), N, ctx, notif); + } + template + void validateRequest(PackParams, unsigned, CallCtx& ctx, bool notif = false) { + constexpr auto desc = describe::Get(); + constexpr auto rawNames = describe::field_names(); + constexpr auto count = desc.fields_count; + static const std::array names = prepNames(rawNames); + validateRequest(names.data(), count, ctx, notif); + } + template + static std::array prepNames(const Src& s) { + std::array res; + for (size_t i{}; i < N; ++i) {res[i] = s[i];} + return res; + } + void runMiddlewares(Request &req); + void runRouteMiddlewares(string_view route, Request &req); + std::exception_ptr excHandlers(string_view route, string_view method, ContextPtr ctx, std::exception& exc) noexcept; + void registerCall(std::string& name, Call call); + void doHandle(uint32_t internal, JsonView req, uint32_t timeout); + void handleExtension(CallCtx &ctx); + + struct Impl; + FastPimpl d; +}; + +namespace detail { +template T get(JsonView json, unsigned idx, TraceFrame& frame) { + TraceFrame next(idx, frame); + if constexpr (is_optional_v) { + return json.Value(idx, JsonView{}, frame).Get(next); + } else { + return json.At(idx, next).Get(next); + } +} +template T get(JsonView json, string_view key, TraceFrame& frame) { + TraceFrame next(key, frame); + if constexpr (is_optional_v) { + return json.Value(key, JsonView{}, frame).Get(next); + } else { + return json.At(key, frame).Get(next); + } +} +} + +} //rpcxx + +#endif // RPCXX_SERVER_HPP diff --git a/include/rpcxx/transport.hpp b/include/rpcxx/transport.hpp new file mode 100644 index 0000000..5e06edc --- /dev/null +++ b/include/rpcxx/transport.hpp @@ -0,0 +1,112 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#ifndef RPCXX_TRANSPORT_HPP +#define RPCXX_TRANSPORT_HPP + +#include "common.hpp" +#include "protocol.hpp" +#include "handler.hpp" + +namespace rpcxx +{ + +struct RequestNotify { + string method; + Json params; +}; + +struct RequestMethod : RequestNotify { + millis timeout; + Promise cb; +}; + +struct Batch { + std::vector notifs; + std::vector methods; +}; + +struct IClientTransport : IHandler { + virtual void SendBatch(Batch batch) = 0; + virtual void SendNotify(string_view method, JsonView params) = 0; + virtual void SendMethod(Method method, JsonView params, Promise cb) = 0; + virtual ~IClientTransport() = default; +protected: + void DoHandle(Request& req, Promise cb) noexcept override; + void DoHandleNotify(Request& req) noexcept override; +}; + +struct ForwardToHandler final : IClientTransport { + ForwardToHandler(rc::Weak h = nullptr) noexcept : h(h) {} + rc::Weak SetHandler(rc::Weak handler); +protected: + rc::Weak h; + void SendBatch(Batch batch) override; + void SendNotify(string_view method, JsonView params) override; + void SendMethod(Method method, JsonView params, Promise cb) override; +}; + +//! Bidirectional transport for both server (any IHandler) and Client +struct IAsyncTransport : IClientTransport { + IAsyncTransport(Protocol proto, rc::Weak h = nullptr); + + rc::Weak SetHandler(rc::Weak handler); + void ClearAllPending(); + void CheckTimeouts(); + void Receive(JsonView msg, ContextPtr ctx); + void Receive(JsonView msg); + + ~IAsyncTransport() override; + IAsyncTransport(const IAsyncTransport&) = delete; + IAsyncTransport(IAsyncTransport&&) = delete; +protected: + // These should be used/implemented by subclass + virtual void Send(JsonView msg) = 0; + virtual void TimeoutHappened(string_view method, Promise& target); + virtual void NoServerFound(); +private: + void SendBatch(Batch batch) final; + void SendNotify(string_view method, JsonView params) final; + void SendMethod(Method method, JsonView params, Promise cb) final; + + struct Impl; + FastPimpl d; +}; + +struct Transport final : IAsyncTransport { + using Sender = MoveFunc; + + Transport(Protocol proto = Protocol::json_v2_compliant); + + void OnReply(Sender callback); +protected: + // These should be used/implemented by subclass + void Send(JsonView msg) override; + + Sender sender; +}; + +} + +#endif //RPCXX_TRANSPORT_HPP diff --git a/include/rpcxx/utils.hpp b/include/rpcxx/utils.hpp new file mode 100644 index 0000000..64c054a --- /dev/null +++ b/include/rpcxx/utils.hpp @@ -0,0 +1,77 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#ifndef RPCXX_UTILS_HPP +#define RPCXX_UTILS_HPP + +#include +#include +#include +#include + +namespace rpcxx +{ + +template +struct FastPimpl { + template + FastPimpl(Args&&...a) noexcept(std::is_nothrow_constructible_v) { + static_assert(alignof(T) <= align); + static_assert(sizeof(T) <= size); + new (buff) T(std::forward(a)...); + } + T* data() noexcept { + return std::launder(reinterpret_cast(buff)); + } + const T* data() const noexcept { + return std::launder(reinterpret_cast(buff)); + } + T* operator->() noexcept { + return data(); + } + const T* operator->() const noexcept { + return data(); + } + FastPimpl(const FastPimpl& o) noexcept(std::is_nothrow_copy_constructible_v) { + new (buff) T(*o.data()); + } + FastPimpl(FastPimpl&& o) noexcept(std::is_nothrow_move_constructible_v) { + new (buff) T(std::move(*o.data())); + } + FastPimpl& operator=(const FastPimpl& o) noexcept(std::is_nothrow_copy_assignable_v) { + *data() = *o.data(); + } + FastPimpl& operator=(FastPimpl& o) noexcept(std::is_nothrow_move_assignable_v) { + *data() = std::move(*o.data()); + } + ~FastPimpl() { + data()->~T(); + } +protected: + alignas(align) char buff[size]; +}; + +} + +#endif //RPCXX_UTILS_HPP diff --git a/qtc_helper.py b/qtc_helper.py new file mode 100644 index 0000000..28811ba --- /dev/null +++ b/qtc_helper.py @@ -0,0 +1,204 @@ +# This file is a part of RPCXX project + +""" +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +from dumper import Children, SubItem, UnnamedSubItem, DumperBase +from utils import DisplayFormat, TypeCode + +def _printNull(d, value): + d.putValue("") + +def _printBool(d, value): + num = value["d"]["boolean"].integer() + if num == 1: d.putValue("true") + elif num == 0: d.putValue("false") + else: d.putValue("" % num) + +def _printFloat(d, value): + try: + d.putValue(value["d"]["number"].floatingPoint()) + except Exception as e: + d.putValue("" % str(e)) + +def _printInt(d, value): + try: + d.putValue(value["d"]["integer"].integer()) + except Exception as e: + d.putValue("" % str(e)) + +def _printUint(d, value): + try: + d.putValue(value["d"]["uinteger"].integer()) + except Exception as e: + d.putValue("" % str(e)) + +def _printBin(d, value): + size = value["size"].integer() + if not size: + d.putValue("") + else: + d.putArrayData(value["d"]["binary"].pointer(), size, d.lookupType("uint8_t").stripTypedefs()) + +def _printStr(d, value): + size = value["size"].integer() + if not size: + d.putValue("") + d.putCharArrayHelper(value["d"]["string"].pointer(), size, d.createType("char"), d.currentItemFormat()) + +def _printDiscard(d, value): + d.putValue("") + d.putExpandable() + if d.isExpanded(): + with Children(d): + with SubItem(d, ""): + _printStr(d, value) + +def attempt(d, func, name): + try: + return func() + except Exception as e: + d.putValue("%s: %s" % (name, str(e))) + +def _printArray(d, value): + size = attempt(d, lambda: value["size"].integer(), "arr size") + ptr = attempt(d, lambda: value["d"]["array"].pointer(), "arr ptr") + if size is None: return + if ptr is None: return + d.putValue("" % size) + d.putExpandable() + if d.isExpanded(): + try: + d.putArrayData(ptr, size, d.createType("jv::JsonView")) + except Exception as e: + d.putValue("" % (size, ptr, e)) + +def _printObject(d, value): + size = attempt(d, lambda: value["size"].integer(), "obj size") + ptr = attempt(d, lambda: value["d"]["object"].pointer(), "obj ptr") + if size is None: return + if ptr is None: return + d.putValue("" % size) + d.putExpandable() + if d.isExpanded(): + try: + d.putArrayData(ptr, size, d.lookupType("jv::JsonPair")) + except Exception as e: + d.putValue("" % (size, ptr, e)) + +def _printInvalid(d, value): + d.putValue("") + +def _printCustom(d, value): + d.putValue(" ptr(%i) size(%i)" % value["d"]["custom"].pointer() % value["size"].integer()) + +_jsonPrinters = { + 0: _printNull, + 1: _printBin, + 2: _printBool, + 4: _printFloat, + 8: _printStr, + 16: _printInt, + 32: _printUint, + 64: _printArray, + 128: _printObject, + 256: _printDiscard, + 512: _printCustom, +} + +def qdump__jv__JsonView(d, value): + printer = _jsonPrinters.get(value["data"]["type"].integer(), _printInvalid) + try: + printer(d, value["data"]) + except Exception as e: + d.putValue("<%s => %s>" % (printer.__name__, str(e))) + +def qdump__jv__Json(d, value): + qdump__jv__JsonView(d, value["view"]) + +_mutPrinters = { + 0: lambda d, v: d.putValue(""), + 1: lambda d, v: d.putItem(v["bin"].dereference()), + 2: lambda d, v: d.putItem(v["boolean"]), + 4: lambda d, v: d.putItem(v["number"]), + 8: lambda d, v: d.putItem(v["str"].dereference()), + 16: lambda d, v: d.putValue(v["integer"].integer()), + 32: lambda d, v: d.putValue(v["uinteger"].integer()), + 64: lambda d, v: d.putItem(v["arr"].dereference()), + 128: lambda d, v: d.putItem(v["obj"].dereference()), +} + +def qdump__jv__BasicMutJson(d, value): + t = value["data"]["type"].integer() + _mutPrinters.get(t, _printInvalid)(d, value["data"]) + +def qdump__jv__JsonPointer(d, value): + size = value["size"].integer() + ptr = value["keys"].pointer() + d.putValue("JsonPointer[%i]" % size) + d.putExpandable() + if d.isExpanded(): + d.putArrayData(ptr, size, d.lookupType("jv::JsonKey")) + +def qdump__rpcxx__FastPimpl(d, value): + t = value.type[0] + addr = value["buff"].address() + d.putAddress(addr) + d.putValue('@0x%x' % addr) + d.putExpandable() + if d.isExpanded(): + with Children(d): + d.putFields(d.createValue(addr, t)) + +# TODO: +# def qdump__rc__Weak(d, value): +# t = value.type[0] +# data = value["block"]["data"] +# if not data.address(): +# d.putValue("") +# else: +# d.putTypedPointer(data["data"].address(), t) + +def qdump__std__basic_string_view(d, value): + ch_t = value.type[0] + if d.isMsvcTarget(): + data = value["_Mydata"] + size = value["_Mysize"] + else: + data = value["_M_str"] + size = value["_M_len"] + d.putCharArrayHelper( + data.pointer(), size.integer(), ch_t, d.currentItemFormat() + ) + +from stdtypes import qdumpHelper_std__string + +def qdump__std__filesystem__path(d, value): + str = value["_M_pathname"] + if d.isMsvcTarget(): + ch = d.createType("wchar_t") + else: + ch = d.createType("char") + qdumpHelper_std__string(d, str, ch, d.currentItemFormat()) + +def qdump__std__filesystem____cxx11__path(d, value): + qdump__std__filesystem__path(d, value) diff --git a/src/future.cpp b/src/future.cpp new file mode 100644 index 0000000..d31ef47 --- /dev/null +++ b/src/future.cpp @@ -0,0 +1,104 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#include "future/future.hpp" + +#include + +fut::Base::~Base() { + if (auto notif = notify.exchange(nullptr)) { + notif(this, false); + } + while (chain && chain->_refs.load(std::memory_order_acquire) == 1) { + auto next = std::move(chain->chain); + chain = {}; + chain = std::move(next); + } +} + +namespace { +struct NotifyCtx { + fut::Base::Notify notif; + rc::Strong data; + + NotifyCtx(fut::Base::Notify notif, rc::Strong data) noexcept : + notif(notif), data(data) + {} + NotifyCtx(const NotifyCtx&) noexcept = delete; + NotifyCtx(NotifyCtx&& o) noexcept : + notif(std::exchange(o.notif, nullptr)), + data(std::exchange(o.data, nullptr)) + {} + + ~NotifyCtx() { + if (notif) notif(data.get(), false); + } +}; +} + +void fut::d::continueChain(rc::Strong data, bool once) noexcept +{ + do { + auto fs = data->flags.load(std::memory_order_acquire); + if (!(fs & Base::fullfilled) || fs & Base::in_continue) { + break; + } + auto notif = data->notify.exchange(nullptr); + if (!notif) { + break; + } + auto exec = data->exec; + if (exec) { + data->flags.fetch_or(Base::in_continue); + auto status = exec->Execute([ctx = NotifyCtx{notif, data}]() mutable { + auto n = std::exchange(ctx.notif, nullptr); + auto d = std::exchange(ctx.data, nullptr); + assert(n && d && "Executor::Job executed more than once"); + n(d.get(), true); + }); + data->flags.fetch_and(~Base::in_continue); + if (status != Executor::Done) { + break; // Stop chain + } + } else { + data->flags.fetch_or(Base::in_continue); + notif(data.get(), true); + data->flags.fetch_and(~Base::in_continue); + } + auto ch = std::move(data->chain); + data = std::move(ch); + } while(data && !once); +} + +fut::Future fut::Resolved() { + fut::Promise prom; + prom(); + return prom.GetFuture(); +} + +void fut::d::onLastExc() +{ + fputs("-- Future.AtLast handler exception thrown\n", stderr); + std::terminate(); +} diff --git a/src/json_view/algo.cpp b/src/json_view/algo.cpp new file mode 100644 index 0000000..f50d8ed --- /dev/null +++ b/src/json_view/algo.cpp @@ -0,0 +1,215 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#include "json_view/algo.hpp" +#include "json_view/pointer.hpp" +#include "meta/visit.hpp" +#include + +using namespace jv; + +JsonView jv::Flatten(JsonView src, Arena& alloc, unsigned int depth) +{ + DepthError::Check(depth); + src.AssertType(t_object); + ArenaVector result(alloc); + DeepIterate(src, alloc, + [&](JsonPointer ptr, JsonView item){ + result.push_back(JsonPair{CopyString(ptr.Join(), alloc), Copy(item, alloc)}); + }, + depth); + return JsonView{result.data(), unsigned(result.size())}; +} + +namespace { +template +JsonView doCopy(JsonView src, Arena& alloc, unsigned int depth) { + DepthError::Check(depth--); + switch (src.GetType()) { + case t_binary: { + if (flags & NoCopyStrings) { + return src; + } else { + return JsonView::Binary(CopyString(src.GetBinary(), alloc)); + } + } + case t_string: { + if (flags & NoCopyStrings) { + return src; + } else { + return JsonView(CopyString(src.GetString(), alloc)); + } + } + case t_array: { + auto arr = MakeArrayOf(src.GetUnsafe().size, alloc); + for (auto i = 0u; i < src.GetUnsafe().size; ++i) { + arr[i] = doCopy(src.GetUnsafe().d.array[i], alloc, depth); + } + return JsonView{arr, src.GetUnsafe().size}.WithFlagsUnsafe(src.GetFlags()); + } + case t_object: { + auto obj = MakeObjectOf(src.GetUnsafe().size, alloc); + for (auto i = 0u; i < src.GetUnsafe().size; ++i) { + auto& curr = src.GetUnsafe().d.object[i]; + if (flags & NoCopyStrings) { + obj[i].key = curr.key; + } else { + obj[i].key = CopyString(curr.key, alloc); + } + obj[i].value = doCopy(curr.value, alloc, depth); + } + return JsonView{obj, src.GetUnsafe().size}.WithFlagsUnsafe(src.GetFlags()); + } + default: { + return src; + } + } +} +} + +JsonView jv::Copy(JsonView src, Arena& alloc, unsigned int depth, unsigned flags) +{ + switch (flags) { + case NoCopyStrings: { + return doCopy(src, alloc, depth); + } + case NoCopyBinary: { + return doCopy(src, alloc, depth); + } + default: { + return doCopy<0>(src, alloc, depth); + } + } +} + +bool jv::DeepEqual(JsonView lhs, JsonView rhs, unsigned int depth, double margin) +{ + DepthError::Check(depth--); + auto& data = lhs.GetUnsafe(); + auto& other = rhs.GetUnsafe(); + constexpr auto numType = t_signed | t_unsigned; + switch (data.type) { + case t_signed: + case t_unsigned: { + if (!(other.type & numType)) { + if (other.type == t_number) { + if (data.type == t_signed) { + return std::abs(other.d.number - data.d.integer) < margin; + } else { + return std::abs(other.d.number - data.d.uinteger) < margin; + } + } + return false; + } + if (data.d.integer == other.d.integer) { + if (data.type != other.type) { + // bits are same, but signess is not. same bitpattern may mean + // that one is a big unsigned, while other is < 0 + // in such case both values as ints would be minus + return data.d.integer >= 0 && other.d.integer >= 0; + } else { + return true; + } + } else { + return false; + } + } + case t_array: { + if (other.type != data.type) + return false; + if (data.size != other.size) + return false; + for (auto i = 0u; i < data.size; ++i) { + if (!DeepEqual(data.d.array[i], other.d.array[i], depth, margin)) + return false; + } + return true; + } + case t_number: { + if (other.type != data.type) { + if (other.type & numType) { + if (other.type == t_signed) { + return std::abs(data.d.number - other.d.integer) < margin; + } else { + return std::abs(data.d.number - other.d.uinteger) < margin; + } + } + return false; + } + if (meta_Unlikely(std::isnan(data.d.number))) { + return std::isnan(other.d.number); + } + if (meta_Unlikely(std::isinf(data.d.number))) { + return std::isinf(other.d.number); + } + auto diff = std::abs(data.d.number - other.d.number); + auto res = diff < margin; + return res; + } + case t_object: { + if (other.type != data.type) + return false; + if (data.size != other.size) + return false; + bool bothSorted = lhs.HasFlag(f_sorted) && rhs.HasFlag(f_sorted); + if (meta_Unlikely(bothSorted)) { + for (auto i = 0u; i < data.size; ++i) { + if (data.d.object[i].key != other.d.object[i].key) + return false; + if (!DeepEqual(data.d.object[i].value, other.d.object[i].value, depth, margin)) { + return false; + } + } + } else { + for (auto i = 0u; i < data.size; ++i) { + if (auto v = rhs.FindVal(data.d.object[i].key)) { + auto our = data.d.object[i].value; + if (!DeepEqual(our, *v, depth, margin)) { + return false; + } + } else { + return false; + } + } + } + return true; + } + case t_boolean: { + if (other.type != data.type) + return false; + return data.d.boolean == other.d.boolean; + } + case t_binary: + case t_string: { + if (other.type != data.type) + return false; + if (data.size != other.size) + return false; + return lhs.GetStringUnsafe() == rhs.GetStringUnsafe(); + } + default: { + return other.type == data.type; + } + } +} diff --git a/src/json_view/alloc_adapter.hpp b/src/json_view/alloc_adapter.hpp new file mode 100644 index 0000000..b3c9a0b --- /dev/null +++ b/src/json_view/alloc_adapter.hpp @@ -0,0 +1,51 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#pragma once + +#include +#include +#include "json_view/alloc.hpp" + +namespace { + +struct RapidArenaAllocator { + static constexpr bool kNeedFree = false; + jv::Arena* alloc; + void* Malloc(size_t size) { + (void)kNeedFree; + return alloc->Allocate(size, alignof(void*)); + } + static void Free(void*) {} + void* Realloc(void* data, size_t was, size_t cap) { + if (!cap) { + return nullptr; + } + auto ptr = Malloc(cap); + memcpy(ptr, data, was); + return ptr; + } +}; + +} diff --git a/src/json_view/dump_json.cpp b/src/json_view/dump_json.cpp new file mode 100644 index 0000000..1e80820 --- /dev/null +++ b/src/json_view/dump_json.cpp @@ -0,0 +1,144 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#include "json_view/dump.hpp" +#include +#include "rapidjson/internal/strtod.h" +#include "rapidjson/writer.h" +#include "rapidjson/prettywriter.h" +#include "alloc_adapter.hpp" + +using namespace jv; +using namespace std::string_view_literals; +using namespace rapidjson; + +namespace { + +struct Stream { + using Ch = char; + membuff::Out &out; + void PutUnsafe(char ch) { + out.Write(ch); + } + void Put(char ch) { + out.Write(ch); + } + void Flush() {} +}; + +constexpr auto wrFlags = kWriteNanAndInfNullFlag; + +using Pretty = PrettyWriter, UTF8<>, RapidArenaAllocator, wrFlags>; +using Basic = Writer, UTF8<>, RapidArenaAllocator, wrFlags>; + +template +struct Override : public Target { + using Target::Target; + void DoRawNumber(const char* beg, const char* end) { + if constexpr (std::is_same_v) { + this->PrettyPrefix(kNumberType); + } else { + this->Prefix(kNumberType); + } + this->os_->out.Write(beg, end - beg); + } +}; + +template +static void visit(JsonView json, unsigned depth, Writer& wr) { + DepthError::Check(depth--); + switch (json.GetType()) { + case t_array: { + wr.StartArray(); + for (auto i: json.Array(false)) { + visit(i, depth, wr); + } + wr.EndArray(json.GetUnsafe().size); + break; + } + case t_object:{ + wr.StartObject(); + for (auto [key, v]: json.Object(false)) { + assert(!key.empty()); + wr.Key(key.data(), key.size(), false); + visit(v, depth, wr); + } + wr.EndObject(json.GetUnsafe().size); + break; + } + case t_number: { + wr.Double(json.GetUnsafe().d.number); + break; + } + case t_signed: { + char buff[50]; + auto [ptr, ec] = std::to_chars(std::begin(buff), std::end(buff), json.GetUnsafe().d.integer); + wr.DoRawNumber(buff, ptr); + break; + } + case t_unsigned: { + char buff[50]; + auto [ptr, ec] = std::to_chars(std::begin(buff), std::end(buff), json.GetUnsafe().d.uinteger); + wr.DoRawNumber(buff, ptr); + break; + } + case t_null: { + wr.Null(); + break; + } + case t_string: { + auto str = json.GetStringUnsafe(); + if (auto s = str.data()) { + wr.String(s, str.size()); + } else { + wr.String("", 0); + } + break; + } + case t_boolean: { + wr.Bool(json.GetUnsafe().d.boolean); + break; + } + default: { + break; + } + } +} + +} + +void jv::DumpJsonInto(membuff::Out &out, JsonView json, DumpOptions opts) +{ + Stream stream{out}; + DefaultArena arena; + RapidArenaAllocator alloc{&arena}; + if (opts.pretty) { + Override writer(stream, &alloc, 32); + writer.SetIndent(opts.indentChar, opts.indent); + visit(json, opts.maxDepth, writer); + } else { + Override writer(stream, &alloc); + visit(json, opts.maxDepth, writer); + } +} diff --git a/src/json_view/dump_msgpack.cpp b/src/json_view/dump_msgpack.cpp new file mode 100644 index 0000000..d20114b --- /dev/null +++ b/src/json_view/dump_msgpack.cpp @@ -0,0 +1,223 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#define NOMINMAX +#include "json_view/dump.hpp" +#include "endian.hpp" + +using namespace jv; + +namespace { +template +static auto toBig(const T& raw) noexcept +{ + union { + std::array res; + uint_for_t uint; + } helper; + memcpy(helper.res.data(), &raw, sizeof(T)); + if constexpr (endian::host != endian::big) { + if constexpr (sizeof(T) == 1) { + //noop + } else if constexpr (sizeof(T) == 2) { + helper.uint = htobe16(helper.uint); + } else if constexpr (sizeof(T) == 4) { + helper.uint = htobe32(helper.uint); + } else if constexpr (sizeof(T) == 8) { + helper.uint = htobe64(helper.uint); + } else { + static_assert(always_false, "unsupported"); + } + } + return helper.res; +} +} + +static void writeType(uint8_t what, membuff::Out &out) { + out.Write(what); +} + +[[maybe_unused]] +static void write(std::string_view what, membuff::Out &out){ + out.Write(what); +} + +template +static void write(T what, membuff::Out &out){ + auto temp = toBig(what); + out.Write(temp.data(), temp.size()); +}; + +using std::numeric_limits; + +static inline void writeString(string_view sv, membuff::Out &out) +{ + if (sv.size() <= 0b11111) { + writeType(uint8_t(0b10100000 | sv.size()), out); + } else if (sv.size() <= numeric_limits::max()) { + writeType(0xd9, out); + write(uint8_t(sv.size()), out); + } else if (sv.size() <= numeric_limits::max()) { + writeType(0xda, out); + write(uint16_t(sv.size()), out); + } else { + writeType(0xdb, out); + write(uint32_t(sv.size()), out); + } + out.Write(sv); +} + +static inline void writeNegInt(int64_t i, membuff::Out& out) { + if (i >= -32) { + writeType(int8_t(i), out); + } else if (i >= numeric_limits::min()) { + writeType(0xD0, out); + write(int8_t(i), out); + } else if (i >= numeric_limits::min()) { + writeType(0xD1, out); + write(int16_t(i), out); + } else if (i >= numeric_limits::min()) { + writeType(0xD2, out); + write(int32_t(i), out); + } else { + writeType(0xD3, out); + write(int64_t(i), out); + } +} + +static inline void writePosInt(uint64_t i, membuff::Out& out) { + if (i < 128) { + writeType(uint8_t(i), out); + } else if (i <= numeric_limits::max()) { + writeType(0xCC, out); + write(uint8_t(i), out); + } else if (i <= numeric_limits::max()) { + writeType(0xCD, out); + write(uint16_t(i), out); + } else if (i <= numeric_limits::max()) { + writeType(0xCE, out); + write(uint32_t(i), out); + } else { + writeType(0xCF, out); + write(uint64_t(i), out); + } +} + +#if !defined(__clang__) && !defined(_WIN32) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wuseless-cast" +#endif + +void jv::DumpMsgPackInto(membuff::Out &out, JsonView json, DumpOptions opts) +{ + DepthError::Check(opts.maxDepth); + switch (json.GetType()) { + case t_array: { + auto sz = json.GetUnsafe().size; + if (sz <= 0b1111) { + writeType(uint8_t(0b10010000 | sz), out); + } else if (sz <= numeric_limits::max()) { + writeType(0xdc, out); + write(uint16_t(sz), out); + } else { + writeType(0xdd, out); + write(uint32_t(sz), out); + } + opts.maxDepth--; + for (auto v: json.Array()) { + DumpMsgPackInto(out, v, opts); + } + break; + } + case t_object: { + auto sz = json.GetUnsafe().size; + if (sz <= 0b1111) { + writeType(uint8_t(0b10000000 | sz), out); + } else if (sz <= numeric_limits::max()) { + writeType(0xde, out); + write(uint16_t(sz), out); + } else { + writeType(0xdf, out); + write(uint32_t(sz), out); + } + opts.maxDepth--; + for (auto [k, v]: json.Object()) { + writeString(k, out); + DumpMsgPackInto(out, v, opts); + } + break; + } + case t_null: { + return writeType(uint8_t(0xc0), out); + } + case t_boolean: { + return writeType(json.GetUnsafe().d.boolean ? uint8_t(0xc3) : uint8_t(0xc2), out); + } + case t_number: { + writeType(uint8_t(0xcb), out); + return write(json.GetUnsafe().d.number, out); + } + case t_signed: { + auto i = json.GetUnsafe().d.integer; + if (i < 0) { + writeNegInt(i, out); + } else { + writePosInt(uint64_t(i), out); + } + break; + } + case t_unsigned: { + writePosInt(json.GetUnsafe().d.uinteger, out); + break; + } + case t_binary: { + auto bin = json.GetUnsafe().d.binary; + auto sz = json.GetUnsafe().size; + if (sz <= numeric_limits::max()) { + writeType(0xc4, out); + write(uint8_t(sz), out); + } else if (sz <= numeric_limits::max()) { + writeType(0xc5, out); + write(uint16_t(sz), out); + } else { + writeType(0xc6, out); + write(uint32_t(sz), out); + } + auto sv = std::string_view{reinterpret_cast(bin), sz}; + write(sv, out); + break; + } + case t_string: { + writeString(json.GetStringUnsafe(), out); + break; + } + default: { + break; + } + } +} + +#if !defined(__clang__) && !defined(_WIN32) +#pragma GCC diagnostic pop +#endif diff --git a/src/json_view/endian.hpp b/src/json_view/endian.hpp new file mode 100644 index 0000000..4b74e07 --- /dev/null +++ b/src/json_view/endian.hpp @@ -0,0 +1,235 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#pragma once +#include +#include +// "License": Public Domain +// I, Mathias Panzenböck, place this file hereby into the public domain. Use it at your own risk for whatever you like. +// In case there are jurisdictions that don't support putting things in the public domain you can also consider it to +// be "dual licensed" under the BSD, MIT and Apache licenses, if you want to. This code is trivial anyway. Consider it +// an example on how to get the endian conversion functions on different platforms. + +template +struct uint_for_bytes {}; +template<> +struct uint_for_bytes<1> { + using type = uint8_t; +}; +template<> +struct uint_for_bytes<2> { + using type = uint16_t; +}; +template<> +struct uint_for_bytes<4> { + using type = uint32_t; +}; +template<> +struct uint_for_bytes<8> { + using type = uint64_t; +}; +template +using uint_for_t = typename uint_for_bytes::type; + +#ifndef PORTABLE_ENDIAN_H__ +#define PORTABLE_ENDIAN_H__ + +#if (defined(_WIN16) || defined(_WIN32) || defined(_WIN64)) && !defined(__WINDOWS__) + +# define __WINDOWS__ + +#endif + +#if defined(__linux__) || defined(__CYGWIN__) + +# include + +#elif defined(__APPLE__) + +# include + +# define htobe16(x) OSSwapHostToBigInt16(x) +# define htole16(x) OSSwapHostToLittleInt16(x) +# define be16toh(x) OSSwapBigToHostInt16(x) +# define le16toh(x) OSSwapLittleToHostInt16(x) + +# define htobe32(x) OSSwapHostToBigInt32(x) +# define htole32(x) OSSwapHostToLittleInt32(x) +# define be32toh(x) OSSwapBigToHostInt32(x) +# define le32toh(x) OSSwapLittleToHostInt32(x) + +# define htobe64(x) OSSwapHostToBigInt64(x) +# define htole64(x) OSSwapHostToLittleInt64(x) +# define be64toh(x) OSSwapBigToHostInt64(x) +# define le64toh(x) OSSwapLittleToHostInt64(x) + +# define __BYTE_ORDER BYTE_ORDER +# define __BIG_ENDIAN BIG_ENDIAN +# define __LITTLE_ENDIAN LITTLE_ENDIAN +# define __PDP_ENDIAN PDP_ENDIAN + +#elif defined(__OpenBSD__) + +# include + +# define __BYTE_ORDER BYTE_ORDER +# define __BIG_ENDIAN BIG_ENDIAN +# define __LITTLE_ENDIAN LITTLE_ENDIAN +# define __PDP_ENDIAN PDP_ENDIAN + +#elif defined(__NetBSD__) || defined(__FreeBSD__) || defined(__DragonFly__) + +# include + +# define be16toh(x) betoh16(x) +# define le16toh(x) letoh16(x) + +# define be32toh(x) betoh32(x) +# define le32toh(x) letoh32(x) + +# define be64toh(x) betoh64(x) +# define le64toh(x) letoh64(x) + +#elif defined(__WINDOWS__) + +# include +# ifdef __GNUC__ +# include +# endif + +# if BYTE_ORDER == LITTLE_ENDIAN +# define htobe16(x) htons(x) +# define htole16(x) (x) +# define be16toh(x) ntohs(x) +# define le16toh(x) (x) + +# define htobe32(x) htonl(x) +# define htole32(x) (x) +# define be32toh(x) ntohl(x) +# define le32toh(x) (x) + +# ifdef __MINGW32__ +# define htonll(x) ((1==htonl(1)) ? (x) : (((uint64_t)htonl((x) & 0xFFFFFFFFUL)) << 32) | htonl((uint32_t)((x) >> 32))) +# define ntohll(x) ((1==ntohl(1)) ? (x) : (((uint64_t)ntohl((x) & 0xFFFFFFFFUL)) << 32) | ntohl((uint32_t)((x) >> 32))) +# endif + +# define htobe64(x) htonll(x) +# define htole64(x) (x) +# define be64toh(x) ntohll(x) +# define le64toh(x) (x) + +# elif BYTE_ORDER == BIG_ENDIAN +# define IS_BIG_ENDIAN + + /* that would be xbox 360 */ +# define htobe16(x) (x) +# define htole16(x) __builtin_bswap16(x) +# define be16toh(x) (x) +# define le16toh(x) __builtin_bswap16(x) + +# define htobe32(x) (x) +# define htole32(x) __builtin_bswap32(x) +# define be32toh(x) (x) +# define le32toh(x) __builtin_bswap32(x) + +# define htobe64(x) (x) +# define htole64(x) __builtin_bswap64(x) +# define be64toh(x) (x) +# define le64toh(x) __builtin_bswap64(x) + +# else + +# error byte order not supported + +# endif + +# define __BYTE_ORDER BYTE_ORDER +# define __BIG_ENDIAN BIG_ENDIAN +# define __LITTLE_ENDIAN LITTLE_ENDIAN +# define __PDP_ENDIAN PDP_ENDIAN + +#elif defined(__QNXNTO__) + +# include + +# define __LITTLE_ENDIAN 1234 +# define __BIG_ENDIAN 4321 +# define __PDP_ENDIAN 3412 + +# if defined(__BIGENDIAN__) + +# define __BYTE_ORDER __BIG_ENDIAN + +# define htobe16(x) (x) +# define htobe32(x) (x) +# define htobe64(x) (x) + +# define htole16(x) ENDIAN_SWAP16(x) +# define htole32(x) ENDIAN_SWAP32(x) +# define htole64(x) ENDIAN_SWAP64(x) + +# elif defined(__LITTLEENDIAN__) + +# define __BYTE_ORDER __LITTLE_ENDIAN + +# define htole16(x) (x) +# define htole32(x) (x) +# define htole64(x) (x) + +# define htobe16(x) ENDIAN_SWAP16(x) +# define htobe32(x) ENDIAN_SWAP32(x) +# define htobe64(x) ENDIAN_SWAP64(x) + +# else + +# error byte order not supported + +# endif + +# define be16toh(x) ENDIAN_BE16(x) +# define be32toh(x) ENDIAN_BE32(x) +# define be64toh(x) ENDIAN_BE64(x) +# define le16toh(x) ENDIAN_LE16(x) +# define le32toh(x) ENDIAN_LE32(x) +# define le64toh(x) ENDIAN_LE64(x) + +#else + +# error platform not supported + +#endif + +#if __BYTE_ORDER == __LITTLE_ENDIAN +# define IS_BIG_ENDIAN 0 +#else +# define IS_BIG_ENDIAN 1 +#endif + +enum class endian { + big, + little, + host = IS_BIG_ENDIAN ? big : little, +}; + +#endif diff --git a/src/json_view/json_view.cpp b/src/json_view/json_view.cpp new file mode 100644 index 0000000..b67b8b6 --- /dev/null +++ b/src/json_view/json_view.cpp @@ -0,0 +1,218 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#include "json_view/json_view.hpp" +#include "json_view/dump.hpp" +#include "json_view/json.hpp" +#include "json_view/pointer.hpp" +#include "json_view/algo.hpp" + +using namespace jv; +using namespace std::string_view_literals; + +const JsonView *JsonView::Find(const JsonPointer &ptr, const TraceFrame &frame) const { + const JsonView* current = this; + for (auto& part: ptr) { + current = part.Visit([&](string_view key){ + return current->FindVal(key, TraceFrame(key, frame)); + }, [&](size_t idx){ + return current->Find(idx, TraceFrame(idx, frame)); + }); + if (!current) + return nullptr; + } + return current; +} + +std::string JsonView::Dump(bool pretty) const +{ + return jv::DumpJson(*this, {pretty}); +} + +std::string JsonView::DumpMsgPack() const +{ + return jv::DumpMsgPack(*this); +} + +void JsonView::throwMissmatch(Type wanted, TraceFrame const& frame) const { + TypeMissmatch exc(frame); + exc.wanted = wanted; + exc.was = data.type; + throw exc; +} + +void JsonView::throwKeyError(string_view key, TraceFrame const& frame) const +{ + KeyError err(frame); + err.missing = key; + throw err; +} + +void JsonView::throwIndexError(unsigned int key, TraceFrame const& frame) const +{ + IndexError err(frame); + err.actualSize = data.size; + err.wanted = key; + throw err; +} + +void JsonView::throwIntRangeError(int64_t min, uint64_t max, TraceFrame const& frame) const +{ + IntRangeError err(frame); + if (data.type == t_signed){ + err.isUnsigned = false; + err.was.i = data.d.integer; + } else if (data.type == t_unsigned) { + err.isUnsigned = true; + err.was.u = data.d.uinteger; + } + err.min = min; + err.max = max; + throw err; +} + +JsonException::JsonException(const TraceFrame &frame) : + trace{frame.PrintTrace()} +{ + if (!trace.empty()) + trace += ": "; +} + +JsonException::JsonException(const JsonPointer &ptr) : + trace{ptr.Join('.')} +{ + if (!trace.empty()) + trace += ": "; +} + +ForeignError::ForeignError(std::string msg, TraceFrame const& frame) : + JsonException(frame) +{ + nested = std::make_exception_ptr(std::runtime_error(std::move(msg))); +} + +ForeignError::ForeignError(std::string msg, const JsonPointer &ptr) : + JsonException(ptr) +{ + nested = std::make_exception_ptr(std::runtime_error(std::move(msg))); +} + +const char *ForeignError::what() const noexcept try +{ + if (!msg.empty()) { + return msg.c_str(); + } + try { + if (nested) { + std::rethrow_exception(nested); + } else { + throw std::runtime_error(""); + } + } catch (std::exception& e) { + msg += trace + e.what(); + } + return msg.c_str(); +} catch (...) { + return "Foreign Json Error (+ OOM)"; +} + + +const char *KeyError::what() const noexcept try +{ + if (!msg.empty()) { + return msg.c_str(); + } + msg += trace; + msg += "key not found: "sv; + msg += missing; + return msg.c_str(); +} catch (...) { + return "Json Key Error (+ OOM)"; +} + +const char *IndexError::what() const noexcept try +{ + if (!msg.empty()) { + return msg.c_str(); + } + msg += trace; + msg += "index not found: "sv; + msg += std::to_string(wanted); + msg += " (was size: "sv; + msg += std::to_string(actualSize) + ')'; + return msg.c_str(); +} catch (...) { + return "Json Index Error (+ OOM)"; +} + +static std::string printMask(jv::Type mask) { + std::string msg; + if (!mask) + return "null"; + for (auto i = 1u; i <= t_discarded; i <<= 1) { + if (auto single = mask & i) { + if (!msg.empty()) { + msg += '|'; + } + msg += JsonView::PrintType(jv::Type(single)); + } + } + return msg; +} + +const char *TypeMissmatch::what() const noexcept try +{ + if (!msg.empty()) { + return msg.c_str(); + } + msg += trace; + msg += "type missmatch: (was: "sv; + msg += std::string(JsonView::PrintType(was)); + msg += " => wanted: "sv; + msg += printMask(wanted) + ')'; + return msg.c_str(); +} catch (...) { + return "Json Type Missmatch (+ OOM)"; +} + +const char *IntRangeError::what() const noexcept try +{ + if (!msg.empty()) { + return msg.c_str(); + } + msg += trace + "integer "; + if (isUnsigned) { + msg += std::to_string(was.u); + } else { + msg += std::to_string(was.i); + } + msg += " could not fit in range: ["sv; + msg += std::to_string(min); + msg += " - "sv; + msg += std::to_string(max); + msg += ']'; + return msg.c_str(); +} catch (...) { + return "Json Int Range Error (+ OOM)"; +} diff --git a/src/json_view/parse_json.cpp b/src/json_view/parse_json.cpp new file mode 100644 index 0000000..b651e24 --- /dev/null +++ b/src/json_view/parse_json.cpp @@ -0,0 +1,288 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#include "json_view/algo.hpp" +#include "json_view/parse.hpp" +#include +#include +#include +#include +#include +#include "rapidjson/reader.h" +#include "rapidjson/error/en.h" +#include "alloc_adapter.hpp" + +using namespace jv; +using namespace rapidjson; + +namespace { + + +struct LimitedStream : InsituStringStream { + char* end; + LimitedStream(char* beg, size_t len) : + InsituStringStream(beg), + end(beg + len) + {} + Ch Peek() { return meta_Unlikely(src_ == end) ? 0 : *src_; } + Ch Take() { return meta_Unlikely(src_ == end) ? 0 : *src_++; } +}; + +} + +template<> +struct rapidjson::StreamTraits { + enum { copyOptimization = 1 }; +}; + +namespace { + +struct SaxHandler +{ + typedef typename UTF8<>::Ch Ch; + enum Tag { + val, + arr, + obj, + }; + Arena& alloc; + ParseSettings opts; + struct State { + union { + JsonView* array; + JsonPair* object; + }; + unsigned capacity; + unsigned size; + string_view key; + Tag tag = val; + }; + ArenaVector stack = ArenaVector(alloc); + JsonView result = {}; + State current = {}; + + void appendToArray(JsonView view) { + if (meta_Unlikely(current.size == current.capacity)) { + auto newCap = current.capacity ? current.capacity * 2 : 2; + auto newArr = MakeArrayOf(newCap, alloc); + if (current.array) { + memcpy(newArr, current.array, sizeof(JsonView) * current.size); + } + current.array = newArr; + current.capacity = newCap; + } + current.array[current.size++] = view; + } + + + void appendToObject(JsonView view) { + if (meta_Unlikely(current.size == current.capacity)) { + auto newCap = current.capacity ? current.capacity * 2 : 2; + auto newObj = MakeObjectOf(newCap, alloc); + if (current.object) { + memcpy(newObj, current.object, sizeof(JsonPair) * current.size); + } + current.object = newObj; + current.capacity = newCap; + } + if (opts.sorted) { + current.size = SortedInsertJson(current.object, current.size, {current.key, view}, current.capacity); + } else { + current.object[current.size++] = {current.key, view}; + } + } + + std::true_type doAdd(JsonView view) { + if (meta_Unlikely(current.tag == val)) { + result = view; + } else if (current.tag == obj) { + appendToObject(view); + } else { //arr + assert(current.tag == arr); + appendToArray(view); + } + return {}; + } + + std::false_type RawNumber(const char*, size_t, bool) { + return {}; + } + void Push() { + if (meta_Unlikely(stack.size() == opts.maxDepth)) { + throw DepthError{}; + } + stack.push_back(current); + current = {}; + } + State Pop() { + auto was = current; + current = stack.back(); + stack.pop_back(); + return was; + } + std::true_type StartArray() { + Push(); + current.tag = arr; + return {}; + } + std::true_type StartObject() { + Push(); + current.tag = obj; + return {}; + } + std::true_type EndArray(SizeType) { + auto was = Pop(); + assert(was.tag == arr); + doAdd(JsonView(was.array, was.size)); + return {}; + } + std::true_type EndObject(SizeType) { + auto was = Pop(); + assert(was.tag == obj); + Data curr; + curr.size = was.size; + curr.d.object = was.object; + curr.type = t_object; + curr.flags = Flags(opts.sorted * f_sorted); + doAdd(JsonView(curr)); + return {}; + } + std::true_type Null() { + return doAdd(nullptr); + } + std::true_type Bool(bool v) { + return doAdd(JsonView(v)); + } + std::true_type String(const Ch* str, rapidjson::SizeType len, bool) { + doAdd(string_view{str, len}); + return {}; + } + std::true_type Key(const Ch* str, rapidjson::SizeType len, bool) { + current.key = {str, len}; + return {}; + } + JsonView Result() const noexcept { + return result; + } + + std::true_type Int(int v) { + doAdd(JsonView(v)); + return {}; + } + std::true_type Uint(unsigned v) { + doAdd(JsonView(v)); + return {}; + } + std::true_type Int64(int64_t v) { + doAdd(JsonView(v)); + return {}; + } + std::true_type Uint64(uint64_t v) { + doAdd(JsonView(v)); + return {}; + } + std::true_type Double(double v) { + doAdd(JsonView(v)); + return {}; + } +}; + +static std::string atOffset(string_view src, size_t offs) { + size_t line = 0; + size_t col = 0; + for (auto ch: src.substr(0, offs)) { + if (ch == '\n') { + line++; + col = 0; + } else { + col++; + } + } + return " @ line(" + std::to_string(line) + ") col(" + std::to_string(col) + ")"; +} + +static jv::JsonView parseOwnedBuff(char* buff, size_t len, Arena& alloc, ParseSettings params) { + RapidArenaAllocator rapidAlloc{&alloc}; + GenericReader, UTF8<>, RapidArenaAllocator> reader(&rapidAlloc); + LimitedStream stream(buff, len); + constexpr auto flags = kParseCommentsFlag | kParseTrailingCommasFlag + | kParseInsituFlag | kParseIterativeFlag + | kParseNanAndInfFlag; + SaxHandler handler{alloc, params}; + try { + reader.Parse(stream, handler); + } catch (ParsingError& e) { + e.position = reader.GetErrorOffset(); + } + if (meta_Unlikely(reader.HasParseError())) { + auto offs = reader.GetErrorOffset(); + auto msg = GetParseError_En(reader.GetParseErrorCode()) + atOffset({buff, len}, offs); + ParsingError err(std::move(msg)); + err.position = offs; + throw std::move(err); + } + return handler.Result(); +} + +} + +jv::JsonView jv::ParseJson(membuff::In& data, Arena& alloc, ParseSettings params) { + ArenaString buff(alloc); + if (auto hint = data.TryTotalLeft()) { + buff.reserve(hint); + } + char temp[2048]; + while (auto read = data.Read(temp, sizeof(temp))) { + buff.Append({temp, read}); + } + return parseOwnedBuff(buff.data(), buff.size(), alloc, params); +} + +jv::JsonView jv::ParseJsonFile(std::filesystem::path const& file, Arena& alloc, ParseSettings params) { + std::ifstream source(file); + if (!source.is_open()) { + throw ParsingError("Could not open: " + file.string()); + } + try { + return ParseJson(source, alloc, params); + } catch (ParsingError& e) { + ParsingError wrap(file.string() + ": " + e.what()); + wrap.position = e.position; + throw std::move(wrap); + } +} + +jv::JsonView jv::ParseJson(string_view json, Arena& alloc, ParseSettings params) { + ArenaString buff(json, alloc); + return parseOwnedBuff(buff.data(), buff.size(), alloc, params); +} + +jv::JsonView jv::ParseJsonInPlace(char* buff, size_t len, Arena& alloc, ParseSettings params) { + return parseOwnedBuff(buff, len, alloc, params); +} + +jv::JsonView jv::ParseJson(std::istream& data, Arena& alloc, ParseSettings params) { + membuff::IStreamIn in(data); + return ParseJson(in, alloc, params); +} diff --git a/src/json_view/parse_msgpack.cpp b/src/json_view/parse_msgpack.cpp new file mode 100644 index 0000000..5a8f39b --- /dev/null +++ b/src/json_view/parse_msgpack.cpp @@ -0,0 +1,317 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#include "json_view/algo.hpp" +#include "json_view/parse.hpp" +#include +#include +#include "endian.hpp" + +using namespace jv; + +#define _CHECK(jv) if (meta_Unlikely(jv.Is(t_discarded))) return jv +#define _LEN(state, l) if (meta_Unlikely(state.Left() < l)) return ErrEOF + +namespace { + +static auto ErrEOF = JsonView::Discarded("unexpected eof"); +static auto ErrOOM = JsonView::Discarded("unexpected oom"); +static auto ErrTooDeep = JsonView::Discarded("recursion is too deep"); + +struct State { + const char* ptr; + const char* end; + ParseSettings& opts; + meta_alwaysInline + constexpr size_t Left() const noexcept { + return end - ptr; + } + meta_alwaysInline + const char* Consume(size_t amount) noexcept { + return std::exchange(ptr, ptr + amount); + } +}; + +JsonView parseObject(unsigned count, State& state, Arena& alloc, unsigned depth) noexcept; +JsonView parseArray(unsigned count, State& state, Arena& alloc, unsigned depth) noexcept; + +template +static T fromBig(const char* raw) noexcept +{ + static_assert(std::is_trivial_v); + union { + T res; + uint_for_t uint; + } helper; + memcpy(&helper.res, raw, sizeof(T)); + if constexpr (endian::host != endian::big) { + if constexpr (sizeof(T) == 1) { + //noop + } else if constexpr (sizeof(T) == 2) { + helper.uint = be16toh(helper.uint); + } else if constexpr (sizeof(T) == 4) { + helper.uint = be32toh(helper.uint); + } else if constexpr (sizeof(T) == 8) { + helper.uint = be64toh(helper.uint); + } else { + static_assert(always_false, "unsupported"); + } + } + return helper.res; +} + +template +inline JsonView unpackTrivial(State& state) noexcept +{ + _LEN(state, sizeof(T)); + return fromBig(state.Consume(sizeof(T))); +} + +template +JsonView unpackStr(State& state) noexcept +{ + auto len = unpackTrivial(state); + _CHECK(len); + auto act = len.GetUnsafe().d.uinteger; + _LEN(state, act); + return string_view(state.Consume(act), act); +} + +template +JsonView unpackBin(State& state) noexcept +{ + auto len = unpackTrivial(state); + _CHECK(len); + auto act = len.GetUnsafe().d.uinteger + add; + _LEN(state, act); + return JsonView::Binary({state.Consume(act), size_t(act)}); +} + +template +JsonView unpackArr(State& state, Arena& alloc, unsigned depth) noexcept +{ + auto len = unpackTrivial(state); + _CHECK(len); + return parseArray(unsigned(len.GetUnsafe().d.uinteger), state, alloc, depth); +} + +template +JsonView unpackObj(State& state, Arena& alloc, unsigned depth) noexcept +{ + auto len = unpackTrivial(state); + _CHECK(len); + return parseObject(unsigned(len.GetUnsafe().d.uinteger), state, alloc, depth); +} + +template +static JsonView unpackExt(State& state) noexcept { + constexpr auto typeTag = 1; + constexpr auto total = size + typeTag; + if (meta_Unlikely(state.Left() < total)) + return ErrEOF; + return JsonView::Binary({state.Consume(total), total}); +} + +JsonView parseOne(State& state, Arena& alloc, unsigned depth) noexcept +{ + if (meta_Unlikely(!depth)) { + return ErrTooDeep; + } + if (meta_Unlikely(state.ptr == state.end)) { + return ErrEOF; + } + static_assert(std::numeric_limits::is_iec559, "non IEEE 754 float"); + static_assert(std::numeric_limits::is_iec559, "non IEEE 754 double"); + auto head = uint8_t(*state.ptr++); + switch (head) { + case 0xc0: return JsonView{nullptr}; + case 0xc1: return JsonView::Discarded("0xC1 is not allowed in MsgPack"); + case 0xc2: return JsonView{false}; + case 0xc3: return JsonView{true}; + case 0xcc: return unpackTrivial(state); + case 0xcd: return unpackTrivial(state); + case 0xce: return unpackTrivial(state); + case 0xcf: return unpackTrivial(state); + case 0xd0: return unpackTrivial(state); + case 0xd1: return unpackTrivial(state); + case 0xd2: return unpackTrivial(state); + case 0xd3: return unpackTrivial(state); + case 0xca: return unpackTrivial(state); + case 0xcb: return unpackTrivial(state); + case 0xd9: return unpackStr(state); + case 0xda: return unpackStr(state); + case 0xdb: return unpackStr(state); + case 0xc4: return unpackBin(state); + case 0xc5: return unpackBin(state); + case 0xc6: return unpackBin(state); + case 0xdc: return unpackArr(state, alloc, depth - 1); + case 0xdd: return unpackArr(state, alloc, depth - 1); + case 0xde: return unpackObj(state, alloc, depth - 1); + case 0xdf: return unpackObj(state, alloc, depth - 1); + case 0xd4: return unpackExt<1>(state); + case 0xd5: return unpackExt<2>(state); + case 0xd6: return unpackExt<4>(state); + case 0xd7: return unpackExt<8>(state); + case 0xd8: return unpackExt<16>(state); + case 0xc7: return unpackBin(state); + case 0xc8: return unpackBin(state); + case 0xc9: return unpackBin(state); + case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: case 8: case 9: case 10: case 11: case 12: + case 13: case 14: case 15: case 16: case 17: case 18: case 19: case 20: case 21: case 22: case 23: + case 24: case 25: case 26: case 27: case 28: case 29: case 30: case 31: case 32: case 33: case 34: + case 35: case 36: case 37: case 38: case 39: case 40: case 41: case 42: case 43: case 44: case 45: + case 46: case 47: case 48: case 49: case 50: case 51: case 52: case 53: case 54: case 55: case 56: + case 57: case 58: case 59: case 60: case 61: case 62: case 63: case 64: case 65: case 66: case 67: + case 68: case 69: case 70: case 71: case 72: case 73: case 74: case 75: case 76: case 77: case 78: + case 79: case 80: case 81: case 82: case 83: case 84: case 85: case 86: case 87: case 88: case 89: + case 90: case 91: case 92: case 93: case 94: case 95: case 96: case 97: case 98: case 99: case 100: + case 101: case 102: case 103: case 104: case 105: case 106: case 107: case 108: case 109: case 110: + case 111: case 112: case 113: case 114: case 115: case 116: case 117: case 118: case 119: case 120: + case 121: case 122: case 123: case 124: case 125: case 126: + case 127: { //pos fixint + return uint8_t(head); + } + case 0x80: case 0x81: case 0x82: case 0x83: case 0x84: case 0x85: case 0x86: case 0x87: case 0x88: + case 0x89: case 0x8a: case 0x8b: case 0x8c: case 0x8d: case 0x8e: + case 0x8f: { //fixmap + return parseObject(head & 0b1111, state, alloc, depth - 1); + } + case 0x90: case 0x91: case 0x92: case 0x93: case 0x94: case 0x95: case 0x96: case 0x97: + case 0x98: case 0x99: case 0x9a: case 0x9b: case 0x9c: case 0x9d: case 0x9e: + case 0x9f: { //fixarr + return parseArray(head & 0b1111, state, alloc, depth - 1); + } + case 0xa0: case 0xa1: case 0xa2: case 0xa3: case 0xa4: case 0xa5: case 0xa6: case 0xa7: + case 0xa8: case 0xa9: case 0xaa: case 0xab: case 0xac: case 0xad: case 0xae: case 0xaf: + case 0xb0: case 0xb1: case 0xb2: case 0xb3: case 0xb4: case 0xb5: case 0xb6: case 0xb7: + case 0xb8: case 0xb9: case 0xba: case 0xbb: case 0xbc: case 0xbd: case 0xbe: + case 0xbf: { //fixstr + size_t len = head & 0b11111; + if (meta_Unlikely(state.Left() < len)) { + return ErrEOF; + } + return string_view(state.Consume(len), len); + } + case 0xe0: case 0xe1: case 0xe2: case 0xe3: case 0xe4: case 0xe5: case 0xe6: case 0xe7: + case 0xe8: case 0xe9: case 0xea: case 0xeb: case 0xec: case 0xed: case 0xee: case 0xef: + case 0xf0: case 0xf1: case 0xf2: case 0xf3: case 0xf4: case 0xf5: case 0xf6: case 0xf7: case 0xf8: + case 0xf9: case 0xfa: case 0xfb: case 0xfc: case 0xfd: case 0xfe: + case 0xff: { //neg fixint + return int8_t(head); + } + default: { + return {JsonView::Discarded("unknown type")}; + } + } +} + +JsonView parseObject(unsigned count, State& state, Arena& alloc, unsigned int depth) noexcept try +{ + auto obj = MakeObjectOf(count, alloc); + if (state.opts.sorted) { + unsigned size = 0; + for (size_t i = 0u; i < count; ++i) { + auto key = parseOne(state, alloc, depth); + _CHECK(key); + if (meta_Unlikely(!key.Is(t_string))) + return JsonView::Discarded("keys must be string"); + auto value = parseOne(state, alloc, depth); + _CHECK(value); + size = SortedInsertJson(obj, size, {key.GetStringUnsafe(), value}, count); + } + Data result{t_object, f_sorted, size, {}}; + result.d.object = obj; + return JsonView(result); + } else { + for (size_t i = 0u; i < count; ++i) { + auto key = parseOne(state, alloc, depth); + _CHECK(key); + if (meta_Unlikely(!key.Is(t_string))) + return JsonView::Discarded("keys must be string"); + obj[i].key = key.GetStringUnsafe(); + obj[i].value = parseOne(state, alloc, depth); + _CHECK(obj[i].value); + } + return JsonView(obj, count); + } +} catch(...) { + return ErrOOM; +} + +JsonView parseArray(unsigned int count, State& state, Arena& alloc, unsigned int depth) noexcept try +{ + auto arr = MakeArrayOf(count, alloc); + for (size_t i = 0u; i < count; ++i) { + arr[i] = parseOne(state, alloc, depth); + _CHECK(arr[i]); + } + return JsonView{arr, count}; +} catch (...) { + return ErrOOM; +} + +} // anon + +jv::ParseResult jv::ParseMsgPackInPlace(string_view data, Arena& alloc, ParseSettings opts) +{ + State state{data.data(), data.data() + data.size(), opts}; + auto result = parseOne(state, alloc, opts.maxDepth); + auto consumed = size_t(state.ptr - data.data()); + if (result.Is(t_discarded)) { + if (result.GetDiscardReason() == ErrOOM.GetDiscardReason()) { + throw std::bad_alloc(); + } + auto err = ParsingError( + "msgpack parse error: " + + std::string(result.GetDiscardReason()) + + " @" + std::to_string(state.ptr - data.data())); + err.position = state.ptr - data.data(); + throw err; + } + return {result, consumed}; +} + +jv::ParseResult jv::ParseMsgPack(string_view data, Arena& alloc, ParseSettings params) +{ + ArenaString buff(data, alloc); + return ParseMsgPackInPlace(buff, alloc, params); +} + +jv::ParseResult jv::ParseMsgPackInPlace(const void* data, size_t size, Arena& alloc, ParseSettings params) +{ + return ParseMsgPackInPlace(string_view{static_cast(data), size}, alloc, params); +} + +jv::ParseResult jv::ParseMsgPack(membuff::In& reader, Arena& alloc, ParseSettings params) +{ + ArenaString buff(alloc); + if (auto hint = reader.TryTotalLeft()) { + buff.resize(hint); + } + char temp[2048]; + while (auto read = reader.Read(temp, sizeof(temp))) { + buff.Append({temp, read}); + } + return ParseMsgPackInPlace(buff, alloc, params); +} diff --git a/src/json_view/pointer.cpp b/src/json_view/pointer.cpp new file mode 100644 index 0000000..8674739 --- /dev/null +++ b/src/json_view/pointer.cpp @@ -0,0 +1,239 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#include "json_view/pointer.hpp" +#include + +using namespace jv; + +namespace { + +static JsonKey parseOne(string_view raw, bool onlyNumbers) { + if (raw.empty()) { + return JsonKey{""}; + } else if (onlyNumbers) { + unsigned idx = 0; + auto end = raw.data() + raw.size(); + auto [ptr, ec] = std::from_chars(raw.data(), end, idx, 10); + if (ec != std::errc{}) { + throw std::runtime_error("Invalid number on JsonPointer"); + } + if (ptr != end) { + throw std::runtime_error("Invalid number on JsonPointer"); + } + return JsonKey{idx}; + } else { + return JsonKey{raw}; + } +} + +static constexpr bool needPercentEncode(char c) { + return !( + (c >= '0' && c <= '9') + || (c >= 'A' && c <='Z') + || (c >= 'a' && c <= 'z') + || c == '-' + || c == '.' + || c == '_' + || c == '~'); +} + +static char percentDecode(size_t& idx, string_view src) { + char c = 0; + for (int j = 0; j < 2; j++) { + c = c << 4; + auto h = src[idx++]; + if (h >= '0' && h <= '9') c = static_cast(c + h - '0'); + else if (h >= 'A' && h <= 'F') c = static_cast(c + h - 'A' + 10); + else if (h >= 'a' && h <= 'f') c = static_cast(c + h - 'a' + 10); + else { + throw std::runtime_error("Invalid percent encoding in Json Pointer"); + } + } + return c; +} + +static void parseTokens( + char sep, + char* meta_Restrict storage, + size_t cap, + size_t len, + const char* meta_Restrict src, + JsonKey* out) +{ + size_t toks = 0; + bool escape = false; + bool onlyNumbers = true; + bool isUri = src[0] == '#'; + size_t idx = 0; + size_t ptr = 0; + size_t last = 0; + if (isUri) { + idx++; + } + if (*src == sep) { + idx++; + } + auto append = [&](char ch){ + (void)cap; assert(ptr < cap); + storage[ptr++] = ch; + }; + auto add_token = [&]{ + append('\0'); + out[toks++] = parseOne({storage + last, ptr - last - 1}, onlyNumbers); + last = ptr + 1; + onlyNumbers = true; + }; + while (idx < len) { + char ch = src[idx++]; + if (ch > '9' || ch < '0') { + onlyNumbers = false; + } + if (ch == sep) { + add_token(); + } + if (escape) { + if (ch == '0') { + append('~'); + } else if (ch == '1') { + append(sep); + } else { + throw std::runtime_error("Invalid escape in Json Pointer"); + } + } else if (ch == '~') { + escape = true; + } else { + if (isUri) { + if (ch == '%') { + if (meta_Unlikely(len - idx < 2)) { + throw std::runtime_error("Percent encoding missing tail in Json Pointer"); + } + append(percentDecode(idx, src)); + continue; + } else if (needPercentEncode(ch)) { + throw std::runtime_error("Percent encode missing in Json Pointer"); + } + } + append(ch); + } + if (idx == len) { + add_token(); + } + } +} + +} + +JsonPointer JsonPointer::FromString(string_view ptr, Arena &alloc, char sep) +{ + if (ptr.empty()) { + return {}; + } + if (ptr.size() >= (std::numeric_limits::max)()) { + throw std::runtime_error("json pointer is too big"); + } + auto count = unsigned(std::count(ptr.begin(), ptr.end(), sep)); + if (ptr[0] != sep) count++; + ArenaString storage(alloc); + storage.reserve(ptr.size() + 1 + count); + auto tokens = static_cast(alloc(sizeof(JsonKey) * count, alignof(JsonKey))); + parseTokens(sep, storage.data(), storage.capacity(), ptr.size(), ptr.data(), tokens); + return {tokens, count}; +} + +JsonPointer JsonPointer::FromString(string_view ptr, Arena& alloc) +{ + return FromString(ptr, alloc, '/'); +} + +std::string JsonPointer::Join(char sep, bool uri) const +{ + membuff::StringOut buff; + JoinInto(buff, sep, uri); + return buff.Consume(); +} + +namespace { + +static void writeNumber(membuff::Out &buff, unsigned num) { + char temp[std::numeric_limits::digits10 + 1]; + auto [ptr, ec] = std::to_chars(std::begin(temp), std::end(temp), num); + buff.Write(temp, ptr - temp); +} + +struct PercHelper { + char data[3]; + constexpr operator string_view() const { + return {data, 3}; + } +}; + +static constexpr const char hexDigits[16] = { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + 'A', 'B', 'C', 'D', 'E', 'F' }; + +constexpr PercHelper PercentEncode(char c) { + auto u = static_cast(c); + return {{'%', hexDigits[u >> 4], hexDigits[u & 15]}}; +} + +template +static void writeString(membuff::Out &buff, string_view str) { + for (auto ch: str) { + if (ch == '~') { + buff.Write("~0"); + } else if (ch == '/') { + buff.Write("~1"); + } else if (uri && needPercentEncode(ch)) { + buff.Write(PercentEncode(ch)); + } else { + buff.Write(ch); + } + } +} + +template +void write(membuff::Out& buff, char sep, JsonPointer ptr) { + for (auto& part: ptr) { + part.Visit([&](string_view key){ + buff.Write(sep); + writeString(buff, key); + }, [&](unsigned idx){ + buff.Write(sep); + writeNumber(buff, idx); + }); + } +} + +} + + +void JsonPointer::JoinInto(membuff::Out &out, char sep, bool uri) const +{ + if (uri) { + write(out, sep, *this); + } else { + write(out, sep, *this); + } +} diff --git a/src/rpcxx/client.cpp b/src/rpcxx/client.cpp new file mode 100644 index 0000000..a05c9d3 --- /dev/null +++ b/src/rpcxx/client.cpp @@ -0,0 +1,95 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#include "rpcxx/client.hpp" + +using namespace rpcxx; + +Client::Client(rc::Weak t) { + SetTransport(std::move(t)); +} + +Client::~Client() +{ + +} + +rc::Weak Client::GetTransport() noexcept { + return transport; +} + +rc::Weak Client::SetTransport(rc::Weak t) noexcept { + return std::exchange(transport, std::move(t)); +} + +static string addPref(string_view base, string_view prefix) { + if (prefix.empty()) { + return string{base}; + } else { + return string{prefix} + '/' + string{base}; + } +} + +void Client::NotifyRaw(string_view method, JsonView params) { + if (batchActive) { + currentBatch.notifs.push_back(RequestNotify{addPref(method, prefix), Json{params}}); + } else { + tr().SendNotify(addPref(method, prefix), params); + } +} + +void Client::SetPrefix(string prefix) +{ + this->prefix.swap(prefix); +} + +IClientTransport &Client::tr() { + auto tr = transport.peek(); + if (meta_Unlikely(!tr)) { + throw ClientTransportMissing{}; + } + return *tr; +} + +void Client::batchDone() { + if (!batchActive) { + throw std::runtime_error("Batch was not active"); + } + tr().SendBatch(std::move(currentBatch)); + batchActive = false; +} + +void Client::sendRequest(Promise cb, Method method, JsonView params) { + if (batchActive) { + auto name = addPref(method.name, prefix); + RequestMethod meth; + meth.method = name; + meth.params = Json{params}; + meth.timeout = method.timeout; + meth.cb = std::move(cb); + currentBatch.methods.push_back(std::move(meth)); + } else { + tr().SendMethod(Method{addPref(method.name, prefix), method.timeout}, params, std::move(cb)); + } +} diff --git a/src/rpcxx/context.cpp b/src/rpcxx/context.cpp new file mode 100644 index 0000000..78ce47d --- /dev/null +++ b/src/rpcxx/context.cpp @@ -0,0 +1,55 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#include "rpcxx/context.hpp" + +using namespace rpcxx; + +Context::Context() +{ + +} + +void Context::CloneFrom(const Context &other) +{ + data = other.data; +} + +std::any &Context::SetValue(string_view name) +{ + if (auto it = data.find(name); it != data.end()) { + return it->second; + } else { + return data[string{name}]; + } +} + +std::any *Context::Value(string_view name) +{ + if (auto it = data.find(name); it != data.end()) { + return &it->second; + } else { + return nullptr; + } +} diff --git a/src/rpcxx/handler.cpp b/src/rpcxx/handler.cpp new file mode 100644 index 0000000..b609023 --- /dev/null +++ b/src/rpcxx/handler.cpp @@ -0,0 +1,148 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#include "rpcxx/handler.hpp" + +namespace rpcxx { + +using AllRoutes = std::map, std::less<>>; + +struct IHandler::Impl { + AllRoutes routes; +}; + +IHandler::IHandler() +{ + +} + +void IHandler::OnForward(string_view, Request &, Promise &) noexcept +{ + +} + +void IHandler::OnForwardNotify(string_view, Request &) +{ + +} + +rc::Weak IHandler::GetRoute(string_view route) +{ + auto it = d->routes.find(route); + if (it == d->routes.end()) { + return nullptr; + } else { + return it->second; + } +} + +void IHandler::SetRoute(string_view route, rc::Weak handler) +{ + if (route.empty()) { + throw std::runtime_error("SetRoute(): cannot be empty"); + } + if (route.find_first_of('/') != string_view::npos) { + throw std::runtime_error("Route name must not contain any '/'"); + } + if (auto h = handler.peek()) { + d->routes[string{route}] = h; + } else { + auto it = d->routes.find(route); + if (it != d->routes.end()) { + d->routes.erase(it); + } + } +} + + +//! Important, that this a stable route sanitizers +//! @param method: /a/b////c//d/ +//! @returns a/b/c/d (no prefix and trailing '/', no duplicates) +static string_view sanitizeSlashes(string& storage, string_view method) { + storage.resize(method.size()); + size_t i = 0; + size_t o = 0; + bool lastSlash = false; + for (; i < method.size(); ++i) { + auto ch = method[i]; + if (ch == '/') { + lastSlash = true; + } else { + if (lastSlash) { + storage[o++] = '/'; + } + storage[o++] = ch; + lastSlash = false; + } + } + storage.resize(o); + auto res = string_view{storage}; + return res.size() && res.front() == '/' ? res.substr(1) : res; +} + +static IHandler* tryRoute(string& storage, AllRoutes& rs, string_view& method, string_view& outRoute) { + auto raw = sanitizeSlashes(storage, method); + method = raw; + auto pos = raw.find_first_of('/'); + if (pos == string_view::npos) return nullptr; + std::string_view maybeRoute = raw.substr(0, pos); + auto it = rs.find(maybeRoute); + if (it == rs.end()) return nullptr; + auto h = it->second.peek(); + if (!h) return nullptr; + outRoute = maybeRoute; + method = raw.substr(pos + 1); + return h; +} + +void IHandler::Handle(Request &request, Promise cb) noexcept +{ + string storage; + string_view route; + if (auto h = tryRoute(storage, d->routes, request.method.name, route)) { + OnForward(route, request, cb); + h->Handle(request, std::move(cb)); + } else { + DoHandle(request, std::move(cb)); + } +} + +void IHandler::HandleNotify(Request &request) +{ + string storage; + string_view route; + if (auto h = tryRoute(storage, d->routes, request.method.name, route)) { + OnForwardNotify(route, request); + h->HandleNotify(request); + } else { + DoHandleNotify(request); + } +} + +IHandler::~IHandler() +{ + +} + +} diff --git a/src/rpcxx/server.cpp b/src/rpcxx/server.cpp new file mode 100644 index 0000000..ad89121 --- /dev/null +++ b/src/rpcxx/server.cpp @@ -0,0 +1,299 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#include "rpcxx/server.hpp" +#include "rpcxx/exception.hpp" +#include + +using namespace rpcxx; + +struct rpcxx::Server::Impl { + Impl() { + exec = new StoppableExecutor(); + fallbackCtx = new Context; + current = fallbackCtx; + } + ~Impl() { + exec->Stop(); + } + rc::Strong exec; + std::map> calls; + std::vector selfMiddlewares; + std::vector EHandlers; + std::vector routeMiddlewares; + std::vector routeEHandlers; + std::unique_ptr fallback; + ContextPtr fallbackCtx; + ContextPtr current; +}; + +void Server::runMiddlewares(Request& req) +{ + for (auto m = d->selfMiddlewares.rbegin(); m != d->selfMiddlewares.rend(); ++m) { + (*m)(req); + } +} + +void Server::runRouteMiddlewares(string_view route, Request &req) +{ + for (auto m = d->routeMiddlewares.rbegin(); m != d->routeMiddlewares.rend(); ++m) { + (*m)(route, req); + } +} + +std::exception_ptr Server::excHandlers(string_view route, string_view method, ContextPtr ctx, std::exception &exc) noexcept +try { + std::unique_ptr override; + ExceptionContext ectx = {route, method, std::move(ctx), &exc}; + if (route.empty()) { + for (auto h = d->EHandlers.rbegin(); h != d->EHandlers.rend(); ++h) { + if (auto next = (*h)(ectx)) { + override.reset(next); + ectx.exception = override.get(); + } + } + } else { + for (auto h = d->routeEHandlers.rbegin(); h != d->routeEHandlers.rend(); ++h) { + if (auto next = (*h)(route, ectx)) { + override.reset(next); + ectx.exception = override.get(); + } + } + } + if (!override) return {}; + if (auto r = dynamic_cast(ectx.exception)) { + return std::make_exception_ptr(std::move(*r)); + } else { + return std::make_exception_ptr(RpcException(ectx.exception->what(), ErrorCode::internal)); + } +} catch (std::exception& unhandled) { + fprintf(stderr, "RPC: Server => exception during handling of exceptions: %s\n", unhandled.what()); + return std::make_exception_ptr(RpcException("Internal Error", ErrorCode::internal)); +} + +void Server::AddMiddleware(Middleware mw) +{ + d->selfMiddlewares.push_back(std::move(mw)); +} + +void Server::AddRouteMiddleware(RouteMiddleware mw) +{ + d->routeMiddlewares.push_back(std::move(mw)); +} + +ContextPtr Server::CurrentContext() +{ + return d->current; +} + +void Server::doAddExceptionHandler(ExceptionHandler h) +{ + d->EHandlers.push_back(std::move(h)); +} + +void Server::doAddRouteExceptionHandler(RouteExceptionHandler h) +{ + d->routeEHandlers.push_back(std::move(h)); +} + +void Server::OnForward(string_view route, Request &req, Promise &cb) noexcept +{ + auto orig = std::move(cb); + cb = Promise{}; + // todo: use timeout somehow + cb.GetFuture().AtLast( + GetExecutor(), + [this, MV(orig), r = string{route}, ctx = req.context, m = string{req.method.name}] + (Result res) mutable { + try { + orig(res.get()); + } catch (std::exception& e) { + auto over = excHandlers(r, m, ctx, e); + orig(over ? over : std::current_exception()); + } + }); + try { + runRouteMiddlewares(route, req); + } catch (std::exception& e) { + auto over = excHandlers(route, req.method.name, std::move(req.context), e); + cb(over ? std::move(over) : std::current_exception()); + } +} + +void Server::OnForwardNotify(string_view route, Request &req) +{ + runRouteMiddlewares(route, req); +} + +void Server::SetFallback(Fallback handler) +{ + if (handler) { + d->fallback.reset(new Fallback{std::move(handler)}); + } else { + d->fallback.reset(); + } +} + +Executor *Server::GetExecutor() const noexcept +{ + return d->exec.get(); +} + +rpcxx::Server::Server() : d() { + +} + +rpcxx::Server::~Server() +{} + +std::vector rpcxx::Server::RegisteredMethods() const +{ + std::vector res; + for (auto& it: d->calls) { + res.emplace_back(it.first); + } + return res; +} + +bool rpcxx::Server::IsMethodRegistered(std::string_view method) const +{ + return d->calls.find(method) != d->calls.end(); +} + +void rpcxx::Server::Unregister(std::string_view method) { + if (auto it = d->calls.find(method); it != d->calls.end()) { + d->calls.erase(it); + } else { + throw std::runtime_error("Cannot unregister method, not found: " + std::string(method)); + } +} + +void Server::DoHandleNotify(Request& req) try +{ + auto& alloc = req.alloc; + Promise cb{nullptr}; + CallCtx ctx{req, alloc, cb}; + d->current = req.context; + defer revert([&]{ + d->current = d->fallbackCtx; + }); + runMiddlewares(req); + auto found = d->calls.find(req.method.name); + if (found != d->calls.end()) { + found->second(ctx); + } +} catch (std::exception& e) { + auto over = excHandlers("", req.method.name, req.context, e); + std::rethrow_exception(over ? std::move(over) : std::current_exception()); +} + +void Server::DoHandle(Request& req, Promise cb) noexcept try +{ + auto& alloc = req.alloc; + CallCtx ctx{req, alloc, cb}; + d->current = req.context; + defer revert([&]{ + d->current = d->fallbackCtx; + }); + runMiddlewares(req); + if (req.method.name.substr(0, 4) == "rpc.") { + handleExtension(ctx); + return; + } + auto found = d->calls.find(req.method.name); + if (found != d->calls.end()) { + found->second(ctx); + } else if (d->fallback) { + ctx.cb((*d->fallback)(req, alloc)); + } else { + JsonPair data[] = { + {"was_method", req.method.name} + }; + auto exc = RpcException("Method not found", ErrorCode::method_not_found, Json(data)); + auto over = excHandlers("", req.method.name, req.context, exc); + ctx.cb(over ? over : std::make_exception_ptr(std::move(exc))); + } +} catch (std::exception& e) { + auto over = excHandlers("", req.method.name, req.context, e); + cb(over ? std::move(over) : std::current_exception()); +} + +void rpcxx::Server::registerCall(std::string& name, Call call) { + if (name.find("rpc.") == 0) { + throw std::runtime_error("methods cannot start with 'rpc.' - reserved for extensions"); + } + auto ok = d->calls.try_emplace(std::move(name), std::move(call)).second; + if (!ok) { + throw std::runtime_error("Method already registered: " + std::string(name)); + } +} + +void rpcxx::Server::validateRequest(const string *names, unsigned nargs, CallCtx& ctx, bool notif) +{ + if (notif) { + if (meta_Unlikely(ctx.IsMethodCall())) + throw RpcException("Expected a notification call, called as method", ErrorCode::invalid_request); + } else if (meta_Unlikely(!ctx.IsMethodCall())) { + throw RpcException("Expected a method call, called as notify", ErrorCode::invalid_request); + } + if (names) { + if (!ctx.req.params.Is(jv::t_object)) { + auto toPrint = MakeArrayOf(nargs, ctx.alloc); + for (auto i = 0u; i < nargs; ++i) { + toPrint[i] = JsonView{names[i]}; + } + JsonPair data[] = { + {"params_count", nargs}, + {"was_type", ctx.req.params.GetTypeName()}, + {"params_names", JsonView{toPrint, nargs}} + }; + throw RpcException("Method expected named params", ErrorCode::invalid_params, Json(data)); + } + } else if (nargs) { + if (!ctx.req.params.Is(jv::t_array)) { + JsonPair data[] = { + {"params_count", nargs}, + {"was_type", ctx.req.params.GetTypeName()} + }; + throw RpcException("Method expected positional params", ErrorCode::invalid_params, Json(data)); + } + } +} + +void rpcxx::Server::handleExtension(CallCtx& ctx) +{ + if (ctx.req.method.name == "rpc.list") { + auto arr = MakeArrayOf(unsigned(d->calls.size()), ctx.alloc); + unsigned count = 0; + for (auto& it: d->calls) { + arr[count++] = JsonView(it.first); + } + ctx.cb(JsonView(arr, count)); + } else { + JsonPair data[] = { + {"was_ext", ctx.req.method.name} + }; + throw RpcException{"Could not find extension", ErrorCode::method_not_found, Json(data)}; + } +} diff --git a/src/rpcxx/transport.cpp b/src/rpcxx/transport.cpp new file mode 100644 index 0000000..78ebe94 --- /dev/null +++ b/src/rpcxx/transport.cpp @@ -0,0 +1,474 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#include "rpcxx/transport.hpp" +#include "json_view/dump.hpp" +#include "rpcxx/protocol.hpp" +#include +using namespace rpcxx; +using namespace std::chrono; +namespace { +struct Transact { + string method; + Promise prom; + millis timeout; +}; + +static void error(string loc, std::exception& e) { + fprintf(stderr, "RPC: Unexpected in '%s': %s\n", loc.c_str(), e.what()); +} + +} + +struct IAsyncTransport::Impl { + size_t id = 0; + Protocol proto = {}; + std::unordered_map pending; + rc::Weak handler = nullptr; + steady_clock::time_point last = steady_clock::now(); + rc::Strong exec = new StoppableExecutor; + + ~Impl() { + pending.clear(); + exec->Stop(); + } + + template + void wrapNotif(string_view method, JsonView params, Fn f) { + if (proto == Protocol::json_v2_compliant) { + Formatter fmt; + f(fmt.MakeNotify(method, params)); + } else { + Formatter fmt; + f(fmt.MakeNotify(method, params)); + } + } + template + void wrapMethod(size_t next, string_view method, JsonView params, Fn f) { + if (proto == Protocol::json_v2_compliant) { + Formatter fmt; + f(fmt.MakeRequest(next, method, params)); + } else { + Formatter fmt; + f(fmt.MakeRequest(next, method, params)); + } + } + + template + static void sendResult(IAsyncTransport* self, JsonView id, Result res) noexcept try { + Formatter fmt; + try { + self->Send(fmt.MakeResponce(id, res.get())); + } catch (RpcException& e) { + self->Send(fmt.MakeError(id, e)); + } catch (std::exception& e) { + RpcException wrap(e.what(), ErrorCode::internal); + self->Send(fmt.MakeError(id, wrap)); + } + } catch (std::exception& e) { + error("sendResult()", e); + } + + IHandler* getHandler(IAsyncTransport* self) { + auto h = handler.peek(); + if (meta_Unlikely(!h)) { + self->NoServerFound(); + return nullptr; + } else { + return h; + } + } + + template + void handleServer(IAsyncTransport* self, string_view method, JsonView req, ContextPtr ctx, Arena& alloc) { + using F = Fields; + TraceFrame root; + TraceFrame reqFrame("", root); + auto id = req.Value(F::Id, JsonView{}, reqFrame); + auto p = req.Value(F::Params, EmptyArray(), reqFrame); + Request prepReq{alloc}; + prepReq.method = Method{method, rpcxx::NoTimeout}; + prepReq.params = p; + prepReq.context = ctx; + if (id.Is(t_null)) { + if (auto h = getHandler(self)) { + h->HandleNotify(prepReq); + } + } else { + Promise cb; + cb.GetFuture() + .AtLast(exec, [self, id = jv::Json(id)](auto result) mutable noexcept { + sendResult(self, id.View(), result); + }); + if (auto h = getHandler(self)) { + h->Handle(prepReq, std::move(cb)); + } + } + } + + struct Batch : rc::DefaultBase { + IAsyncTransport* self; + size_t left; + std::vector parts; + DefaultArena<1024> alloc; + + JsonView Result() { + return JsonView(parts.data(), unsigned(parts.size())); + } + }; + + template + static void addBatchResp(JsonView id, Batch& b, Result res) noexcept try { + Formatter fmt; + JsonView part; + try { + part = Copy(fmt.MakeResponce(id, res.get()), b.alloc); + } catch (RpcException& e) { + part = Copy(fmt.MakeError(id, e), b.alloc); + } catch (std::exception& e) { + RpcException wrap(e.what(), ErrorCode::internal); + part = Copy(fmt.MakeError(id, wrap), b.alloc); + } + try { + b.parts.push_back(part); + } catch (std::exception& e) { + error("push into batch", e); + } + if (!--b.left) { + try { + b.self->Send(b.Result()); + } catch (std::exception& e) { + error("send batch responce", e); + } + } + } catch (std::exception& e) { + error("addBatchResponce()", e); + } + + // I HATE JSONRPC 2.0 + template + void handleServerBatch(IAsyncTransport* self, JsonView req, ContextPtr ctx, Arena& alloc) { + using F = Fields; + unsigned idx = 0; + TraceFrame root; + TraceFrame batchFrame("", root); + rc::Strong batch = new Batch; + batch->left = req.Array(false).size(); + batch->self = self; + auto h = getHandler(self); + if (!h) return; + for (auto part: req.Array(false)) { + batch->left--; + try { + TraceFrame frame(idx++, batchFrame); + auto id = part.Value(F::Id, JsonView{}, frame); + auto p = part.Value(F::Params, EmptyArray(), frame); + auto method = part.At(F::Method, frame).GetString(TraceFrame(F::Method, frame)); + Request req{alloc}; + req.method = Method{method, rpcxx::NoTimeout}; + req.params = p; + req.context = ctx; + if (id.Is(t_null)) { + h->HandleNotify(req); + } else { + Promise cb; + cb.GetFuture() + .AtLast(exec, [batch, id = Json(id)](auto result) mutable noexcept { + addBatchResp(id.View(), *batch, result); + }); + batch->left++; + h->Handle(req, std::move(cb)); + } + } catch (RpcException& e) { + Formatter fmt; + batch->parts.push_back(Copy(fmt.MakeError(nullptr, e), batch->alloc)); + } catch (std::exception& e) { + Formatter fmt; + RpcException wrap(e.what(), ErrorCode::internal); + batch->parts.push_back(Copy(fmt.MakeError(nullptr, wrap), batch->alloc)); + } + } + } + template + void handleRespToClient(JsonView resp) { + using F = Fields; + const JsonView* id = resp.FindVal(F::Id); + if (meta_Unlikely(!id)) { + throw RpcException("Could not find 'id' in responce to client", ErrorCode::parse); + } + TraceFrame root; + auto num = id->Get(TraceFrame{F::Id, root}); + auto p = pending.find(num); + if (meta_Unlikely(p == pending.end())) { + JsonPair data[] = {{"was_id", num}}; + throw RpcException("Could not find match id with any pending request => " + resp.Dump(), + ErrorCode::invalid_request, + jv::Json(data)); + } + if (const JsonView* r = resp.FindVal(F::Result); meta_Likely(r)) { + p->second.prom(*r); + } else if (const JsonView* e = resp.FindVal(F::Error)) { + p->second.prom(e->Get(TraceFrame{F::Error, TraceFrame{}})); + } else { + throw RpcException("missing 'error' or 'result' fields", ErrorCode::invalid_request); + } + pending.erase(p); + } + template + void handle(IAsyncTransport* self, JsonView msg, ContextPtr ctx, Arena& alloc) { + using F = Fields; + auto method = msg.FindVal(F::Method); + if (!method) { + handleRespToClient(msg); + } else { + handleServer(self, method->GetString(TraceFrame{F::Method, TraceFrame{}}), msg, ctx, alloc); + } + } + template + void handleBatch(IAsyncTransport* self, JsonView msg, ContextPtr ctx, Arena& alloc) { + using F = Fields; + auto arr = msg.Array(false); + auto method = arr.begin()[0].FindVal(F::Method, TraceFrame("", TraceFrame{})); + if (!method) { + for (auto part: msg.Array()) { + try { + handleRespToClient(part); + } catch (...) { + //pass => exception in one of responces is not an error + } + } + } else { + handleServerBatch(self, msg, ctx, alloc); + } + } + void addPending(string method, size_t id, Promise cb, millis timeout) { + Transact tr{std::move(method), std::move(cb), timeout}; + auto [iter, ok] = pending.try_emplace(id, std::move(tr)); + if (meta_Unlikely(!ok)) { + iter->second.prom(FutureError(iter->second.method + ": Timeout Error")); + iter->second = std::move(tr); + } + } +}; + +IAsyncTransport::IAsyncTransport(Protocol proto, rc::Weak h) +{ + d->handler = h; + d->proto = proto; +} + +rc::Weak IAsyncTransport::SetHandler(rc::Weak handler) +{ + return std::exchange(d->handler, handler); +} + +void IAsyncTransport::ClearAllPending() +{ + for (auto& [_, t]: d->pending) { + t.prom(FutureError("Manual Cancel")); + } +} + +void IAsyncTransport::TimeoutHappened(string_view method, Promise &target) +{ + target(FutureError(string{method} + ": Timeout Error")); +} + +void IAsyncTransport::NoServerFound() +{ + throw RpcException("Server not registered", ErrorCode::internal); +} + +void IAsyncTransport::SendBatch(Batch batch) +{ + DefaultArena alloc; + auto _totalCount = batch.methods.size() + batch.notifs.size(); + auto size = unsigned(_totalCount); + if (meta_Unlikely(size < _totalCount)) { + throw std::runtime_error("Batch is too big"); + } + auto arr = MakeArrayOf(size, alloc); + unsigned idx = 0; + for (auto& n: batch.notifs) { + d->wrapNotif(n.method, n.params.View(), [&](JsonView formatted){ + arr[idx++] = Copy(formatted, alloc); + }); + } + for (auto& m: batch.methods) { + auto next = d->id++; + d->addPending(m.method, next, std::move(m.cb), m.timeout); + d->wrapMethod(next, m.method, m.params.View(), [&](JsonView formatted){ + arr[idx++] = Copy(formatted, alloc); + }); + } + try { + Send(JsonView(arr, size)); + } catch (std::exception& e) { + error("Send Batch Request", e); + } +} + +void IAsyncTransport::SendNotify(string_view method, JsonView params) try +{ + d->wrapNotif(method, params, [&](JsonView req){ + Send(req); + }); +} catch (std::exception& e) { + error("Send Notify (" + string{method} + ')', e); +} + +void IAsyncTransport::SendMethod(Method method, JsonView params, Promise cb) try +{ + auto next = d->id++; + d->addPending(string{method.name}, next, std::move(cb), method.timeout); + d->wrapMethod(next, method.name, params, [&](JsonView req){ + Send(req); + }); +} catch (std::exception& e) { + error("Send Method", e); +} + +IAsyncTransport::~IAsyncTransport() +{ + +} + +void IAsyncTransport::CheckTimeouts() +{ + auto now = steady_clock::now(); + auto diff = duration_cast(now - d->last).count(); + d->last = now; + auto it = d->pending.begin(); + while (it != d->pending.end()) { + if (it->second.timeout == NoTimeout) { + ++it; + } else if (it->second.timeout > diff) { + it->second.timeout -= diff; + ++it; + } else { + TimeoutHappened(it->second.method, it->second.prom); + it = d->pending.erase(it); + } + } +} + +void IAsyncTransport::Receive(JsonView msg, ContextPtr ctx) +{ + DefaultArena alloc; + if (msg.Is(t_array)) { + if (meta_Unlikely(msg.Array(false).size() == 0)) { + throw RpcException("Empty batch array", ErrorCode::invalid_request); + } + switch (d->proto) { + case Protocol::json_v2_compliant: + return d->handleBatch(this, msg, ctx, alloc); + case Protocol::json_v2_minified: + return d->handleBatch(this, msg, ctx, alloc); + } + } else if (meta_Unlikely(!msg.Is(jv::t_object))) { + JsonPair data[] = {{"was_type", msg.GetTypeName()}}; + throw RpcException("Request/Responce should be an array or object", + ErrorCode::invalid_request, + jv::Json(data)); + } else { + switch (d->proto) { + case Protocol::json_v2_compliant: + return d->handle(this, msg, ctx, alloc); + case Protocol::json_v2_minified: + return d->handle(this, msg, ctx, alloc); + } + } +} + +void IAsyncTransport::Receive(JsonView msg) +{ + Receive(msg, new Context); +} + +rc::Weak ForwardToHandler::SetHandler(rc::Weak handler) +{ + return std::exchange(h, handler); +} + +void ForwardToHandler::SendBatch(Batch batch) +{ + for (auto& m: batch.methods) { + SendMethod(Method{m.method, m.timeout}, m.params.View(), std::move(m.cb)); + } + for (auto& n: batch.notifs) { + SendNotify(n.method, n.params.View()); + } +} + +void ForwardToHandler::SendNotify(string_view method, JsonView params) +{ + DefaultArena alloc; + if (auto handler = h.peek(); meta_Likely(handler)) { + Request req{alloc}; + req.method = Method{method, rpcxx::NoTimeout}; + req.params = params; + handler->HandleNotify(req); + } +} + +void ForwardToHandler::SendMethod(Method method, JsonView params, Promise cb) +{ + DefaultArena alloc; + if (auto handler = h.peek(); meta_Likely(handler)) { + Request req{alloc}; + req.method = method; + req.params = params; + handler->Handle(req, std::move(cb)); + } +} + +void IClientTransport::DoHandle(Request& req, Promise cb) noexcept try { + SendMethod(req.method, req.params, std::move(cb)); +} catch (std::exception& e) { + error("Client => forward method", e); +} + +void IClientTransport::DoHandleNotify(Request& req) noexcept try { + SendNotify(req.method.name, req.params); +} catch (std::exception& e) { + error("Client => forward notify", e); +} + +Transport::Transport(Protocol proto) : IAsyncTransport(proto) +{ + +} + +void Transport::OnReply(Sender cb) +{ + sender = std::move(cb); +} + +void Transport::Send(JsonView msg) +{ + if (!sender) { + throw std::runtime_error("Could not send: sender not registered"); + } + sender(msg); +} diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt new file mode 100644 index 0000000..c8018a2 --- /dev/null +++ b/test/CMakeLists.txt @@ -0,0 +1,77 @@ +# This file is a part of RPCXX project + +#[[ +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +]] + + +add_library(rpcxx-test-deps INTERFACE) +if(RPCXX_TEST_SANITIZERS AND NOT MSVC) + target_compile_options(rpcxx-test-deps INTERFACE -fsanitize=undefined,address) + target_link_options(rpcxx-test-deps INTERFACE -fsanitize=undefined,address) +endif() +target_link_libraries(rpcxx-test-deps INTERFACE rpcxx doctest) +if (CMAKE_COMPILER_IS_GNUCXX) + target_compile_options(rpcxx-test-deps INTERFACE -Wpedantic -Wall -Wextra) +endif() +if(MSVC) + target_link_options(rpcxx-test-deps INTERFACE /STACK:10000000) +endif() + +function(do_register target source) + add_executable(${target} ${source}) + target_link_libraries(${target} PUBLIC rpcxx-test-deps) + add_test(NAME ${target} COMMAND ${target}) +endfunction() + +do_register(rpcxx-rpc-test rpcxx/rpc_test.cpp) +do_register(rpcxx-json-test rpcxx/json_test.cpp) +do_register(rpcxx-msgpack-test rpcxx/msgpack_test.cpp) +do_register(rpcxx-future-test rpcxx/future_test.cpp) +do_register(rpcxx-describe-test rpcxx/describe_test.cpp) + +if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + target_compile_options(rpcxx-test-deps INTERFACE -ftime-trace) + + add_library(rpcxx-fuzz-deps INTERFACE) + target_link_libraries(rpcxx-fuzz-deps INTERFACE rpcxx) + target_compile_options(rpcxx-fuzz-deps INTERFACE -fsanitize=fuzzer,address) + target_link_options(rpcxx-fuzz-deps INTERFACE -fsanitize=fuzzer,address) + + add_executable(rpcxx-msgpack-fuzz rpcxx/msgpack_fuzzing.cpp) + target_link_libraries(rpcxx-msgpack-fuzz PRIVATE rpcxx-fuzz-deps) + + add_executable(rpcxx-json-fuzz rpcxx/json_fuzzing.cpp) + target_link_libraries(rpcxx-json-fuzz PRIVATE rpcxx-fuzz-deps) +endif() + +add_library(rpcxx-bench-deps INTERFACE) +target_link_libraries(rpcxx-bench-deps INTERFACE rpcxx benchmark) +target_compile_options(rpcxx-bench-deps INTERFACE -fno-omit-frame-pointer) + +add_executable(rpcxx-json-bench rpcxx/json_bench.cpp) +target_link_libraries(rpcxx-json-bench PUBLIC rpcxx-bench-deps) +add_executable(rpcxx-rpc-bench rpcxx/rpc_bench.cpp) +target_link_libraries(rpcxx-rpc-bench PUBLIC rpcxx-bench-deps) + +if (RPCXX_TEST_RPS) + add_subdirectory(rps) +endif() diff --git a/test/rpcxx/describe_test.cpp b/test/rpcxx/describe_test.cpp new file mode 100644 index 0000000..cbfd8a8 --- /dev/null +++ b/test/rpcxx/describe_test.cpp @@ -0,0 +1,173 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#include "rpcxx/rpcxx.hpp" +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include "doctest/doctest.h" + + +using namespace rpcxx; +using namespace describe; + +struct custom {}; +struct more_custom : custom { + constexpr bool operator()() { + return true; + } +}; + +template +struct Victim : SkipMissing { + T a, b, c; +}; + +template +DESCRIBE(Victim, &_::a, &_::b, &_::c) + +using Current = Victim; + +constexpr auto vict = describe::Get(); + +namespace test_basic { + +using SHOULD_HAVE = get_attrs_t; +using SHOULD_HAVE_ALSO = get_attrs_t; +static_assert(std::is_same_v); + +} + +const auto a = vict.get<&Victim::a>(); +const auto b = vict.get<&Victim::b>(); +const auto c = vict.get<&Victim::c>(); + +TEST_CASE("basic get") { + CHECK(a.name == "a"); + CHECK(b.name == "b"); + CHECK(c.name == "c"); +} + +namespace idx { + +const auto by_ind = vict.index_of(describe::by_index<1>(vict)); +const auto mem_b = vict.index_of<&Victim::b>(); +const auto f_b = vict.index_of(b); +const auto name_b = vict.index_of("b"); + +TEST_CASE("idx get") { + CHECK(by_ind == mem_b); + CHECK(mem_b == f_b); + CHECK(f_b == name_b); +} + +} + +template struct NotEq : FieldValidator { + static constexpr auto validate = [](auto val){ + if (val == i) { + throw std::runtime_error("value is eq to: " + std::to_string(i)); + } + }; +}; + +template +DESCRIBE_FIELD_ATTRS(Victim, b, custom, NotEq<3>) +template +DESCRIBE_FIELD_ATTRS(Victim, c, more_custom, SkipMissing) + +TEST_CASE("Attrs") { + CHECK(describe::has_attr_v); + CHECK(!describe::has_attr_v); + CHECK(describe::has_attr_v); + CHECK(describe::has_attr_v, decltype(b)>); + CHECK(describe::has_attr_v); + CHECK(describe::has_attr_v); + CHECK(describe::extract_attr_t()()); +} + +struct Inheritance : describe::Attrs { + int c, d; +}; +DESCRIBE(Inheritance, &_::c, &_::d) + +constexpr auto chk = describe::Get(); +constexpr auto chkC = chk.get<&Inheritance::c>(); +constexpr auto chkD = chk.get<&Inheritance::d>(); + +TEST_CASE("Inheritance") { + CHECK(describe::has_attr_v); + CHECK(!describe::has_attr_v); + CHECK(!describe::has_attr_v); +} + +struct Outside {}; + +DESCRIBE_ATTRS(Outside, custom) +static_assert(describe::has_attr_v); + +enum Lol { + kek, chebureck +}; + +enum class SecondLol { + kek, chebureck +}; + +DESCRIBE(Lol, kek, chebureck) +DESCRIBE(SecondLol, _::kek, _::chebureck) + +template void testEnum() { + const auto desc = describe::Get(); + const auto dKek = desc.get(); + const auto dCheb = desc.get(); + CHECK(dKek.value == kek); + CHECK(dKek.name == "kek"); + CHECK(dCheb.value == chebureck); + CHECK(dCheb.name == "chebureck"); +} + +struct SelfValidate : ValidatedWith { + int a; + static void validate(SelfValidate&) { + throw std::runtime_error("!"); + } +}; + +DESCRIBE(SelfValidate, &_::a) + +static_assert(has_attr_v); + +TEST_CASE("describe atrrs") { + testEnum(); + testEnum(); + Victim v = {}; + a.get(v) = 1; + CHECK(a.get(v) == 1); + CHECK(b.get(v) == 0); + CHECK_NOTHROW(Json::Parse(R"({"b": 2})")->GetTo(v)); + CHECK(b.get(v) == 2); + CHECK(a.get(v) == 1); + CHECK_THROWS(Json::Parse(R"({"b": 3})")->GetTo(v)); + SelfValidate self; + CHECK_THROWS(Json::Parse(R"({"a": 3})")->GetTo(self)); +} diff --git a/test/rpcxx/future_test.cpp b/test/rpcxx/future_test.cpp new file mode 100644 index 0000000..3873644 --- /dev/null +++ b/test/rpcxx/future_test.cpp @@ -0,0 +1,450 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#include "rpcxx/rpcxx.hpp" +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include "doctest/doctest.h" +#include "future/to_std_fut.hpp" +#include + +using namespace rpcxx; + +struct TestBig { + size_t vals[10] = {}; +}; + +inline auto Ignore() { + return [](auto&&...) {}; +} + +using namespace std::chrono_literals; + +auto in = [](auto time) { + SharedPromise prom; + std::thread([=]{ + std::this_thread::sleep_for(time); + prom(); + }).detach(); + return prom.GetFuture(); +}; + +struct TestExecutor : Executor { + Status Execute(Job job) noexcept override { + in(0.1s).AtLastSync([MV(job)](auto){ + job(); + }); + return Defer; + } +}; + +TEST_CASE("rc") { + rc::Strong p = new fut::Data; + rc::Strong p2 = new fut::Data; + auto p3 = std::move(p); + rc::Strong e = new StoppableExecutor; + for (auto mode: {true, false}) { + SharedPromise prom; + bool hit = false; + if (!mode) { + e->Stop(); + } + (void)prom.GetFuture().Then(e, [&]{ + hit = true; + }); + prom(); + CHECK(hit == mode); + } +} + +TEST_CASE("memory") { + SharedPromise prom; + auto fut = prom.GetFuture(); +} + +TEST_CASE("thread safety") { + int counter = 0; + auto fut = GatherTuple(in(0.5s), in(0.25s), in(0.15s)) + .ThenSync([&]{ + counter++; + }) + .Then(new TestExecutor, [&]{ + counter++; + }) + .ThenSync([&]{ + counter++; + return in(0.15s); + }) + .ThenSync([&]{ + counter++; + }) + .Then(new TestExecutor, [&]{ + counter++; + }); + ToStdFuture(std::move(fut)).get(); + CHECK(counter == 5); +} + +TEST_CASE("move func") +{ + auto test = [](auto functor) { + std::vector> fs; + for (auto i = 0; i < 30; ++i) { + fs.push_back(functor); + } + int acc = 0; + for (auto& f: fs) { + acc += f(1); + } + CHECK(acc == 60); + std::vector> lol; + for (auto& f: fs) { + lol.push_back(std::move(f)); + } + for (auto& f: lol) { + acc += f(1); + } + CHECK(acc == 120); + }; + GIVEN("invalid") { + CHECK_THROWS(MoveFunc()()); + } + GIVEN("small functor") { + auto small = [](int a) { + return a + 1; + }; + test(small); + } + GIVEN("big functor") { + auto big = [a = TestBig{{1}}](int b) { + return b + a.vals[0]; + }; + test(big); + } +} + +TEST_CASE("future") +{ + SUBCASE("try") { + bool got = false; + fut::Rejected(std::runtime_error("123")) + .TrySync([](auto exc){ + CHECK(!exc); + }) + .AtLastSync([&](auto res){ + CHECK(res); + got = true; + }); + CHECK(got); + got = false; + fut::Rejected(std::runtime_error("original")) + .TrySync([](auto exc){ + CHECK(!exc); + throw std::runtime_error("changed"); + }) + .AtLastSync([&](auto res){ + CHECK(!res); + try { + res.get(); + } catch (std::exception& e) { + CHECK(string_view{e.what()} == string_view{"changed"}); + } + got = true; + }); + CHECK(got); + } + SUBCASE("basic") { + Promise prom; + Future fut = prom.GetFuture(); + prom(1); + CHECK(fut.IsValid()); + fut.AtLastSync(Ignore()); + CHECK(!fut.IsValid()); + } + SUBCASE("small_1") { + Promise prom; + Future fut = prom.GetFuture(); + prom(1); + int res = 0; + CHECK(fut.IsValid()); + fut + .ThenSync([&](int passed){ + res = passed; + }) + .AtLastSync(Ignore()); + CHECK(!fut.IsValid()); + CHECK_EQ(res, 1); + } + SUBCASE("small_2") { + rpcxx::Promise prom2; + int res = 0; + CHECK_EQ(res, 0); + prom2.GetFuture() + .ThenSync([&](int passed){ + res = passed; + }) + .AtLastSync(Ignore()); + prom2(2); + CHECK_EQ(res, 2); + } + SUBCASE("string") { + auto fut = Future::FromFunction([](auto prom){ + prom("123"); + }).ThenSync([](string s){ + return s; + }); + auto res = ToStdFuture(std::move(fut)).get(); + CHECK_EQ(res, "123"); + } + SUBCASE("chaining") { + int first = 0, second = 0, third = 0; + rpcxx::Promise prom; + prom.GetFuture() + .ThenSync([&](int a){ + return first = a; + }) + .ThenSync([&](int b){ + return second = b + 5; + }) + .ThenSync([&](int c){ + return third = c + 5; + }) + .AtLastSync(Ignore()); + prom(30); + CHECK_EQ(first, 30); + CHECK_EQ(second, 35); + CHECK_EQ(third, 40); + } + SUBCASE("deferred") { + size_t hits = 0; + auto fut = fut::Resolved() + .ThenSync([&]{ + hits++; + return fut::Resolved(); + }) + .ThenSync([&]{ + hits++; + return in(0.2s); + }) + .ThenSync([&]{ + hits++; + return fut::Resolved(); + }) + .ThenSync([&]{ + hits++; + return in(0.2s); + }) + .ThenSync([&]{ + hits++; + return fut::Resolved(); + }) + .ThenSync([&]{ + CHECK_EQ(hits, 5); + }); + fut::ToStdFuture(std::move(fut)).get(); + CHECK_EQ(hits, 5); + } + SUBCASE("exception") { + try { + auto p = Promise(); + auto f = p.GetFuture(); + throw 1; + } catch (...) {} + } + SUBCASE("gather") { + SUBCASE("ok") { + Promise one; + Promise two; + int first = 0, second = 0; + Promise three; + GatherTuple(one.GetFuture(), two.GetFuture(), three.GetFuture()) + .ThenSync([&](std::tuple res){ + auto [a, b, _] = res; + first = a; + second = b; + }) + .AtLastSync(Ignore()); + CHECK_EQ(first, 0); + CHECK_EQ(second, 0); + one(1); + CHECK_EQ(first, 0); + CHECK_EQ(second, 0); + two(2); + CHECK_EQ(first, 0); + CHECK_EQ(second, 0); + three(); + CHECK_EQ(first, 1); + CHECK_EQ(second, 2); + } + SUBCASE("error") { + Promise one; + Promise two; + Promise three; + int first = 0, second = 0; + bool errCaught = false; + GatherTuple(one.GetFuture(), two.GetFuture(), three.GetFuture()) + .AtLastSync([&](Result> res){ + if (auto&& err = res.get_exception()) { + errCaught = true; + (void)err; + } else { + auto [a, b, _] = res.get(); + first = a; + second = b; + } + }); + CHECK_EQ(first, 0); + CHECK_EQ(second, 0); + one(1); + CHECK_EQ(first, 0); + CHECK_EQ(second, 0); + two(std::runtime_error("err!")); + CHECK_EQ(first, 0); + CHECK_EQ(second, 0); + CHECK(errCaught); + three(); + } + } + SUBCASE("big") { + rpcxx::Promise setter; + rpcxx::Promise setter2; + setter(TestBig{{1, 2, 3}}); + int res = 0; + setter.GetFuture() + .ThenSync([&](TestBig passed){res = passed.vals[2];}) + .AtLastSync(Ignore()); + CHECK_EQ(res, 3); + setter2.GetFuture() + .ThenSync([&](TestBig passed){res = passed.vals[2];}) + .AtLastSync(Ignore()); + setter2(TestBig{{3, 2, 1}}); + CHECK_EQ(res, 1); + } + SUBCASE("gather_vec") { + GIVEN("voids") { + std::vector> futs; + std::vector> proms; + bool hit = false; + GIVEN("ok") { + for (auto i = 0; i < 10; ++i) { + futs.push_back(proms.emplace_back().GetFuture()); + } + Gather(std::move(futs)).AtLastSync([&](auto res){ + CHECK(res); + hit = true; + }); + CHECK(!hit); + for (auto& p: proms) { + CHECK(!hit); + p(); + } + CHECK(hit); + } + GIVEN("err") { + for (auto i = 0; i < 10; ++i) { + futs.push_back(proms.emplace_back().GetFuture()); + } + Gather(std::move(futs)).AtLastSync([&](auto res){ + CHECK(!res); + hit = true; + }); + CHECK(!hit); + for (auto& p: proms) { + p(std::bad_alloc()); + CHECK(hit); + } + CHECK(hit); + } + } + GIVEN("empty") { + int hits = 0; + Gather(std::vector>{}).AtLastSync([&](auto res){ + hits++; + CHECK(res); + }); + Gather(std::vector>{}).AtLastSync([&](auto res){ + hits++; + CHECK(res); + }); + CHECK(hits == 2); + } + std::vector> futs; + std::vector> proms; + bool hit = false; + auto checkOnes = [&]{ + Gather(std::move(futs)).AtLastSync([&](auto vec){ + hit = true; + for (auto& r: vec.get()) { + CHECK(r == 1); + } + }); + }; + auto checkExc = [&]{ + Gather(std::move(futs)).AtLastSync([&](auto vec){ + hit = true; + CHECK(vec.get_exception()); + }); + }; + GIVEN("from result") { + for (auto i = 0; i < 100; ++i) { + futs.push_back(fut::Resolved(1)); + } + checkOnes(); + CHECK(hit); + } + GIVEN("from err") { + for (auto i = 0; i < 100; ++i) { + futs.push_back(fut::Resolved(1)); + } + futs.push_back(fut::Rejected(std::runtime_error("1"))); + checkExc(); + CHECK(hit); + } + GIVEN("from promise") { + for (auto i = 0; i < 10; ++i) { + futs.push_back(proms.emplace_back().GetFuture()); + } + checkOnes(); + CHECK(!hit); + for (auto& p: proms) { + CHECK(!hit); + p(1); + } + CHECK(hit); + } + GIVEN("from promise error") { + for (auto i = 0; i < 10; ++i) { + futs.push_back(proms.emplace_back().GetFuture()); + } + checkExc(); + CHECK(!hit); + for (auto& p: proms) { + p(std::runtime_error("")); + CHECK(hit); + } + CHECK(hit); + } + + } +} + diff --git a/test/rpcxx/json_bench.cpp b/test/rpcxx/json_bench.cpp new file mode 100644 index 0000000..93fe2dc --- /dev/null +++ b/test/rpcxx/json_bench.cpp @@ -0,0 +1,135 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#include "rpcxx/rpcxx.hpp" +#include +#include "json_samples.hpp" + +using namespace rpcxx; + +static void Dump(benchmark::State& state, string_view sample) +{ + auto json = Json::Parse(sample); + for (auto _: state) { + benchmark::DoNotOptimize(json->Dump()); + } +} +BENCHMARK_CAPTURE(Dump, books, BooksSample); +BENCHMARK_CAPTURE(Dump, big, BigSample); +BENCHMARK_CAPTURE(Dump, rpc, RPCSample); + +static void Parse(benchmark::State& state, string_view sample) +{ + for (auto _: state) { + DefaultArena alloc; + try { + benchmark::DoNotOptimize(ParseJson(sample, alloc)); + } catch (...) {} + } +} + +static void ParseInSitu(benchmark::State& state, std::string_view sample) +{ + std::string orig{sample}; + std::string curr = orig; + for (auto _: state) { + std::string curr = orig; + DefaultArena alloc; + try { + benchmark::DoNotOptimize(ParseJsonInPlace(curr.data(), curr.size(), alloc)); + } catch (...) {} + } +} + +BENCHMARK_CAPTURE(Parse, books, BooksSample); +BENCHMARK_CAPTURE(Parse, big, BigSample); +BENCHMARK_CAPTURE(Parse, rpc, RPCSample); +BENCHMARK_CAPTURE(Parse, rpc_mini, MinifiedRPCSample); +BENCHMARK_CAPTURE(Parse, early_fail, EarlyFailSample); +BENCHMARK_CAPTURE(Parse, late_fail, LateFailSample); + +BENCHMARK_CAPTURE(ParseInSitu, books, BooksSample); +BENCHMARK_CAPTURE(ParseInSitu, big, BigSample); +BENCHMARK_CAPTURE(ParseInSitu, rpc, RPCSample); +BENCHMARK_CAPTURE(ParseInSitu, rpc_mini, MinifiedRPCSample); +BENCHMARK_CAPTURE(ParseInSitu, early_fail, EarlyFailSample); +BENCHMARK_CAPTURE(ParseInSitu, late_fail, LateFailSample); + +static void Parse_MsgPack(benchmark::State& state, const void* data, size_t len) { + for (auto _: state) { + DefaultArena alloc; + benchmark::DoNotOptimize(ParseMsgPackInPlace(data, len, alloc)); + } +} +BENCHMARK_CAPTURE(Parse_MsgPack, rpc, MsgPackRPC, sizeof(MsgPackRPC)); +BENCHMARK_CAPTURE(Parse_MsgPack, books, MsgPackBooks, sizeof(MsgPackBooks)); + +struct TestChild +{ + int a; + bool b; + std::string kek; +}; +DESCRIBE(TestChild, &_::a, &_::b, &_::kek) + +struct TestData +{ + int a; + bool b; + std::string kek; + std::map children; +}; +DESCRIBE(TestData, &_::a, &_::b, &_::kek, &_::children); + +static TestChild testDataPart { + 1, true, "lol" +}; + +static TestData testData { + 2, false, "asdasdasdadasda", + { + {"a", testDataPart}, + {"c", testDataPart}, + } +}; + + +static void Serialize(benchmark::State& state) { + DefaultArena alloc; + for (auto _: state) { + benchmark::DoNotOptimize(JsonView::From(testData, alloc)); + } +} +BENCHMARK(Serialize); + +static void DeSerialize(benchmark::State& state) { + DefaultArena alloc; + JsonView json = JsonView::From(testData, alloc); + for (auto _: state) { + benchmark::DoNotOptimize(json.Get()); + } +} +BENCHMARK(DeSerialize); + +BENCHMARK_MAIN(); diff --git a/test/rpcxx/json_fuzzing.cpp b/test/rpcxx/json_fuzzing.cpp new file mode 100644 index 0000000..56ff1bc --- /dev/null +++ b/test/rpcxx/json_fuzzing.cpp @@ -0,0 +1,59 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#include "rpcxx/rpcxx.hpp" +#include + +using namespace rpcxx; + +// see http://llvm.org/docs/LibFuzzer.html +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) +{ + try + { + auto ch = reinterpret_cast(data); + auto orig = Json::Parse({ch, size}); + try + { + auto back = orig.View().Dump(); + auto round = Json::Parse(back); + if (!DeepEqual(round.View(), orig.View(), 300, INFINITY)) { + printf("%s != %s\n", orig->Dump(true).c_str(), round->Dump(true).c_str()); + DeepEqual(round.View(), orig.View()); + assert(!"Returned json is not equal"); + } + } + catch(std::bad_alloc&) {} + catch(jv::DepthError&) {} + catch (...) + { + assert(false); + } + } + catch(jv::DepthError&) {} + catch(std::bad_alloc&) {} + catch (std::runtime_error&) {} + // return 0 - non-zero return values are reserved for future use + return 0; +} diff --git a/test/rpcxx/json_samples.hpp b/test/rpcxx/json_samples.hpp new file mode 100644 index 0000000..c8ba74b --- /dev/null +++ b/test/rpcxx/json_samples.hpp @@ -0,0 +1,154 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#pragma once + +#include +#include +#include "rpcxx/rpcxx.hpp" + +inline auto RPCSample = R"({ + "method": "methodName", + "id": "arbitrary-something", + "params": [3, 2, {"epic": "param"}], + "jsonrpc": "2.0" +})"; + +inline auto MinifiedRPCSample = + R"({"method":"methodName","id":"arbitrary-something","params":[3,2,{"epic":"param"}],"jsonrpc":"2.0"})"; + +inline auto EarlyFailSample = R"({ + "method"2: "methodName", + "id": "arbitrary-something", + "params": [3, 2, {"epic": "param"}], + "jsonrpc": "2.0" +})"; + +inline auto BooksSample = R"EOF({ + "glossary": [ + { + "title": "example glossary", + "GlossDiv": { + "title": "S", + "GlossList": { + "GlossEntry": { + "ID": "SGML", + "SortAs": "SGML", + "GlossTerm": "Standard Generalized Markup Language", + "Acronym": "SGML", + "Abbrev": "ISO 8879:1986", + "GlossDef": { + "para": "A meta-markup language, used to create markup languages such as DocBook.", + "GlossSeeAlso": ["GML", "XML"] + }, + "GlossSee": "markup" + } + } + } + }, + { + "title": "example glossary", + "GlossDiv": { + "title": "S", + "GlossList": { + "GlossEntry": { + "ID": "SGML", + "SortAs": "SGML", + "GlossTerm": "Standard Generalized Markup Language", + "Acronym": "SGML", + "Abbrev": "ISO 8879:1986", + "GlossDef": { + "para": "A meta-markup language, used to create markup languages such as DocBook.", + "GlossSeeAlso": ["GML", "XML"] + }, + "GlossSee": "markup" + } + } + } + }, + { + "title": "example glossary", + "GlossDiv": { + "title": "S", + "GlossList": { + "GlossEntry": { + "ID": "SGML", + "SortAs": "SGML", + "GlossTerm": "Standard Generalized Markup Language", + "Acronym": "SGML", + "Abbrev": "ISO 8879:1986", + "GlossDef": { + "para": "A meta-markup language, used to create markup languages such as DocBook.", + "GlossSeeAlso": ["GML", "XML"] + }, + "GlossSee": "markup" + } + } + } + } +] +})EOF"; + +inline std::string BigSample = []{ + using namespace std::string_literals; + auto part = std::string(BooksSample); + auto bigPart = '['+part+','+part+','+part+','+part+','+part+','+part+','+part+','+part+']'; + auto biggerPart = '['+bigPart+','+bigPart+','+bigPart+','+bigPart+','+bigPart+','+bigPart+','+bigPart+','+bigPart+']'; + return R"({"big1":)"+biggerPart+R"(, "big2":)"+biggerPart+'}'; +}(); + +inline auto LateFailSample = R"( + { + "title": "example glossary", + "GlossDiv": { + "title": "S", + "GlossList": { + "GlossEntry": { + "ID": "SGML", + "SortAs": "SGML", + "GlossTerm": "Standard Generalized Markup Language", + "Acronym": "SGML", + "Abbrev": "ISO 8879:1986", + "GlossDef": { + "para": "A meta-markup language, used to create markup languages such as DocBook.", + "GlossSeeAlso": ["GML", "XML"] + }, + "GlossSee": "markup" + } + } + } + 123} +)"; + + +static constexpr uint8_t MsgPackRPC[] = { + 0x84, 0xA6, 0x6D, 0x65, 0x74, 0x68, 0x6F, 0x64, 0xAA, 0x6D, 0x65, 0x74, 0x68, 0x6F, 0x64, 0x4E, 0x61, 0x6D, 0x65, 0xA2, 0x69, 0x64 + , 0xB3, 0x61, 0x72, 0x62, 0x69, 0x74, 0x72, 0x61, 0x72, 0x79, 0x2D, 0x73, 0x6F, 0x6D, 0x65, 0x74, 0x68, 0x69, 0x6E, 0x67, 0xA6 + , 0x70, 0x61, 0x72, 0x61, 0x6D, 0x73, 0x93, 0x03, 0x02, 0x81, 0xA4, 0x65, 0x70, 0x69, 0x63, 0xA5, 0x70, 0x61, 0x72, 0x61, 0x6D + , 0xA7, 0x6A, 0x73, 0x6F, 0x6E, 0x72, 0x70, 0x63, 0xA3, 0x32, 0x2E, 0x30 +}; + +static constexpr uint8_t MsgPackBooks[] = { + 129, 168, 103, 108, 111, 115, 115, 97, 114, 121, 147, 130, 165, 116, 105, 116, 108, 101, 176, 101, 120, 97, 109, 112, 108, 101, 32, 103, 108, 111, 115, 115, 97, 114, 121, 168, 71, 108, 111, 115, 115, 68, 105, 118, 130, 165, 116, 105, 116, 108, 101, 161, 83, 169, 71, 108, 111, 115, 115, 76, 105, 115, 116, 129, 170, 71, 108, 111, 115, 115, 69, 110, 116, 114, 121, 135, 162, 73, 68, 164, 83, 71, 77, 76, 166, 83, 111, 114, 116, 65, 115, 164, 83, 71, 77, 76, 169, 71, 108, 111, 115, 115, 84, 101, 114, 109, 217, 36, 83, 116, 97, 110, 100, 97, 114, 100, 32, 71, 101, 110, 101, 114, 97, 108, 105, 122, 101, 100, 32, 77, 97, 114, 107, 117, 112, 32, 76, 97, 110, 103, 117, 97, 103, 101, 167, 65, 99, 114, 111, 110, 121, 109, 164, 83, 71, 77, 76, 166, 65, 98, 98, 114, 101, 118, 173, 73, 83, 79, 32, 56, 56, 55, 57, 58, 49, 57, 56, 54, 168, 71, 108, 111, 115, 115, 68, 101, 102, 130, 164, 112, 97, 114, 97, 217, 72, 65, 32, 109, 101, 116, 97, 45, 109, 97, 114, 107, 117, 112, 32, 108, 97, 110, 103, 117, 97, 103, 101, 44, 32, 117, 115, 101, 100, 32, 116, 111, 32, 99, 114, 101, 97, 116, 101, 32, 109, 97, 114, 107, 117, 112, 32, 108, 97, 110, 103, 117, 97, 103, 101, 115, 32, 115, 117, 99, 104, 32, 97, 115, 32, 68, 111, 99, 66, 111, 111, 107, 46, 172, 71, 108, 111, 115, 115, 83, 101, 101, 65, 108, 115, 111, 146, 163, 71, 77, 76, 163, 88, 77, 76, 168, 71, 108, 111, 115, 115, 83, 101, 101, 166, 109, 97, 114, 107, 117, 112, 130, 165, 116, 105, 116, 108, 101, 176, 101, 120, 97, 109, 112, 108, 101, 32, 103, 108, 111, 115, 115, 97, 114, 121, 168, 71, 108, 111, 115, 115, 68, 105, 118, 130, 165, 116, 105, 116, 108, 101, 161, 83, 169, 71, 108, 111, 115, 115, 76, 105, 115, 116, 129, 170, 71, 108, 111, 115, 115, 69, 110, 116, 114, 121, 135, 162, 73, 68, 164, 83, 71, 77, 76, 166, 83, 111, 114, 116, 65, 115, 164, 83, 71, 77, 76, 169, 71, 108, 111, 115, 115, 84, 101, 114, 109, 217, 36, 83, 116, 97, 110, 100, 97, 114, 100, 32, 71, 101, 110, 101, 114, 97, 108, 105, 122, 101, 100, 32, 77, 97, 114, 107, 117, 112, 32, 76, 97, 110, 103, 117, 97, 103, 101, 167, 65, 99, 114, 111, 110, 121, 109, 164, 83, 71, 77, 76, 166, 65, 98, 98, 114, 101, 118, 173, 73, 83, 79, 32, 56, 56, 55, 57, 58, 49, 57, 56, 54, 168, 71, 108, 111, 115, 115, 68, 101, 102, 130, 164, 112, 97, 114, 97, 217, 72, 65, 32, 109, 101, 116, 97, 45, 109, 97, 114, 107, 117, 112, 32, 108, 97, 110, 103, 117, 97, 103, 101, 44, 32, 117, 115, 101, 100, 32, 116, 111, 32, 99, 114, 101, 97, 116, 101, 32, 109, 97, 114, 107, 117, 112, 32, 108, 97, 110, 103, 117, 97, 103, 101, 115, 32, 115, 117, 99, 104, 32, 97, 115, 32, 68, 111, 99, 66, 111, 111, 107, 46, 172, 71, 108, 111, 115, 115, 83, 101, 101, 65, 108, 115, 111, 146, 163, 71, 77, 76, 163, 88, 77, 76, 168, 71, 108, 111, 115, 115, 83, 101, 101, 166, 109, 97, 114, 107, 117, 112, 130, 165, 116, 105, 116, 108, 101, 176, 101, 120, 97, 109, 112, 108, 101, 32, 103, 108, 111, 115, 115, 97, 114, 121, 168, 71, 108, 111, 115, 115, 68, 105, 118, 130, 165, 116, 105, 116, 108, 101, 161, 83, 169, 71, 108, 111, 115, 115, 76, 105, 115, 116, 129, 170, 71, 108, 111, 115, 115, 69, 110, 116, 114, 121, 135, 162, 73, 68, 164, 83, 71, 77, 76, 166, 83, 111, 114, 116, 65, 115, 164, 83, 71, 77, 76, 169, 71, 108, 111, 115, 115, 84, 101, 114, 109, 217, 36, 83, 116, 97, 110, 100, 97, 114, 100, 32, 71, 101, 110, 101, 114, 97, 108, 105, 122, 101, 100, 32, 77, 97, 114, 107, 117, 112, 32, 76, 97, 110, 103, 117, 97, 103, 101, 167, 65, 99, 114, 111, 110, 121, 109, 164, 83, 71, 77, 76, 166, 65, 98, 98, 114, 101, 118, 173, 73, 83, 79, 32, 56, 56, 55, 57, 58, 49, 57, 56, 54, 168, 71, 108, 111, 115, 115, 68, 101, 102, 130, 164, 112, 97, 114, 97, 217, 72, 65, 32, 109, 101, 116, 97, 45, 109, 97, 114, 107, 117, 112, 32, 108, 97, 110, 103, 117, 97, 103, 101, 44, 32, 117, 115, 101, 100, 32, 116, 111, 32, 99, 114, 101, 97, 116, 101, 32, 109, 97, 114, 107, 117, 112, 32, 108, 97, 110, 103, 117, 97, 103, 101, 115, 32, 115, 117, 99, 104, 32, 97, 115, 32, 68, 111, 99, 66, 111, 111, 107, 46, 172, 71, 108, 111, 115, 115, 83, 101, 101, 65, 108, 115, 111, 146, 163, 71, 77, 76, 163, 88, 77, 76, 168, 71, 108, 111, 115, 115, 83, 101, 101, 166, 109, 97, 114, 107, 117, 112 +}; diff --git a/test/rpcxx/json_test.cpp b/test/rpcxx/json_test.cpp new file mode 100644 index 0000000..0e6d04f --- /dev/null +++ b/test/rpcxx/json_test.cpp @@ -0,0 +1,413 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#include "rpcxx/rpcxx.hpp" +#include "json_samples.hpp" +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include "doctest/doctest.h" + +template<> struct doctest::StringMaker { + static String convert(const rpcxx::Json& value) { + return value.View().Dump().c_str(); + } +}; +template<> struct doctest::StringMaker { + static String convert(const rpcxx::JsonView& value) { + return value.Dump().c_str(); + } +}; + +using namespace rpcxx; +using namespace describe; + +TEST_CASE("json") +{ + SUBCASE("deep recursion") { + constexpr size_t size = 500'000; + string sample(size * 2, '['); + std::fill(sample.begin() + size, sample.end(), ']'); + CHECK_THROWS((void)Json::Parse(sample)); + } + SUBCASE("view basic") { + JsonView json{5}; + CHECK_EQ(json.Get(), 5); + CHECK(DeepEqual(0, 0)); + json = "a"; + CHECK_EQ(json.Get(), "a"); + CHECK_EQ(json.Get(), "a"); + } + SUBCASE("resize") { + auto empty = MutableJson{t_array}; + auto json = empty.Copy(); + json.GetArray().resize(10); + } + SUBCASE("asign") { + MutableJson wow; + wow["a"] = string_view{"3123"}; + wow["a"] = 312311; + wow["b"]["c"] = 3123; + CHECK(wow["b"]["c"].GetInt() == 3123); + } + SUBCASE("conversion") { + DefaultArena alloc; + string_view raw = R"({"key": 123, "hello": "world", "arr": [true, "2", 3]})"; + auto orig = ParseJson(raw, alloc); + auto persistent = orig.Get(); + auto back = JsonView::From(persistent, alloc); + CHECK(DeepEqual(orig, back)); + } +} + +TEST_CASE("algos") +{ + SUBCASE("basic") { + DefaultArena alloc; + auto json = Json::Parse(R"({"key": 123, "hello": "world", "arr": [true, "2", 3], "z": "w"})"); + auto flat = Flatten(json.View(), alloc); + MutableJson back; + MutableJson merged = MutableJson(json.View()); + Unflatten(back, flat); + CHECK(DeepEqual(flat["/arr/0"], JsonView(true))); + CHECK(DeepEqual(flat["/arr/1"], string_view{"2"})); + CHECK(DeepEqual(flat["/arr/2"], 3)); + CHECK(DeepEqual(back.View(alloc), json.View())); + + auto patch = Json::Parse(R"({"lol": "kek"})"); + MergePatch(merged, patch.View()); + CHECK(merged.View(alloc)["lol"].GetString() == "kek"); + CHECK(patch.View()["lol"].GetString() == "kek"); + + patch = Json::Parse(R"({"lol": [1, 2, 3]})"); + MergePatch(merged, patch.View()); + CHECK(merged.View(alloc)["lol"].Size() == 3); + + patch = Json::Parse(R"({"lol1": [1, 2, 3]})"); + MergePatch(merged, patch.View()); + patch = Json::Parse(R"({"lol": null})"); + MergePatch(merged, patch.View()); + CHECK(!merged.View(alloc).Find("lol")); + + CHECK(merged.View(alloc)["arr"].Size()); + patch = Json::Parse(R"({"arr": []})"); + MergePatch(merged, patch.View()); + CHECK(merged.View(alloc)["arr"].Size() == 0); + } +} + +TEST_CASE("parse json") { + SUBCASE("fuzz victims") { + std::vector samples = { + "1e1", + }; + for (auto& s: samples) { + auto j = Json::Parse(s); + auto b = Json::Parse(j->Dump()); + CHECK(DeepEqual(j.View(), b.View())); + } + } + SUBCASE("alloc") { + DefaultArena alloc; + ArenaString str("123", alloc); + str.Append(str); + str.Append(str); + str.Append(str); + str.Append(str); + ArenaVector strs(alloc); + ArenaVector vec(alloc); + for (auto i = 0; i < 10; ++i) { + strs.push_back(str); + vec.push_back(i); + } + vec.push_back(2); + CHECK(vec[9] == 9); + CHECK(strs[0] == str); + CHECK(strs[9] == str); + } + GIVEN("basic") { + DefaultArena alloc; + ParseSettings opts; + string_view raw = R"({ + "key": 123, + "hello": "world", + "arr": [ + true, + "2", + 3, + "false", + false, + {}, + [{}], + "abrobrababor" + ] + })"; + auto run = [&]{ + auto json = ParseJson(raw, alloc, opts); + auto key = json.At("key"); + CHECK_EQ(key.Get(), 123); + auto hello = json.At("hello"); + CHECK_EQ(hello.GetString(), "world"); + auto arr = json.At("arr"); + CHECK_EQ(arr.At(0).Get(), true); + CHECK_EQ(arr.At(1).Get(), "2"); + CHECK_EQ(arr.At(2).Get(), 3); + CHECK_EQ(arr.At(3).GetString(), "false"); + CHECK_EQ(arr.At(4).Get(), false); + }; + run(); + opts.sorted = true; + run(); + } + GIVEN("escaped") { + DefaultArena alloc; + string_view raw = R"({"key": 123, "he\"llo": "wo\"rld", "arr": [true, "2", 3]})"; + auto mut = ParseJson(raw, alloc); + CHECK(mut.At("he\"llo").GetString() == "wo\"rld"); + } + GIVEN("non terminated") { + DefaultArena alloc; + char sample[] = {R"({"key": 123, "hello": "world"}123)"}; + CHECK_THROWS((void)ParseJson(sample, alloc)); + CHECK_NOTHROW((void)ParseJson(string_view{sample, sizeof(sample) - 4}, alloc)); + } + GIVEN("books") { + DefaultArena alloc; + auto json = ParseJson(BooksSample, alloc); + auto nested = json + ["glossary"][1] + ["GlossDiv"] + ["GlossList"] + ["GlossEntry"] + ["GlossDef"] + ["GlossSeeAlso"][0]; + CHECK_EQ(nested.GetString(), "GML"); + } + GIVEN("big") { + DefaultArena alloc; + auto res = ParseJson(BigSample, alloc); + CHECK(res.At("big1").Is(jv::t_array)); + } + GIVEN("empties") { + DefaultArena alloc; + auto sample = R"({"array":[], "object": {}})"; + auto empty = ParseJson(sample, alloc); + CHECK(empty["array"].Is(t_array)); + CHECK_EQ(empty["array"].Size(), 0); + CHECK(empty["object"].Is(jv::t_object)); + CHECK_EQ(empty["object"].Size(), 0); + } + SUBCASE("dump") { + DefaultArena alloc; + GIVEN("rpc sample") { + auto json = ParseJson(RPCSample, alloc); + auto serialized = DumpJson(json); + auto back = ParseJson(serialized, alloc); + CHECK(DeepEqual(json, back)); + } + GIVEN("books sample") { + auto json = ParseJson(BooksSample, alloc); + auto serialized = DumpJson(json); + auto back = ParseJson(serialized, alloc); + CHECK(DeepEqual(json, back)); + } + } +} + +enum Lol { + kek, chebureck +}; +DESCRIBE(Lol, kek, chebureck) + +struct Nested { + int a; + std::string b; + Lol en = kek; +}; +DESCRIBE(Nested, &_::a, &_::b, &_::en) + +namespace test { + +struct Data { + int a; + uint32_t b; + Nested nested; + void Validate(JsonView json); + + int lol(int b); +}; + +DESCRIBE(test::Data, &_::a, &_::b, &_::nested, &_::lol) + +TEST_CASE("json describe") { + CHECK(describe::Get().fields_count == 3); + CHECK(describe::Get().methods_count == 1); + CHECK(describe::Get().index_of<&Data::a>() == 0); + CHECK(describe::Get().index_of<&Data::b>() == 1); + CHECK(describe::Get().index_of(Field<&Data::a>{}) == 0); + CHECK(describe::Get().index_of(Field<&Data::b>{}) == 1); +} + +} + + +struct Derived : test::Data {}; + +void test::Data::Validate(JsonView) +{ + if (b > 2000) { + throw "b is too big!"; + } +} + +TEST_CASE("deserialize") +{ + SUBCASE("basic") { + auto sample = R"({ + "a":5, "b": 150, + "nested": {"a": 123, "b": "avava", "en": "chebureck"} + })"; + constexpr auto dataDesc = describe::Get(); + constexpr auto nestedDesc = describe::Get(); + static_assert(dataDesc.name == "test::Data"); + static_assert(nestedDesc.name == "Nested"); + auto res = Json::Parse(sample).View().Get(); + CHECK_EQ(res.a, 5); + CHECK_EQ(res.b, 150); + CHECK_EQ(res.nested.a, 123); + CHECK_EQ(res.nested.en, chebureck); + CHECK_EQ(res.nested.b, "avava"); + auto sample_bad = R"({ + "a":5, "b": -15, + "nested": {"a": 123, "b": 3} + })"; + auto badJson = Json::Parse(sample_bad); + CHECK_THROWS(badJson.View().Get()); + } + SUBCASE("containers") { + auto sample_map = R"({"a":5, "b": -15, "c": -15})"; + auto sample_vec = R"([54, -15])"; + using int_map = std::map; + using int_vec = std::vector; + static_assert(is_assoc_container_v); + static_assert(!is_index_container_v); + static_assert(!is_assoc_container_v); + static_assert(is_index_container_v); + auto map = Json::Parse(sample_map).View().Get(); + CHECK_EQ(map["a"], 5); + CHECK_EQ(map["b"], -15); + auto vec = Json::Parse(sample_vec).View().Get(); + CHECK_EQ(vec.at(0), 54); + CHECK_EQ(vec.at(1), -15); + } +} + +TEST_CASE("describe") { + SUBCASE("dump struct") { + DefaultArena alloc; + auto lol = DumpStruct(alloc); + CHECK(lol["a"].GetString() == "int32"); + CHECK(lol["b"].GetString() == "uint32"); + CHECK(lol["nested"]["a"].GetString() == "int32"); + CHECK(lol["nested"]["b"].GetString() == "string"); + CHECK(lol["nested"]["en"].GetString() == "Lol"); + } + SUBCASE("methods") { + auto desc = describe::Get(); + desc.for_each_field([](auto f){ + if (f.name == "lol") { + CHECK(f.is_method); + } + }); + } +} + +TEST_CASE("exceptions") +{ + auto sample = R"({ + "a":5, "b": 150, + "nested": {"a": 123, "b": 3}, + "arr": [1.0, -2, 300], + "empty": [], + "empty_obj": {} + })"; + auto raw = Json::Parse(sample); + auto json = raw.View(); + + CHECK_THROWS_AS(json.At("c"), KeyError); + CHECK_THROWS_AS(json["empty_obj"].At("3"), KeyError); + CHECK_THROWS_AS(json["arr"][2].Get(), IntRangeError); + CHECK_THROWS_AS(json["arr"][1].Get(), IntRangeError); + CHECK_NOTHROW(json["arr"][2].Get()); + CHECK_NOTHROW(json["arr"][2].Get()); + CHECK_NOTHROW(json["arr"][1].Get()); + + CHECK_NOTHROW(json["arr"][1]); + CHECK_THROWS_AS(json["arr"][3], IndexError); + CHECK_THROWS_AS(json["empty"][0], IndexError); + + CHECK_THROWS_AS(json["arr"][0].Get(), TypeMissmatch); + CHECK_THROWS_AS(json["arr"][2].Size(), TypeMissmatch); + CHECK_THROWS_AS(json["nested"][0], TypeMissmatch); + CHECK_THROWS_AS(json.Get(), TypeMissmatch); + CHECK_THROWS_AS(json["nested"].Get(), TypeMissmatch); +} + + +TEST_CASE("serialize") +{ + SUBCASE("basic") { + DefaultArena ctx; + test::Data data{1, 2, {3, "123"}}; + auto res = JsonView::From(data, ctx); + CHECK_EQ(res["a"].Get(), 1); + CHECK_EQ(res["b"].Get(), 2); + CHECK_EQ(res["nested"]["a"].Get(), 3); + CHECK_EQ(res["nested"]["b"].Get(), "123"); + CHECK_EQ(res["nested"]["en"].Get(), "kek"); + } + SUBCASE("containers") { + DefaultArena ctx; + test::Data data{1, 2, {3, "123"}}; + std::vector vec{data, data, data}; + auto ser_vec = JsonView::From(vec, ctx); + auto test_one = [](JsonView j){ + CHECK_EQ(j["a"].Get(), 1); + CHECK_EQ(j["b"].Get(), 2); + CHECK_EQ(j["nested"]["a"].Get(), 3); + CHECK_EQ(j["nested"]["b"].Get(), "123"); + }; + test_one(ser_vec[0]); + test_one(ser_vec[1]); + test_one(ser_vec[2]); + std::map map { + {"lol", data}, + {"kek", data}, + {"cheburek", data} + }; + auto ser_map = JsonView::From(map, ctx); + test_one(ser_map["lol"]); + test_one(ser_map["kek"]); + test_one(ser_map["cheburek"]); + } +} + diff --git a/test/rpcxx/msgpack_fuzzing.cpp b/test/rpcxx/msgpack_fuzzing.cpp new file mode 100644 index 0000000..936baaa --- /dev/null +++ b/test/rpcxx/msgpack_fuzzing.cpp @@ -0,0 +1,66 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#include "rpcxx/rpcxx.hpp" +#include + +using namespace rpcxx; + +// see http://llvm.org/docs/LibFuzzer.html +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) +{ + try + { + DefaultArena alloc; + auto orig = rpcxx::ParseMsgPackInPlace(data, size, alloc).result; + try + { + auto back = orig.DumpMsgPack(); + auto round = rpcxx::Json::FromMsgPack(back); + if(!DeepEqual(round.View(), orig)) { + std::cerr << "Missmatch: \n" + << orig.Dump(true) + << "\n not equal to: " + << round.View().Dump(true) + << "\n serialized: "; + for (auto ch: back) { + std::cerr << ",0x" << std::hex << ch; + } + std::cerr << std::endl; + std::exit(1); + } + } + catch (std::bad_alloc&) {} + catch (jv::DepthError&) {} + catch (...) { + // parsing a MessagePack serialization must not fail + assert(false); + } + } + catch (std::bad_alloc&) {} + catch (std::runtime_error&) {} + catch (jv::DepthError&) {} + // return 0 - non-zero return values are reserved for future use + return 0; +} diff --git a/test/rpcxx/msgpack_test.cpp b/test/rpcxx/msgpack_test.cpp new file mode 100644 index 0000000..c3eaf4d --- /dev/null +++ b/test/rpcxx/msgpack_test.cpp @@ -0,0 +1,1281 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +// Часть тест кейсов взята из nlohmann/json +// Lohmann, N. (2023). JSON for Modern C++ (Version 3.11.3) [Computer software]. https://github.com/nlohmann + +#include "json_samples.hpp" +#include +#include +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include "doctest/doctest.h" + +using namespace rpcxx; + +#define SECTION(x) SUBCASE(x) + +using json = Json; + +json operator""_json(const char* s, size_t sz) { + return json::Parse({s, sz}); +} + +static bool operator==(Json const& l, Json const& r) { + return DeepEqual(l.View(), r.View()); +} + +static bool operator==(Json const& l, JsonView const& r) { + return DeepEqual(l.View(), r); +} + +template<> struct doctest::StringMaker> { + static String convert(const std::vector& value) { + string res = "["; + for (auto i: value) { + res += "," + std::to_string(i); + } + res += "]"; + return res.c_str(); + } +}; + +std::vector DumpAsVec(JsonView j) { + membuff::StringOut> buff; + DumpMsgPackInto(buff, j); + return buff.Consume(); +} + +std::vector DumpAsVec(Json j) { + return DumpAsVec(j.View()); +} + +Json JsonFromVec(const std::vector& bytes, bool = false, bool = false) { + DefaultArena ctx; + auto res = Json{ParseMsgPackInPlace(bytes.data(), bytes.size(), ctx).result}; + return res; +} + +using doctest::Approx; + +TEST_CASE("small fixstr") { + DefaultArena ctx; + JsonView sample("aboba"); + auto ser = DumpMsgPack(sample); + auto back = ParseMsgPackInPlace(ser, ctx).result; + CHECK(sample == back); +} +TEST_CASE("depth limit") { + DefaultArena ctx; + std::vector sample(250, 0x91); + sample.push_back(0xc0); + CHECK_THROWS((void)ParseMsgPackInPlace(sample.data(), sample.size(), ctx, {30})); + CHECK_NOTHROW((void)ParseMsgPackInPlace(sample.data(), sample.size(), ctx, {251})); +} +TEST_CASE("roundtrip") { + DefaultArena ctx; + auto do_one = [&](auto sample, size_t len) { + auto json = ParseMsgPackInPlace(sample, len, ctx); + auto serialized = DumpMsgPack(json); + auto back = ParseMsgPackInPlace(serialized, ctx); + CHECK(DeepEqual(json, back)); + }; + GIVEN("rpc sample") { + do_one(MsgPackRPC, sizeof(MsgPackRPC)); + } + GIVEN("books sample") { + do_one(MsgPackBooks, sizeof(MsgPackBooks)); + } +} +TEST_CASE("parse") { + GIVEN("rpc sample") { + DefaultArena ctx; + auto json = ParseMsgPackInPlace(MsgPackRPC, sizeof(MsgPackRPC), ctx).result; + CHECK_EQ(json["params"][0].Get(), 3); + } + GIVEN("books sample") { + DefaultArena ctx; + auto json = ParseMsgPackInPlace(MsgPackBooks, sizeof(MsgPackBooks), ctx).result; + CHECK(json["glossary"][0]["title"].Get() == "example glossary"); + CHECK(json["glossary"][2] + ["GlossDiv"] + ["GlossList"] + ["GlossEntry"] + ["GlossDef"] + ["GlossSeeAlso"] + [1].GetString() == "XML"); + } +} +TEST_CASE("big containers") { + auto bigarr = MutableJson(t_array); + DefaultArena ctx; + bigarr.GetArray().resize(200); + auto bigarrView = bigarr.View(ctx); + auto pack = DumpMsgPack(bigarrView); + auto back = ParseMsgPackInPlace(pack, ctx).result; + CHECK(DeepEqual(back, bigarrView)); + auto bigobj = MutableJson(t_object); + for (auto i = 100; i < 200; ++i) { + bigobj[std::to_string(i)] = bigarr.Copy(); + } + auto source = bigobj.View(ctx); + pack = DumpMsgPack(source); + back = ParseMsgPackInPlace(pack, ctx).result; + CHECK(back == source); +} +TEST_CASE("null") +{ + json const j; + std::vector const expected = {0xc0}; + const auto result = DumpAsVec(j); + CHECK(result == expected); + + // roundtrip + CHECK(JsonFromVec(result) == j); +} + +TEST_CASE("boolean") +{ + SECTION("true") + { + json const j {JsonView(true)}; + std::vector const expected = {0xc3}; + const auto result = DumpAsVec(j); + CHECK(result == expected); + + // roundtrip + CHECK(JsonFromVec(result) == j); + } + + SECTION("false") + { + json const j {JsonView(false)}; + std::vector const expected = {0xc2}; + const auto result = DumpAsVec(j); + CHECK(result == expected); + + // roundtrip + CHECK(JsonFromVec(result) == j); + } +} + +TEST_CASE("number") +{ + SECTION("signed") + { + SECTION("-32..-1 (negative fixnum)") + { + for (auto i = -32; i <= -1; ++i) + { + CAPTURE(i); + + // create JSON value with integer number + json const j {i}; + + // check type + CHECK(j.View().Is(t_signed | jv::t_unsigned)); + + // create expected byte vector + std::vector const expected + { + static_cast(i) + }; + + // compare result + size + const auto result = DumpAsVec(j); + CHECK(result == expected); + CHECK(result.size() == 1); + + // check individual bytes + CHECK(static_cast(result[0]) == i); + + // roundtrip + CHECK(JsonFromVec(result) == j); + } + } + + SECTION("0..127 (positive fixnum)") + { + for (size_t i = 0; i <= 127; ++i) + { + CAPTURE(i); + + // create JSON value with integer number + json j {i}; + + // check type + j.View().AssertType(t_signed | t_unsigned); + + // create expected byte vector + std::vector const expected{static_cast(i)}; + + // compare result + size + const auto result = DumpAsVec(j); + CHECK(result == expected); + CHECK(result.size() == 1); + + // check individual bytes + CHECK(result[0] == i); + + // roundtrip + CHECK(JsonFromVec(result) == j); + } + } + + SECTION("128..255 (int 8)") + { + for (size_t i = 128; i <= 255; ++i) + { + CAPTURE(i); + + // create JSON value with integer number + json j {i}; + + // check type + j.View().AssertType(t_signed | t_unsigned); + + // create expected byte vector + std::vector const expected + { + 0xcc, + static_cast(i), + }; + + // compare result + size + const auto result = DumpAsVec(j); + CHECK(result == expected); + CHECK(result.size() == 2); + + // check individual bytes + CHECK(result[0] == 0xcc); + auto const restored = static_cast(result[1]); + CHECK(restored == i); + + // roundtrip + CHECK(JsonFromVec(result) == j); + } + } + + SECTION("256..65535 (int 16)") + { + for (size_t i = 256; i <= 65535; ++i) + { + CAPTURE(i); + + // create JSON value with integer number + json j {i}; + + // check type + j.View().AssertType(t_signed | t_unsigned); + + // create expected byte vector + std::vector const expected + { + 0xcd, + static_cast((i >> 8) & 0xff), + static_cast(i & 0xff), + }; + + // compare result + size + const auto result = DumpAsVec(j); + CHECK(result == expected); + CHECK(result.size() == 3); + + // check individual bytes + CHECK(result[0] == 0xcd); + auto const restored = static_cast(static_cast(result[1]) * 256 + static_cast(result[2])); + CHECK(restored == i); + + // roundtrip + CHECK(JsonFromVec(result) == j); + } + } + + SECTION("65536..4294967295 (int 32)") + { + for (uint32_t i : + { + 65536u, 77777u, 1048576u, 4294967295u + }) + { + CAPTURE(i); + + // create JSON value with integer number + json j {i}; + + // check type + j.View().AssertType(t_signed | t_unsigned); + + // create expected byte vector + std::vector const expected + { + 0xce, + static_cast((i >> 24) & 0xff), + static_cast((i >> 16) & 0xff), + static_cast((i >> 8) & 0xff), + static_cast(i & 0xff), + }; + + // compare result + size + const auto result = DumpAsVec(j); + CHECK(result == expected); + CHECK(result.size() == 5); + + // check individual bytes + CHECK(result[0] == 0xce); + uint32_t const restored = (static_cast(result[1]) << 030) + + (static_cast(result[2]) << 020) + + (static_cast(result[3]) << 010) + + static_cast(result[4]); + CHECK(restored == i); + + // roundtrip + CHECK(JsonFromVec(result) == j); + } + } + + SECTION("4294967296..9223372036854775807 (int 64)") + { + for (uint64_t i : + { + 4294967296LU, 9223372036854775807LU + }) + { + CAPTURE(i); + + // create JSON value with integer number + json j {i}; + + // check type + j.View().AssertType(t_signed | t_unsigned); + + // create expected byte vector + std::vector const expected + { + 0xcf, + static_cast((i >> 070) & 0xff), + static_cast((i >> 060) & 0xff), + static_cast((i >> 050) & 0xff), + static_cast((i >> 040) & 0xff), + static_cast((i >> 030) & 0xff), + static_cast((i >> 020) & 0xff), + static_cast((i >> 010) & 0xff), + static_cast(i & 0xff), + }; + + // compare result + size + const auto result = DumpAsVec(j); + CHECK(result == expected); + CHECK(result.size() == 9); + + // check individual bytes + CHECK(result[0] == 0xcf); + uint64_t const restored = (static_cast(result[1]) << 070) + + (static_cast(result[2]) << 060) + + (static_cast(result[3]) << 050) + + (static_cast(result[4]) << 040) + + (static_cast(result[5]) << 030) + + (static_cast(result[6]) << 020) + + (static_cast(result[7]) << 010) + + static_cast(result[8]); + CHECK(restored == i); + + // roundtrip + CHECK(JsonFromVec(result) == j); + } + } + + SECTION("-128..-33 (int 8)") + { + for (auto i = -128; i <= -33; ++i) + { + CAPTURE(i); + + // create JSON value with integer number + json const j {i}; + + // check type + CHECK(j.View().Is(t_signed | t_unsigned)); + + // create expected byte vector + std::vector const expected + { + 0xd0, + static_cast(i), + }; + + // compare result + size + const auto result = DumpAsVec(j); + CHECK(result == expected); + CHECK(result.size() == 2); + + // check individual bytes + CHECK(result[0] == 0xd0); + CHECK(static_cast(result[1]) == i); + + // roundtrip + CHECK(JsonFromVec(result) == j); + } + } + + SECTION("-9263 (int 16)") + { + json const j {-9263}; + std::vector const expected = {0xd1, 0xdb, 0xd1}; + + const auto result = DumpAsVec(j); + CHECK(result == expected); + + auto const restored = static_cast((result[1] << 8) + result[2]); + CHECK(restored == -9263); + + // roundtrip + CHECK(JsonFromVec(result) == j); + } + + SECTION("-32768..-129 (int 16)") + { + for (int16_t i = -32768; i <= static_cast(-129); ++i) + { + CAPTURE(i); + + // create JSON value with integer number + json const j {i}; + + // check type + CHECK(j.View().Is(t_unsigned | t_signed)); + + // create expected byte vector + std::vector const expected + { + 0xd1, + static_cast((i >> 8) & 0xff), + static_cast(i & 0xff), + }; + + // compare result + size + const auto result = DumpAsVec(j); + CHECK(result == expected); + CHECK(result.size() == 3); + + // check individual bytes + CHECK(result[0] == 0xd1); + auto const restored = static_cast((result[1] << 8) + result[2]); + CHECK(restored == i); + + // roundtrip + CHECK(JsonFromVec(result) == j); + } + } + + SECTION("-32769..-2147483648") + { + std::vector const numbers + { + -32769, + -65536, + -77777, + -1048576, + -2147483648LL, + }; + for (auto i : numbers) + { + CAPTURE(i); + + // create JSON value with integer number + json const j {i}; + + // check type + CHECK(j->Is(jv::t_any_integer)); + + // create expected byte vector + std::vector const expected + { + 0xd2, + static_cast((i >> 24) & 0xff), + static_cast((i >> 16) & 0xff), + static_cast((i >> 8) & 0xff), + static_cast(i & 0xff), + }; + + // compare result + size + const auto result = DumpAsVec(j); + CHECK(result == expected); + CHECK(result.size() == 5); + + // check individual bytes + CHECK(result[0] == 0xd2); + uint32_t const restored = (static_cast(result[1]) << 030) + + (static_cast(result[2]) << 020) + + (static_cast(result[3]) << 010) + + static_cast(result[4]); + CHECK(static_cast(restored) == i); + + // roundtrip + CHECK(JsonFromVec(result) == j); + } + } + + SECTION("-9223372036854775808..-2147483649 (int 64)") + { + std::vector const numbers + { + (std::numeric_limits::min)(), + -2147483649LL, + }; + for (auto i : numbers) + { + CAPTURE(i); + + // create JSON value with unsigned integer number + json const j {i}; + + // check type + CHECK(j.View().Is(t_any_integer)); + + // create expected byte vector + std::vector const expected + { + 0xd3, + static_cast((i >> 070) & 0xff), + static_cast((i >> 060) & 0xff), + static_cast((i >> 050) & 0xff), + static_cast((i >> 040) & 0xff), + static_cast((i >> 030) & 0xff), + static_cast((i >> 020) & 0xff), + static_cast((i >> 010) & 0xff), + static_cast(i & 0xff), + }; + + // compare result + size + const auto result = DumpAsVec(j); + CHECK(result == expected); + CHECK(result.size() == 9); + + // check individual bytes + CHECK(result[0] == 0xd3); + int64_t const restored = (static_cast(result[1]) << 070) + + (static_cast(result[2]) << 060) + + (static_cast(result[3]) << 050) + + (static_cast(result[4]) << 040) + + (static_cast(result[5]) << 030) + + (static_cast(result[6]) << 020) + + (static_cast(result[7]) << 010) + + static_cast(result[8]); + CHECK(restored == i); + + // roundtrip + CHECK(JsonFromVec(result) == j); + } + } + } + + SECTION("unsigned") + { + SECTION("0..127 (positive fixnum)") + { + for (size_t i = 0; i <= 127; ++i) + { + CAPTURE(i); + + // create JSON value with unsigned integer number + json const j {i}; + + // check type + CHECK(j->Is(t_unsigned)); + + // create expected byte vector + std::vector const expected{static_cast(i)}; + + // compare result + size + const auto result = DumpAsVec(j); + CHECK(result == expected); + CHECK(result.size() == 1); + + // check individual bytes + CHECK(result[0] == i); + + // roundtrip + CHECK(JsonFromVec(result) == j); + } + } + + SECTION("128..255 (uint 8)") + { + for (size_t i = 128; i <= 255; ++i) + { + CAPTURE(i); + + // create JSON value with unsigned integer number + json const j {i}; + + // check type + CHECK(j->Is(t_unsigned)); + + // create expected byte vector + std::vector const expected + { + 0xcc, + static_cast(i), + }; + + // compare result + size + const auto result = DumpAsVec(j); + CHECK(result == expected); + CHECK(result.size() == 2); + + // check individual bytes + CHECK(result[0] == 0xcc); + auto const restored = static_cast(result[1]); + CHECK(restored == i); + + // roundtrip + CHECK(JsonFromVec(result) == j); + } + } + + SECTION("256..65535 (uint 16)") + { + for (size_t i = 256; i <= 65535; ++i) + { + CAPTURE(i); + + // create JSON value with unsigned integer number + json const j {i}; + + // check type + CHECK(j->Is(t_unsigned)); + + // create expected byte vector + std::vector const expected + { + 0xcd, + static_cast((i >> 8) & 0xff), + static_cast(i & 0xff), + }; + + // compare result + size + const auto result = DumpAsVec(j); + CHECK(result == expected); + CHECK(result.size() == 3); + + // check individual bytes + CHECK(result[0] == 0xcd); + auto const restored = static_cast(static_cast(result[1]) * 256 + static_cast(result[2])); + CHECK(restored == i); + + // roundtrip + CHECK(JsonFromVec(result) == j); + } + } + + SECTION("65536..4294967295 (uint 32)") + { + for (const uint32_t i : + { + 65536u, 77777u, 1048576u, 4294967295u + }) + { + CAPTURE(i); + + // create JSON value with unsigned integer number + json const j {i}; + + // check type + CHECK(j->Is(t_unsigned)); + + // create expected byte vector + std::vector const expected + { + 0xce, + static_cast((i >> 24) & 0xff), + static_cast((i >> 16) & 0xff), + static_cast((i >> 8) & 0xff), + static_cast(i & 0xff), + }; + + // compare result + size + const auto result = DumpAsVec(j); + CHECK(result == expected); + CHECK(result.size() == 5); + + // check individual bytes + CHECK(result[0] == 0xce); + uint32_t const restored = (static_cast(result[1]) << 030) + + (static_cast(result[2]) << 020) + + (static_cast(result[3]) << 010) + + static_cast(result[4]); + CHECK(restored == i); + + // roundtrip + CHECK(JsonFromVec(result) == j); + } + } + + SECTION("4294967296..18446744073709551615 (uint 64)") + { + for (const uint64_t i : + { + 4294967296LU, 18446744073709551615LU + }) + { + CAPTURE(i); + + // create JSON value with unsigned integer number + json const j {i}; + + // check type + CHECK(j->Is(t_unsigned)); + + // create expected byte vector + std::vector const expected + { + 0xcf, + static_cast((i >> 070) & 0xff), + static_cast((i >> 060) & 0xff), + static_cast((i >> 050) & 0xff), + static_cast((i >> 040) & 0xff), + static_cast((i >> 030) & 0xff), + static_cast((i >> 020) & 0xff), + static_cast((i >> 010) & 0xff), + static_cast(i & 0xff), + }; + + // compare result + size + const auto result = DumpAsVec(j); + CHECK(result == expected); + CHECK(result.size() == 9); + + // check individual bytes + CHECK(result[0] == 0xcf); + uint64_t const restored = (static_cast(result[1]) << 070) + + (static_cast(result[2]) << 060) + + (static_cast(result[3]) << 050) + + (static_cast(result[4]) << 040) + + (static_cast(result[5]) << 030) + + (static_cast(result[6]) << 020) + + (static_cast(result[7]) << 010) + + static_cast(result[8]); + CHECK(restored == i); + + // roundtrip + CHECK(JsonFromVec(result) == j); + } + } + } + + SECTION("float") + { + SECTION("3.1415925") + { + double const v = 3.1415925; + json const j {v}; + std::vector const expected = + { + 0xcb, 0x40, 0x09, 0x21, 0xfb, 0x3f, 0xa6, 0xde, 0xfc + }; + const auto result = DumpAsVec(j); + CHECK(result == expected); + + // roundtrip + CHECK(JsonFromVec(result) == j); + CHECK(JsonFromVec(result) == v); + } + + SECTION("1.0") + { + double const v = 1.0; + json const j {v}; + std::vector const expected = { + 0xcb, + 0x3f, 0xf0, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00 + }; + const auto result = DumpAsVec(j); + CHECK(result == expected); + + // roundtrip + CHECK(JsonFromVec(result) == j); + CHECK(JsonFromVec(result) == v); + } + + SECTION("128.128") + { + double const v = 128.1280059814453125; + json const j {v}; + std::vector const expected = { + 0xcb, 0x40, 0x60, 0x04, 0x18, 0xa0, + 0x00, 0x00, 0x00 + }; + const auto result = DumpAsVec(j); + CHECK(result == expected); + + // roundtrip + CHECK(JsonFromVec(result) == j); + CHECK(JsonFromVec(result) == v); + } + } +} + +TEST_CASE("string") +{ + SECTION("N = 0..31") + { + // explicitly enumerate the first byte for all 32 strings + const std::vector first_bytes = + { + 0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, + 0xa9, 0xaa, 0xab, 0xac, 0xad, 0xae, 0xaf, 0xb0, 0xb1, + 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, + 0xbb, 0xbc, 0xbd, 0xbe, 0xbf + }; + + for (size_t N = 0; N < first_bytes.size(); ++N) + { + CAPTURE(N); + + // create JSON value with string containing of N * 'x' + const auto s = std::string(N, 'x'); + json const j {JsonView(s)}; + + // create expected byte vector + std::vector expected; + expected.push_back(first_bytes[N]); + for (size_t i = 0; i < N; ++i) + { + expected.push_back('x'); + } + + // check first byte + CHECK((first_bytes[N] & 0x1f) == N); + + // compare result + size + const auto result = DumpAsVec(j); + CHECK(result == expected); + CHECK(result.size() == N + 1); + // check that no null byte is appended + if (N > 0) + { + CHECK(result.back() != '\x00'); + } + + // roundtrip + CHECK(JsonFromVec(result) == j); + } + } + + SECTION("N = 32..255") + { + for (size_t N = 32; N <= 255; ++N) + { + CAPTURE(N); + + // create JSON value with string containing of N * 'x' + const auto s = std::string(N, 'x'); + json const j {JsonView(s)}; + + // create expected byte vector + std::vector expected; + expected.push_back(0xd9); + expected.push_back(static_cast(N)); + for (size_t i = 0; i < N; ++i) + { + expected.push_back('x'); + } + + // compare result + size + const auto result = DumpAsVec(j); + CHECK(result == expected); + CHECK(result.size() == N + 2); + // check that no null byte is appended + CHECK(result.back() != '\x00'); + + // roundtrip + CHECK(JsonFromVec(result) == j); + } + } + + SECTION("N = 256..65535") + { + for (size_t N : + { + 256u, 999u, 1025u, 3333u, 2048u, 65535u + }) + { + CAPTURE(N); + + // create JSON value with string containing of N * 'x' + const auto s = std::string(N, 'x'); + json const j {JsonView(s)}; + + // create expected byte vector (hack: create string first) + std::vector expected(N, 'x'); + // reverse order of commands, because we insert at begin() + expected.insert(expected.begin(), static_cast(N & 0xff)); + expected.insert(expected.begin(), static_cast((N >> 8) & 0xff)); + expected.insert(expected.begin(), 0xda); + + // compare result + size + const auto result = DumpAsVec(j); + CHECK(result == expected); + CHECK(result.size() == N + 3); + // check that no null byte is appended + CHECK(result.back() != '\x00'); + + // roundtrip + CHECK(JsonFromVec(result) == j); + } + } + + SECTION("N = 65536..4294967295") + { + for (size_t N : + { + 65536u, 77777u, 1048576u + }) + { + CAPTURE(N); + + // create JSON value with string containing of N * 'x' + const auto s = std::string(N, 'x'); + json const j = json{JsonView(s)}; + + // create expected byte vector (hack: create string first) + std::vector expected(N, 'x'); + // reverse order of commands, because we insert at begin() + expected.insert(expected.begin(), static_cast(N & 0xff)); + expected.insert(expected.begin(), static_cast((N >> 8) & 0xff)); + expected.insert(expected.begin(), static_cast((N >> 16) & 0xff)); + expected.insert(expected.begin(), static_cast((N >> 24) & 0xff)); + expected.insert(expected.begin(), 0xdb); + + // compare result + size + const auto result = DumpAsVec(j); + CHECK(result == expected); + CHECK(result.size() == N + 5); + // check that no null byte is appended + CHECK(result.back() != '\x00'); + + // roundtrip + CHECK(JsonFromVec(result) == j); + } + } +} + +TEST_CASE("array") +{ + SECTION("empty") + { + json const j = json{EmptyArray()}; + std::vector const expected = {0x90}; + const auto result = DumpAsVec(j); + CHECK(result == expected); + + // roundtrip + CHECK(JsonFromVec(result) == j); + } + + SECTION("[null]") + { + json const j = json::Parse("[null]"); + std::vector const expected = {0x91, 0xc0}; + const auto result = DumpAsVec(j); + CHECK(result == expected); + + // roundtrip + CHECK(JsonFromVec(result) == j); + } + + SECTION("[1,2,3,4,5]") + { + json const j = json::Parse("[1,2,3,4,5]"); + std::vector const expected = {0x95, 0x01, 0x02, 0x03, 0x04, 0x05}; + const auto result = DumpAsVec(j); + CHECK(result == expected); + + // roundtrip + CHECK(JsonFromVec(result) == j); + } + + SECTION("[[[[]]]]") + { + json const j = json::Parse("[[[[]]]]"); + std::vector const expected = {0x91, 0x91, 0x91, 0x90}; + const auto result = DumpAsVec(j); + CHECK(result == expected); + + // roundtrip + CHECK(JsonFromVec(result) == j); + } + + SECTION("array 16") + { + DefaultArena alloc; + auto j = MutableJson(t_array); + for (auto i = 0; i < 16; ++i) { + j.GetArray().push_back(MutableJson(JsonView{nullptr})); + } + std::vector expected(j.GetArray().size() + 3, 0xc0); // all null + expected[0] = 0xdc; // array 16 + expected[1] = 0x00; // size (0x0010), byte 0 + expected[2] = 0x10; // size (0x0010), byte 1 + const auto result = DumpAsVec(j.View(alloc)); + CHECK(result == expected); + + // roundtrip + CHECK(JsonFromVec(result) == j.View(alloc)); + } + + SECTION("array 32") + { + DefaultArena alloc; + auto j = MutableJson(t_array); + for (auto i = 0; i < 65536; ++i) { + j.GetArray().push_back(MutableJson(JsonView{nullptr})); + } + std::vector expected(j.GetArray().size() + 5, 0xc0); // all null + expected[0] = 0xdd; // array 32 + expected[1] = 0x00; // size (0x00100000), byte 0 + expected[2] = 0x01; // size (0x00100000), byte 1 + expected[3] = 0x00; // size (0x00100000), byte 2 + expected[4] = 0x00; // size (0x00100000), byte 3 + const auto result = DumpAsVec(j.View(alloc)); + //CHECK(result == expected); + + CHECK(result.size() == expected.size()); + for (size_t i = 0; i < expected.size(); ++i) + { + CAPTURE(i); + CHECK(result[i] == expected[i]); + } + + // roundtrip + CHECK(JsonFromVec(result) == j.View(alloc)); + } +} + +TEST_CASE("object") +{ + SECTION("empty") + { + auto j = json::Parse("{}"); + std::vector const expected = {0x80}; + const auto result = DumpAsVec(j); + CHECK(result == expected); + + // roundtrip + CHECK(JsonFromVec(result) == j); + } + + SECTION("{\"\":null}") + { + json const j = json::Parse("{\"\":null}"); + std::vector const expected = {0x81, 0xa0, 0xc0}; + const auto result = DumpAsVec(j); + CHECK(result == expected); + + // roundtrip + CHECK(JsonFromVec(result) == j); + } + + SECTION("{\"a\": {\"b\": {\"c\": {}}}}") + { + json const j = json::Parse(R"({"a": {"b": {"c": {}}}})"); + std::vector const expected = + { + 0x81, 0xa1, 0x61, 0x81, 0xa1, 0x62, 0x81, 0xa1, 0x63, 0x80 + }; + const auto result = DumpAsVec(j); + CHECK(result == expected); + + // roundtrip + CHECK(JsonFromVec(result) == j); + } + + SECTION("map 16") + { + json const j = R"({"00": null, "01": null, "02": null, "03": null, + "04": null, "05": null, "06": null, "07": null, + "08": null, "09": null, "10": null, "11": null, + "12": null, "13": null, "14": null, "15": null})"_json; + + const auto result = DumpAsVec(j); + + // Checking against an expected vector byte by byte is + // difficult, because no assumption on the order of key/value + // pairs are made. We therefore only check the prefix (type and + // size) and the overall size. The rest is then handled in the + // roundtrip check. + CHECK(result.size() == 67); // 1 type, 2 size, 16*4 content + CHECK(result[0] == 0xde); // map 16 + CHECK(result[1] == 0x00); // byte 0 of size (0x0010) + CHECK(result[2] == 0x10); // byte 1 of size (0x0010) + + // roundtrip + CHECK(JsonFromVec(result) == j); + } + + SECTION("map 32") + { + DefaultArena alloc; + auto j = MutableJson(t_object); + for (auto i = 0; i < 65536; ++i) + { + // format i to a fixed width of 5 + // each entry will need 7 bytes: 6 for fixstr, 1 for null + std::stringstream ss; + ss << std::setw(5) << std::setfill('0') << i; + j[ss.str()] = MutableJson(t_null); + } + auto source = j.View(alloc); + CHECK(source.HasFlag(f_sorted)); + const auto result = DumpAsVec(source); + + // Checking against an expected vector byte by byte is + // difficult, because no assumption on the order of key/value + // pairs are made. We therefore only check the prefix (type and + // size) and the overall size. The rest is then handled in the + // roundtrip check. + CHECK(result.size() == 458757); // 1 type, 4 size, 65536*7 content + CHECK(result[0] == 0xdf); // map 32 + CHECK(result[1] == 0x00); // byte 0 of size (0x00010000) + CHECK(result[2] == 0x01); // byte 1 of size (0x00010000) + CHECK(result[3] == 0x00); // byte 2 of size (0x00010000) + CHECK(result[4] == 0x00); // byte 3 of size (0x00010000) + + // roundtrip + auto back = JsonFromVec(result); + CHECK(back->HasFlag(f_sorted)); + CHECK(back == source); + } +} + +TEST_CASE("binary") +{ + SECTION("N = 0..255") + { + for (std::size_t N = 0; N <= 0xFF; ++N) + { + CAPTURE(N); + + // create JSON value with byte array containing of N * 'x' + const auto s = std::vector(N, 'x'); + json j = json(JsonView::Binary(string_view(reinterpret_cast(s.data()), s.size()))); + + // create expected byte vector + std::vector expected; + expected.push_back(static_cast(0xC4)); + expected.push_back(static_cast(N)); + for (size_t i = 0; i < N; ++i) + { + expected.push_back(0x78); + } + + // compare result + size + const auto result = DumpAsVec(j); + CHECK(result == expected); + CHECK(result.size() == N + 2); + // check that no null byte is appended + if (N > 0) + { + CHECK(result.back() != '\x00'); + } + + // roundtrip + CHECK(JsonFromVec(result) == j); + } + } + + SECTION("N = 256..65535") + { + for (std::size_t N : + { + 256u, 999u, 1025u, 3333u, 2048u, 65535u + }) + { + CAPTURE(N); + + // create JSON value with string containing of N * 'x' + const auto s = std::vector(N, 'x'); + json j = json(JsonView::Binary(string_view(reinterpret_cast(s.data()), s.size()))); + + // create expected byte vector (hack: create string first) + std::vector expected(N, 'x'); + // reverse order of commands, because we insert at begin() + expected.insert(expected.begin(), static_cast(N & 0xff)); + expected.insert(expected.begin(), static_cast((N >> 8) & 0xff)); + expected.insert(expected.begin(), 0xC5); + + // compare result + size + const auto result = DumpAsVec(j); + CHECK(result == expected); + CHECK(result.size() == N + 3); + // check that no null byte is appended + CHECK(result.back() != '\x00'); + + // roundtrip + CHECK(JsonFromVec(result) == j); + } + } + + SECTION("N = 65536..4294967295") + { + for (std::size_t N : + { + 65536u, 77777u, 1048576u + }) + { + CAPTURE(N); + + // create JSON value with string containing of N * 'x' + const auto s = std::vector(N, 'x'); + json j = json(JsonView::Binary(string_view(reinterpret_cast(s.data()), s.size()))); + + // create expected byte vector (hack: create string first) + std::vector expected(N, 'x'); + // reverse order of commands, because we insert at begin() + expected.insert(expected.begin(), static_cast(N & 0xff)); + expected.insert(expected.begin(), static_cast((N >> 8) & 0xff)); + expected.insert(expected.begin(), static_cast((N >> 16) & 0xff)); + expected.insert(expected.begin(), static_cast((N >> 24) & 0xff)); + expected.insert(expected.begin(), 0xC6); + + // compare result + size + const auto result = DumpAsVec(j); + CHECK(result == expected); + CHECK(result.size() == N + 5); + // check that no null byte is appended + CHECK(result.back() != '\x00'); + + // roundtrip + CHECK(JsonFromVec(result) == j); + } + } +} + +TEST_CASE("from float32") +{ + auto given = std::vector({0xca, 0x41, 0xc8, 0x00, 0x01}); + json const j = JsonFromVec(given); + CHECK(j->Get() == Approx(25.0000019073486)); +} diff --git a/test/rpcxx/rpc_bench.cpp b/test/rpcxx/rpc_bench.cpp new file mode 100644 index 0000000..6d64b3f --- /dev/null +++ b/test/rpcxx/rpc_bench.cpp @@ -0,0 +1,236 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#include +#include +#include +#include +#include "test_methods.hpp" + +using namespace rpcxx; + +enum format { + direct, + msgpack, + json +}; + +struct MsgPackTr : IAsyncTransport { + MsgPackTr(Protocol proto, IHandler* h) : IAsyncTransport(proto, h) {} + void Send(JsonView msg) override { + membuff::StringOut out; + DumpMsgPackInto(out, msg); + DefaultArena alloc; + auto serial = out.Consume(); + auto back = ParseMsgPackInPlace(serial, alloc); + Receive(back); + } +}; + +struct JsonTr : IAsyncTransport { + JsonTr(Protocol proto, IHandler* h) : IAsyncTransport(proto, h) {} + void Send(JsonView msg) override { + membuff::StringOut out; + DumpJsonInto(out, msg); + DefaultArena alloc; + auto serial = out.Consume(); + auto back = ParseJsonInPlace(serial.data(), serial.size(), alloc); + Receive(back); + } +}; + +template +static void Notify(benchmark::State& state, std::string_view method, Args&&...args) { + auto proto = Protocol(state.range(0)); + auto serv = TestServer(); + auto cli = Client([&]() -> IClientTransport* { + switch (format(state.range(1))) { + case direct: return new ForwardToHandler(&serv); + case msgpack: return new MsgPackTr(proto, &serv); + case json: return new JsonTr(proto, &serv); + } + return nullptr; + }()); + for (auto _ : state) { + try { + cli.Notify(method, args...); + } + catch (...) {} + } +} + +template +static void RunMethod(benchmark::State& state, Ret&&, string_view method, Args&&...args) { + auto proto = Protocol(state.range(0)); + auto serv = TestServer(); + auto cli = Client([&]() -> IClientTransport* { + switch (format(state.range(1))) { + case direct: return new ForwardToHandler(&serv); + case msgpack: return new MsgPackTr(proto, &serv); + case json: return new JsonTr(proto, &serv); + } + return nullptr; + }()); + for (auto _ : state) { + try { + (void)cli.Request(Method{method, NoTimeout}, args...); + } + catch (...) {} + } +} + +struct Big { + int lol[10] = {}; +}; + +static void MoveFuncConstruction(benchmark::State& state) { + for (auto _: state) { + MoveFunc f{[]{ + return 1; + }}; + benchmark::DoNotOptimize(f); + } +} +static void MoveFuncCall(benchmark::State& state) { + MoveFunc f{[]{ + return 1; + }}; + for (auto _: state) { + benchmark::DoNotOptimize(f()); + } +} + +BENCHMARK(MoveFuncConstruction); +BENCHMARK(MoveFuncCall); + +static void StdFuncConstruction(benchmark::State& state) { + for (auto _: state) { + std::function f{[]{ + return 1; + }}; + benchmark::DoNotOptimize(f); + } + +} +static void StdFuncCall(benchmark::State& state) { + std::function f{[]{ + return 1; + }}; + for (auto _: state) { + benchmark::DoNotOptimize(f()); + } + +} + +BENCHMARK(StdFuncConstruction); +BENCHMARK(StdFuncCall); + +static void prepareBench(benchmark::internal::Benchmark* bench) +{ + bench + ->ArgNames({"minified", "transport"}) + ->ArgPair(int(Protocol::json_v2_compliant), int(direct)) + ->ArgPair(int(Protocol::json_v2_compliant), int(msgpack)) + ->ArgPair(int(Protocol::json_v2_compliant), int(json)) + ->ArgPair(int(Protocol::json_v2_minified), int(direct)) + ->ArgPair(int(Protocol::json_v2_minified), int(msgpack)) + ->ArgPair(int(Protocol::json_v2_minified), int(json)); +} + +BENCHMARK_CAPTURE(Notify, + positional, + "notification_silent", + 1, 2 + )->Apply(prepareBench); +BENCHMARK_CAPTURE(Notify, + named, + "notification_silent_named", + Arg("a", 12), Arg("b", 13) + )->Apply(prepareBench); +BENCHMARK_CAPTURE(Notify, + missing_param, + "notification_silent", + 1 + )->Apply(prepareBench); +BENCHMARK_CAPTURE(Notify, + named_type_missmatch, + "notification_silent_named", + Arg("a", 12), Arg("b", "a") + )->Apply(prepareBench); +BENCHMARK_CAPTURE(RunMethod, + positional, int{}, + "calc", + 1, 2 + )->Apply(prepareBench); +BENCHMARK_CAPTURE(RunMethod, + named, int{}, + "calc_named", + Arg("a", 1), Arg("b", 2) + )->Apply(prepareBench); +BENCHMARK_CAPTURE(RunMethod, + missing_param, int{}, + "calc", + 1 + )->Apply(prepareBench); +BENCHMARK_CAPTURE(RunMethod, + missing_param, int{}, + "calc_template", + 1 + )->Apply(prepareBench); +BENCHMARK_CAPTURE(RunMethod, + user_error, string{}, + "throws_error", + "123" + )->Apply(prepareBench); +BENCHMARK_CAPTURE(RunMethod, + named_type_missmatch, int{}, + "calc_named", + Arg("a", 1), Arg("b", "2") + )->Apply(prepareBench); + +static Small part { + "asdasdsadasdads_12312312312312312312312", + "asdasdsadasdads_12312312312312312312312", + "asdasdsadasdads_12312312312312312312312", + "asdasdsadasdads_12312312312312312312312", + "asdasdsadasdads_12312312312312312312312", + "asdasdsadasdads_12312312312312312312312", + "asdasdsadasdads_12312312312312312312312", + "asdasdsadasdads_12312312312312312312312", + "asdasdsadasdads_12312312312312312312312", + "asdasdsadasdads_12312312312312312312312", + "asdasdsadasdads_12312312312312312312312", + "asdasdsadasdads_12312312312312312312312", + "asdasdsadasdads_12312312312312312312312", + "asdasdsadasdads_12312312312312312312312", +}; +static RpcBig big { + {part, part, part, part, part, part, part, part,}, + {123123123, 1231231, 1231231, 1231231, 1231231, 1231231, 1231231} +}; + +BENCHMARK_CAPTURE(RunMethod, big, RpcBig{}, "big", big)->Apply(prepareBench); +BENCHMARK_CAPTURE(RunMethod, big_named, RpcBig{}, "big_named", Arg("body", big))->Apply(prepareBench); + +BENCHMARK_MAIN(); diff --git a/test/rpcxx/rpc_test.cpp b/test/rpcxx/rpc_test.cpp new file mode 100644 index 0000000..510a928 --- /dev/null +++ b/test/rpcxx/rpc_test.cpp @@ -0,0 +1,177 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#include "rpcxx/rpcxx.hpp" +#include "future/to_std_fut.hpp" +#include "test_methods.hpp" +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include "doctest/doctest.h" + +using namespace rpcxx; + +enum format { + direct, + msgpack, + json, +}; + +struct MockTransport : IAsyncTransport { + using IAsyncTransport::IAsyncTransport; + format fmt = direct; + void Send(JsonView msg) override { + switch (fmt) { + case direct: { + Receive(msg); + break; + } + case msgpack: { + membuff::StringOut out; + DumpMsgPackInto(out, msg); + DefaultArena alloc; + auto serial = out.Consume(); + auto back = ParseMsgPackInPlace(serial, alloc); + Receive(back); + break; + } + case json: { + membuff::StringOut out; + DumpJsonInto(out, msg); + DefaultArena alloc; + auto serial = out.Consume(); + auto back = ParseJsonInPlace(serial.data(), serial.size(), alloc); + Receive(back); + break; + } + } + + } +}; + +struct Test { + int a; + string b; +}; + +DESCRIBE(::Test, &_::a, &_::b) + +template +T req(Client& cli, string_view name, Args const&...a) { + return ToStdFuture(cli.Request(Method{name, NoTimeout}, a...)).get(); +} + +void extraMethods(Server& server) { + server.SetRoute("self", &server); + server.Method("add", [](int a, optional b){ + return a + b.value_or(0); + }); + server.Method("copy", [](Test arg){ + return arg; + }); + server.Notify("notif", [](int, int){ + return 0; + }); + server.Method("copy_named", [](Test arg){ + return arg; + }, rpcxx::NamesMap("arg")); +} + +void basicTest(Client& cli) { + CHECK(req(cli, "add", 1, 2) == 3); + + // routes + CHECK(req(cli, "self/add", 1, 2) == 3); + CHECK(req(cli, "/self/add", 1, 2) == 3); + CHECK(req(cli, "/self/add/", 1, 2) == 3); + CHECK(req(cli, "/self/add//", 1, 2) == 3); + CHECK(req(cli, "/////self/////add//////", 1, 2) == 3); + CHECK(req(cli, "/self/add//////////////////////////////", 1, 2) == 3); + CHECK_THROWS(req(cli, "self/add/a/", 1, 2)); + CHECK_THROWS(req(cli, "self/add//a", 1, 2)); + CHECK_THROWS(req(cli, "/./self/add", 1, 2)); + + CHECK(req(cli, "add", 1) == 1); + CHECK(req(cli, "async_ping", "ping") == "pong"); + + // These cases fail on one transport but not the other and vice versa + + //CHECK_NOTHROW(cli.Notify("notif", 1)); + //CHECK_NOTHROW(cli.Notify("notif")); + CHECK_NOTHROW(cli.Notify("notif", 1, 2)); + //CHECK_NOTHROW(cli.Notify("notif", "123")); + + CHECK_THROWS(req(cli, "add1", 1)); + CHECK_THROWS(req(cli, "self/add1", 1)); + CHECK_THROWS(req(cli, "add", "123")); + CHECK_THROWS(req(cli, "add", 1)); + CHECK_THROWS(req(cli, "ping", "pong")); + CHECK(req(cli, "copy", Test{1, ""}).a == 1); + CHECK(req(cli, "copy_named", rpcxx::Arg("arg", Test{1, "123"})).b == "123"); +} + +void batchTest(Client& cli) { + auto b = cli.StartBatch(); + int hits = 0; + cli.Notify("notif", 2, 2); + cli.Notify("notif", 2, 2); + cli.Notify("notif", 1, 2); + (void)cli.Request(Method{"add", NoTimeout}, 1, 2).ThenSync([&](int result){ + hits++; + CHECK(result == 3); + }); + auto a = cli.Request(Method{"async_ping", NoTimeout}, "ping").ThenSync([&](string result){ + hits++; + CHECK(result == "pong"); + }); + (void)cli.Request(Method{"ping", NoTimeout}).AtLastSync([&](auto result){ + hits++; + CHECK(!result); + }); + CHECK(hits == 0); + b.Finish(); + ToStdFuture(std::move(a)).get(); + CHECK(hits == 3); +} + +TEST_CASE("rpc") { + TestServer server; + extraMethods(server); + for (auto format: {direct, json, msgpack}) { + for (auto proto: {Protocol::json_v2_compliant, Protocol::json_v2_minified}) { + CAPTURE(PrintProto(proto)); + rc::Strong fwd = new ForwardToHandler(&server); + rc::Strong send = new MockTransport(proto, &server); + static_cast(send.get())->fmt = format; + Client cli; + for (auto& transport: {fwd, send}) { + cli.SetTransport(transport); + SUBCASE("basic") { + basicTest(cli); + } + SUBCASE("batch") { + batchTest(cli); + } + } + } + } +} diff --git a/test/rpcxx/test_methods.hpp b/test/rpcxx/test_methods.hpp new file mode 100644 index 0000000..96d386a --- /dev/null +++ b/test/rpcxx/test_methods.hpp @@ -0,0 +1,139 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#pragma once + +#include +#include +#include + +using namespace std::chrono_literals; + +struct Small { + std::string field0; + std::string field1; + std::string field2; + std::string field3; + std::string field4; + std::string field5; + std::string field6; + std::string field7; + std::string field8; + std::string field9; + std::string field10; + std::string field11; + std::string field12; + std::string field13; +}; +DESCRIBE(Small,&_::field0,&_::field1,&_::field2,&_::field3, + &_::field4,&_::field5,&_::field6,&_::field7,&_::field8, + &_::field9,&_::field10,&_::field11,&_::field12,&_::field13) +struct RpcBig { + std::vector parts; + std::vector nums; +}; +DESCRIBE(RpcBig, &_::parts, &_::nums) + +struct TestServer : rpcxx::Server { + TestServer() { + Notify("notification2", &TestServer::notification2); + Notify("notification3", [](int, int, std::string){ + + }); + Notify("notification", &TestServer::notification); + Method("throws_error", &TestServer::throws_error); + Method("ping", &TestServer::ping); + Method("method", &TestServer::method); + Method("big", &TestServer::big_method); + Method("big_named", &TestServer::big_method, rpcxx::NamesMap("body")); + Method("async_ping", &TestServer::async_ping); + Method("async_ping_2", []() -> rpcxx::Future { + return rpcxx::Future::FromFunction([](auto prom){ + std::thread([MV(prom)]() noexcept { + std::this_thread::sleep_for(30ms); + prom(2); + }).detach(); + }); + }); + Notify("notification_silent", &TestServer::notification_silent); + Notify("notification_silent_named", &TestServer::notification_silent, rpcxx::NamesMap("a", "b")); + Method("calc", &TestServer::calc); + Method<&TestServer::calc>("calc_template"); + Method("calc_named", &TestServer::calc, rpcxx::NamesMap("a", "b")); + Notify("notification2_named", + &TestServer::notification2, + rpcxx::NamesMap("a", "b")); + Notify("notification2_named_second", [](int, int){}, rpcxx::NamesMap("a_1", "b_1")); + Method("ping_named", + &TestServer::ping, + rpcxx::NamesMap("ping")); + } + RpcBig big_method(RpcBig request) { + for (auto& part: request.parts) { + part.field0 += part.field8; + part.field1 += part.field9; + part.field2 += part.field10; + part.field3 += part.field11; + part.field4 += part.field12; + part.field5 += part.field13; + part.field6 += part.field7; + } + uint32_t sum = 0; + for (auto& i : request.nums) { + i = sum += i; + } + return request; + } + void method() {} + void notification_silent(int&& a, int b) { + a = b; + } + void notification() {} + void notification2(int, int) {} + void notification3(int, int, std::string) {} + int calc(int a, int b) { + return a + b; + } + fut::Future async_ping(std::string ping) { + if (ping != "ping") { + throw std::runtime_error("not ping"); + } + fut::Promise prom; + auto fut = prom.GetFuture(); + std::thread([MV(prom)]() noexcept { + std::this_thread::sleep_for(50ms); + prom("pong"); + }).detach(); + return fut; + } + std::string throws_error(std::string param) { + throw std::runtime_error(std::move(param)); + } + std::string ping(std::string ping) { + if (ping != "ping") { + throw std::runtime_error("not ping"); + } + return "pong"; + } +}; diff --git a/test/rps/CMakeLists.txt b/test/rps/CMakeLists.txt new file mode 100644 index 0000000..538c534 --- /dev/null +++ b/test/rps/CMakeLists.txt @@ -0,0 +1,41 @@ +# This file is a part of RPCXX project + +#[[ +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +]] + + +find_package(Qt5 REQUIRED COMPONENTS Core WebSockets) +set(CMAKE_AUTOMOC ON) + +add_executable(rpcxx-rps-client rps_client.cpp) +add_executable(rpcxx-rps-server rps_server.cpp) + +target_link_libraries(rpcxx-rps-client PRIVATE + rpcxx + Qt5::Core + Qt5::WebSockets +) +target_link_libraries(rpcxx-rps-server PRIVATE + rpcxx + Qt5::Core + Qt5::WebSockets +) diff --git a/test/rps/common.hpp b/test/rps/common.hpp new file mode 100644 index 0000000..de8947c --- /dev/null +++ b/test/rps/common.hpp @@ -0,0 +1,46 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#pragma once +#include +#include + +struct WsTransport final : public QObject, rpcxx::IAsyncTransport { + WsTransport(QWebSocket* ws) : + QObject(ws), + IAsyncTransport(rpcxx::Protocol::json_v2_minified), + sock(ws) + { + connect(ws, &QWebSocket::binaryMessageReceived, this, [this](QByteArray msg){ + jv::DefaultArena alloc; + Receive(jv::ParseMsgPackInPlace(msg.constData(), msg.size(), alloc).result); + }); + } + void Send(jv::JsonView msg) final { + membuff::StringOut out; + DumpMsgPackInto(out, msg); + sock->sendBinaryMessage(out.Consume()); + } + QWebSocket* sock; +}; diff --git a/test/rps/rps_client.cpp b/test/rps/rps_client.cpp new file mode 100644 index 0000000..c949d92 --- /dev/null +++ b/test/rps/rps_client.cpp @@ -0,0 +1,140 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#include "json_view/dump.hpp" +#include "json_view/parse.hpp" +#include "rpcxx/rpcxx.hpp" +#include +#include +#include +#include +#include +#include +#include +#include "common.hpp" + +using namespace rpcxx; + +class TestClient final: public QObject, public rpcxx::Client +{ + Q_OBJECT +public: + TestClient(QWebSocket* sock) : sock(sock) + { + SetTransport(new WsTransport(sock)); + connect(sock, &QWebSocket::binaryMessageReceived, this, [&](const QByteArray& frame){ + DefaultArena alloc; + auto msg = ParseMsgPackInPlace({frame.constData(), unsigned(frame.size())}, alloc); + if (prom.IsValid()) { + prom(Json{msg}); + } + }); + } + + QWebSocket* sock; + Promise prom; +}; + +static std::atomic runs = {}; +static std::atomic print = {}; + +void onDone() { + runs.fetch_add(1, std::memory_order_relaxed); + bool was = true; + if (print.compare_exchange_strong(was, false, std::memory_order_relaxed, std::memory_order_relaxed)) { + std::cout << "RPS: " << runs << std::endl; + runs = 0; + } +} + +const std::string bigStr = std::string(3000, '#'); + +void run_concat(TestClient* cli) +{ + cli->Request(rpcxx::Method{"concat", 3000}, bigStr, bigStr).AtLastSync([cli](auto res){ + onDone(); + run_concat(cli); + }); +} + +void run_calc(TestClient* cli) +{ + cli->Request(rpcxx::Method{"calc", 3000}, 512312, -5123123).AtLastSync([cli](auto res){ + onDone(); + run_calc(cli); + }); +} + +int main(int argc, char *argv[]) +{ + if (argc != 4) { + qDebug() << "Usage: " << argv[0] << " "; + return -1; + } + auto conns = strtoul(argv[1], nullptr, 10); + auto thrs = strtoul(argv[2], nullptr, 10); + std::string_view method = argv[3]; + if (!conns) conns = 1; + if (!thrs) thrs = 1; + QCoreApplication app(argc, argv); + QTimer tmr; + tmr.callOnTimeout([&]{ + print = true; + }); + if (method != "calc" && method != "concat") { + qDebug() << "Invalid method: " << method.data(); + qDebug() << "Available: [calc, concat]"; + return -2; + } + tmr.start(1000); + qDebug() << "starting rps test with " << conns + << "connections per thread (threads: " << thrs << ")"; + for (auto t = 0u; t < thrs; ++t) { + auto thread = new QThread; + app.connect(thread, &QThread::started, [conns, method]{ + for (auto i = 0u; i < conns; ++i) { + auto sock = new QWebSocket; + auto cli = new TestClient(sock); + sock->connect(sock, &QWebSocket::stateChanged, [](auto state){ + if (state == QAbstractSocket::SocketState::UnconnectedState) { + qDebug() << "could not connect"; + std::exit(1); + } + }); + sock->connect(sock, &QWebSocket::connected, [cli, method]{ + if (method == "calc") { + run_calc(cli); + } else { + run_concat(cli); + } + }); + sock->open(QUrl("ws://127.0.0.0:6000")); + } + }); + thread->start(); + } + return app.exec(); +} + +#include "rps_client.moc" diff --git a/test/rps/rps_server.cpp b/test/rps/rps_server.cpp new file mode 100644 index 0000000..0f7556b --- /dev/null +++ b/test/rps/rps_server.cpp @@ -0,0 +1,87 @@ +// This file is a part of RPCXX project + +/* +Copyright 2024 "NEOLANT Service", "NEOLANT Kalinigrad", Alexey Doronin, Anastasia Lugovets, Dmitriy Dyakonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#include "rpcxx/rpcxx.hpp" +#include +#include +#include +#include +#include "common.hpp" + +static size_t conns = 0; + +using namespace rpcxx; + +class TestServer final: public QObject, public rpcxx::Server +{ + Q_OBJECT +public: + ~TestServer() override { + conns--; + } + TestServer(QWebSocket* sock_) : + sock(sock_) + { + conns++; + auto tr = new WsTransport(sock); + connect(sock, &QWebSocket::disconnected, [=]{ + delete tr; + deleteLater(); + }); + tr->SetHandler(this); + Method("calc", [](int a, int b){ + return a + b; + }); + Method("concat", [](string_view a, string_view b){ + string res; + res.reserve(a.size() + b.size()); + res += a; + res += b; + return res; + }); + } + QWebSocket* sock; +}; + +int main(int argc, char *argv[]) +{ + QCoreApplication app(argc, argv); + QWebSocketServer server("test", QWebSocketServer::NonSecureMode); + app.connect(&server, &QWebSocketServer::newConnection, [&]{ + while(server.hasPendingConnections()) { + new TestServer(server.nextPendingConnection()); + } + }); + if (!server.listen(QHostAddress("0.0.0.0"), 6000)) { + return -1; + } + auto tmr = QTimer{}; + tmr.callOnTimeout([]{ + qDebug() << "Serving for: " << conns << " connections"; + }); + tmr.start(1000); + return app.exec(); +} + +#include "rps_server.moc"