diff --git a/.github/workflows/clang-format-check.yml b/.github/workflows/clang-format-check.yml index bb271f51..9dad1f42 100644 --- a/.github/workflows/clang-format-check.yml +++ b/.github/workflows/clang-format-check.yml @@ -9,7 +9,7 @@ on: jobs: formatting-check: name: Formatting Check - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - name: Run clang-format style check for C/C++ programs. diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index cf5a4cd3..e95706ec 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -20,7 +20,7 @@ on: jobs: analyze: name: Analyze - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: actions: read diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 8c446b2d..06d406c6 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -9,7 +9,7 @@ on: jobs: docker: name: Docker build - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 strategy: matrix: with-tests: [0, 1] diff --git a/.github/workflows/ubuntu-clang-tidy.yml b/.github/workflows/ubuntu-clang-tidy.yml index f2d2fbf5..622813e7 100644 --- a/.github/workflows/ubuntu-clang-tidy.yml +++ b/.github/workflows/ubuntu-clang-tidy.yml @@ -9,10 +9,10 @@ on: jobs: ubuntu-clang-tidy-build: name: Build on Ubuntu with clang-tidy checks - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 strategy: matrix: - clang-version: [19] + clang-version: [20] buildmode: [Debug] steps: @@ -22,6 +22,7 @@ jobs: - name: Install dependencies run: | sudo apt update + sudo apt upgrade -y sudo apt install cmake libssl-dev libcurl4-openssl-dev ninja-build -y --no-install-recommends - name: Install clang (with clang-tidy) diff --git a/.github/workflows/ubuntu-special.yml b/.github/workflows/ubuntu-special.yml index bca06e55..a6dae2fa 100644 --- a/.github/workflows/ubuntu-special.yml +++ b/.github/workflows/ubuntu-special.yml @@ -9,10 +9,9 @@ on: jobs: ubuntu-special-build: name: Build on Ubuntu with monitoring / protobuf support - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 strategy: matrix: - compiler: [g++-11] buildmode: [Debug] build-special-from-source: [0, 1] prometheus-options: ["-DBUILD_SHARED_LIBS=ON -DENABLE_PULL=OFF -DENABLE_PUSH=ON -DENABLE_COMPRESSION=OFF -DENABLE_TESTING=OFF"] @@ -25,7 +24,6 @@ jobs: run: | sudo apt update sudo apt install cmake libssl-dev git libcurl4-openssl-dev ninja-build -y --no-install-recommends - echo "CC=$(echo ${{matrix.compiler}} | sed -e 's/^g++/gcc/' | sed 's/+//g')" >> $GITHUB_ENV - name: Install prometheus-cpp run: | @@ -39,14 +37,12 @@ jobs: sudo cmake --install _build env: - CXX: ${{matrix.compiler}} CMAKE_BUILD_TYPE: ${{matrix.buildmode}} if: matrix.build-special-from-source == 0 - name: Configure CMake run: cmake -S . -B build -DCCT_BUILD_PROMETHEUS_FROM_SRC=${{matrix.build-special-from-source}} -DCCT_ENABLE_PROTO=${{matrix.build-special-from-source}} -DCCT_ENABLE_ASAN=OFF -GNinja env: - CXX: ${{matrix.compiler}} CMAKE_BUILD_TYPE: ${{matrix.buildmode}} - name: Build diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index 95413de2..7b0a0c41 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -9,10 +9,10 @@ on: jobs: ubuntu-build: name: Build on Ubuntu - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 strategy: matrix: - compiler: [g++-11, clang++-18] + compiler: [g++-13, clang++-18] buildmode: [Debug, Release] steps: @@ -25,14 +25,6 @@ jobs: sudo apt install cmake libssl-dev libcurl4-openssl-dev ninja-build -y --no-install-recommends echo "CC=$(echo ${{matrix.compiler}} | sed -e 's/^g++/gcc/' | sed 's/+//g')" >> $GITHUB_ENV - - name: Install gcc - run: | - sudo apt install ${{matrix.compiler}} -y --no-install-recommends - - # Temporary workaround for libasan bug stated here: https://github.com/google/sanitizers/issues/1716 - sudo sysctl vm.mmap_rnd_bits=28 - if: startsWith(matrix.compiler, 'g') - - name: Install clang run: | CLANG_VERSION=$(echo ${{matrix.compiler}} | cut -d- -f2) diff --git a/CMakeLists.txt b/CMakeLists.txt index 665b7821..a177642c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -99,7 +99,7 @@ if(CCT_ENABLE_TESTS) enable_testing() endif() -# nlohmann_json - json library +# nlohmann_json - json container library find_package(nlohmann_json CONFIG) if(NOT nlohmann_json_FOUND) FetchContent_Declare( @@ -111,6 +111,18 @@ if(NOT nlohmann_json_FOUND) list(APPEND fetchContentPackagesToMakeAvailable nlohmann_json) endif() +# Glaze - fast json serialization library +find_package(glaze CONFIG) +if(NOT glaze) + FetchContent_Declare( + glaze + URL https://github.com/stephenberry/glaze/archive/refs/tags/v3.6.0.tar.gz + URL_HASH SHA256=d394fed35440bd1cb1a2aec059b967acc43fc04764ecb0915ba24b9f5a9ca0a3 + ) + + list(APPEND fetchContentPackagesToMakeAvailable glaze) +endif() + # prometheus for monitoring support if(CCT_BUILD_PROMETHEUS_FROM_SRC) FetchContent_Declare( diff --git a/INSTALL.md b/INSTALL.md index dfe33fc2..59418c7c 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -54,7 +54,7 @@ But they have partial support which is sufficient to build `coincenter`. The following compilers are known to compile `coincenter` (and are tested in the CI): -- **GCC** version >= 11 +- **GCC** version >= 12 - **Clang** version >= 18 - **MSVC** version >= 19.39 @@ -135,14 +135,15 @@ cmake -S . -B build --preset conan-release In all cases, they do not need to be installed. If they are not found at configure time, `cmake` will fetch sources and compile them automatically. If you are building frequently `coincenter` you can install them to speed up its compilation. -| Library | Description | License | -| -------------------------------------------------------------- | -------------------------------------------------- | -------------------- | -| [amc](https://github.com/AmadeusITGroup/amc.git) | High performance C++ containers (maintained by me) | MIT | -| [googletest](https://github.com/google/googletest.git) | Google Testing and Mocking Framework | BSD-3-Clause License | -| [json](https://github.com/nlohmann/json) | JSON for Modern C++ | MIT | -| [spdlog](https://github.com/gabime/spdlog.git) | Fast C++ logging library | MIT | -| [prometheus-cpp](https://github.com/jupp0r/prometheus-cpp.git) | Prometheus Client Library for Modern C++ | MIT | -| [jwt-cpp](https://github.com/Thalhammer/jwt-cpp) | Creating and validating json web tokens in C++ | MIT | +| Library | Description | License | +| -------------------------------------------------------------- | -------------------------------------------------------------------- | -------------------- | +| [amc](https://github.com/AmadeusITGroup/amc.git) | High performance C++ containers (maintained by me) | MIT | +| [googletest](https://github.com/google/googletest.git) | Google Testing and Mocking Framework | BSD-3-Clause License | +| [json container](https://github.com/nlohmann/json) | JSON for Modern C++ | MIT | +| [json serialization](https://github.com/stephenberry/glaze) | Extremely fast, in memory, JSON and interface library for modern C++ | MIT | +| [spdlog](https://github.com/gabime/spdlog.git) | Fast C++ logging library | MIT | +| [prometheus-cpp](https://github.com/jupp0r/prometheus-cpp.git) | Prometheus Client Library for Modern C++ | MIT | +| [jwt-cpp](https://github.com/Thalhammer/jwt-cpp) | Creating and validating json web tokens in C++ | MIT | ### With cmake diff --git a/src/api/exchanges/src/kucoinprivateapi.cpp b/src/api/exchanges/src/kucoinprivateapi.cpp index 26cee369..58f739ba 100644 --- a/src/api/exchanges/src/kucoinprivateapi.cpp +++ b/src/api/exchanges/src/kucoinprivateapi.cpp @@ -72,7 +72,7 @@ json PrivateQuery(CurlHandle& curlHandle, const APIKey& apiKey, HttpRequestType strToSign.push_back('?'); strToSign.append(postData.str()); } else { - strToSign.append(postData.toJson().dump()); + strToSign.append(postData.toJsonStr()); postDataFormat = CurlOptions::PostDataFormat::kJson; } } diff --git a/src/http-request/src/curlhandle.cpp b/src/http-request/src/curlhandle.cpp index b94a6a56..39702af7 100644 --- a/src/http-request/src/curlhandle.cpp +++ b/src/http-request/src/curlhandle.cpp @@ -197,7 +197,7 @@ std::string_view CurlHandle::query(std::string_view endpoint, const CurlOptions if (appendParametersInQueryStr) { modifiedUrlOutIt = std::ranges::copy(postDataStr, modifiedUrlOutIt + 1).out; } else if (opts.isPostDataInJsonFormat() && !postData.empty()) { - jsonStr = postData.toJson().dump(); + jsonStr = postData.toJsonStr(); optsStr = jsonStr; } else { optsStr = postData.str(); diff --git a/src/objects/include/currencycode.hpp b/src/objects/include/currencycode.hpp index 2f56794f..e885384d 100644 --- a/src/objects/include/currencycode.hpp +++ b/src/objects/include/currencycode.hpp @@ -11,6 +11,7 @@ #include "cct_format.hpp" #include "cct_hash.hpp" #include "cct_invalid_argument_exception.hpp" +#include "cct_json-serialization.hpp" #include "cct_string.hpp" #include "toupperlower.hpp" @@ -303,6 +304,28 @@ struct fmt::formatter { }; #endif +namespace glz::detail { +template <> +struct from { + template + static void op(cct::CurrencyCode &value, auto &&...args) { + char buf[cct::CurrencyCode::kMaxLen]; + std::string_view str = buf; + read::op(str, args...); + value = str; + } +}; + +template <> +struct to { + template + static void op(cct::CurrencyCode &value, auto &&...args) noexcept { + auto str = value.str(); + write::op(str, args...); + } +}; +} // namespace glz::detail + // Specialize std::hash for easy usage of CurrencyCode as unordered_map key namespace std { template <> diff --git a/src/objects/include/monetaryamount.hpp b/src/objects/include/monetaryamount.hpp index f8969fde..f51db711 100644 --- a/src/objects/include/monetaryamount.hpp +++ b/src/objects/include/monetaryamount.hpp @@ -12,6 +12,7 @@ #include "cct_format.hpp" #include "cct_hash.hpp" +#include "cct_json-serialization.hpp" #include "cct_log.hpp" #include "cct_string.hpp" #include "currencycode.hpp" @@ -46,6 +47,8 @@ class MonetaryAmount { enum class RoundType : int8_t { kDown, kUp, kNearest }; + static constexpr std::size_t kMaxNbCharsAmount = std::numeric_limits::digits10 + 3; + /// Constructs a MonetaryAmount with a value of 0 of neutral currency. constexpr MonetaryAmount() noexcept : _amount(0) {} @@ -344,7 +347,6 @@ class MonetaryAmount { using UnsignedAmountType = uint64_t; static constexpr AmountType kMaxAmountFullNDigits = ipow10(std::numeric_limits::digits10); - static constexpr std::size_t kMaxNbCharsAmount = std::numeric_limits::digits10 + 3; void appendCurrencyStr(string &str) const { if (!_curWithDecimals.isNeutral()) { @@ -420,6 +422,28 @@ struct fmt::formatter { }; #endif +namespace glz::detail { +template <> +struct from { + template + static void op(cct::MonetaryAmount &value, auto &&...args) { + char buf[cct::MonetaryAmount::kMaxNbCharsAmount + cct::CurrencyCode::kMaxLen + 1]; + std::string_view str = buf; + read::op(str, args...); + value = cct::MonetaryAmount(str); + } +}; + +template <> +struct to { + template + static void op(cct::MonetaryAmount &value, auto &&...args) noexcept { + auto str = value.str(); + write::op(str, args...); + } +}; +} // namespace glz::detail + namespace std { template <> struct hash { diff --git a/src/objects/test/currencycode_test.cpp b/src/objects/test/currencycode_test.cpp index 4034cee1..1a886bc1 100644 --- a/src/objects/test/currencycode_test.cpp +++ b/src/objects/test/currencycode_test.cpp @@ -194,4 +194,29 @@ TEST(CurrencyCodeTest, Iterator) { EXPECT_EQ("TEST", str); } +struct Foo { + CurrencyCode currencyCode; + + bool operator==(const Foo &) const noexcept = default; +}; + +TEST(CurrencyCodeTest, JsonSerialization) { + Foo foo{"DOGE"}; + + string buffer; + auto res = write_json(foo, buffer); + + EXPECT_FALSE(res); + + EXPECT_EQ(buffer, R"({"currencyCode":"DOGE"})"); +} + +TEST(CurrencyCodeTest, JsonDeserialization) { + auto s = read_json(R"({"currencyCode":"DOGE"})"); + + ASSERT_TRUE(s.has_value()); + + EXPECT_EQ(s.value(), Foo{"DOGE"}); +} + } // namespace cct \ No newline at end of file diff --git a/src/objects/test/monetaryamount_test.cpp b/src/objects/test/monetaryamount_test.cpp index df99ebc5..424b0bac 100644 --- a/src/objects/test/monetaryamount_test.cpp +++ b/src/objects/test/monetaryamount_test.cpp @@ -772,4 +772,29 @@ TEST(MonetaryAmountTest, CurrentMaxNbDecimals) { EXPECT_EQ(ma3.amount(ma3.currentMaxNbDecimals()), 389087900000000000L); } +struct Foo { + MonetaryAmount amount; + + bool operator==(const Foo &) const noexcept = default; +}; + +TEST(MonetaryAmountTest, JsonSerialization) { + Foo foo{MonetaryAmount("15.5DOGE")}; + + string buffer; + auto res = write_json(foo, buffer); + + EXPECT_FALSE(res); + + EXPECT_EQ(buffer, R"({"amount":"15.5 DOGE"})"); +} + +TEST(MonetaryAmountTest, JsonDeserialization) { + auto s = read_json(R"({"amount":"15.5 DOGE"})"); + + ASSERT_TRUE(s.has_value()); + + EXPECT_EQ(s.value(), Foo{MonetaryAmount("15.5 DOGE")}); +} + } // namespace cct \ No newline at end of file diff --git a/src/tech/CMakeLists.txt b/src/tech/CMakeLists.txt index 6a31558e..30735843 100644 --- a/src/tech/CMakeLists.txt +++ b/src/tech/CMakeLists.txt @@ -4,6 +4,8 @@ aux_source_directory(src API_TECH_SRC) add_coincenter_library(tech STATIC ${API_TECH_SRC}) target_link_libraries(coincenter_tech PUBLIC nlohmann_json::nlohmann_json) +target_link_libraries(coincenter_tech PUBLIC glaze::glaze) + if (LINK_AMC) target_link_libraries(coincenter_tech PUBLIC amc::amc) endif() diff --git a/src/tech/include/cct_json-serialization.hpp b/src/tech/include/cct_json-serialization.hpp new file mode 100644 index 00000000..7073f00f --- /dev/null +++ b/src/tech/include/cct_json-serialization.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include + +namespace cct { + +using glz::read_json; +using glz::write_json; + +} // namespace cct \ No newline at end of file diff --git a/src/tech/include/flatkeyvaluestring.hpp b/src/tech/include/flatkeyvaluestring.hpp index 2ba23d61..2d215f4d 100644 --- a/src/tech/include/flatkeyvaluestring.hpp +++ b/src/tech/include/flatkeyvaluestring.hpp @@ -16,10 +16,8 @@ #include #include "cct_cctype.hpp" -#include "cct_json.hpp" #include "cct_string.hpp" #include "cct_type_traits.hpp" -#include "cct_vector.hpp" #include "flat-key-value-string-iterator.hpp" #include "unreachable.hpp" #include "url-encode.hpp" @@ -187,11 +185,11 @@ class FlatKeyValueString { /// The returned string_view is guaranteed to be null-terminated. std::string_view str() const noexcept { return _data; } - /// Converts to a json document. + /// Converts to a json document string. /// Values ending with a ',' will be considered as arrays. /// In this case, sub array values are comma separated values. /// Limitation: all json values will be decoded as strings. - json toJson() const; + string toJsonStr() const; /// Returns a new FlatKeyValueString URL encoded except delimiters. FlatKeyValueString urlEncodeExceptDelimiters() const; @@ -390,24 +388,44 @@ std::string_view FlatKeyValueString::Get(std::s } template -json FlatKeyValueString::toJson() const { - json ret; +string FlatKeyValueString::toJsonStr() const { + string ret; + ret.reserve(2 * (_data.size() + 1U)); + ret.push_back('{'); + + const auto appendStr = [&ret](std::string_view str) { + ret.push_back('"'); + ret.append(str); + ret.push_back('"'); + }; + for (const auto &kv : *this) { const auto key = kv.key(); const auto val = kv.val(); + if (ret.size() != 1U) { + ret.push_back(','); + } + appendStr(key); + ret.push_back(':'); + auto valSize = val.size(); if (valSize == 0 || val.back() != kArrayElemSepChar) { - ret.emplace(key, val); + // standard field case + appendStr(val); continue; } - vector arrayValues; + // array case + ret.push_back('['); if (valSize != 1U) { // Check empty array case for (std::size_t arrayValBeg = 0;;) { std::size_t arrayValSepPos = val.find(kArrayElemSepChar, arrayValBeg); - arrayValues.emplace_back(std::string_view(val.begin() + arrayValBeg, val.begin() + arrayValSepPos)); + if (arrayValBeg != 0) { + ret.push_back(','); + } + appendStr(std::string_view(val.begin() + arrayValBeg, val.begin() + arrayValSepPos)); if (arrayValSepPos + 1U == valSize) { break; } @@ -415,8 +433,9 @@ json FlatKeyValueString::toJson() const { } } - ret.emplace(key, std::move(arrayValues)); + ret.push_back(']'); } + ret.push_back('}'); return ret; } diff --git a/src/tech/test/flatkeyvaluestring_test.cpp b/src/tech/test/flatkeyvaluestring_test.cpp index 968386ce..1a919f72 100644 --- a/src/tech/test/flatkeyvaluestring_test.cpp +++ b/src/tech/test/flatkeyvaluestring_test.cpp @@ -6,7 +6,6 @@ #include #include "cct_exception.hpp" -#include "cct_json.hpp" #include "cct_string.hpp" namespace cct { @@ -112,15 +111,15 @@ TEST(FlatKeyValueStringTest, WithNullTerminatingCharAsSeparator) { using ExoticKeyValuePair = FlatKeyValueString<'\0', ':'>; ExoticKeyValuePair kvPairs{{"abc", "354"}, {"tata", "abc"}, {"rm", "xX"}, {"huhu", "haha"}}; - EXPECT_EQ(kvPairs.str(), std::string_view("abc:354\0tata:abc\0rm:xX\0huhu:haha"sv)); + EXPECT_EQ(kvPairs.str(), "abc:354\0tata:abc\0rm:xX\0huhu:haha"sv); kvPairs.set("rm", "Yy3"); - EXPECT_EQ(kvPairs.str(), std::string_view("abc:354\0tata:abc\0rm:Yy3\0huhu:haha"sv)); + EXPECT_EQ(kvPairs.str(), "abc:354\0tata:abc\0rm:Yy3\0huhu:haha"sv); kvPairs.erase("abc"); - EXPECT_EQ(kvPairs.str(), std::string_view("tata:abc\0rm:Yy3\0huhu:haha"sv)); + EXPECT_EQ(kvPairs.str(), "tata:abc\0rm:Yy3\0huhu:haha"sv); kvPairs.erase("rm"); - EXPECT_EQ(kvPairs.str(), std::string_view("tata:abc\0huhu:haha"sv)); + EXPECT_EQ(kvPairs.str(), "tata:abc\0huhu:haha"sv); kvPairs.emplace_back("&newField", "&&newValue&&"); - EXPECT_EQ(kvPairs.str(), std::string_view("tata:abc\0huhu:haha\0&newField:&&newValue&&"sv)); + EXPECT_EQ(kvPairs.str(), "tata:abc\0huhu:haha\0&newField:&&newValue&&"sv); int kvPairPos = 0; for (const auto &kv : kvPairs) { @@ -143,7 +142,7 @@ TEST(FlatKeyValueStringTest, WithNullTerminatingCharAsSeparator) { EXPECT_EQ(kvPairPos, 3); } -TEST(FlatKeyValueStringTest, EmptyConvertToJson) { EXPECT_EQ(KvPairs().toJson(), json()); } +TEST(FlatKeyValueStringTest, EmptyConvertToJson) { EXPECT_EQ(KvPairs().toJsonStr(), "{}"); } class FlatKeyValueStringCase1 : public ::testing::Test { protected: @@ -346,37 +345,10 @@ TEST_F(FlatKeyValueStringCase1, EraseIncrementDecrement) { EXPECT_EQ(itPos, 5); } -TEST_F(FlatKeyValueStringCase1, ConvertToJson) { - json jsonData = kvPairs.toJson(); - - EXPECT_EQ(jsonData["units"].get(), "0.11176"); - EXPECT_EQ(jsonData["price"].get(), "357.78"); - EXPECT_EQ(jsonData["777"].get(), "encoredutravail?"); - EXPECT_EQ(jsonData["hola"].get(), "quetal"); - EXPECT_FALSE(jsonData["hola"].is_array()); - - auto arrayIt = jsonData.find("array1"); - EXPECT_NE(arrayIt, jsonData.end()); - EXPECT_TRUE(arrayIt->is_array()); - EXPECT_EQ(arrayIt->size(), 2U); - - EXPECT_EQ((*arrayIt)[0], "val1"); - EXPECT_EQ((*arrayIt)[1], ""); - - arrayIt = jsonData.find("array2"); - EXPECT_NE(arrayIt, jsonData.end()); - EXPECT_TRUE(arrayIt->is_array()); - EXPECT_EQ(arrayIt->size(), 4U); - - EXPECT_EQ((*arrayIt)[0], ""); - EXPECT_EQ((*arrayIt)[1], "val1"); - EXPECT_EQ((*arrayIt)[2], "val2"); - EXPECT_EQ((*arrayIt)[3], "value"); - - arrayIt = jsonData.find("emptyArray"); - EXPECT_NE(arrayIt, jsonData.end()); - EXPECT_TRUE(arrayIt->is_array()); - EXPECT_TRUE(arrayIt->empty()); +TEST_F(FlatKeyValueStringCase1, ConvertToJsonStr) { + EXPECT_EQ( + kvPairs.toJsonStr(), + R"({"units":"0.11176","price":"357.78","777":"encoredutravail?","hola":"quetal","k":"v","array1":["val1",""],"array2":["","val1","val2","value"],"emptyArray":[]})"); } TEST_F(FlatKeyValueStringCase1, AppendIntegralValues) {