From e852966a9002eea75802e24e12d7a590bd508276 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 18 Sep 2023 09:50:29 -0700 Subject: [PATCH 01/21] docs: update bug report instructions (#246) Now that `LD_LOG_LEVEL` environment variable works as expected, we should call this out in the bug report template. This will aid in gathering more useful debug logs or lead to resolution without further analysis. Example: ``` LD_LOG_LEVEL=debug ./application-that-uses-sdk ``` --- .github/ISSUE_TEMPLATE/client-sdk--bug-report.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/client-sdk--bug-report.md b/.github/ISSUE_TEMPLATE/client-sdk--bug-report.md index 44d5a2bbf..906fc815e 100644 --- a/.github/ISSUE_TEMPLATE/client-sdk--bug-report.md +++ b/.github/ISSUE_TEMPLATE/client-sdk--bug-report.md @@ -22,13 +22,17 @@ assignees: '' A clear and concise description of what you expected to happen. **Logs** - If applicable, add any log output related to your problem. + If applicable, add any log output related to your problem. + To get more logs from the SDK, change the log level using environment variable `LD_LOG_LEVEL`. For example: + ``` + LD_LOG_LEVEL=debug ./your-application + ``` **SDK version** The version of this SDK that you are using. **Language version, developer tools** - For instance, Go 1.11 or Ruby 2.5.3. If you are using a language that requires a separate compiler, such as C, please include the name and version of the compiler too. + For instance, C++17 or C11. If you are using a language that requires a separate compiler, such as C, please include the name and version of the compiler too. **OS/platform** For instance, Ubuntu 16.04, Windows 10, or Android 4.0.3. If your code is running in a browser, please also include the browser type and version. From 69ac1c7ffdf9f282fc1ffdd0791705470874b222 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 21 Sep 2023 11:51:49 -0700 Subject: [PATCH 02/21] fix: build with OpenSSL > 3.0 on all platforms (#249) We were previously using openssl 1.1 on some platforms due to issues with the Github Actions executors. These issues are no longer present, so we can use openssl > 3 instead. - [x] Linux: 3.0.2 - [x] Mac: 3.1.2 - [x] Windows: 3.1.1 --- .github/actions/sdk-release/action.yml | 13 +++++++++---- .github/workflows/client.yml | 11 +++++++---- CMakeLists.txt | 1 - 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/.github/actions/sdk-release/action.yml b/.github/actions/sdk-release/action.yml index f6e789648..22606e270 100644 --- a/.github/actions/sdk-release/action.yml +++ b/.github/actions/sdk-release/action.yml @@ -92,11 +92,17 @@ runs: if: runner.os == 'Windows' uses: ilammy/msvc-dev-cmd@v1 + - name: Upgrade OpenSSL + if: runner.os == 'Windows' + shell: bash + run: | + choco upgrade openssl --no-progress + - name: Build Windows Artifacts if: runner.os == 'Windows' shell: bash env: - OPENSSL_ROOT_DIR: 'C:\Program Files\OpenSSL' + OPENSSL_ROOT_DIR: 'C:\Program Files\OpenSSL-Win64' BOOST_LIBRARY_DIR: 'C:\local\boost_1_81_0\lib64-msvc-14.3' BOOST_LIBRARYDIR: 'C:\local\boost_1_81_0\lib64-msvc-14.3' BOOST_ROOT: ${{ steps.install-boost.outputs.BOOST_ROOT }} @@ -155,9 +161,8 @@ runs: if: runner.os == 'macOS' shell: bash run: | - brew link --overwrite openssl@1.1 - echo "OPENSSL_ROOT_DIR=$(brew --prefix openssl@1.1)" >> "$GITHUB_ENV" - export OPENSSL_ROOT_DIR=$(brew --prefix openssl@1.1) + echo "OPENSSL_ROOT_DIR=$(brew --prefix openssl@3)" >> "$GITHUB_ENV" + export OPENSSL_ROOT_DIR=$(brew --prefix openssl@3) ./scripts/build-release.sh ${{ inputs.sdk_cmake_target }} env: diff --git a/.github/workflows/client.yml b/.github/workflows/client.yml index fcbea06d5..b5cb48289 100644 --- a/.github/workflows/client.yml +++ b/.github/workflows/client.yml @@ -41,10 +41,9 @@ jobs: runs-on: macos-12 steps: - run: | - brew link --overwrite openssl@1.1 - echo "OPENSSL_ROOT_DIR=$(brew --prefix openssl@1.1)" >> "$GITHUB_ENV" + echo "OPENSSL_ROOT_DIR=$(brew --prefix openssl@3)" >> "$GITHUB_ENV" # For debugging - echo "OPENSSL_ROOT_DIR=$(brew --prefix openssl@1.1)" + echo "OPENSSL_ROOT_DIR=$(brew --prefix openssl@3)" - uses: actions/checkout@v3 - uses: ./.github/actions/ci env: @@ -55,11 +54,15 @@ jobs: build-test-windows: runs-on: windows-2022 steps: + - name: Upgrade OpenSSL + shell: bash + run: | + choco upgrade openssl --no-progress - uses: actions/checkout@v3 - uses: ilammy/msvc-dev-cmd@v1 - uses: ./.github/actions/ci env: - OPENSSL_ROOT_DIR: 'C:\Program Files\OpenSSL' + OPENSSL_ROOT_DIR: 'C:\Program Files\OpenSSL-Win64' BOOST_LIBRARY_DIR: 'C:\local\boost_1_81_0\lib64-msvc-14.3' BOOST_LIBRARYDIR: 'C:\local\boost_1_81_0\lib64-msvc-14.3' with: diff --git a/CMakeLists.txt b/CMakeLists.txt index 0f1575442..8c62c3aff 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -54,7 +54,6 @@ if (BUILD_TESTING) endif () set(OPENSSL_USE_STATIC_LIBS ON) -set(OPENSSL_ROOT_DIR "/opt/homebrew/opt/openssl@1.1") find_package(OpenSSL REQUIRED) message(STATUS "LaunchDarkly: using OpenSSL v${OPENSSL_VERSION}") From eb2a8f093996361541e11659165cbecc94c15346 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 21 Sep 2023 12:22:25 -0700 Subject: [PATCH 03/21] fix: catch exception if en_US.utf8-locale missing when parsing datetime headers (#251) The event delivery subsystem inspects the HTTP headers on the event endpoints whenever it posts a batch of events. It parses out the date header, storing it for later use. When emitting debug events, it is used to ensure that events stop emitting even if the host's time is off - as a form of time synchronization. The parsing code is quite ugly, and also wrong - it assumed that `en_US.utf-8` would always be available. This commit hoists the locale loading routine out of the individual flush-workers and into the worker pool. If it can't load the locale, it emits a single `warn` log at startup explaining the effect. --- .../launchdarkly/events/parse_date_header.hpp | 7 ++++-- .../launchdarkly/events/request_worker.hpp | 22 ++++++++++------ .../launchdarkly/events/worker_pool.hpp | 2 -- libs/internal/src/events/request_worker.cpp | 12 ++++++--- libs/internal/src/events/worker_pool.cpp | 25 ++++++++++++++++++- libs/internal/tests/event_processor_test.cpp | 21 ++++++++++------ 6 files changed, 66 insertions(+), 23 deletions(-) diff --git a/libs/internal/include/launchdarkly/events/parse_date_header.hpp b/libs/internal/include/launchdarkly/events/parse_date_header.hpp index 2d78ab43b..4e009e1d6 100644 --- a/libs/internal/include/launchdarkly/events/parse_date_header.hpp +++ b/libs/internal/include/launchdarkly/events/parse_date_header.hpp @@ -8,14 +8,17 @@ namespace launchdarkly::events { template + static std::optional ParseDateHeader( - std::string const& datetime) { + std::string const& datetime, + std::locale const& locale) { // The following comments may not be entirely accurate. // TODO: There must be a better way. std::tm gmt_tm = {}; + std::istringstream string_stream(datetime); - string_stream.imbue(std::locale("en_US.utf-8")); + string_stream.imbue(locale); string_stream >> std::get_time(&gmt_tm, "%a, %d %b %Y %H:%M:%S GMT"); if (string_stream.fail()) { return std::nullopt; diff --git a/libs/internal/include/launchdarkly/events/request_worker.hpp b/libs/internal/include/launchdarkly/events/request_worker.hpp index 22f846469..a7e176c44 100644 --- a/libs/internal/include/launchdarkly/events/request_worker.hpp +++ b/libs/internal/include/launchdarkly/events/request_worker.hpp @@ -24,7 +24,7 @@ enum class State { PermanentlyFailed = 4, }; -std::ostream& operator<<(std::ostream& out, State const& s); +std::ostream& operator<<(std::ostream& out, State const& state); enum class Action { /* No action necessary. */ @@ -39,7 +39,7 @@ enum class Action { NotifyPermanentFailure = 4, }; -std::ostream& operator<<(std::ostream& out, Action const& s); +std::ostream& operator<<(std::ostream& out, Action const& state); /** * Computes the next (state, action) pair from an existing state and an HTTP @@ -99,12 +99,13 @@ class RequestWorker { RequestWorker(boost::asio::any_io_executor io, std::chrono::milliseconds retry_after, std::size_t id, + std::optional date_header_locale, Logger& logger); /** * Returns true if the worker is available for delivery. */ - bool Available() const; + [[nodiscard]] bool Available() const; /** * Passes an EventBatch to the worker for delivery. The delivery may be @@ -135,10 +136,10 @@ class RequestWorker { << batch_->Target() << " with payload: " << batch_->Request().Body().value_or("(no body)"); - requester_.Request( - batch_->Request(), [this, handler](network::HttpResult result) { - OnDeliveryAttempt(std::move(result), std::move(handler)); - }); + requester_.Request(batch_->Request(), + [this, handler](network::HttpResult const& result) { + OnDeliveryAttempt(result, std::move(handler)); + }); return result.get(); } @@ -163,9 +164,14 @@ class RequestWorker { /* Tag used in logs. */ std::string tag_; + /* The en_US locale is used to parse the Date header from HTTP responses. + * On some platforms, this may not be available hence the optional. */ + std::optional date_header_locale_; + Logger& logger_; - void OnDeliveryAttempt(network::HttpResult request, ResultCallback cb); + void OnDeliveryAttempt(network::HttpResult const& request, + ResultCallback cb); }; } // namespace launchdarkly::events diff --git a/libs/internal/include/launchdarkly/events/worker_pool.hpp b/libs/internal/include/launchdarkly/events/worker_pool.hpp index bb66b2ff2..418936d56 100644 --- a/libs/internal/include/launchdarkly/events/worker_pool.hpp +++ b/libs/internal/include/launchdarkly/events/worker_pool.hpp @@ -24,8 +24,6 @@ namespace launchdarkly::events { */ class WorkerPool { public: - using ServerTimeCallback = - std::function; /** * Constructs a new WorkerPool. * @param io The executor used for all workers. diff --git a/libs/internal/src/events/request_worker.cpp b/libs/internal/src/events/request_worker.cpp index a18ed8675..cd632fe6d 100644 --- a/libs/internal/src/events/request_worker.cpp +++ b/libs/internal/src/events/request_worker.cpp @@ -6,13 +6,15 @@ namespace launchdarkly::events { RequestWorker::RequestWorker(boost::asio::any_io_executor io, std::chrono::milliseconds retry_after, std::size_t id, + std::optional date_header_locale, Logger& logger) - : timer_(io), + : timer_(std::move(io)), retry_delay_(retry_after), state_(State::Idle), requester_(timer_.get_executor()), batch_(std::nullopt), tag_("flush-worker[" + std::to_string(id) + "]: "), + date_header_locale_(std::move(date_header_locale)), logger_(logger) {} bool RequestWorker::Available() const { @@ -47,7 +49,7 @@ static bool IsSuccess(network::HttpResult const& result) { http::status_class::successful; } -void RequestWorker::OnDeliveryAttempt(network::HttpResult result, +void RequestWorker::OnDeliveryAttempt(network::HttpResult const& result, ResultCallback callback) { auto [next_state, action] = NextState(state_, result); @@ -81,11 +83,15 @@ void RequestWorker::OnDeliveryAttempt(network::HttpResult result, batch_.reset(); break; case Action::ParseDateAndReset: { + if (!date_header_locale_) { + batch_.reset(); + break; + } auto headers = result.Headers(); if (auto date = headers.find("Date"); date != headers.end()) { if (auto server_time = ParseDateHeader( - date->second)) { + date->second, *date_header_locale_)) { callback(batch_->Count(), *server_time); } } diff --git a/libs/internal/src/events/worker_pool.cpp b/libs/internal/src/events/worker_pool.cpp index 19ff94b2d..370b60bed 100644 --- a/libs/internal/src/events/worker_pool.cpp +++ b/libs/internal/src/events/worker_pool.cpp @@ -5,14 +5,37 @@ namespace launchdarkly::events { +std::optional GetLocale(std::string const& locale, + std::string const& tag, + Logger& logger) { + try { + return std::locale(locale); + } catch (std::runtime_error) { + LD_LOG(logger, LogLevel::kWarn) + << tag << " couldn't load " << locale + << " locale. If debug events are enabled, they may be emitted for " + "longer than expected"; + return std::nullopt; + } +} + WorkerPool::WorkerPool(boost::asio::any_io_executor io, std::size_t pool_size, std::chrono::milliseconds delivery_retry_delay, Logger& logger) : io_(io), workers_() { + // The en_US.utf-8 locale is used whenever a date is parsed from the HTTP + // headers returned by the event-delivery endpoints. If the locale is + // unavailable, then the workers will skip the parsing step. + // + // This may result in debug events being emitted for longer than expected + // if the host's time is way out of sync. + std::optional date_header_locale = + GetLocale("en_US.utf-8", "event-processor", logger); + for (std::size_t i = 0; i < pool_size; i++) { workers_.emplace_back(std::make_unique( - io_, delivery_retry_delay, i, logger)); + io_, delivery_retry_delay, i, date_header_locale, logger)); } } diff --git a/libs/internal/tests/event_processor_test.cpp b/libs/internal/tests/event_processor_test.cpp index d210f8e8c..b1b788598 100644 --- a/libs/internal/tests/event_processor_test.cpp +++ b/libs/internal/tests/event_processor_test.cpp @@ -59,9 +59,15 @@ TEST(WorkerPool, PoolReturnsNullptrWhenNoWorkerAvaialable) { ioc_thread.join(); } +class EventProcessorTests : public ::testing::Test { + public: + EventProcessorTests() : locale("en_US.utf-8") {} + std::locale locale; +}; + // This test is a temporary test that exists only to ensure the event processor // compiles; it should be replaced by more robust tests (and contract tests.) -TEST(EventProcessorTests, ProcessorCompiles) { +TEST_F(EventProcessorTests, ProcessorCompiles) { using namespace launchdarkly; Logger logger{ @@ -95,11 +101,12 @@ TEST(EventProcessorTests, ProcessorCompiles) { ioc_thread.join(); } -TEST(EventProcessorTests, ParseValidDateHeader) { +TEST_F(EventProcessorTests, ParseValidDateHeader) { using namespace launchdarkly; using Clock = std::chrono::system_clock; - auto date = events::ParseDateHeader("Wed, 21 Oct 2015 07:28:00 GMT"); + auto date = + events::ParseDateHeader("Wed, 21 Oct 2015 07:28:00 GMT", locale); ASSERT_TRUE(date); @@ -107,21 +114,21 @@ TEST(EventProcessorTests, ParseValidDateHeader) { std::chrono::microseconds(1445412480000000)); } -TEST(EventProcessorTests, ParseInvalidDateHeader) { +TEST_F(EventProcessorTests, ParseInvalidDateHeader) { using namespace launchdarkly; auto not_a_date = events::ParseDateHeader( - "this is definitely not a date"); + "this is definitely not a date", locale); ASSERT_FALSE(not_a_date); auto not_gmt = events::ParseDateHeader( - "Wed, 21 Oct 2015 07:28:00 PST"); + "Wed, 21 Oct 2015 07:28:00 PST", locale); ASSERT_FALSE(not_gmt); auto missing_year = events::ParseDateHeader( - "Wed, 21 Oct 07:28:00 GMT"); + "Wed, 21 Oct 07:28:00 GMT", locale); ASSERT_FALSE(missing_year); } From a2f20306418f376de0593c92f898863cd15a6520 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 21 Sep 2023 13:25:39 -0700 Subject: [PATCH 04/21] chore: release main (#252) :robot: I have created a release *beep* *boop* ---
launchdarkly-cpp-client: 3.0.9 ### Dependencies * The following workspace dependencies were updated * dependencies * launchdarkly-cpp-internal bumped from 0.1.9 to 0.1.10
launchdarkly-cpp-internal: 0.1.10 ## [0.1.10](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-internal-v0.1.9...launchdarkly-cpp-internal-v0.1.10) (2023-09-21) ### Bug Fixes * catch exception if en_US.utf8-locale missing when parsing datetime headers ([#251](https://github.com/launchdarkly/cpp-sdks/issues/251)) ([eb2a8f0](https://github.com/launchdarkly/cpp-sdks/commit/eb2a8f093996361541e11659165cbecc94c15346))
--- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .release-please-manifest.json | 4 ++-- libs/client-sdk/CHANGELOG.md | 6 ++++++ libs/client-sdk/package.json | 4 ++-- libs/internal/CHANGELOG.md | 7 +++++++ libs/internal/package.json | 2 +- 5 files changed, 18 insertions(+), 5 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index db3c31f38..fddbbb09c 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,6 +1,6 @@ { - "libs/client-sdk": "3.0.8", + "libs/client-sdk": "3.0.9", "libs/server-sent-events": "0.1.3", "libs/common": "0.3.6", - "libs/internal": "0.1.9" + "libs/internal": "0.1.10" } diff --git a/libs/client-sdk/CHANGELOG.md b/libs/client-sdk/CHANGELOG.md index 216b092c0..a0851b31b 100644 --- a/libs/client-sdk/CHANGELOG.md +++ b/libs/client-sdk/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to the LaunchDarkly Client-Side SDK for C/C++ will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org). +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * launchdarkly-cpp-internal bumped from 0.1.9 to 0.1.10 + ## [3.0.8](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-client-v3.0.7...launchdarkly-cpp-client-v3.0.8) (2023-09-13) diff --git a/libs/client-sdk/package.json b/libs/client-sdk/package.json index d1128f73d..b30bd99cb 100644 --- a/libs/client-sdk/package.json +++ b/libs/client-sdk/package.json @@ -1,10 +1,10 @@ { "name": "launchdarkly-cpp-client", "description": "This package.json exists for modeling dependencies for the release process.", - "version": "3.0.8", + "version": "3.0.9", "private": true, "dependencies": { - "launchdarkly-cpp-internal": "0.1.9", + "launchdarkly-cpp-internal": "0.1.10", "launchdarkly-cpp-common": "0.3.6", "launchdarkly-cpp-sse-client": "0.1.3" } diff --git a/libs/internal/CHANGELOG.md b/libs/internal/CHANGELOG.md index 5feb50541..ea45d1b82 100644 --- a/libs/internal/CHANGELOG.md +++ b/libs/internal/CHANGELOG.md @@ -46,6 +46,13 @@ * dependencies * launchdarkly-cpp-common bumped from 0.3.5 to 0.3.6 +## [0.1.10](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-internal-v0.1.9...launchdarkly-cpp-internal-v0.1.10) (2023-09-21) + + +### Bug Fixes + +* catch exception if en_US.utf8-locale missing when parsing datetime headers ([#251](https://github.com/launchdarkly/cpp-sdks/issues/251)) ([eb2a8f0](https://github.com/launchdarkly/cpp-sdks/commit/eb2a8f093996361541e11659165cbecc94c15346)) + ## [0.1.5](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-internal-v0.1.4...launchdarkly-cpp-internal-v0.1.5) (2023-06-30) diff --git a/libs/internal/package.json b/libs/internal/package.json index c474b1b36..5dd3d781b 100644 --- a/libs/internal/package.json +++ b/libs/internal/package.json @@ -1,7 +1,7 @@ { "name": "launchdarkly-cpp-internal", "description": "This package.json exists for modeling dependencies for the release process.", - "version": "0.1.9", + "version": "0.1.10", "private": true, "dependencies": { "launchdarkly-cpp-common": "0.3.6" From 5309250946925939ea3930cd0a322d57ae5342a5 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 5 Oct 2023 13:49:52 -0700 Subject: [PATCH 05/21] chore: update CODEOWNERS (#254) --- CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index 7d0dac3c8..412f35ea2 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,2 +1,2 @@ # Repository Maintainers -* @launchdarkly/team-sdk +* @launchdarkly/team-sdk-c From fa6662d3e71c077ff4b98fa6aa6bec8a49a696ad Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 10 Oct 2023 14:23:49 -0700 Subject: [PATCH 06/21] chore: detect where OpenSSL is installed on Windows runner (#256) We have intermittent CI failures because OpenSSL is installed to either `Program Files\OpenSSL-Win64` or `Program Files\OpenSSL`. I'm not entirely sure why, but it seems to be luck of the draw on the runner. This should detect where it was installed and pass it to CMake. We can't force the install location because that requires a licensed chocolatey. Chocolatey also writes out the install location - for this particular package - but we'd have to process its output. Instead, this just tests the two known installation directories. --- .github/actions/sdk-release/action.yml | 12 ++++++++++-- .github/workflows/client.yml | 10 +++++++++- .github/workflows/manual-sdk-release-artifacts.yml | 7 ++++--- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/.github/actions/sdk-release/action.yml b/.github/actions/sdk-release/action.yml index 22606e270..3c4eb762e 100644 --- a/.github/actions/sdk-release/action.yml +++ b/.github/actions/sdk-release/action.yml @@ -97,12 +97,20 @@ runs: shell: bash run: | choco upgrade openssl --no-progress - + - name: Determine OpenSSL Installation Directory + if: runner.os == 'Windows' + shell: bash + run: | + if [ -d "C:\Program Files\OpenSSL-Win64" ]; then + echo "OPENSSL_ROOT_DIR=C:\Program Files\OpenSSL-Win64" >> "$GITHUB_ENV" + else + echo "OPENSSL_ROOT_DIR=C:\Program Files\OpenSSL" >> "$GITHUB_ENV" + fi - name: Build Windows Artifacts if: runner.os == 'Windows' shell: bash env: - OPENSSL_ROOT_DIR: 'C:\Program Files\OpenSSL-Win64' + OPENSSL_ROOT_DIR: ${{ env.OPENSSL_ROOT_DIR }} BOOST_LIBRARY_DIR: 'C:\local\boost_1_81_0\lib64-msvc-14.3' BOOST_LIBRARYDIR: 'C:\local\boost_1_81_0\lib64-msvc-14.3' BOOST_ROOT: ${{ steps.install-boost.outputs.BOOST_ROOT }} diff --git a/.github/workflows/client.yml b/.github/workflows/client.yml index b5cb48289..4ff3603c4 100644 --- a/.github/workflows/client.yml +++ b/.github/workflows/client.yml @@ -58,11 +58,19 @@ jobs: shell: bash run: | choco upgrade openssl --no-progress + - name: Determine OpenSSL Installation Directory + shell: bash + run: | + if [ -d "C:\Program Files\OpenSSL-Win64" ]; then + echo "OPENSSL_ROOT_DIR=C:\Program Files\OpenSSL-Win64" >> "$GITHUB_ENV" + else + echo "OPENSSL_ROOT_DIR=C:\Program Files\OpenSSL" >> "$GITHUB_ENV" + fi - uses: actions/checkout@v3 - uses: ilammy/msvc-dev-cmd@v1 - uses: ./.github/actions/ci env: - OPENSSL_ROOT_DIR: 'C:\Program Files\OpenSSL-Win64' + OPENSSL_ROOT_DIR: ${{ env.OPENSSL_ROOT_DIR }} BOOST_LIBRARY_DIR: 'C:\local\boost_1_81_0\lib64-msvc-14.3' BOOST_LIBRARYDIR: 'C:\local\boost_1_81_0\lib64-msvc-14.3' with: diff --git a/.github/workflows/manual-sdk-release-artifacts.yml b/.github/workflows/manual-sdk-release-artifacts.yml index fab15f26b..792f514bf 100644 --- a/.github/workflows/manual-sdk-release-artifacts.yml +++ b/.github/workflows/manual-sdk-release-artifacts.yml @@ -1,5 +1,6 @@ # Checks out the tag, builds release builds, and attaches them to the release for the tag. # If you need to change build scripts, then update the tag to include the modifications. +# NOTE: This workflow uses sdk-release/action.yml @ the tag specified in the workflow_dispatch input. on: workflow_dispatch: inputs: @@ -55,18 +56,18 @@ jobs: sdk_path: ${{ needs.split-input.outputs.sdk_path}} sdk_cmake_target: ${{ needs.split-input.outputs.sdk_cmake_target}} release-sdk-provenance: - needs: ['release-sdk'] + needs: [ 'release-sdk' ] strategy: matrix: # Generates a combined attestation for each platform os: [ linux, windows, macos ] - permissions: + permissions: actions: read id-token: write contents: write uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.7.0 with: base64-subjects: "${{ needs.release-sdk.outputs[format('hashes-{0}', matrix.os)] }}" - upload-assets: true + upload-assets: true upload-tag-name: ${{ inputs.tag }} provenance-name: ${{ format('{0}-multiple-provenance.intoto.jsonl', matrix.os) }} From 7f4f168f47619d7fa8b8952feade485261c69049 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 11 Oct 2023 09:50:32 -0700 Subject: [PATCH 07/21] fix: treat warnings as errors in CI (#253) Many users might compile the SDK using a "warnings as errors" flag. To support this use-case, update CI to compile the sdk with CMAKE_COMPILE_WARNING_AS_ERROR=TRUE. This commit also resolves existing warnings, with the exception of OpenSSL deprecation warnings which have been supressed using CMake generator expressions. --- .clang-format | 1 + cmake/expected.cmake | 3 +- libs/client-sdk/src/bindings/c/builder.cpp | 2 +- .../data_sources/streaming_data_source.cpp | 16 ++-- .../include/launchdarkly/attributes.hpp | 8 +- .../launchdarkly/detail/c_binding_helpers.hpp | 4 +- .../launchdarkly/detail/unreachable.hpp | 14 +++ libs/common/include/launchdarkly/value.hpp | 17 ++-- libs/common/src/attribute_reference.cpp | 9 +- .../src/bindings/c/data/evaluation_detail.cpp | 1 + libs/common/src/bindings/c/value.cpp | 3 +- libs/common/src/value.cpp | 91 +++++++++++-------- .../launchdarkly/network/asio_requester.hpp | 8 +- libs/internal/src/CMakeLists.txt | 7 +- .../internal/src/serialization/json_value.cpp | 3 +- scripts/build.sh | 2 +- 16 files changed, 121 insertions(+), 68 deletions(-) create mode 100644 libs/common/include/launchdarkly/detail/unreachable.hpp diff --git a/.clang-format b/.clang-format index 4eba9f053..c6fa7e954 100644 --- a/.clang-format +++ b/.clang-format @@ -2,4 +2,5 @@ BasedOnStyle: Chromium IndentWidth: 4 QualifierAlignment: Right +NamespaceIndentation: None ... diff --git a/cmake/expected.cmake b/cmake/expected.cmake index 18864faa6..e2bbf4dde 100644 --- a/cmake/expected.cmake +++ b/cmake/expected.cmake @@ -7,9 +7,10 @@ if (${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.24") cmake_policy(SET CMP0135 NEW) endif () +set(EXPECTED_BUILD_TESTS OFF) FetchContent_Declare(tl-expected GIT_REPOSITORY https://github.com/TartanLlama/expected.git GIT_TAG 292eff8bd8ee230a7df1d6a1c00c4ea0eb2f0362 - ) +) FetchContent_MakeAvailable(tl-expected) diff --git a/libs/client-sdk/src/bindings/c/builder.cpp b/libs/client-sdk/src/bindings/c/builder.cpp index 8f88b59ee..0259ba83b 100644 --- a/libs/client-sdk/src/bindings/c/builder.cpp +++ b/libs/client-sdk/src/bindings/c/builder.cpp @@ -93,7 +93,7 @@ LDClientConfigBuilder_Build(LDClientConfigBuilder b, LD_ASSERT_NOT_NULL(b); LD_ASSERT_NOT_NULL(out_config); - return launchdarkly::ConsumeBuilder(b, out_config); + return launchdarkly::detail::ConsumeBuilder(b, out_config); } LD_EXPORT(void) diff --git a/libs/client-sdk/src/data_sources/streaming_data_source.cpp b/libs/client-sdk/src/data_sources/streaming_data_source.cpp index 4d1caf6f5..45de5a41c 100644 --- a/libs/client-sdk/src/data_sources/streaming_data_source.cpp +++ b/libs/client-sdk/src/data_sources/streaming_data_source.cpp @@ -1,3 +1,11 @@ +#include "streaming_data_source.hpp" + +#include +#include +#include +#include +#include + #include #include #include @@ -6,13 +14,6 @@ #include -#include "streaming_data_source.hpp" - -#include -#include -#include -#include - namespace launchdarkly::client_side::data_sources { static char const* const kCouldNotParseEndpoint = @@ -30,6 +31,7 @@ static char const* DataSourceErrorToString(launchdarkly::sse::Error error) { case sse::Error::ReadTimeout: return "read timeout reached"; } + launchdarkly::detail::unreachable(); } StreamingDataSource::StreamingDataSource( diff --git a/libs/common/include/launchdarkly/attributes.hpp b/libs/common/include/launchdarkly/attributes.hpp index f37ab2a69..b9e68a850 100644 --- a/libs/common/include/launchdarkly/attributes.hpp +++ b/libs/common/include/launchdarkly/attributes.hpp @@ -118,8 +118,8 @@ class Attributes final { : key_(std::move(key)), name_(std::move(name)), anonymous_(anonymous), - custom_attributes_(std::move(attributes)), - private_attributes_(std::move(private_attributes)) {} + private_attributes_(std::move(private_attributes)), + custom_attributes_(std::move(attributes)) {} friend std::ostream& operator<<(std::ostream& out, Attributes const& attrs) { @@ -142,9 +142,13 @@ class Attributes final { } Attributes(Attributes const& context) = default; + Attributes(Attributes&& context) = default; + ~Attributes() = default; + Attributes& operator=(Attributes const&) = default; + Attributes& operator=(Attributes&&) = default; private: diff --git a/libs/common/include/launchdarkly/detail/c_binding_helpers.hpp b/libs/common/include/launchdarkly/detail/c_binding_helpers.hpp index d9cb21d05..6a800e1fe 100644 --- a/libs/common/include/launchdarkly/detail/c_binding_helpers.hpp +++ b/libs/common/include/launchdarkly/detail/c_binding_helpers.hpp @@ -6,7 +6,7 @@ #include -namespace launchdarkly { +namespace launchdarkly::detail { template struct has_result_type : std::false_type {}; @@ -105,5 +105,5 @@ bool OptReturnReinterpretCast(std::optional& opt, #define LD_ASSERT_NOT_NULL(param) LD_ASSERT(param != nullptr) -} // namespace launchdarkly +} // namespace launchdarkly::detail // NOLINTEND cppcoreguidelines-pro-type-reinterpret-cast diff --git a/libs/common/include/launchdarkly/detail/unreachable.hpp b/libs/common/include/launchdarkly/detail/unreachable.hpp new file mode 100644 index 000000000..f0dff1993 --- /dev/null +++ b/libs/common/include/launchdarkly/detail/unreachable.hpp @@ -0,0 +1,14 @@ +namespace launchdarkly::detail { + +// This may be replaced with a standard routine when C++23 is available. +[[noreturn]] inline void unreachable() { +// Uses compiler specific extensions if possible. +// Even if no extension is used, undefined behavior is still raised by +// an empty function body and the noreturn attribute. +#if defined(__GNUC__) // GCC, Clang, ICC + __builtin_unreachable(); +#elif defined(_MSC_VER) // MSVC + __assume(false); +#endif +} +} // namespace launchdarkly::detail diff --git a/libs/common/include/launchdarkly/value.hpp b/libs/common/include/launchdarkly/value.hpp index 9d3a82019..1b386b7b6 100644 --- a/libs/common/include/launchdarkly/value.hpp +++ b/libs/common/include/launchdarkly/value.hpp @@ -119,7 +119,7 @@ class Value final { using iterator_category = std::forward_iterator_tag; using difference_type = std::ptrdiff_t; - using value_type = std::pair; + using value_type = std::pair; using pointer = value_type const*; using reference = value_type const&; @@ -374,7 +374,7 @@ class Value final { static Value const& Null(); friend std::ostream& operator<<(std::ostream& out, Value const& value) { - switch (value.type_) { + switch (value.Type()) { case Type::kNull: out << "null()"; break; @@ -409,16 +409,17 @@ class Value final { operator int() const { return AsInt(); } private: - std::variant storage_; - enum Type type_; + struct null_type {}; + + std::variant storage_; // Empty constants used when accessing the wrong type. // These are not inline static const because of this bug: // https://developercommunity.visualstudio.com/t/inline-static-destructors-are-called-multiple-time/1157794 - static const std::string empty_string_; - static const Array empty_vector_; - static const Object empty_map_; - static const Value null_value_; + static std::string const empty_string_; + static Array const empty_vector_; + static Object const empty_map_; + static Value const null_value_; }; bool operator==(Value const& lhs, Value const& rhs); diff --git a/libs/common/src/attribute_reference.cpp b/libs/common/src/attribute_reference.cpp index a0dfabef2..d6ccec765 100644 --- a/libs/common/src/attribute_reference.cpp +++ b/libs/common/src/attribute_reference.cpp @@ -229,10 +229,11 @@ AttributeReference::AttributeReference(char const* ref_str) std::string AttributeReference::PathToStringReference( std::vector path) { // Approximate size to reduce resizes. - auto size = std::accumulate(path.begin(), path.end(), 0, - [](auto sum, auto const& component) { - return sum + component.size() + 1; - }); + std::size_t size = + std::accumulate(path.begin(), path.end(), std::size_t{0}, + [](std::size_t sum, auto const& component) { + return sum + component.size() + 1; + }); std::string redaction_name; redaction_name.reserve(size); diff --git a/libs/common/src/bindings/c/data/evaluation_detail.cpp b/libs/common/src/bindings/c/data/evaluation_detail.cpp index c91c17f1d..c565e70e4 100644 --- a/libs/common/src/bindings/c/data/evaluation_detail.cpp +++ b/libs/common/src/bindings/c/data/evaluation_detail.cpp @@ -11,6 +11,7 @@ #define FROM_REASON(ptr) (reinterpret_cast(ptr)); using namespace launchdarkly; +using namespace launchdarkly::detail; LD_EXPORT(void) LDEvalDetail_Free(LDEvalDetail detail) { diff --git a/libs/common/src/bindings/c/value.cpp b/libs/common/src/bindings/c/value.cpp index ffbc75153..484d9897f 100644 --- a/libs/common/src/bindings/c/value.cpp +++ b/libs/common/src/bindings/c/value.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include using launchdarkly::Value; @@ -59,7 +60,7 @@ LD_EXPORT(enum LDValueType) LDValue_Type(LDValue val) { case Value::Type::kArray: return LDValueType_Array; } - LD_ASSERT(!"Unsupported value type."); + launchdarkly::detail::unreachable(); } LD_EXPORT(bool) LDValue_GetBool(LDValue val) { diff --git a/libs/common/src/value.cpp b/libs/common/src/value.cpp index 1db2b19a9..d2abf9c9a 100644 --- a/libs/common/src/value.cpp +++ b/libs/common/src/value.cpp @@ -1,5 +1,3 @@ -#pragma clang diagnostic push - #include #include @@ -7,57 +5,76 @@ namespace launchdarkly { -const std::string Value::empty_string_; -const Value::Array Value::empty_vector_; -const Value::Object Value::empty_map_; -const Value Value::null_value_; +std::string const Value::empty_string_; +Value::Array const Value::empty_vector_; +Value::Object const Value::empty_map_; +Value const Value::null_value_; + +Value::Value() : storage_{null_type{}} {} -Value::Value() : type_(Value::Type::kNull), storage_{0.0} {} +Value::Value(bool boolean) : storage_{boolean} {} -Value::Value(bool boolean) : type_(Value::Type::kBool), storage_{boolean} {} +Value::Value(double num) : storage_{num} {} -Value::Value(double num) : type_(Value::Type::kNumber), storage_{num} {} +Value::Value(int num) : storage_{(double)num} {} -Value::Value(int num) : type_(Value::Type::kNumber), storage_{(double)num} {} +Value::Value(std::string str) : storage_{std::move(str)} {} -Value::Value(std::string str) - : type_(Value::Type::kString), storage_{std::move(str)} {} +Value::Value(char const* str) : storage_{std::string(str)} {} -Value::Value(char const* str) - : type_(Value::Type::kString), storage_{std::string(str)} {} +Value::Value(std::vector arr) : storage_{std::move(arr)} {} -Value::Value(std::vector arr) - : type_(Value::Type::kArray), storage_{std::move(arr)} {} +Value::Value(std::map obj) : storage_{std::move(obj)} {} -Value::Value(std::map obj) - : type_(Value::Type::kObject), storage_{std::move(obj)} {} +template +inline constexpr bool always_false_v = false; enum Value::Type Value::Type() const { - return type_; + return std::visit( + [](auto const& arg) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + return Type::kNull; + } else if constexpr (std::is_same_v) { + return Type::kBool; + } else if constexpr (std::is_same_v) { + return Type::kNumber; + } else if constexpr (std::is_same_v) { + return Type::kString; + } else if constexpr (std::is_same_v) { + return Type::kArray; + } else if constexpr (std::is_same_v) { + return Type::kObject; + } else { + static_assert(always_false_v, + "all value types must be visited"); + } + }, + storage_); } bool Value::IsNull() const { - return type_ == Type::kNull; + return std::holds_alternative(storage_); } bool Value::IsBool() const { - return type_ == Type::kBool; + return std::holds_alternative(storage_); } bool Value::IsNumber() const { - return type_ == Type::kNumber; + return std::holds_alternative(storage_); } bool Value::IsString() const { - return type_ == Type::kString; + return std::holds_alternative(storage_); } bool Value::IsArray() const { - return type_ == Type::kArray; + return std::holds_alternative(storage_); } bool Value::IsObject() const { - return type_ == Type::kObject; + return std::holds_alternative(storage_); } Value const& Value::Null() { @@ -67,42 +84,42 @@ Value const& Value::Null() { } bool Value::AsBool() const { - if (type_ == Type::kBool) { + if (IsBool()) { return std::get(storage_); } return false; } int Value::AsInt() const { - if (type_ == Type::kNumber) { + if (IsNumber()) { return static_cast(std::get(storage_)); } return 0; } double Value::AsDouble() const { - if (type_ == Type::kNumber) { + if (IsNumber()) { return std::get(storage_); } return 0.0; } std::string const& Value::AsString() const { - if (type_ == Type::kString) { + if (IsString()) { return std::get(storage_); } return empty_string_; } Value::Array const& Value::AsArray() const { - if (type_ == Type::kArray) { + if (IsArray()) { return std::get(storage_); } return empty_vector_; } Value::Object const& Value::AsObject() const { - if (type_ == Type::kObject) { + if (IsObject()) { return std::get(storage_); } return empty_map_; @@ -110,19 +127,17 @@ Value::Object const& Value::AsObject() const { Value::Value(std::optional opt_str) : storage_{0.0} { if (opt_str.has_value()) { - type_ = Type::kString; storage_ = opt_str.value(); } else { - type_ = Type::kNull; + storage_ = null_type{}; } } Value::Value(std::initializer_list values) - : type_(Type::kArray), storage_(std::vector(values)) {} + : storage_(std::vector(values)) {} + +Value::Value(Value::Array arr) : storage_(std::move(arr)) {} -Value::Value(Value::Array arr) - : storage_(std::move(arr)), type_(Type::kArray) {} -Value::Value(Value::Object obj) - : storage_(std::move(obj)), type_(Type::kObject) {} +Value::Value(Value::Object obj) : storage_(std::move(obj)) {} Value::Array::Iterator::Iterator(std::vector::const_iterator iterator) : iterator_(iterator) {} diff --git a/libs/internal/include/launchdarkly/network/asio_requester.hpp b/libs/internal/include/launchdarkly/network/asio_requester.hpp index bc231b2c0..5c5e56ea7 100644 --- a/libs/internal/include/launchdarkly/network/asio_requester.hpp +++ b/libs/internal/include/launchdarkly/network/asio_requester.hpp @@ -2,6 +2,8 @@ #include "http_requester.hpp" +#include + #include #include #include @@ -44,6 +46,10 @@ static bool NeedsRedirect(HttpResult const& res) { res.Status() == 308 && res.Headers().count("location") != 0; } +/** + * Converts the given HttpMethod to a boost beast HTTP verb. + * If the verb is unrecognized, returns http::verb::get. + */ static http::verb ConvertMethod(HttpMethod method) { switch (method) { case HttpMethod::kPost: @@ -55,7 +61,7 @@ static http::verb ConvertMethod(HttpMethod method) { case HttpMethod::kPut: return http::verb::put; } - assert(!"Method not found. Ensure all method cases covered."); + launchdarkly::detail::unreachable(); } static http::request MakeBeastRequest( diff --git a/libs/internal/src/CMakeLists.txt b/libs/internal/src/CMakeLists.txt index d8768e0b7..7c0f69c29 100644 --- a/libs/internal/src/CMakeLists.txt +++ b/libs/internal/src/CMakeLists.txt @@ -5,7 +5,7 @@ file(GLOB HEADER_LIST CONFIGURE_DEPENDS "${LaunchDarklyInternalSdk_SOURCE_DIR}/include/launchdarkly/network/*.hpp" "${LaunchDarklyInternalSdk_SOURCE_DIR}/include/launchdarkly/serialization/*.hpp" "${LaunchDarklyInternalSdk_SOURCE_DIR}/include/launchdarkly/serialization/events/*.hpp" - ) +) # Automatic library: static or dynamic based on user config. add_library(${LIBNAME} OBJECT @@ -36,6 +36,11 @@ add_library(${LIBNAME} OBJECT add_library(launchdarkly::internal ALIAS ${LIBNAME}) +# TODO(SC-209963): Remove once OpenSSL deprecated hash function usage has been updated +target_compile_options(${LIBNAME} PRIVATE + $<$,$,$>: + -Wno-deprecated-declarations> +) set_property(TARGET ${LIBNAME} PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL") diff --git a/libs/internal/src/serialization/json_value.cpp b/libs/internal/src/serialization/json_value.cpp index 551322763..be80e6622 100644 --- a/libs/internal/src/serialization/json_value.cpp +++ b/libs/internal/src/serialization/json_value.cpp @@ -1,3 +1,4 @@ +#include #include #include @@ -48,7 +49,7 @@ Value tag_invoke(boost::json::value_to_tag const& unused, } // The above switch is exhaustive, so this can only happen if a new // type is added to boost::json::value. - assert(!"All types need to be handled."); + launchdarkly::detail::unreachable(); } void tag_invoke(boost::json::value_from_tag const&, diff --git a/scripts/build.sh b/scripts/build.sh index 6b5527447..45d1c4322 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -17,6 +17,6 @@ cd build # script ends. trap cleanup EXIT -cmake -G Ninja -D BUILD_TESTING="$2" .. +cmake -G Ninja -DCMAKE_COMPILE_WARNING_AS_ERROR=TRUE -D BUILD_TESTING="$2" .. cmake --build . --target "$1" From 0a55e7b6dc168c7e58312f63c7bb2829e57c6481 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 11 Oct 2023 10:30:47 -0700 Subject: [PATCH 08/21] chore: release main (#257) :robot: I have created a release *beep* *boop* ---
launchdarkly-cpp-client: 3.0.10 ## [3.0.10](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-client-v3.0.9...launchdarkly-cpp-client-v3.0.10) (2023-10-11) ### Bug Fixes * treat warnings as errors in CI ([#253](https://github.com/launchdarkly/cpp-sdks/issues/253)) ([7f4f168](https://github.com/launchdarkly/cpp-sdks/commit/7f4f168f47619d7fa8b8952feade485261c69049)) ### Dependencies * The following workspace dependencies were updated * dependencies * launchdarkly-cpp-internal bumped from 0.1.10 to 0.1.11 * launchdarkly-cpp-common bumped from 0.3.6 to 0.3.7
launchdarkly-cpp-common: 0.3.7 ## [0.3.7](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-common-v0.3.6...launchdarkly-cpp-common-v0.3.7) (2023-10-11) ### Bug Fixes * treat warnings as errors in CI ([#253](https://github.com/launchdarkly/cpp-sdks/issues/253)) ([7f4f168](https://github.com/launchdarkly/cpp-sdks/commit/7f4f168f47619d7fa8b8952feade485261c69049))
launchdarkly-cpp-internal: 0.1.11 ## [0.1.11](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-internal-v0.1.10...launchdarkly-cpp-internal-v0.1.11) (2023-10-11) ### Bug Fixes * treat warnings as errors in CI ([#253](https://github.com/launchdarkly/cpp-sdks/issues/253)) ([7f4f168](https://github.com/launchdarkly/cpp-sdks/commit/7f4f168f47619d7fa8b8952feade485261c69049)) ### Dependencies * The following workspace dependencies were updated * dependencies * launchdarkly-cpp-common bumped from 0.3.6 to 0.3.7
--- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .release-please-manifest.json | 6 +++--- libs/client-sdk/CHANGELOG.md | 15 +++++++++++++++ libs/client-sdk/CMakeLists.txt | 2 +- .../include/launchdarkly/client_side/client.hpp | 2 +- libs/client-sdk/package.json | 6 +++--- libs/client-sdk/tests/client_c_bindings_test.cpp | 2 +- libs/client-sdk/tests/client_test.cpp | 2 +- libs/common/CHANGELOG.md | 7 +++++++ libs/common/package.json | 2 +- libs/internal/CHANGELOG.md | 14 ++++++++++++++ libs/internal/package.json | 4 ++-- 11 files changed, 49 insertions(+), 13 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index fddbbb09c..2fc2308c9 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,6 +1,6 @@ { - "libs/client-sdk": "3.0.9", + "libs/client-sdk": "3.0.10", "libs/server-sent-events": "0.1.3", - "libs/common": "0.3.6", - "libs/internal": "0.1.10" + "libs/common": "0.3.7", + "libs/internal": "0.1.11" } diff --git a/libs/client-sdk/CHANGELOG.md b/libs/client-sdk/CHANGELOG.md index a0851b31b..252100808 100644 --- a/libs/client-sdk/CHANGELOG.md +++ b/libs/client-sdk/CHANGELOG.md @@ -8,6 +8,21 @@ All notable changes to the LaunchDarkly Client-Side SDK for C/C++ will be docume * dependencies * launchdarkly-cpp-internal bumped from 0.1.9 to 0.1.10 +## [3.0.10](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-client-v3.0.9...launchdarkly-cpp-client-v3.0.10) (2023-10-11) + + +### Bug Fixes + +* treat warnings as errors in CI ([#253](https://github.com/launchdarkly/cpp-sdks/issues/253)) ([7f4f168](https://github.com/launchdarkly/cpp-sdks/commit/7f4f168f47619d7fa8b8952feade485261c69049)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * launchdarkly-cpp-internal bumped from 0.1.10 to 0.1.11 + * launchdarkly-cpp-common bumped from 0.3.6 to 0.3.7 + ## [3.0.8](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-client-v3.0.7...launchdarkly-cpp-client-v3.0.8) (2023-09-13) diff --git a/libs/client-sdk/CMakeLists.txt b/libs/client-sdk/CMakeLists.txt index 0d4dffd46..65743711c 100644 --- a/libs/client-sdk/CMakeLists.txt +++ b/libs/client-sdk/CMakeLists.txt @@ -6,7 +6,7 @@ cmake_minimum_required(VERSION 3.19) project( LaunchDarklyCPPClient - VERSION 3.0.8 # {x-release-please-version} + VERSION 3.0.10 # {x-release-please-version} DESCRIPTION "LaunchDarkly C++ Client SDK" LANGUAGES CXX C ) diff --git a/libs/client-sdk/include/launchdarkly/client_side/client.hpp b/libs/client-sdk/include/launchdarkly/client_side/client.hpp index a4eb5866d..79d7242a5 100644 --- a/libs/client-sdk/include/launchdarkly/client_side/client.hpp +++ b/libs/client-sdk/include/launchdarkly/client_side/client.hpp @@ -324,7 +324,7 @@ class Client : public IClient { private: inline static char const* const kVersion = - "3.0.8"; // {x-release-please-version} + "3.0.10"; // {x-release-please-version} std::unique_ptr client; }; diff --git a/libs/client-sdk/package.json b/libs/client-sdk/package.json index b30bd99cb..8524c3f0e 100644 --- a/libs/client-sdk/package.json +++ b/libs/client-sdk/package.json @@ -1,11 +1,11 @@ { "name": "launchdarkly-cpp-client", "description": "This package.json exists for modeling dependencies for the release process.", - "version": "3.0.9", + "version": "3.0.10", "private": true, "dependencies": { - "launchdarkly-cpp-internal": "0.1.10", - "launchdarkly-cpp-common": "0.3.6", + "launchdarkly-cpp-internal": "0.1.11", + "launchdarkly-cpp-common": "0.3.7", "launchdarkly-cpp-sse-client": "0.1.3" } } diff --git a/libs/client-sdk/tests/client_c_bindings_test.cpp b/libs/client-sdk/tests/client_c_bindings_test.cpp index f67e477a2..41171437f 100644 --- a/libs/client-sdk/tests/client_c_bindings_test.cpp +++ b/libs/client-sdk/tests/client_c_bindings_test.cpp @@ -27,7 +27,7 @@ TEST(ClientBindings, MinimalInstantiation) { char const* version = LDClientSDK_Version(); ASSERT_TRUE(version); - ASSERT_STREQ(version, "3.0.8"); // {x-release-please-version} + ASSERT_STREQ(version, "3.0.10"); // {x-release-please-version} LDClientSDK_Free(sdk); } diff --git a/libs/client-sdk/tests/client_test.cpp b/libs/client-sdk/tests/client_test.cpp index c2daffe20..243a9c351 100644 --- a/libs/client-sdk/tests/client_test.cpp +++ b/libs/client-sdk/tests/client_test.cpp @@ -16,7 +16,7 @@ TEST(ClientTest, ClientConstructedWithMinimalConfigAndContext) { char const* version = client.Version(); ASSERT_TRUE(version); - ASSERT_STREQ(version, "3.0.8"); // {x-release-please-version} + ASSERT_STREQ(version, "3.0.10"); // {x-release-please-version} } TEST(ClientTest, AllFlagsIsEmpty) { diff --git a/libs/common/CHANGELOG.md b/libs/common/CHANGELOG.md index 51015db42..45c1336c9 100644 --- a/libs/common/CHANGELOG.md +++ b/libs/common/CHANGELOG.md @@ -12,6 +12,13 @@ * dependencies * launchdarkly-cpp-sse-client bumped from 0.1.1 to 0.1.2 +## [0.3.7](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-common-v0.3.6...launchdarkly-cpp-common-v0.3.7) (2023-10-11) + + +### Bug Fixes + +* treat warnings as errors in CI ([#253](https://github.com/launchdarkly/cpp-sdks/issues/253)) ([7f4f168](https://github.com/launchdarkly/cpp-sdks/commit/7f4f168f47619d7fa8b8952feade485261c69049)) + ## [0.3.6](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-common-v0.3.5...launchdarkly-cpp-common-v0.3.6) (2023-09-13) diff --git a/libs/common/package.json b/libs/common/package.json index a5898337e..151a67d60 100644 --- a/libs/common/package.json +++ b/libs/common/package.json @@ -1,6 +1,6 @@ { "name": "launchdarkly-cpp-common", "description": "This package.json exists for modeling dependencies for the release process.", - "version": "0.3.6", + "version": "0.3.7", "private": true } diff --git a/libs/internal/CHANGELOG.md b/libs/internal/CHANGELOG.md index ea45d1b82..61d6d7ed2 100644 --- a/libs/internal/CHANGELOG.md +++ b/libs/internal/CHANGELOG.md @@ -46,6 +46,20 @@ * dependencies * launchdarkly-cpp-common bumped from 0.3.5 to 0.3.6 +## [0.1.11](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-internal-v0.1.10...launchdarkly-cpp-internal-v0.1.11) (2023-10-11) + + +### Bug Fixes + +* treat warnings as errors in CI ([#253](https://github.com/launchdarkly/cpp-sdks/issues/253)) ([7f4f168](https://github.com/launchdarkly/cpp-sdks/commit/7f4f168f47619d7fa8b8952feade485261c69049)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * launchdarkly-cpp-common bumped from 0.3.6 to 0.3.7 + ## [0.1.10](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-internal-v0.1.9...launchdarkly-cpp-internal-v0.1.10) (2023-09-21) diff --git a/libs/internal/package.json b/libs/internal/package.json index 5dd3d781b..4ec18e48d 100644 --- a/libs/internal/package.json +++ b/libs/internal/package.json @@ -1,9 +1,9 @@ { "name": "launchdarkly-cpp-internal", "description": "This package.json exists for modeling dependencies for the release process.", - "version": "0.1.10", + "version": "0.1.11", "private": true, "dependencies": { - "launchdarkly-cpp-common": "0.3.6" + "launchdarkly-cpp-common": "0.3.7" } } From ed23c9a347665529a09d18111bb9d3b699381728 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 13 Oct 2023 13:16:57 -0700 Subject: [PATCH 09/21] feat: clean up LD CMake variables & allow for OpenSSL dynamic link (#255) This adds documentation of the various options used to control the SDK build. It also cleans up the options by prefixing them with `LD_` and making use of `cmake_dependent_option` to expose only relevant options. The default, hands-off configuration is: - Build the SDK and its unit tests (with sanitizer support) - The SDK artifact is a static library - Build example apps - Static link OpenSSL Users can then tweak away from the default: - Disable unit test build (or disable sanitizers) - Enable contract test build - Disable example app build - Make the SDK artifact a shared library - Link OpenSSL dynamically Finally, to simply disable all testing stuff and just produce the SDK, the common `BUILD_TESTING` flag can be disabled. That'll force any of the testing related flags off - use case is build scripts / package maintainers. --- CMakeLists.txt | 96 ++++++++++++++++++++------ examples/hello-c-client/CMakeLists.txt | 2 +- libs/client-sdk/CMakeLists.txt | 2 +- libs/client-sdk/README.md | 38 ++++++++++ libs/client-sdk/src/CMakeLists.txt | 18 +++-- libs/common/CMakeLists.txt | 2 +- libs/internal/CMakeLists.txt | 2 +- libs/server-sent-events/CMakeLists.txt | 2 +- scripts/build-release.sh | 2 +- scripts/build-windows.sh | 2 +- scripts/build.sh | 4 +- 11 files changed, 133 insertions(+), 37 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8c62c3aff..4b2cabda1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,6 +3,7 @@ # Required for Apple Silicon support. cmake_minimum_required(VERSION 3.19) +include(CMakeDependentOption) project( LaunchDarklyCPPSDKs @@ -13,6 +14,50 @@ project( include(GNUInstallDirs) +option(BUILD_TESTING "Top-level switch for testing. Turn off to disable unit and contract tests." ON) + +option(LD_BUILD_SHARED_LIBS "Build the SDKs as shared libraries" OFF) + +cmake_dependent_option(LD_BUILD_UNIT_TESTS + "Build the C++ unit tests." + ON # default to enabling unit tests + BUILD_TESTING;NOT LD_BUILD_SHARED_LIBS # only exposed if top-level switch is on, and also only when building + # static libs. This is because we have hidden visibility of symbols by default (to only expose our C API.) + OFF # otherwise, off +) + +# If you want to run the unit tests with valgrind, then LD_TESTING_SANITIZERS must of OFF. +cmake_dependent_option(LD_TESTING_SANITIZERS + "Enable sanitizers for unit tests." + ON # default to enabling sanitizers + LD_BUILD_UNIT_TESTS # only expose if unit tests enabled.. + OFF # otherwise, off +) + +cmake_dependent_option(LD_BUILD_CONTRACT_TESTS + "Build contract test service." + OFF # default to disabling contract tests, since they require running a service + BUILD_TESTING # only expose if top-level switch is on.. + OFF # otherwise, off +) + +# The general strategy is to produce a fat artifact containing all of our dependencies so users +# only have a single thing to link. We should support this either being a static or shared library. +# Because OpenSSL is a large, and security relevant dependency, we should have a separate option +# to link against that statically or dynamically. + +option(LD_DYNAMIC_LINK_OPENSSL + "Dynamically link OpenSSL instead of building with static library" + OFF # default to linking OpenSSL statically +) + +option(LD_BUILD_EXAMPLES "Build hello-world examples." ON) + + +if (LD_BUILD_SHARED_LIBS AND LD_BUILD_UNIT_TESTS) + message(WARNING "LaunchDarkly: unit testing isn't supported while building shared libraries. Switch to static libraries or disable unit tests.") +endif () + # All projects in this repo should share the same version of 3rd party depends. # It's the only way to remain sane. set(CMAKE_FILES "${CMAKE_CURRENT_SOURCE_DIR}/cmake") @@ -20,15 +65,12 @@ set(CMAKE_CXX_STANDARD 17) set(CMAKE_POSITION_INDEPENDENT_CODE ON) -option(BUILD_TESTING "Enable C++ unit tests." ON) -# If you want to run the unit tests with valgrind, then TESTING_SANITIZERS must of OFF. -option(TESTING_SANITIZERS "Enable sanitizers for unit tests." ON) - -if (BUILD_TESTING) +if (LD_BUILD_UNIT_TESTS) + message(STATUS "LaunchDarkly: building unit tests") set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -D_GLIBCXX_DEBUG") add_compile_definitions(LAUNCHDARKLY_USE_ASSERT) - if (TESTING_SANITIZERS) + if (LD_TESTING_SANITIZERS) if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fsanitize=undefined -fsanitize=leak") elseif (CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang") @@ -53,13 +95,22 @@ if (BUILD_TESTING) enable_testing() endif () -set(OPENSSL_USE_STATIC_LIBS ON) +if (LD_DYNAMIC_LINK_OPENSSL) + message(STATUS "LaunchDarkly: searching for shared OpenSSL library") + set(OPENSSL_USE_STATIC_LIBS OFF) +else () + message(STATUS "LaunchDarkly: searching for static OpenSSL library") + set(OPENSSL_USE_STATIC_LIBS ON) +endif () + find_package(OpenSSL REQUIRED) message(STATUS "LaunchDarkly: using OpenSSL v${OPENSSL_VERSION}") +# Even though the main SDK might be a static or shared lib, boost should always statically +# linked into the binary. set(Boost_USE_STATIC_LIBS ON) -if (BUILD_SHARED_LIBS) +if (NOT LD_BUILD_STATIC_LIBS) # When building a shared library we hide all symbols # aside from this we have specifically exported for the C-API. set(CMAKE_CXX_VISIBILITY_PRESET hidden) @@ -71,25 +122,24 @@ set(Boost_USE_STATIC_RUNTIME OFF) find_package(Boost 1.81 REQUIRED COMPONENTS json url coroutine) message(STATUS "LaunchDarkly: using Boost v${Boost_VERSION}") -add_subdirectory(libs/client-sdk) - -set(ORIGINAL_BUILD_SHARED_LIBS "${BUILD_SHARED_LIBS}") -set(BUILD_SHARED_LIBS OFF) +include(${CMAKE_FILES}/certify.cmake) +add_subdirectory(vendor/foxy) -# Always build the common libraries as static libs. +# Common, internal, and server-sent-events are built as "object" libraries. add_subdirectory(libs/common) add_subdirectory(libs/internal) add_subdirectory(libs/server-sent-events) -set(ORIGINAL_BUILD_SHARED_LIBS "${BUILD_SHARED_LIBS}") - -set(BUILD_TESTING OFF) -include(${CMAKE_FILES}/certify.cmake) -add_subdirectory(vendor/foxy) - -set(BUILD_TESTING "${ORIGINAL_BUILD_TESTING}") +# Built as static or shared depending on LD_BUILD_STATIC_LIBS variable. +# This target "links" in common, internal, and sse as object libraries. +add_subdirectory(libs/client-sdk) -set(BUILD_SHARED_LIBS "${ORIGINAL_BUILD_SHARED_LIBS}") +if (LD_BUILD_CONTRACT_TESTS) + message(STATUS "LaunchDarkly: building contract tests") + add_subdirectory(contract-tests) +endif () -add_subdirectory(contract-tests) -add_subdirectory(examples) +if (LD_BUILD_EXAMPLES) + message(STATUS "LaunchDarkly: building examples") + add_subdirectory(examples) +endif () diff --git a/examples/hello-c-client/CMakeLists.txt b/examples/hello-c-client/CMakeLists.txt index 5d9ce32bf..c22cd5614 100644 --- a/examples/hello-c-client/CMakeLists.txt +++ b/examples/hello-c-client/CMakeLists.txt @@ -12,4 +12,4 @@ set(THREADS_PREFER_PTHREAD_FLAG ON) find_package(Threads REQUIRED) add_executable(hello-c main.c) -target_link_libraries(hello-c PRIVATE launchdarkly::client launchdarkly::sse launchdarkly::common Threads::Threads) +target_link_libraries(hello-c PRIVATE launchdarkly::client Threads::Threads) diff --git a/libs/client-sdk/CMakeLists.txt b/libs/client-sdk/CMakeLists.txt index 65743711c..4677324fe 100644 --- a/libs/client-sdk/CMakeLists.txt +++ b/libs/client-sdk/CMakeLists.txt @@ -30,6 +30,6 @@ include(FetchContent) # Add main SDK sources. add_subdirectory(src) -if (BUILD_TESTING) +if (LD_BUILD_UNIT_TESTS) add_subdirectory(tests) endif () diff --git a/libs/client-sdk/README.md b/libs/client-sdk/README.md index ff4f8b957..4ec15f1c4 100644 --- a/libs/client-sdk/README.md +++ b/libs/client-sdk/README.md @@ -69,6 +69,44 @@ gcc -I $(pwd)/include -Llib -fPIE -g main.c liblaunchdarkly-cpp-client.so The examples here are to help with getting started, but generally speaking the SDK should be incorporated using your build system (CMake for instance). +### CMake Usage + +First, add the SDK to your project: + +```cmake +add_subdirectory(path-to-sdk-repo) +``` + +Currently `find_package` is not yet supported. + +This will expose the `launchdarkly::client` target. Next, link the target to your executable or library: + +```cmake +target_link_libraries(my-target PRIVATE launchdarkly::client) +``` + +Various CMake options are available to customize the SDK build. + +| Option | Description | Default | Requires | +|---------------------------|----------------------------------------------------------------------------------------|--------------------|-------------------------------------------| +| `BUILD_TESTING` | Coarse-grained switch; turn off to disable all testing and only build the SDK targets. | On | N/A | +| `LD_BUILD_UNIT_TESTS` | Whether C++ unit tests are built. | On | `BUILD_TESTING; NOT LD_BUILD_SHARED_LIBS` | +| `LD_TESTING_SANITIZERS` | Whether sanitizers should be enabled. | On | `LD_BUILD_UNIT_TESTS` | +| `LD_BUILD_CONTRACT_TESTS` | Whether the contract test service (used in CI) is built. | Off | `BUILD_TESTING` | +| `LD_BUILD_EXAMPLES` | Whether example apps (hello world) are built. | On | N/A | +| `LD_BUILD_SHARED_LIBS` | Whether the SDK is built as a static or shared library. | Off (static lib) | N/A | +| `LD_DYNAMIC_LINK_OPENSSL` | Whether OpenSSL be dynamically linked. | Off (static link) | N/A | + +**Note:** _if building the SDK as a shared library, then unit tests won't be able to link correctly since the SDK's C++ +symbols aren't exposed. To run unit tests, build the SDK as a static library._ + +Example usage: + +```bash +# Build the SDK as a shared library +cmake -GNinja .. -DLD_BUILD_SHARED_LIBS=On +``` + Learn more ----------- diff --git a/libs/client-sdk/src/CMakeLists.txt b/libs/client-sdk/src/CMakeLists.txt index 4ae3f9557..e2ac0c36e 100644 --- a/libs/client-sdk/src/CMakeLists.txt +++ b/libs/client-sdk/src/CMakeLists.txt @@ -3,9 +3,15 @@ file(GLOB HEADER_LIST CONFIGURE_DEPENDS "${LaunchDarklyCPPClient_SOURCE_DIR}/include/launchdarkly/client_side/*.hpp" ) -# Automatic library: static or dynamic based on user config. +if (LD_BUILD_SHARED_LIBS) + message(STATUS "LaunchDarkly: building client-sdk as shared library") + add_library(${LIBNAME} SHARED) +else () + message(STATUS "LaunchDarkly: building client-sdk as static library") + add_library(${LIBNAME} STATIC) +endif () -add_library(${LIBNAME} +target_sources(${LIBNAME} PRIVATE ${HEADER_LIST} data_sources/streaming_data_source.cpp data_sources/data_source_event_handler.cpp @@ -43,14 +49,16 @@ add_library(${LIBNAME} serialization/json_all_flags.cpp flag_manager/flag_manager.cpp flag_manager/flag_persistence.cpp - bindings/c/sdk.cpp) + bindings/c/sdk.cpp +) + -if (MSVC OR (NOT BUILD_SHARED_LIBS)) +if (MSVC OR LD_BUILD_STATIC_LIBS) target_link_libraries(${LIBNAME} PUBLIC launchdarkly::common PRIVATE Boost::headers Boost::json Boost::url launchdarkly::sse launchdarkly::internal foxy) else () - # The default static lib builds, for linux, are positition independent. + # The default static lib builds, for linux, are position independent. # So they do not link into a shared object without issues. So, when # building shared objects do not link the static libraries and instead # use the "src.hpp" files for required libraries. diff --git a/libs/common/CMakeLists.txt b/libs/common/CMakeLists.txt index 08e8d6e1f..0423c4b74 100644 --- a/libs/common/CMakeLists.txt +++ b/libs/common/CMakeLists.txt @@ -30,6 +30,6 @@ include(${CMAKE_FILES}/expected.cmake) # Add main SDK sources. add_subdirectory(src) -if (BUILD_TESTING) +if (LD_BUILD_UNIT_TESTS) add_subdirectory(tests) endif () diff --git a/libs/internal/CMakeLists.txt b/libs/internal/CMakeLists.txt index 1df835d0f..2be93f630 100644 --- a/libs/internal/CMakeLists.txt +++ b/libs/internal/CMakeLists.txt @@ -28,6 +28,6 @@ include(FetchContent) # Add main SDK sources. add_subdirectory(src) -if (BUILD_TESTING) +if (LD_BUILD_UNIT_TESTS) add_subdirectory(tests) endif () diff --git a/libs/server-sent-events/CMakeLists.txt b/libs/server-sent-events/CMakeLists.txt index 04e346659..a1daa302b 100644 --- a/libs/server-sent-events/CMakeLists.txt +++ b/libs/server-sent-events/CMakeLists.txt @@ -31,6 +31,6 @@ include(FetchContent) add_subdirectory(src) -if (BUILD_TESTING) +if (LD_BUILD_UNIT_TESTS) add_subdirectory(tests) endif () diff --git a/scripts/build-release.sh b/scripts/build-release.sh index 922ab9554..c06d26824 100755 --- a/scripts/build-release.sh +++ b/scripts/build-release.sh @@ -17,7 +17,7 @@ cd .. # Build a dynamic release. mkdir -p build-dynamic && cd build-dynamic mkdir -p release -cmake -G Ninja -D CMAKE_BUILD_TYPE=Release -D BUILD_TESTING=OFF -D BUILD_SHARED_LIBS=ON -D CMAKE_INSTALL_PREFIX=./release .. +cmake -G Ninja -D CMAKE_BUILD_TYPE=Release -D BUILD_TESTING=OFF -D LD_BUILD_SHARED_LIBS=ON -D CMAKE_INSTALL_PREFIX=./release .. cmake --build . --target "$1" cmake --install . diff --git a/scripts/build-windows.sh b/scripts/build-windows.sh index faaad3f7e..226af72c8 100644 --- a/scripts/build-windows.sh +++ b/scripts/build-windows.sh @@ -20,7 +20,7 @@ cd .. # Build a dynamic debug release. mkdir -p build-dynamic-debug && cd build-dynamic-debug mkdir -p release -cmake -G Ninja -D CMAKE_BUILD_TYPE=Debug -D BUILD_TESTING=OFF -D BUILD_SHARED_LIBS=ON -D CMAKE_INSTALL_PREFIX=./release .. +cmake -G Ninja -D CMAKE_BUILD_TYPE=Debug -D BUILD_TESTING=OFF -D LD_BUILD_SHARED_LIBS=ON -D CMAKE_INSTALL_PREFIX=./release .. cmake --build . --target "$1" cmake --install . diff --git a/scripts/build.sh b/scripts/build.sh index 45d1c4322..7b77203f0 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -5,7 +5,7 @@ # ./scripts/build.sh my-build-target ON # # $1 the name of the target. For example "launchdarkly-cpp-common". -# $2 ON/OFF which enables/disables building in a test configuration. +# $2 ON/OFF which enables/disables building in a test configuration (unit tests + contract tests.) function cleanup { cd .. @@ -17,6 +17,6 @@ cd build # script ends. trap cleanup EXIT -cmake -G Ninja -DCMAKE_COMPILE_WARNING_AS_ERROR=TRUE -D BUILD_TESTING="$2" .. +cmake -G Ninja -D CMAKE_COMPILE_WARNING_AS_ERROR=TRUE -D BUILD_TESTING="$2" -D LD_BUILD_UNIT_TESTS="$2" -D LD_BUILD_CONTRACT_TESTS="$2" .. cmake --build . --target "$1" From 1d10abe8cf795af48d2e26ded572b752a2d74212 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 16 Oct 2023 10:15:46 -0700 Subject: [PATCH 10/21] chore: release main (#258) :robot: I have created a release *beep* *boop* ---
launchdarkly-cpp-client: 3.1.0 ## [3.1.0](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-client-v3.0.10...launchdarkly-cpp-client-v3.1.0) (2023-10-13) ### Features * clean up LD CMake variables & allow for OpenSSL dynamic link ([#255](https://github.com/launchdarkly/cpp-sdks/issues/255)) ([ed23c9a](https://github.com/launchdarkly/cpp-sdks/commit/ed23c9a347665529a09d18111bb9d3b699381728)) ### Dependencies * The following workspace dependencies were updated * dependencies * launchdarkly-cpp-internal bumped from 0.1.11 to 0.2.0 * launchdarkly-cpp-common bumped from 0.3.7 to 0.4.0 * launchdarkly-cpp-sse-client bumped from 0.1.3 to 0.2.0
launchdarkly-cpp-common: 0.4.0 ## [0.4.0](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-common-v0.3.7...launchdarkly-cpp-common-v0.4.0) (2023-10-13) ### Features * clean up LD CMake variables & allow for OpenSSL dynamic link ([#255](https://github.com/launchdarkly/cpp-sdks/issues/255)) ([ed23c9a](https://github.com/launchdarkly/cpp-sdks/commit/ed23c9a347665529a09d18111bb9d3b699381728))
launchdarkly-cpp-internal: 0.2.0 ## [0.2.0](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-internal-v0.1.11...launchdarkly-cpp-internal-v0.2.0) (2023-10-13) ### Features * clean up LD CMake variables & allow for OpenSSL dynamic link ([#255](https://github.com/launchdarkly/cpp-sdks/issues/255)) ([ed23c9a](https://github.com/launchdarkly/cpp-sdks/commit/ed23c9a347665529a09d18111bb9d3b699381728)) ### Dependencies * The following workspace dependencies were updated * dependencies * launchdarkly-cpp-common bumped from 0.3.7 to 0.4.0
launchdarkly-cpp-sse-client: 0.2.0 ## [0.2.0](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-sse-client-v0.1.3...launchdarkly-cpp-sse-client-v0.2.0) (2023-10-13) ### Features * clean up LD CMake variables & allow for OpenSSL dynamic link ([#255](https://github.com/launchdarkly/cpp-sdks/issues/255)) ([ed23c9a](https://github.com/launchdarkly/cpp-sdks/commit/ed23c9a347665529a09d18111bb9d3b699381728))
--- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .release-please-manifest.json | 8 ++++---- libs/client-sdk/CHANGELOG.md | 16 ++++++++++++++++ libs/client-sdk/CMakeLists.txt | 2 +- .../include/launchdarkly/client_side/client.hpp | 2 +- libs/client-sdk/package.json | 8 ++++---- libs/client-sdk/tests/client_c_bindings_test.cpp | 2 +- libs/client-sdk/tests/client_test.cpp | 2 +- libs/common/CHANGELOG.md | 7 +++++++ libs/common/package.json | 2 +- libs/internal/CHANGELOG.md | 14 ++++++++++++++ libs/internal/package.json | 4 ++-- libs/server-sent-events/CHANGELOG.md | 7 +++++++ libs/server-sent-events/package.json | 2 +- 13 files changed, 60 insertions(+), 16 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 2fc2308c9..27c7342cc 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,6 +1,6 @@ { - "libs/client-sdk": "3.0.10", - "libs/server-sent-events": "0.1.3", - "libs/common": "0.3.7", - "libs/internal": "0.1.11" + "libs/client-sdk": "3.1.0", + "libs/server-sent-events": "0.2.0", + "libs/common": "0.4.0", + "libs/internal": "0.2.0" } diff --git a/libs/client-sdk/CHANGELOG.md b/libs/client-sdk/CHANGELOG.md index 252100808..8d2fadd67 100644 --- a/libs/client-sdk/CHANGELOG.md +++ b/libs/client-sdk/CHANGELOG.md @@ -8,6 +8,22 @@ All notable changes to the LaunchDarkly Client-Side SDK for C/C++ will be docume * dependencies * launchdarkly-cpp-internal bumped from 0.1.9 to 0.1.10 +## [3.1.0](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-client-v3.0.10...launchdarkly-cpp-client-v3.1.0) (2023-10-13) + + +### Features + +* clean up LD CMake variables & allow for OpenSSL dynamic link ([#255](https://github.com/launchdarkly/cpp-sdks/issues/255)) ([ed23c9a](https://github.com/launchdarkly/cpp-sdks/commit/ed23c9a347665529a09d18111bb9d3b699381728)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * launchdarkly-cpp-internal bumped from 0.1.11 to 0.2.0 + * launchdarkly-cpp-common bumped from 0.3.7 to 0.4.0 + * launchdarkly-cpp-sse-client bumped from 0.1.3 to 0.2.0 + ## [3.0.10](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-client-v3.0.9...launchdarkly-cpp-client-v3.0.10) (2023-10-11) diff --git a/libs/client-sdk/CMakeLists.txt b/libs/client-sdk/CMakeLists.txt index 4677324fe..a38787f90 100644 --- a/libs/client-sdk/CMakeLists.txt +++ b/libs/client-sdk/CMakeLists.txt @@ -6,7 +6,7 @@ cmake_minimum_required(VERSION 3.19) project( LaunchDarklyCPPClient - VERSION 3.0.10 # {x-release-please-version} + VERSION 3.1.0 # {x-release-please-version} DESCRIPTION "LaunchDarkly C++ Client SDK" LANGUAGES CXX C ) diff --git a/libs/client-sdk/include/launchdarkly/client_side/client.hpp b/libs/client-sdk/include/launchdarkly/client_side/client.hpp index 79d7242a5..b628600a8 100644 --- a/libs/client-sdk/include/launchdarkly/client_side/client.hpp +++ b/libs/client-sdk/include/launchdarkly/client_side/client.hpp @@ -324,7 +324,7 @@ class Client : public IClient { private: inline static char const* const kVersion = - "3.0.10"; // {x-release-please-version} + "3.1.0"; // {x-release-please-version} std::unique_ptr client; }; diff --git a/libs/client-sdk/package.json b/libs/client-sdk/package.json index 8524c3f0e..387f0594b 100644 --- a/libs/client-sdk/package.json +++ b/libs/client-sdk/package.json @@ -1,11 +1,11 @@ { "name": "launchdarkly-cpp-client", "description": "This package.json exists for modeling dependencies for the release process.", - "version": "3.0.10", + "version": "3.1.0", "private": true, "dependencies": { - "launchdarkly-cpp-internal": "0.1.11", - "launchdarkly-cpp-common": "0.3.7", - "launchdarkly-cpp-sse-client": "0.1.3" + "launchdarkly-cpp-internal": "0.2.0", + "launchdarkly-cpp-common": "0.4.0", + "launchdarkly-cpp-sse-client": "0.2.0" } } diff --git a/libs/client-sdk/tests/client_c_bindings_test.cpp b/libs/client-sdk/tests/client_c_bindings_test.cpp index 41171437f..c32e40146 100644 --- a/libs/client-sdk/tests/client_c_bindings_test.cpp +++ b/libs/client-sdk/tests/client_c_bindings_test.cpp @@ -27,7 +27,7 @@ TEST(ClientBindings, MinimalInstantiation) { char const* version = LDClientSDK_Version(); ASSERT_TRUE(version); - ASSERT_STREQ(version, "3.0.10"); // {x-release-please-version} + ASSERT_STREQ(version, "3.1.0"); // {x-release-please-version} LDClientSDK_Free(sdk); } diff --git a/libs/client-sdk/tests/client_test.cpp b/libs/client-sdk/tests/client_test.cpp index 243a9c351..eadb43fc7 100644 --- a/libs/client-sdk/tests/client_test.cpp +++ b/libs/client-sdk/tests/client_test.cpp @@ -16,7 +16,7 @@ TEST(ClientTest, ClientConstructedWithMinimalConfigAndContext) { char const* version = client.Version(); ASSERT_TRUE(version); - ASSERT_STREQ(version, "3.0.10"); // {x-release-please-version} + ASSERT_STREQ(version, "3.1.0"); // {x-release-please-version} } TEST(ClientTest, AllFlagsIsEmpty) { diff --git a/libs/common/CHANGELOG.md b/libs/common/CHANGELOG.md index 45c1336c9..e964afa2d 100644 --- a/libs/common/CHANGELOG.md +++ b/libs/common/CHANGELOG.md @@ -12,6 +12,13 @@ * dependencies * launchdarkly-cpp-sse-client bumped from 0.1.1 to 0.1.2 +## [0.4.0](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-common-v0.3.7...launchdarkly-cpp-common-v0.4.0) (2023-10-13) + + +### Features + +* clean up LD CMake variables & allow for OpenSSL dynamic link ([#255](https://github.com/launchdarkly/cpp-sdks/issues/255)) ([ed23c9a](https://github.com/launchdarkly/cpp-sdks/commit/ed23c9a347665529a09d18111bb9d3b699381728)) + ## [0.3.7](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-common-v0.3.6...launchdarkly-cpp-common-v0.3.7) (2023-10-11) diff --git a/libs/common/package.json b/libs/common/package.json index 151a67d60..ca700b009 100644 --- a/libs/common/package.json +++ b/libs/common/package.json @@ -1,6 +1,6 @@ { "name": "launchdarkly-cpp-common", "description": "This package.json exists for modeling dependencies for the release process.", - "version": "0.3.7", + "version": "0.4.0", "private": true } diff --git a/libs/internal/CHANGELOG.md b/libs/internal/CHANGELOG.md index 61d6d7ed2..6ae7dd859 100644 --- a/libs/internal/CHANGELOG.md +++ b/libs/internal/CHANGELOG.md @@ -46,6 +46,20 @@ * dependencies * launchdarkly-cpp-common bumped from 0.3.5 to 0.3.6 +## [0.2.0](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-internal-v0.1.11...launchdarkly-cpp-internal-v0.2.0) (2023-10-13) + + +### Features + +* clean up LD CMake variables & allow for OpenSSL dynamic link ([#255](https://github.com/launchdarkly/cpp-sdks/issues/255)) ([ed23c9a](https://github.com/launchdarkly/cpp-sdks/commit/ed23c9a347665529a09d18111bb9d3b699381728)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * launchdarkly-cpp-common bumped from 0.3.7 to 0.4.0 + ## [0.1.11](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-internal-v0.1.10...launchdarkly-cpp-internal-v0.1.11) (2023-10-11) diff --git a/libs/internal/package.json b/libs/internal/package.json index 4ec18e48d..24ec5c2bd 100644 --- a/libs/internal/package.json +++ b/libs/internal/package.json @@ -1,9 +1,9 @@ { "name": "launchdarkly-cpp-internal", "description": "This package.json exists for modeling dependencies for the release process.", - "version": "0.1.11", + "version": "0.2.0", "private": true, "dependencies": { - "launchdarkly-cpp-common": "0.3.7" + "launchdarkly-cpp-common": "0.4.0" } } diff --git a/libs/server-sent-events/CHANGELOG.md b/libs/server-sent-events/CHANGELOG.md index f2947853c..8ac5d8d50 100644 --- a/libs/server-sent-events/CHANGELOG.md +++ b/libs/server-sent-events/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.2.0](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-sse-client-v0.1.3...launchdarkly-cpp-sse-client-v0.2.0) (2023-10-13) + + +### Features + +* clean up LD CMake variables & allow for OpenSSL dynamic link ([#255](https://github.com/launchdarkly/cpp-sdks/issues/255)) ([ed23c9a](https://github.com/launchdarkly/cpp-sdks/commit/ed23c9a347665529a09d18111bb9d3b699381728)) + ## [0.1.3](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-sse-client-v0.1.2...launchdarkly-cpp-sse-client-v0.1.3) (2023-09-13) diff --git a/libs/server-sent-events/package.json b/libs/server-sent-events/package.json index fc34f0855..4984ca021 100644 --- a/libs/server-sent-events/package.json +++ b/libs/server-sent-events/package.json @@ -2,6 +2,6 @@ "name": "launchdarkly-cpp-sse-client", "description": "This package.json exists for modeling dependencies for the release process.", "private": true, - "version": "0.1.3", + "version": "0.2.0", "dependencies": {} } From f68692794ea06f0103a22b8a7a0fcfe1237f7dfa Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 16 Oct 2023 11:41:53 -0700 Subject: [PATCH 11/21] fix: cmake_dependent_option needs list argument (#259) The dependent arguments in `cmake_dependent_option` need to be quoted (list). Otherwise, it seems the behavior is incorrect. In this case, a release failed because `BUILD_TESTING=Off` didn't actually turn off the `LD_BUILD_UNIT_TESTS` variable. I've tested locally and it now works as expected. --- CMakeLists.txt | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4b2cabda1..0d0ccb793 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,25 +20,25 @@ option(LD_BUILD_SHARED_LIBS "Build the SDKs as shared libraries" OFF) cmake_dependent_option(LD_BUILD_UNIT_TESTS "Build the C++ unit tests." - ON # default to enabling unit tests - BUILD_TESTING;NOT LD_BUILD_SHARED_LIBS # only exposed if top-level switch is on, and also only when building + ON # default to enabling unit tests + "BUILD_TESTING;NOT LD_BUILD_SHARED_LIBS" # only exposed if top-level switch is on, and also only when building # static libs. This is because we have hidden visibility of symbols by default (to only expose our C API.) - OFF # otherwise, off + OFF # otherwise, off ) # If you want to run the unit tests with valgrind, then LD_TESTING_SANITIZERS must of OFF. cmake_dependent_option(LD_TESTING_SANITIZERS "Enable sanitizers for unit tests." ON # default to enabling sanitizers - LD_BUILD_UNIT_TESTS # only expose if unit tests enabled.. + "LD_BUILD_UNIT_TESTS" # only expose if unit tests enabled.. OFF # otherwise, off ) cmake_dependent_option(LD_BUILD_CONTRACT_TESTS "Build contract test service." - OFF # default to disabling contract tests, since they require running a service - BUILD_TESTING # only expose if top-level switch is on.. - OFF # otherwise, off + OFF # default to disabling contract tests, since they require running a service + "BUILD_TESTING" # only expose if top-level switch is on.. + OFF # otherwise, off ) # The general strategy is to produce a fat artifact containing all of our dependencies so users @@ -65,7 +65,6 @@ set(CMAKE_CXX_STANDARD 17) set(CMAKE_POSITION_INDEPENDENT_CODE ON) - if (LD_BUILD_UNIT_TESTS) message(STATUS "LaunchDarkly: building unit tests") set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -D_GLIBCXX_DEBUG") From 8dd473f825d4d05f1bc4f94621f7e4a4fefab929 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 19 Oct 2023 08:59:29 -0700 Subject: [PATCH 12/21] fix: LD_BUILD_SHARED_LIBS build flag usage (#260) In my previous PR cleaning up cmake variables, during development I renamed the `BUILD_SHARED_LIBS` flag to `BUILD_STATIC_LIBS`, but then reconsidered and stuck with the original naming. Unfortunately I didn't un-rename some usages of it - so we ended up having some instances of `LD_BUILD_STATIC_LIBS` in our `CMakeLists.txt`. This removes usage of `LD_BUILD_STATIC_LIBS`. --- CMakeLists.txt | 4 ++-- cmake/certify.cmake | 8 +++----- libs/client-sdk/src/CMakeLists.txt | 4 ++-- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0d0ccb793..eca9f9711 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -109,7 +109,7 @@ message(STATUS "LaunchDarkly: using OpenSSL v${OPENSSL_VERSION}") # linked into the binary. set(Boost_USE_STATIC_LIBS ON) -if (NOT LD_BUILD_STATIC_LIBS) +if (LD_BUILD_SHARED_LIBS) # When building a shared library we hide all symbols # aside from this we have specifically exported for the C-API. set(CMAKE_CXX_VISIBILITY_PRESET hidden) @@ -129,7 +129,7 @@ add_subdirectory(libs/common) add_subdirectory(libs/internal) add_subdirectory(libs/server-sent-events) -# Built as static or shared depending on LD_BUILD_STATIC_LIBS variable. +# Built as static or shared depending on LD_BUILD_SHARED_LIBS variable. # This target "links" in common, internal, and sse as object libraries. add_subdirectory(libs/client-sdk) diff --git a/cmake/certify.cmake b/cmake/certify.cmake index 32235ec67..f28da6fba 100644 --- a/cmake/certify.cmake +++ b/cmake/certify.cmake @@ -10,16 +10,14 @@ endif () FetchContent_Declare(boost_certify GIT_REPOSITORY https://github.com/djarek/certify.git GIT_TAG 97f5eebfd99a5d6e99d07e4820240994e4e59787 - ) +) set(BUILD_TESTING OFF) FetchContent_GetProperties(boost_certify) -if(NOT boost_certify_POPULATED) +if (NOT boost_certify_POPULATED) FetchContent_Populate(boost_certify) add_subdirectory(${boost_certify_SOURCE_DIR} ${boost_certify_BINARY_DIR} EXCLUDE_FROM_ALL) -endif() +endif () set(BUILD_TESTING "${ORIGINAL_BUILD_TESTING}") - -set(BUILD_SHARED_LIBS "${ORIGINAL_BUILD_SHARED_LIBS}") diff --git a/libs/client-sdk/src/CMakeLists.txt b/libs/client-sdk/src/CMakeLists.txt index e2ac0c36e..da88dac4f 100644 --- a/libs/client-sdk/src/CMakeLists.txt +++ b/libs/client-sdk/src/CMakeLists.txt @@ -53,7 +53,7 @@ target_sources(${LIBNAME} PRIVATE ) -if (MSVC OR LD_BUILD_STATIC_LIBS) +if (MSVC OR (NOT LD_BUILD_SHARED_LIBS)) target_link_libraries(${LIBNAME} PUBLIC launchdarkly::common PRIVATE Boost::headers Boost::json Boost::url launchdarkly::sse launchdarkly::internal foxy) @@ -76,7 +76,7 @@ set_property(TARGET ${LIBNAME} PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL") install(TARGETS ${LIBNAME}) -if (BUILD_SHARED_LIBS AND MSVC) +if (LD_BUILD_SHARED_LIBS AND MSVC) install(FILES $ DESTINATION bin OPTIONAL) endif () # Using PUBLIC_HEADERS would flatten the include. From 5815bf14834e5be497c7bf335f12cdff1f68dba4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 19 Oct 2023 09:31:28 -0700 Subject: [PATCH 13/21] chore: release main (#261) :robot: I have created a release *beep* *boop* ---
launchdarkly-cpp-client: 3.1.1 ## [3.1.1](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-client-v3.1.0...launchdarkly-cpp-client-v3.1.1) (2023-10-19) ### Bug Fixes * LD_BUILD_SHARED_LIBS build flag usage ([#260](https://github.com/launchdarkly/cpp-sdks/issues/260)) ([8dd473f](https://github.com/launchdarkly/cpp-sdks/commit/8dd473f825d4d05f1bc4f94621f7e4a4fefab929))
--- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .release-please-manifest.json | 2 +- libs/client-sdk/CHANGELOG.md | 7 +++++++ libs/client-sdk/CMakeLists.txt | 2 +- .../client-sdk/include/launchdarkly/client_side/client.hpp | 2 +- libs/client-sdk/package.json | 2 +- libs/client-sdk/tests/client_c_bindings_test.cpp | 2 +- libs/client-sdk/tests/client_test.cpp | 2 +- 7 files changed, 13 insertions(+), 6 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 27c7342cc..d437e2da7 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,5 +1,5 @@ { - "libs/client-sdk": "3.1.0", + "libs/client-sdk": "3.1.1", "libs/server-sent-events": "0.2.0", "libs/common": "0.4.0", "libs/internal": "0.2.0" diff --git a/libs/client-sdk/CHANGELOG.md b/libs/client-sdk/CHANGELOG.md index 8d2fadd67..c0a8d5f43 100644 --- a/libs/client-sdk/CHANGELOG.md +++ b/libs/client-sdk/CHANGELOG.md @@ -8,6 +8,13 @@ All notable changes to the LaunchDarkly Client-Side SDK for C/C++ will be docume * dependencies * launchdarkly-cpp-internal bumped from 0.1.9 to 0.1.10 +## [3.1.1](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-client-v3.1.0...launchdarkly-cpp-client-v3.1.1) (2023-10-19) + + +### Bug Fixes + +* LD_BUILD_SHARED_LIBS build flag usage ([#260](https://github.com/launchdarkly/cpp-sdks/issues/260)) ([8dd473f](https://github.com/launchdarkly/cpp-sdks/commit/8dd473f825d4d05f1bc4f94621f7e4a4fefab929)) + ## [3.1.0](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-client-v3.0.10...launchdarkly-cpp-client-v3.1.0) (2023-10-13) diff --git a/libs/client-sdk/CMakeLists.txt b/libs/client-sdk/CMakeLists.txt index a38787f90..a812e1f69 100644 --- a/libs/client-sdk/CMakeLists.txt +++ b/libs/client-sdk/CMakeLists.txt @@ -6,7 +6,7 @@ cmake_minimum_required(VERSION 3.19) project( LaunchDarklyCPPClient - VERSION 3.1.0 # {x-release-please-version} + VERSION 3.1.1 # {x-release-please-version} DESCRIPTION "LaunchDarkly C++ Client SDK" LANGUAGES CXX C ) diff --git a/libs/client-sdk/include/launchdarkly/client_side/client.hpp b/libs/client-sdk/include/launchdarkly/client_side/client.hpp index b628600a8..3250f2e6a 100644 --- a/libs/client-sdk/include/launchdarkly/client_side/client.hpp +++ b/libs/client-sdk/include/launchdarkly/client_side/client.hpp @@ -324,7 +324,7 @@ class Client : public IClient { private: inline static char const* const kVersion = - "3.1.0"; // {x-release-please-version} + "3.1.1"; // {x-release-please-version} std::unique_ptr client; }; diff --git a/libs/client-sdk/package.json b/libs/client-sdk/package.json index 387f0594b..5656f0123 100644 --- a/libs/client-sdk/package.json +++ b/libs/client-sdk/package.json @@ -1,7 +1,7 @@ { "name": "launchdarkly-cpp-client", "description": "This package.json exists for modeling dependencies for the release process.", - "version": "3.1.0", + "version": "3.1.1", "private": true, "dependencies": { "launchdarkly-cpp-internal": "0.2.0", diff --git a/libs/client-sdk/tests/client_c_bindings_test.cpp b/libs/client-sdk/tests/client_c_bindings_test.cpp index c32e40146..8991044ff 100644 --- a/libs/client-sdk/tests/client_c_bindings_test.cpp +++ b/libs/client-sdk/tests/client_c_bindings_test.cpp @@ -27,7 +27,7 @@ TEST(ClientBindings, MinimalInstantiation) { char const* version = LDClientSDK_Version(); ASSERT_TRUE(version); - ASSERT_STREQ(version, "3.1.0"); // {x-release-please-version} + ASSERT_STREQ(version, "3.1.1"); // {x-release-please-version} LDClientSDK_Free(sdk); } diff --git a/libs/client-sdk/tests/client_test.cpp b/libs/client-sdk/tests/client_test.cpp index eadb43fc7..b12b7dc64 100644 --- a/libs/client-sdk/tests/client_test.cpp +++ b/libs/client-sdk/tests/client_test.cpp @@ -16,7 +16,7 @@ TEST(ClientTest, ClientConstructedWithMinimalConfigAndContext) { char const* version = client.Version(); ASSERT_TRUE(version); - ASSERT_STREQ(version, "3.1.0"); // {x-release-please-version} + ASSERT_STREQ(version, "3.1.1"); // {x-release-please-version} } TEST(ClientTest, AllFlagsIsEmpty) { From df91a6b0c5160e5e943629b8591ec8d3bfe37bdb Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 20 Oct 2023 10:46:12 -0700 Subject: [PATCH 14/21] build: disallow contract tests when building shared libs (#264) There's no point trying to build contract tests when building the SDK as a shared lib, since we don't expose a C++ ABI (get a bunch of undefined symbol errors.) Can also remove the warning message that I had previously - it's not actually necessary if you use `cmake_dependent_option` correctly. --- CMakeLists.txt | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index eca9f9711..e2fa31650 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,9 +36,9 @@ cmake_dependent_option(LD_TESTING_SANITIZERS cmake_dependent_option(LD_BUILD_CONTRACT_TESTS "Build contract test service." - OFF # default to disabling contract tests, since they require running a service - "BUILD_TESTING" # only expose if top-level switch is on.. - OFF # otherwise, off + OFF # default to disabling contract tests, since they require running a service + "BUILD_TESTING;NOT LD_BUILD_SHARED_LIBS" # only expose if top-level switch is on and using static libs, since C++ symbols needed would be hidden. + OFF # otherwise, off ) # The general strategy is to produce a fat artifact containing all of our dependencies so users @@ -53,11 +53,6 @@ option(LD_DYNAMIC_LINK_OPENSSL option(LD_BUILD_EXAMPLES "Build hello-world examples." ON) - -if (LD_BUILD_SHARED_LIBS AND LD_BUILD_UNIT_TESTS) - message(WARNING "LaunchDarkly: unit testing isn't supported while building shared libraries. Switch to static libraries or disable unit tests.") -endif () - # All projects in this repo should share the same version of 3rd party depends. # It's the only way to remain sane. set(CMAKE_FILES "${CMAKE_CURRENT_SOURCE_DIR}/cmake") From da7cf7c4dd3150fc28974bb27c90c51becf80400 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 20 Oct 2023 12:36:08 -0700 Subject: [PATCH 15/21] chore: remove all client-side test suppressions (#262) Since converting user-style JSON is now gated behind a capability, we no longer have any test suppressions. --- .github/workflows/client.yml | 1 - .../sdk-contract-tests/test-suppressions.txt | 33 ------------------- 2 files changed, 34 deletions(-) delete mode 100644 contract-tests/sdk-contract-tests/test-suppressions.txt diff --git a/.github/workflows/client.yml b/.github/workflows/client.yml index 4ff3603c4..e60b9984b 100644 --- a/.github/workflows/client.yml +++ b/.github/workflows/client.yml @@ -29,7 +29,6 @@ jobs: with: # Inform the test harness of test service's port. test_service_port: ${{ env.TEST_SERVICE_PORT }} - extra_params: '-skip-from ./contract-tests/sdk-contract-tests/test-suppressions.txt' build-test: runs-on: ubuntu-22.04 steps: diff --git a/contract-tests/sdk-contract-tests/test-suppressions.txt b/contract-tests/sdk-contract-tests/test-suppressions.txt deleted file mode 100644 index cba85bdd6..000000000 --- a/contract-tests/sdk-contract-tests/test-suppressions.txt +++ /dev/null @@ -1,33 +0,0 @@ -# The Client doesn't need to know how to deserialize users. -context type/convert/old user to context/{"key": ""} -context type/convert/old user to context/{"key": "a"} -context type/convert/old user to context/{"key": "a"} -context type/convert/old user to context/{"key": "a", "custom": {"b": true}} -context type/convert/old user to context/{"key": "a", "custom": {"b": 1}} -context type/convert/old user to context/{"key": "a", "custom": {"b": "c"}} -context type/convert/old user to context/{"key": "a", "custom": {"b": [1, 2]}} -context type/convert/old user to context/{"key": "a", "custom": {"b": {"c": 1}}} -context type/convert/old user to context/{"key": "a", "custom": {"b": 1, "c": 2}} -context type/convert/old user to context/{"key": "a", "custom": {"b": 1, "c": null}} -context type/convert/old user to context/{"key": "a", "custom": {}} -context type/convert/old user to context/{"key": "a", "custom": null} -context type/convert/old user to context/{"key": "a", "anonymous": true} -context type/convert/old user to context/{"key": "a", "anonymous": false} -context type/convert/old user to context/{"key": "a", "anonymous": null} -context type/convert/old user to context/{"key": "a", "privateAttributeNames": ["b"]} -context type/convert/old user to context/{"key": "a", "privateAttributeNames": []} -context type/convert/old user to context/{"key": "a", "privateAttributeNames": null} -context type/convert/old user to context/{"key": "a", "name": "b"} -context type/convert/old user to context/{"key": "a", "name": null} -context type/convert/old user to context/{"key": "a", "firstName": "b"} -context type/convert/old user to context/{"key": "a", "firstName": null} -context type/convert/old user to context/{"key": "a", "lastName": "b"} -context type/convert/old user to context/{"key": "a", "lastName": null} -context type/convert/old user to context/{"key": "a", "email": "b"} -context type/convert/old user to context/{"key": "a", "email": null} -context type/convert/old user to context/{"key": "a", "country": "b"} -context type/convert/old user to context/{"key": "a", "country": null} -context type/convert/old user to context/{"key": "a", "avatar": "b"} -context type/convert/old user to context/{"key": "a", "avatar": null} -context type/convert/old user to context/{"key": "a", "ip": "b"} -context type/convert/old user to context/{"key": "a", "ip": null} From ed8b6e01d8b500ff852fe15d4367972648c05659 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 20 Oct 2023 15:39:26 -0700 Subject: [PATCH 16/21] chore: add data source status backwards-compat unit test (#265) Adds a unit test that should catch if we make a breaking change to the types accessible via `client.DataSourceStatus()`. This is useful because the upcoming server-side SDK refactors the Data Source Status types into the common library, and we must therefore provide aliases in the client SDK. --- libs/client-sdk/tests/client_test.cpp | 36 +++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/libs/client-sdk/tests/client_test.cpp b/libs/client-sdk/tests/client_test.cpp index b12b7dc64..f84aca6b7 100644 --- a/libs/client-sdk/tests/client_test.cpp +++ b/libs/client-sdk/tests/client_test.cpp @@ -30,7 +30,7 @@ TEST(ClientTest, BoolVariationDefaultPassesThrough) { Client client(ConfigBuilder("sdk-123").Build().value(), ContextBuilder().Kind("cat", "shadow").Build()); - const std::string flag = "extra-cat-food"; + std::string const flag = "extra-cat-food"; std::vector values = {true, false}; for (auto const& v : values) { ASSERT_EQ(client.BoolVariation(flag, v), v); @@ -41,7 +41,7 @@ TEST(ClientTest, BoolVariationDefaultPassesThrough) { TEST(ClientTest, StringVariationDefaultPassesThrough) { Client client(ConfigBuilder("sdk-123").Build().value(), ContextBuilder().Kind("cat", "shadow").Build()); - const std::string flag = "treat"; + std::string const flag = "treat"; std::vector values = {"chicken", "fish", "cat-grass"}; for (auto const& v : values) { ASSERT_EQ(client.StringVariation(flag, v), v); @@ -52,7 +52,7 @@ TEST(ClientTest, StringVariationDefaultPassesThrough) { TEST(ClientTest, IntVariationDefaultPassesThrough) { Client client(ConfigBuilder("sdk-123").Build().value(), ContextBuilder().Kind("cat", "shadow").Build()); - const std::string flag = "weight"; + std::string const flag = "weight"; std::vector values = {0, 12, 13, 24, 1000}; for (auto const& v : values) { ASSERT_EQ(client.IntVariation("weight", v), v); @@ -63,7 +63,7 @@ TEST(ClientTest, IntVariationDefaultPassesThrough) { TEST(ClientTest, DoubleVariationDefaultPassesThrough) { Client client(ConfigBuilder("sdk-123").Build().value(), ContextBuilder().Kind("cat", "shadow").Build()); - const std::string flag = "weight"; + std::string const flag = "weight"; std::vector values = {0.0, 12.0, 13.0, 24.0, 1000.0}; for (auto const& v : values) { ASSERT_EQ(client.DoubleVariation(flag, v), v); @@ -75,7 +75,7 @@ TEST(ClientTest, JsonVariationDefaultPassesThrough) { Client client(ConfigBuilder("sdk-123").Build().value(), ContextBuilder().Kind("cat", "shadow").Build()); - const std::string flag = "assorted-values"; + std::string const flag = "assorted-values"; std::vector values = { Value({"running", "jumping"}), Value(3), Value(1.0), Value(true), Value(std::map{{"weight", 20}})}; @@ -84,3 +84,29 @@ TEST(ClientTest, JsonVariationDefaultPassesThrough) { ASSERT_EQ(*client.JsonVariationDetail(flag, v), v); } } + +// This test mainly serves to catch any changes made to the types in the Data +// Source Status API that are not backwards-compatible. +TEST(ClientTest, DataSourceStatus) { + Client client(ConfigBuilder("sdk-123").Build().value(), + ContextBuilder().Kind("cat", "shadow").Build()); + + client_side::data_sources::DataSourceStatus ds_status = + client.DataSourceStatus().Status(); + + std::optional + last_err = ds_status.LastError(); + + ASSERT_FALSE(last_err); + + client_side::data_sources::DataSourceStatus::DataSourceState state = + ds_status.State(); + + ASSERT_EQ(state, client_side::data_sources::DataSourceStatus:: + DataSourceState::kInitializing); + + client_side::data_sources::DataSourceStatus::DateTime date = + ds_status.StateSince(); + + ASSERT_NE(date, client_side::data_sources::DataSourceStatus::DateTime{}); +} From 75eece3a46870fdb6bf4384c112700558099c4d1 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 23 Oct 2023 10:49:53 -0700 Subject: [PATCH 17/21] feat: server-side SDK (#160) This commit contains the C++ server-side SDK, omitting any persistent store capability. It modifies the internals of the client-side SDK in backwards compatible ways, and should trigger a client-side release. Followup commits should remove the server-side branch from CI. --------- Co-authored-by: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Co-authored-by: Molly --- .clang-tidy | 2 +- .github/workflows/client.yml | 10 +- .github/workflows/common.yml | 4 +- .github/workflows/cpp-linter.yml | 2 +- .github/workflows/internal.yml | 4 +- .github/workflows/server.yml | 79 ++ .github/workflows/sse.yml | 2 +- CMakeLists.txt | 1 + README.md | 37 +- architecture/event_processor.md | 145 ++++ architecture/server_data_source_arch.md | 118 +++ architecture/server_store_arch.md | 98 +++ cmake/rfc3339_timestamp.cmake | 31 + contract-tests/CMakeLists.txt | 4 +- .../CMakeLists.txt | 12 +- .../README.md | 0 .../include/client_entity.hpp | 2 +- .../include/entity_manager.hpp | 2 +- .../include/server.hpp | 0 .../include/session.hpp | 0 .../src/client_entity.cpp | 43 +- .../src/entity_manager.cpp | 0 .../src/main.cpp | 2 +- .../src/server.cpp | 0 .../src/session.cpp | 6 +- contract-tests/data-model/CMakeLists.txt | 20 + .../include/data_model/data_model.hpp} | 6 + contract-tests/data-model/src/data_model.cpp | 6 + .../sdk-contract-tests/src/definitions.cpp | 6 - .../server-contract-tests/CMakeLists.txt | 30 + .../server-contract-tests/README.md | 35 + .../include/client_entity.hpp | 37 + .../include/entity_manager.hpp | 54 ++ .../server-contract-tests/include/server.hpp | 50 ++ .../server-contract-tests/include/session.hpp | 95 +++ .../src/client_entity.cpp | 393 +++++++++ .../src/entity_manager.cpp | 153 ++++ .../server-contract-tests/src/main.cpp | 66 ++ .../server-contract-tests/src/server.cpp | 34 + .../server-contract-tests/src/session.cpp | 146 ++++ .../test-suppressions.txt | 7 + examples/CMakeLists.txt | 3 + .../CMakeLists.txt | 15 + examples/client-and-server-coexistence/main.c | 50 ++ examples/hello-c-client/CMakeLists.txt | 6 +- examples/hello-c-client/main.c | 4 +- examples/hello-c-server/CMakeLists.txt | 15 + examples/hello-c-server/main.c | 78 ++ examples/hello-cpp-client/CMakeLists.txt | 6 +- examples/hello-cpp-client/main.cpp | 4 +- examples/hello-cpp-server/CMakeLists.txt | 15 + examples/hello-cpp-server/main.cpp | 59 ++ libs/client-sdk/README.md | 24 +- .../client_side/bindings/c/config/config.h | 4 +- .../launchdarkly/client_side/bindings/c/sdk.h | 75 +- .../client_side/data_source_status.hpp | 225 ++---- libs/client-sdk/src/CMakeLists.txt | 12 - libs/client-sdk/src/bindings/c/sdk.cpp | 40 - libs/client-sdk/src/client_impl.cpp | 59 +- libs/client-sdk/src/client_impl.hpp | 16 +- .../data_source_event_handler.cpp | 24 +- .../data_source_event_handler.hpp | 2 +- .../src/data_sources/data_source_status.cpp | 81 -- .../data_source_status_manager.cpp | 128 --- .../data_source_status_manager.hpp | 89 +-- .../data_sources/data_source_update_sink.cpp | 23 - .../data_sources/data_source_update_sink.hpp | 32 +- .../src/data_sources/null_data_source.hpp | 5 +- .../src/data_sources/polling_data_source.hpp | 4 +- .../data_sources/streaming_data_source.cpp | 2 - .../data_sources/streaming_data_source.hpp | 4 +- .../src/event_processor/event_processor.cpp | 25 - .../src/event_processor/event_processor.hpp | 29 - .../src/flag_manager/flag_persistence.cpp | 22 +- .../src/flag_manager/flag_store.cpp | 1 - .../src/flag_manager/flag_updater.cpp | 31 +- .../src/serialization/json_all_flags.cpp | 54 -- .../src/serialization/json_all_flags.hpp | 27 - .../tests/client_c_bindings_test.cpp | 2 +- .../tests/data_source_status_test.cpp | 36 + .../tests/flag_persistence_test.cpp | 2 +- libs/client-sdk/tests/flag_store_test.cpp | 12 +- libs/client-sdk/tests/flag_updater_test.cpp | 16 +- .../launchdarkly/attribute_reference.hpp | 7 +- .../bindings/c/data_source/error_info.h | 59 ++ .../bindings/c/data_source/error_kind.h | 52 ++ ...ared_function_argument_macro_definitions.h | 8 + .../include/launchdarkly/config/client.hpp | 2 - .../include/launchdarkly/config/server.hpp | 1 + .../config/shared/built/events.hpp | 15 +- .../launchdarkly/config/shared/defaults.hpp | 6 +- libs/common/include/launchdarkly/context.hpp | 5 +- .../launchdarkly/data/evaluation_detail.hpp | 22 +- .../launchdarkly/data/evaluation_reason.hpp | 36 + .../launchdarkly/data/evaluation_result.hpp | 5 +- .../data_sources/data_source_status_base.hpp | 80 ++ .../data_source_status_error_info.hpp | 62 ++ .../data_source_status_error_kind.hpp | 44 + .../launchdarkly/detail/c_binding_helpers.hpp | 7 +- .../launchdarkly/logging/log_level.hpp | 4 + libs/common/include/launchdarkly/value.hpp | 20 + libs/common/src/CMakeLists.txt | 4 + libs/common/src/attribute_reference.cpp | 6 + .../src/bindings/c/data_source/error_info.cpp | 46 ++ libs/common/src/config/events.cpp | 13 +- libs/common/src/context.cpp | 4 +- libs/common/src/data/evaluation_detail.cpp | 14 +- libs/common/src/data/evaluation_reason.cpp | 33 + libs/common/src/data/evaluation_result.cpp | 12 +- .../data_source_status_error_info.cpp | 19 + .../data_source_status_error_kind.cpp | 30 + libs/common/src/log_level.cpp | 5 + libs/common/src/value.cpp | 19 + libs/common/tests/data_source_status_test.cpp | 71 ++ libs/common/tests/value_test.cpp | 10 +- .../include/launchdarkly/context_filter.hpp | 10 +- .../data_model/context_aware_reference.hpp | 65 ++ .../launchdarkly/data_model/context_kind.hpp | 15 + .../include/launchdarkly/data_model/flag.hpp | 112 +++ .../data_model/item_descriptor.hpp | 66 ++ .../launchdarkly/data_model/rule_clause.hpp | 43 + .../launchdarkly/data_model/sdk_data_set.hpp | 27 + .../launchdarkly/data_model/segment.hpp | 56 ++ .../data_sources/data_source.hpp | 4 +- .../data_source_status_manager_base.hpp | 189 +++++ .../include/launchdarkly/encoding/base_16.hpp | 21 + .../include/launchdarkly/encoding/sha_1.hpp | 10 + .../include/launchdarkly/encoding/sha_256.hpp | 2 +- .../events/asio_event_processor.hpp | 46 +- .../launchdarkly/events/client_events.hpp | 50 -- .../launchdarkly/events/common_events.hpp | 39 - .../events/data/common_events.hpp | 92 +++ .../launchdarkly/events/data/events.hpp | 19 + .../events/data/server_events.hpp | 12 + .../events/{ => detail}/event_batch.hpp | 8 +- .../launchdarkly/events/detail/lru_cache.hpp | 43 + .../events/{ => detail}/outbox.hpp | 13 +- .../events/{ => detail}/parse_date_header.hpp | 4 +- .../events/{ => detail}/request_worker.hpp | 6 +- .../events/{ => detail}/summarizer.hpp | 14 +- .../events/{ => detail}/worker_pool.hpp | 15 +- .../events/event_processor_interface.hpp} | 6 +- .../include/launchdarkly/events/events.hpp | 15 - .../events}/null_event_processor.hpp | 6 +- .../serialization/events/json_events.hpp | 21 +- .../serialization/json_attributes.hpp | 4 +- .../serialization/json_context.hpp | 7 +- .../json_context_aware_reference.hpp | 46 ++ .../serialization/json_context_kind.hpp | 28 + .../serialization/json_evaluation_reason.hpp | 13 +- .../serialization/json_evaluation_result.hpp | 21 +- .../launchdarkly/serialization/json_flag.hpp | 107 +++ .../serialization/json_item_descriptor.hpp | 57 ++ .../serialization/json_primitives.hpp | 121 +++ .../serialization/json_rule_clause.hpp | 39 + .../serialization/json_sdk_data_set.hpp | 15 + .../serialization/json_segment.hpp | 43 + .../launchdarkly/serialization/json_value.hpp | 13 +- .../serialization/value_mapping.hpp | 114 ++- .../signals}/boost_signal_connection.hpp | 4 +- libs/internal/src/CMakeLists.txt | 20 +- libs/internal/src/data_model/flag.cpp | 51 ++ libs/internal/src/data_model/rule_clause.cpp | 62 ++ libs/internal/src/encoding/sha_1.cpp | 18 + libs/internal/src/encoding/sha_256.cpp | 15 +- .../src/events/asio_event_processor.cpp | 134 ++-- .../{client_events.cpp => common_events.cpp} | 10 +- libs/internal/src/events/event_batch.cpp | 6 +- libs/internal/src/events/lru_cache.cpp | 32 + .../src/events}/null_event_processor.cpp | 6 +- libs/internal/src/events/outbox.cpp | 8 +- libs/internal/src/events/request_worker.cpp | 8 +- libs/internal/src/events/summarizer.cpp | 12 +- libs/internal/src/events/worker_pool.cpp | 8 +- .../src/serialization/events/json_events.cpp | 31 +- .../src/serialization/json_attributes.cpp | 1 + .../src/serialization/json_context.cpp | 13 +- .../src/serialization/json_context_kind.cpp | 33 + .../serialization/json_evaluation_reason.cpp | 10 + .../serialization/json_evaluation_result.cpp | 158 ++-- libs/internal/src/serialization/json_flag.cpp | 369 +++++++++ .../src/serialization/json_primitives.cpp | 69 ++ .../src/serialization/json_rule_clause.cpp | 178 +++++ .../src/serialization/json_sdk_data_set.cpp | 28 + .../src/serialization/json_segment.cpp | 147 ++++ .../internal/src/serialization/json_value.cpp | 31 +- .../src/serialization/value_mapping.cpp | 24 + .../src/signals}/boost_signal_connection.cpp | 6 +- .../tests/data_model_serialization_test.cpp | 753 ++++++++++++++++++ .../internal/tests/evaluation_result_test.cpp | 138 ++-- libs/internal/tests/event_processor_test.cpp | 27 +- .../tests/event_serialization_test.cpp | 31 +- libs/internal/tests/event_summarizer_test.cpp | 15 +- libs/internal/tests/ld_logger_test.cpp | 6 +- libs/internal/tests/lru_cache_test.cpp | 65 ++ libs/internal/tests/request_worker_test.cpp | 3 +- libs/internal/tests/sha_1_test.cpp | 20 + libs/internal/tests/sha_256_test.cpp | 19 +- libs/server-sdk/CMakeLists.txt | 38 + libs/server-sdk/Doxyfile | 94 +++ libs/server-sdk/README.md | 137 ++++ libs/server-sdk/docs/doc.md | 9 + .../server_side/all_flags_state.hpp | 172 ++++ .../c/all_flags_state/all_flags_state.h | 116 +++ .../server_side/bindings/c/config/builder.h | 346 ++++++++ .../server_side/bindings/c/config/config.h | 26 + .../launchdarkly/server_side/bindings/c/sdk.h | 581 ++++++++++++++ .../server_side/change_notifier.hpp | 42 + .../launchdarkly/server_side/client.hpp | 355 +++++++++ .../server_side/data_source_status.hpp | 117 +++ .../integrations/persistent_store_core.hpp | 214 +++++ .../serialization/json_all_flags_state.hpp | 16 + libs/server-sdk/src/CMakeLists.txt | 98 +++ .../src/all_flags_state/all_flags_state.cpp | 86 ++ .../all_flags_state_builder.cpp | 71 ++ .../all_flags_state_builder.hpp | 43 + .../all_flags_state/json_all_flags_state.cpp | 49 ++ .../c/all_flags_state/all_flags_state.cpp | 56 ++ libs/server-sdk/src/bindings/c/builder.cpp | 289 +++++++ libs/server-sdk/src/bindings/c/config.cpp | 16 + libs/server-sdk/src/bindings/c/sdk.cpp | 414 ++++++++++ libs/server-sdk/src/boost.cpp | 6 + libs/server-sdk/src/client.cpp | 135 ++++ libs/server-sdk/src/client_impl.cpp | 451 +++++++++++ libs/server-sdk/src/client_impl.hpp | 195 +++++ .../data_source_event_handler.cpp | 241 ++++++ .../data_source_event_handler.hpp | 124 +++ .../src/data_sources/data_source_status.cpp | 40 + .../data_source_status_manager.hpp | 28 + .../data_sources/data_source_update_sink.hpp | 31 + .../src/data_sources/null_data_source.cpp | 19 + .../src/data_sources/null_data_source.hpp | 23 + .../src/data_sources/polling_data_source.cpp | 245 ++++++ .../src/data_sources/polling_data_source.hpp | 58 ++ .../data_sources/streaming_data_source.cpp | 152 ++++ .../data_sources/streaming_data_source.hpp | 55 ++ libs/server-sdk/src/data_store/data_kind.hpp | 7 + libs/server-sdk/src/data_store/data_store.hpp | 81 ++ .../src/data_store/data_store_updater.cpp | 76 ++ .../src/data_store/data_store_updater.hpp | 121 +++ .../src/data_store/dependency_tracker.cpp | 197 +++++ .../src/data_store/dependency_tracker.hpp | 156 ++++ .../server-sdk/src/data_store/descriptors.hpp | 12 + .../src/data_store/memory_store.cpp | 75 ++ .../src/data_store/memory_store.hpp | 50 ++ .../persistent/expiration_tracker.cpp | 152 ++++ .../persistent/expiration_tracker.hpp | 145 ++++ .../persistent/persistent_data_store.cpp | 3 + .../persistent/persistent_data_store.hpp | 51 ++ .../server-sdk/src/data_store/tagged_data.hpp | 37 + libs/server-sdk/src/evaluation/bucketing.cpp | 209 +++++ libs/server-sdk/src/evaluation/bucketing.hpp | 123 +++ .../evaluation/detail/evaluation_stack.cpp | 30 + .../evaluation/detail/evaluation_stack.hpp | 60 ++ .../evaluation/detail/semver_operations.cpp | 203 +++++ .../evaluation/detail/semver_operations.hpp | 88 ++ .../detail/timestamp_operations.cpp | 55 ++ .../detail/timestamp_operations.hpp | 16 + .../src/evaluation/evaluation_error.cpp | 62 ++ .../src/evaluation/evaluation_error.hpp | 29 + libs/server-sdk/src/evaluation/evaluator.cpp | 225 ++++++ libs/server-sdk/src/evaluation/evaluator.hpp | 70 ++ libs/server-sdk/src/evaluation/operators.cpp | 146 ++++ libs/server-sdk/src/evaluation/operators.hpp | 10 + libs/server-sdk/src/evaluation/rules.cpp | 214 +++++ libs/server-sdk/src/evaluation/rules.hpp | 60 ++ libs/server-sdk/src/events/event_factory.cpp | 94 +++ libs/server-sdk/src/events/event_factory.hpp | 57 ++ libs/server-sdk/src/events/event_scope.hpp | 48 ++ libs/server-sdk/tests/CMakeLists.txt | 20 + .../server-sdk/tests/all_flags_state_test.cpp | 150 ++++ libs/server-sdk/tests/bucketing_tests.cpp | 367 +++++++++ libs/server-sdk/tests/client_test.cpp | 79 ++ .../tests/data_source_event_handler_test.cpp | 202 +++++ .../tests/data_store_updater_test.cpp | 430 ++++++++++ .../tests/dependency_tracker_test.cpp | 298 +++++++ .../tests/evaluation_stack_test.cpp | 53 ++ libs/server-sdk/tests/evaluator_tests.cpp | 248 ++++++ libs/server-sdk/tests/event_factory_tests.cpp | 42 + libs/server-sdk/tests/event_scope_test.cpp | 73 ++ .../tests/expiration_tracker_test.cpp | 122 +++ libs/server-sdk/tests/memory_store_test.cpp | 286 +++++++ libs/server-sdk/tests/operator_tests.cpp | 260 ++++++ libs/server-sdk/tests/rule_tests.cpp | 251 ++++++ libs/server-sdk/tests/semver_tests.cpp | 66 ++ .../tests/server_c_bindings_test.cpp | 161 ++++ libs/server-sdk/tests/spy_event_processor.hpp | 65 ++ libs/server-sdk/tests/spy_logger.hpp | 94 +++ libs/server-sdk/tests/test_store.cpp | 307 +++++++ libs/server-sdk/tests/test_store.hpp | 31 + libs/server-sdk/tests/timestamp_tests.cpp | 88 ++ 291 files changed, 18390 insertions(+), 1503 deletions(-) create mode 100644 .github/workflows/server.yml create mode 100644 architecture/event_processor.md create mode 100644 architecture/server_data_source_arch.md create mode 100644 architecture/server_store_arch.md create mode 100644 cmake/rfc3339_timestamp.cmake rename contract-tests/{sdk-contract-tests => client-contract-tests}/CMakeLists.txt (62%) rename contract-tests/{sdk-contract-tests => client-contract-tests}/README.md (100%) rename contract-tests/{sdk-contract-tests => client-contract-tests}/include/client_entity.hpp (96%) rename contract-tests/{sdk-contract-tests => client-contract-tests}/include/entity_manager.hpp (97%) rename contract-tests/{sdk-contract-tests => client-contract-tests}/include/server.hpp (100%) rename contract-tests/{sdk-contract-tests => client-contract-tests}/include/session.hpp (100%) rename contract-tests/{sdk-contract-tests => client-contract-tests}/src/client_entity.cpp (87%) rename contract-tests/{sdk-contract-tests => client-contract-tests}/src/entity_manager.cpp (100%) rename contract-tests/{sdk-contract-tests => client-contract-tests}/src/main.cpp (96%) rename contract-tests/{sdk-contract-tests => client-contract-tests}/src/server.cpp (100%) rename contract-tests/{sdk-contract-tests => client-contract-tests}/src/session.cpp (97%) create mode 100644 contract-tests/data-model/CMakeLists.txt rename contract-tests/{sdk-contract-tests/include/definitions.hpp => data-model/include/data_model/data_model.hpp} (97%) create mode 100644 contract-tests/data-model/src/data_model.cpp delete mode 100644 contract-tests/sdk-contract-tests/src/definitions.cpp create mode 100644 contract-tests/server-contract-tests/CMakeLists.txt create mode 100644 contract-tests/server-contract-tests/README.md create mode 100644 contract-tests/server-contract-tests/include/client_entity.hpp create mode 100644 contract-tests/server-contract-tests/include/entity_manager.hpp create mode 100644 contract-tests/server-contract-tests/include/server.hpp create mode 100644 contract-tests/server-contract-tests/include/session.hpp create mode 100644 contract-tests/server-contract-tests/src/client_entity.cpp create mode 100644 contract-tests/server-contract-tests/src/entity_manager.cpp create mode 100644 contract-tests/server-contract-tests/src/main.cpp create mode 100644 contract-tests/server-contract-tests/src/server.cpp create mode 100644 contract-tests/server-contract-tests/src/session.cpp create mode 100644 contract-tests/server-contract-tests/test-suppressions.txt create mode 100644 examples/client-and-server-coexistence/CMakeLists.txt create mode 100644 examples/client-and-server-coexistence/main.c create mode 100644 examples/hello-c-server/CMakeLists.txt create mode 100644 examples/hello-c-server/main.c create mode 100644 examples/hello-cpp-server/CMakeLists.txt create mode 100644 examples/hello-cpp-server/main.cpp delete mode 100644 libs/client-sdk/src/data_sources/data_source_status_manager.cpp delete mode 100644 libs/client-sdk/src/data_sources/data_source_update_sink.cpp delete mode 100644 libs/client-sdk/src/event_processor/event_processor.cpp delete mode 100644 libs/client-sdk/src/event_processor/event_processor.hpp delete mode 100644 libs/client-sdk/src/serialization/json_all_flags.cpp delete mode 100644 libs/client-sdk/src/serialization/json_all_flags.hpp create mode 100644 libs/client-sdk/tests/data_source_status_test.cpp create mode 100644 libs/common/include/launchdarkly/bindings/c/data_source/error_info.h create mode 100644 libs/common/include/launchdarkly/bindings/c/data_source/error_kind.h create mode 100644 libs/common/include/launchdarkly/bindings/c/shared_function_argument_macro_definitions.h create mode 100644 libs/common/include/launchdarkly/data_sources/data_source_status_base.hpp create mode 100644 libs/common/include/launchdarkly/data_sources/data_source_status_error_info.hpp create mode 100644 libs/common/include/launchdarkly/data_sources/data_source_status_error_kind.hpp create mode 100644 libs/common/src/bindings/c/data_source/error_info.cpp create mode 100644 libs/common/src/data_sources/data_source_status_error_info.cpp create mode 100644 libs/common/src/data_sources/data_source_status_error_kind.cpp create mode 100644 libs/common/tests/data_source_status_test.cpp create mode 100644 libs/internal/include/launchdarkly/data_model/context_aware_reference.hpp create mode 100644 libs/internal/include/launchdarkly/data_model/context_kind.hpp create mode 100644 libs/internal/include/launchdarkly/data_model/flag.hpp create mode 100644 libs/internal/include/launchdarkly/data_model/item_descriptor.hpp create mode 100644 libs/internal/include/launchdarkly/data_model/rule_clause.hpp create mode 100644 libs/internal/include/launchdarkly/data_model/sdk_data_set.hpp create mode 100644 libs/internal/include/launchdarkly/data_model/segment.hpp rename libs/{client-sdk/src => internal/include/launchdarkly}/data_sources/data_source.hpp (84%) create mode 100644 libs/internal/include/launchdarkly/data_sources/data_source_status_manager_base.hpp create mode 100644 libs/internal/include/launchdarkly/encoding/base_16.hpp create mode 100644 libs/internal/include/launchdarkly/encoding/sha_1.hpp delete mode 100644 libs/internal/include/launchdarkly/events/client_events.hpp delete mode 100644 libs/internal/include/launchdarkly/events/common_events.hpp create mode 100644 libs/internal/include/launchdarkly/events/data/common_events.hpp create mode 100644 libs/internal/include/launchdarkly/events/data/events.hpp create mode 100644 libs/internal/include/launchdarkly/events/data/server_events.hpp rename libs/internal/include/launchdarkly/events/{ => detail}/event_batch.hpp (92%) create mode 100644 libs/internal/include/launchdarkly/events/detail/lru_cache.hpp rename libs/internal/include/launchdarkly/events/{ => detail}/outbox.hpp (79%) rename libs/internal/include/launchdarkly/events/{ => detail}/parse_date_header.hpp (93%) rename libs/internal/include/launchdarkly/events/{ => detail}/request_worker.hpp (97%) rename libs/internal/include/launchdarkly/events/{ => detail}/summarizer.hpp (89%) rename libs/internal/include/launchdarkly/events/{ => detail}/worker_pool.hpp (93%) rename libs/{client-sdk/src/event_processor.hpp => internal/include/launchdarkly/events/event_processor_interface.hpp} (89%) delete mode 100644 libs/internal/include/launchdarkly/events/events.hpp rename libs/{client-sdk/src/event_processor => internal/include/launchdarkly/events}/null_event_processor.hpp (64%) create mode 100644 libs/internal/include/launchdarkly/serialization/json_context_aware_reference.hpp create mode 100644 libs/internal/include/launchdarkly/serialization/json_context_kind.hpp create mode 100644 libs/internal/include/launchdarkly/serialization/json_flag.hpp create mode 100644 libs/internal/include/launchdarkly/serialization/json_item_descriptor.hpp create mode 100644 libs/internal/include/launchdarkly/serialization/json_primitives.hpp create mode 100644 libs/internal/include/launchdarkly/serialization/json_rule_clause.hpp create mode 100644 libs/internal/include/launchdarkly/serialization/json_sdk_data_set.hpp create mode 100644 libs/internal/include/launchdarkly/serialization/json_segment.hpp rename libs/{client-sdk/src => internal/include/launchdarkly/signals}/boost_signal_connection.hpp (78%) create mode 100644 libs/internal/src/data_model/flag.cpp create mode 100644 libs/internal/src/data_model/rule_clause.cpp create mode 100644 libs/internal/src/encoding/sha_1.cpp rename libs/internal/src/events/{client_events.cpp => common_events.cpp} (57%) create mode 100644 libs/internal/src/events/lru_cache.cpp rename libs/{client-sdk/src/event_processor => internal/src/events}/null_event_processor.cpp (54%) create mode 100644 libs/internal/src/serialization/json_context_kind.cpp create mode 100644 libs/internal/src/serialization/json_flag.cpp create mode 100644 libs/internal/src/serialization/json_primitives.cpp create mode 100644 libs/internal/src/serialization/json_rule_clause.cpp create mode 100644 libs/internal/src/serialization/json_sdk_data_set.cpp create mode 100644 libs/internal/src/serialization/json_segment.cpp rename libs/{client-sdk/src => internal/src/signals}/boost_signal_connection.cpp (55%) create mode 100644 libs/internal/tests/data_model_serialization_test.cpp create mode 100644 libs/internal/tests/lru_cache_test.cpp create mode 100644 libs/internal/tests/sha_1_test.cpp create mode 100644 libs/server-sdk/CMakeLists.txt create mode 100644 libs/server-sdk/Doxyfile create mode 100644 libs/server-sdk/README.md create mode 100644 libs/server-sdk/docs/doc.md create mode 100644 libs/server-sdk/include/launchdarkly/server_side/all_flags_state.hpp create mode 100644 libs/server-sdk/include/launchdarkly/server_side/bindings/c/all_flags_state/all_flags_state.h create mode 100644 libs/server-sdk/include/launchdarkly/server_side/bindings/c/config/builder.h create mode 100644 libs/server-sdk/include/launchdarkly/server_side/bindings/c/config/config.h create mode 100644 libs/server-sdk/include/launchdarkly/server_side/bindings/c/sdk.h create mode 100644 libs/server-sdk/include/launchdarkly/server_side/change_notifier.hpp create mode 100644 libs/server-sdk/include/launchdarkly/server_side/client.hpp create mode 100644 libs/server-sdk/include/launchdarkly/server_side/data_source_status.hpp create mode 100644 libs/server-sdk/include/launchdarkly/server_side/integrations/persistent_store_core.hpp create mode 100644 libs/server-sdk/include/launchdarkly/server_side/serialization/json_all_flags_state.hpp create mode 100644 libs/server-sdk/src/CMakeLists.txt create mode 100644 libs/server-sdk/src/all_flags_state/all_flags_state.cpp create mode 100644 libs/server-sdk/src/all_flags_state/all_flags_state_builder.cpp create mode 100644 libs/server-sdk/src/all_flags_state/all_flags_state_builder.hpp create mode 100644 libs/server-sdk/src/all_flags_state/json_all_flags_state.cpp create mode 100644 libs/server-sdk/src/bindings/c/all_flags_state/all_flags_state.cpp create mode 100644 libs/server-sdk/src/bindings/c/builder.cpp create mode 100644 libs/server-sdk/src/bindings/c/config.cpp create mode 100644 libs/server-sdk/src/bindings/c/sdk.cpp create mode 100644 libs/server-sdk/src/boost.cpp create mode 100644 libs/server-sdk/src/client.cpp create mode 100644 libs/server-sdk/src/client_impl.cpp create mode 100644 libs/server-sdk/src/client_impl.hpp create mode 100644 libs/server-sdk/src/data_sources/data_source_event_handler.cpp create mode 100644 libs/server-sdk/src/data_sources/data_source_event_handler.hpp create mode 100644 libs/server-sdk/src/data_sources/data_source_status.cpp create mode 100644 libs/server-sdk/src/data_sources/data_source_status_manager.hpp create mode 100644 libs/server-sdk/src/data_sources/data_source_update_sink.hpp create mode 100644 libs/server-sdk/src/data_sources/null_data_source.cpp create mode 100644 libs/server-sdk/src/data_sources/null_data_source.hpp create mode 100644 libs/server-sdk/src/data_sources/polling_data_source.cpp create mode 100644 libs/server-sdk/src/data_sources/polling_data_source.hpp create mode 100644 libs/server-sdk/src/data_sources/streaming_data_source.cpp create mode 100644 libs/server-sdk/src/data_sources/streaming_data_source.hpp create mode 100644 libs/server-sdk/src/data_store/data_kind.hpp create mode 100644 libs/server-sdk/src/data_store/data_store.hpp create mode 100644 libs/server-sdk/src/data_store/data_store_updater.cpp create mode 100644 libs/server-sdk/src/data_store/data_store_updater.hpp create mode 100644 libs/server-sdk/src/data_store/dependency_tracker.cpp create mode 100644 libs/server-sdk/src/data_store/dependency_tracker.hpp create mode 100644 libs/server-sdk/src/data_store/descriptors.hpp create mode 100644 libs/server-sdk/src/data_store/memory_store.cpp create mode 100644 libs/server-sdk/src/data_store/memory_store.hpp create mode 100644 libs/server-sdk/src/data_store/persistent/expiration_tracker.cpp create mode 100644 libs/server-sdk/src/data_store/persistent/expiration_tracker.hpp create mode 100644 libs/server-sdk/src/data_store/persistent/persistent_data_store.cpp create mode 100644 libs/server-sdk/src/data_store/persistent/persistent_data_store.hpp create mode 100644 libs/server-sdk/src/data_store/tagged_data.hpp create mode 100644 libs/server-sdk/src/evaluation/bucketing.cpp create mode 100644 libs/server-sdk/src/evaluation/bucketing.hpp create mode 100644 libs/server-sdk/src/evaluation/detail/evaluation_stack.cpp create mode 100644 libs/server-sdk/src/evaluation/detail/evaluation_stack.hpp create mode 100644 libs/server-sdk/src/evaluation/detail/semver_operations.cpp create mode 100644 libs/server-sdk/src/evaluation/detail/semver_operations.hpp create mode 100644 libs/server-sdk/src/evaluation/detail/timestamp_operations.cpp create mode 100644 libs/server-sdk/src/evaluation/detail/timestamp_operations.hpp create mode 100644 libs/server-sdk/src/evaluation/evaluation_error.cpp create mode 100644 libs/server-sdk/src/evaluation/evaluation_error.hpp create mode 100644 libs/server-sdk/src/evaluation/evaluator.cpp create mode 100644 libs/server-sdk/src/evaluation/evaluator.hpp create mode 100644 libs/server-sdk/src/evaluation/operators.cpp create mode 100644 libs/server-sdk/src/evaluation/operators.hpp create mode 100644 libs/server-sdk/src/evaluation/rules.cpp create mode 100644 libs/server-sdk/src/evaluation/rules.hpp create mode 100644 libs/server-sdk/src/events/event_factory.cpp create mode 100644 libs/server-sdk/src/events/event_factory.hpp create mode 100644 libs/server-sdk/src/events/event_scope.hpp create mode 100644 libs/server-sdk/tests/CMakeLists.txt create mode 100644 libs/server-sdk/tests/all_flags_state_test.cpp create mode 100644 libs/server-sdk/tests/bucketing_tests.cpp create mode 100644 libs/server-sdk/tests/client_test.cpp create mode 100644 libs/server-sdk/tests/data_source_event_handler_test.cpp create mode 100644 libs/server-sdk/tests/data_store_updater_test.cpp create mode 100644 libs/server-sdk/tests/dependency_tracker_test.cpp create mode 100644 libs/server-sdk/tests/evaluation_stack_test.cpp create mode 100644 libs/server-sdk/tests/evaluator_tests.cpp create mode 100644 libs/server-sdk/tests/event_factory_tests.cpp create mode 100644 libs/server-sdk/tests/event_scope_test.cpp create mode 100644 libs/server-sdk/tests/expiration_tracker_test.cpp create mode 100644 libs/server-sdk/tests/memory_store_test.cpp create mode 100644 libs/server-sdk/tests/operator_tests.cpp create mode 100644 libs/server-sdk/tests/rule_tests.cpp create mode 100644 libs/server-sdk/tests/semver_tests.cpp create mode 100644 libs/server-sdk/tests/server_c_bindings_test.cpp create mode 100644 libs/server-sdk/tests/spy_event_processor.hpp create mode 100644 libs/server-sdk/tests/spy_logger.hpp create mode 100644 libs/server-sdk/tests/test_store.cpp create mode 100644 libs/server-sdk/tests/test_store.hpp create mode 100644 libs/server-sdk/tests/timestamp_tests.cpp diff --git a/.clang-tidy b/.clang-tidy index 418e839a5..154cb4e32 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,5 +1,5 @@ --- CheckOptions: - { key: readability-identifier-length.IgnoredParameterNames, value: 'i|j|k|c|os|it' } - - { key: readability-identifier-length.IgnoredVariableNames, value: 'ec|id' } + - { key: readability-identifier-length.IgnoredVariableNames, value: 'ec|id|it' } - { key: readability-identifier-length.IgnoredLoopCounterNames, value: 'i|j|k|c|os|it' } diff --git a/.github/workflows/client.yml b/.github/workflows/client.yml index e60b9984b..02252dd4e 100644 --- a/.github/workflows/client.yml +++ b/.github/workflows/client.yml @@ -6,7 +6,7 @@ on: paths-ignore: - '**.md' #Do not need to run CI for markdown changes. pull_request: - branches: [ main ] + branches: [ main, server-side ] paths-ignore: - '**.md' @@ -16,12 +16,12 @@ jobs: env: # Port the test service (implemented in this repo) should bind to. TEST_SERVICE_PORT: 8123 - TEST_SERVICE_BINARY: ./build/contract-tests/sdk-contract-tests/sdk-tests + TEST_SERVICE_BINARY: ./build/contract-tests/client-contract-tests/client-tests steps: - uses: actions/checkout@v3 - uses: ./.github/actions/ci with: - cmake_target: sdk-tests + cmake_target: client-tests run_tests: false - name: 'Launch test service as background task' run: $TEST_SERVICE_BINARY $TEST_SERVICE_PORT 2>&1 & @@ -36,7 +36,7 @@ jobs: - uses: ./.github/actions/ci with: cmake_target: launchdarkly-cpp-client - build-test-mac: + build-test-client-mac: runs-on: macos-12 steps: - run: | @@ -50,7 +50,7 @@ jobs: with: cmake_target: launchdarkly-cpp-client platform_version: 12 - build-test-windows: + build-test-client-windows: runs-on: windows-2022 steps: - name: Upgrade OpenSSL diff --git a/.github/workflows/common.yml b/.github/workflows/common.yml index 9c1ce1752..438bcdc6c 100644 --- a/.github/workflows/common.yml +++ b/.github/workflows/common.yml @@ -6,12 +6,12 @@ on: paths-ignore: - '**.md' #Do not need to run CI for markdown changes. pull_request: - branches: [ main ] + branches: [ main, server-side ] paths-ignore: - '**.md' jobs: - build-test: + build-test-common: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/cpp-linter.yml b/.github/workflows/cpp-linter.yml index 55a3f4d9e..4a969f4be 100644 --- a/.github/workflows/cpp-linter.yml +++ b/.github/workflows/cpp-linter.yml @@ -7,7 +7,7 @@ on: push: branches: [ "main" ] pull_request: - branches: [ "main" ] + branches: [ "main", server-side ] jobs: cpp-linter: diff --git a/.github/workflows/internal.yml b/.github/workflows/internal.yml index 0986b723d..d116de8bb 100644 --- a/.github/workflows/internal.yml +++ b/.github/workflows/internal.yml @@ -6,12 +6,12 @@ on: paths-ignore: - '**.md' #Do not need to run CI for markdown changes. pull_request: - branches: [ main ] + branches: [ main, server-side ] paths-ignore: - '**.md' jobs: - build-test: + build-test-internal: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/server.yml b/.github/workflows/server.yml new file mode 100644 index 000000000..1e9fec2f8 --- /dev/null +++ b/.github/workflows/server.yml @@ -0,0 +1,79 @@ +name: libs/server-sdk + +on: + push: + branches: [ main ] + paths-ignore: + - '**.md' #Do not need to run CI for markdown changes. + pull_request: + branches: [ main, server-side ] + paths-ignore: + - '**.md' + +jobs: + contract-tests: + runs-on: ubuntu-22.04 + env: + # Port the test service (implemented in this repo) should bind to. + TEST_SERVICE_PORT: 8123 + TEST_SERVICE_BINARY: ./build/contract-tests/server-contract-tests/server-tests + steps: + - uses: actions/checkout@v3 + - uses: ./.github/actions/ci + with: + cmake_target: server-tests + run_tests: false + - name: 'Launch test service as background task' + run: $TEST_SERVICE_BINARY $TEST_SERVICE_PORT 2>&1 & + - uses: ./.github/actions/contract-tests + with: + # Inform the test harness of test service's port. + test_service_port: ${{ env.TEST_SERVICE_PORT }} + extra_params: '-skip-from ./contract-tests/server-contract-tests/test-suppressions.txt' + build-test-server: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + - uses: ./.github/actions/ci + with: + cmake_target: launchdarkly-cpp-server + build-test-server-mac: + runs-on: macos-12 + steps: + - run: | + echo "OPENSSL_ROOT_DIR=$(brew --prefix openssl@3)" >> "$GITHUB_ENV" + # For debugging + echo "OPENSSL_ROOT_DIR=$(brew --prefix openssl@3)" + - uses: actions/checkout@v3 + - uses: ./.github/actions/ci + env: + OPENSSL_ROOT_DIR: ${{ env.OPENSSL_ROOT_DIR }} + with: + cmake_target: launchdarkly-cpp-server + platform_version: 12 + build-test-server-windows: + runs-on: windows-2022 + steps: + - name: Upgrade OpenSSL + shell: bash + run: | + choco upgrade openssl --no-progress + - name: Determine OpenSSL Installation Directory + shell: bash + run: | + if [ -d "C:\Program Files\OpenSSL-Win64" ]; then + echo "OPENSSL_ROOT_DIR=C:\Program Files\OpenSSL-Win64" >> "$GITHUB_ENV" + else + echo "OPENSSL_ROOT_DIR=C:\Program Files\OpenSSL" >> "$GITHUB_ENV" + fi + - uses: actions/checkout@v3 + - uses: ilammy/msvc-dev-cmd@v1 + - uses: ./.github/actions/ci + env: + OPENSSL_ROOT_DIR: ${{ env.OPENSSL_ROOT_DIR }} + BOOST_LIBRARY_DIR: 'C:\local\boost_1_81_0\lib64-msvc-14.3' + BOOST_LIBRARYDIR: 'C:\local\boost_1_81_0\lib64-msvc-14.3' + with: + cmake_target: launchdarkly-cpp-client + platform_version: 2022 + toolset: msvc diff --git a/.github/workflows/sse.yml b/.github/workflows/sse.yml index b132886eb..9452f6389 100644 --- a/.github/workflows/sse.yml +++ b/.github/workflows/sse.yml @@ -11,7 +11,7 @@ on: - '**.md' jobs: - build-test: + build-test-sse: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 diff --git a/CMakeLists.txt b/CMakeLists.txt index e2fa31650..6b2e811f8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -127,6 +127,7 @@ add_subdirectory(libs/server-sent-events) # Built as static or shared depending on LD_BUILD_SHARED_LIBS variable. # This target "links" in common, internal, and sse as object libraries. add_subdirectory(libs/client-sdk) +add_subdirectory(libs/server-sdk) if (LD_BUILD_CONTRACT_TESTS) message(STATUS "LaunchDarkly: building contract tests") diff --git a/README.md b/README.md index 1c0969f34..714d2d15a 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,40 @@ GoogleTest is used for testing. For information on integrating an SDK package please refer to the SDK specific README. +## CMake Usage + +Various CMake options are available to customize the client/server SDK builds. + +| Option | Description | Default | Requires | +|---------------------------|----------------------------------------------------------------------------------------|--------------------|-------------------------------------------| +| `BUILD_TESTING` | Coarse-grained switch; turn off to disable all testing and only build the SDK targets. | On | N/A | +| `LD_BUILD_UNIT_TESTS` | Whether C++ unit tests are built. | On | `BUILD_TESTING; NOT LD_BUILD_SHARED_LIBS` | +| `LD_TESTING_SANITIZERS` | Whether sanitizers should be enabled. | On | `LD_BUILD_UNIT_TESTS` | +| `LD_BUILD_CONTRACT_TESTS` | Whether the contract test service (used in CI) is built. | Off | `BUILD_TESTING` | +| `LD_BUILD_EXAMPLES` | Whether example apps (hello world) are built. | On | N/A | +| `LD_BUILD_SHARED_LIBS` | Whether the SDKs are built as static or shared libraries. | Off (static lib) | N/A | +| `LD_DYNAMIC_LINK_OPENSSL` | Whether OpenSSL be dynamically linked. | Off (static link) | N/A | + +**Note:** _if building the SDKs as shared libraries, then unit tests won't be able to link correctly since the SDK's C++ +symbols aren't exposed. To run unit tests, build a static library._ + +Basic usage example: + +```bash +mkdir -p build && cd build +cmake -G"Unix Makefiles" .. +``` + +Slightly more advanced example - build shared libraries, and don't build any of the testing components: + +```bash +mkdir -p build && cd build +cmake -G"Unix Makefiles" -DLD_BUILD_SHARED_LIBS=On -DBUILD_TESTING=Off .. +``` + +The example uses `make`, but you might instead use [Ninja](https://ninja-build.org/), +MSVC, [etc.](https://cmake.org/cmake/help/latest/manual/cmake-generators.7.html) + ## LaunchDarkly overview [LaunchDarkly](https://www.launchdarkly.com) is a feature management platform that serves trillions of feature flags @@ -76,7 +110,8 @@ our [contributing guidelines](CONTRIBUTING.md) for instructions on how to contri - Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). Disable parts of your application to facilitate maintenance, without taking everything offline. -- LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Read [our documentation](https://docs.launchdarkly.com/sdk) for a complete list. +- LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. + Read [our documentation](https://docs.launchdarkly.com/sdk) for a complete list. - Explore LaunchDarkly - [launchdarkly.com](https://www.launchdarkly.com/ 'LaunchDarkly Main Website') for more information - [docs.launchdarkly.com](https://docs.launchdarkly.com/ 'LaunchDarkly Documentation') for our documentation and SDK diff --git a/architecture/event_processor.md b/architecture/event_processor.md new file mode 100644 index 000000000..022ea59ec --- /dev/null +++ b/architecture/event_processor.md @@ -0,0 +1,145 @@ +# Analytic Event Processor + +The Event Processor is responsible for consuming, batching, and delivering events generated +by the server and client-side LaunchDarkly SDKs. + +```mermaid +classDiagram + IEventProcessor <|-- NullEventProcessor + IEventProcessor <|-- AsioEventProcessor + + AsioEventProcessor *-- LRUCache + AsioEventProcessor *-- Outbox + AsioEventProcessor *-- WorkerPool + AsioEventProcessor *-- Summarizer + + RequestWorker *-- EventBatch + WorkerPool *-- "5" RequestWorker + + TrackEvent -- TrackEventParams: (alias) + InputEvent *-- IdentifyEventParams + InputEvent *-- FeatureEventParams + InputEvent *-- TrackEventParams + + + OutputEvent *-- IndexEvent + OutputEvent *-- FeatureEvent + OutputEvent *-- DebugEvent + OutputEvent *-- IdentifyEvent + OutputEvent *-- TrackEvent + + EventBatch --> Outbox: Pulls individual events from.. + EventBatch --> Summarizer: Pulls summary events from.. + + IEventProcessor --> InputEvent + Outbox --> OutputEvent + + Summarizer --> FeatureEventParams + + + class IEventProcessor { + <> + +SendAsync(InputEvent event) void + +FlushAsync() void + +ShutdownAsync() void + } + + class NullEventProcessor { + + } + + class AsioEventProcessor { + + } + + class EventBatch { + +const Count() size_t + +const Request() network:: HttpRequest + +const Target() std:: string + } + + class LRUCache { + +Notice(std:: string value) bool + +const Size() size_t + +Clear() void + } + + class Outbox { + +PushDiscardingOverflow(std:: vector~OutputEvent~ events) bool + +Consume() std:: vector~OutputEvent~ + +const Empty() bool + } + + class RequestWorker { + +const Available() bool + +AsyncDeliver(EventBatch, delivery_callback) + } + + class WorkerPool { + +Get(worker_callback) void + } + + class Summarizer { + +Update(FeatureEventParams) void + +Finish() + +const StartTime() Time + +const EndTime() Time + } + +%% note: the 'namespace' feature isn't supported on Github yet +%% namespace events { + class InputEvent { + +std:: variant + } + + + class OutputEvent { + +std:: variant + } + + class FeatureEventParams { + + } + + class IdentifyEventParams { + + } + + class TrackEventParams { + + } + + class FeatureEvent { + + } + + class DebugEvent { + + } + + class IdentifyEvent { + + } + + class IndexEvent { + + } + + class TrackEvent { + + } + +%% } +``` + +### Notes + +SDKs may be configured to disable events, so `NullEventProcessor` is made available. This component accepts +events generated +by the SDK and discards them. + +If events are enabled, SDKs use the `AsioEventProcessor` implementation, which is an asynchronous processor +utilizing `boost::asio`. + +Most event definitions are shared between the server and client-side SDKs. Unique to the server-side SDK +is `IndexEvent`. diff --git a/architecture/server_data_source_arch.md b/architecture/server_data_source_arch.md new file mode 100644 index 000000000..254e8675f --- /dev/null +++ b/architecture/server_data_source_arch.md @@ -0,0 +1,118 @@ +# Server Data Source Architecture + +```mermaid +classDiagram + direction LR + Client --* IDataSource + Client --* IDataSourceStatusProvider + + IDataSource <|-- PollingDataSource + IDataSource <|-- StreamingDataSource + + PollingDataSource --* DataSourceStatusManager + StreamingDataSource --* DataSourceStatusManager + + PollingDataSource --* DataSourceEventHandler + StreamingDataSource --* DataSourceEventHandler + + DataSourceEventHandler --> DataSourceStatusManager + + IDataSourceStatusProvider <-- DataSourceStatusManager + + DataSourceStatusManager --> DataSourceState + DataSourceStatusManager --> ErrorInfo + DataSourceStatusManager --> DataSourceStatus + + DataSourceStatus --* DataSourceState + DataSourceStatus --* ErrorInfo + DataSourceEventHandler --> DataSourceEventHandler_MessageStatus + + + note for IDataSource "Common for Client/Server" + + class IDataSource { + <> + +Start() void + +ShutdownAsync(std::function~void()~ ) void + } + + note for IDataSourceStatusProvider "Different for client/server" + + class IDataSourceStatusProvider { + <> + +const Status() DataSourceStatus + +OnDataSourceStatusChange(std::function<void(DataSourceStatus)> ) std::unique_ptr~IConnection~ + +OnDataSourceStatusChangeEx(std::function<bool(DataSourceStatus)> ) void + } + + note for DataSourceState "Different for client/server" + + class DataSourceState { + <> + Initializing + Valid + Interrupted + Off + } + + note for ErrorInfo_ErrorKind "Common for client/server" + + class ErrorInfo_ErrorKind { + <> + Unknown + NetworkError + ErrorResponse + InvalidData + StoreError + } + + note for ErrorInfo "Common for client/server" + + ErrorInfo --* ErrorInfo_ErrorKind + + class ErrorInfo { + +const Kind() ErrorKind + +const StatusCode() StatusCodeType + +const Message() std::string const& + +const Time() DateTime + } + + class PollingDataSource { + + } + + class StreamingDataSource { + + } + + note for DataSourceEventHandler "Different for client/server" + + + class DataSourceEventHandler { + +HandleMessage(std::string const& type, std::string const& data) MessageStatus + } + + note for DataSourceEventHandler_MessageStatus "Common for client/server" + + class DataSourceEventHandler_MessageStatus { + <> + MessageHandled + InvalidMessage + UnhandledVerb + } + + class DataSourceStatus { + +const State() DataSourceState + +const StateSince() DateTime + +const LastError() std::optional~ErrorInfo~ + } + + class DataSourceStatusManager { + +SetState(DataSourceStatus status) void + +SetState(DataSourceState state, StatusCodeType code, std::string message) void + +SetState(DataSourceState state, ErrorInfo_ErrorKind kind, std::string message) void + +SetError(ErrorInfo::ErrorKind kind, std::string message) void + +SetError(StatusCodeType code, std::string message) void + } + +``` \ No newline at end of file diff --git a/architecture/server_store_arch.md b/architecture/server_store_arch.md new file mode 100644 index 000000000..8b9a1ec31 --- /dev/null +++ b/architecture/server_store_arch.md @@ -0,0 +1,98 @@ +# Server Data Store Architecture + +```mermaid + +classDiagram + Client --* IDataStore : Contains an IDataStore which is\n either a MemoryStore or a PersistentStore + Client --* DataStoreUpdater + Client --* IChangeNotifier + + IDataStore <|-- MemoryStore + IDataSourceUpdateSink <|-- MemoryStore + + IDataStore <|-- PersistentStore + IDataSourceUpdateSink <|-- PersistentStore + IDataSourceUpdateSink <|-- DataStoreUpdater + + IChangeNotifier <|-- DataStoreUpdater + + DataStoreUpdater --> IDataStore + + PersistentStore --* MemoryStore : PersistentStore contains a MemoryStore + PersistentStore --* ExpirationTracker + + IPersistentStoreCore <|-- RedisPersistentStore + + note for IPersistentStoreCore "The Get/All/Initialized are behaviorally\n const, but cache/memoize." + + IPersistentStoreCore --> SerializedItemDescriptor + IPersistentStoreCore --> PersistentKind + PersistentStore --* IPersistentStoreCore + + class PersistentKind{ + +std::string namespace + %% There are some cases where the store may need to extract a version from the serialized representation. + %% Specifically when the store cannot put the version in a column, such as with Redis. + +DeserializeVersion(std::string data): uint64_t + } + + class SerializedItemDescriptor{ + +uint64_t version + +bool deleted + +std::string serializedItem + } + + class IPersistentStoreCore { + <> + +Init(OrderedDataSets dataSets) + +Upsert(PersistentKind kind, std::string key, SerializedItemDescriptor descriptor) SerializedItemDescriptor + + +const Get(PersistentKind kind, std::string key) SerializedItemDescriptor + +const All(PersistentKind kind) std::unordered_map<std::string, SerializedItemDescriptor> + + +const Description() std::string const& + +const Initialized() bool + } + + + class IDataSourceUpdateSink{ + <> + +void Init(SDKDataSet allData) + +void Upsert(std::string key, ItemDescriptor~Flag~ data) + +void Upsert(std::string key, ItemDescriptor~Segment~ data) + } + + note for IDataStore "The shared_ptr from GetFlag or GetSegment may be null." + + class IDataStore{ + <> + +const GetFlag(std::string key) std::shared_ptr<const ItemDescriptor<Flag>> + +const GetSegment(std::string key) std::shared_ptr<const ItemDescriptor<Segment>> + +const AllFlags() std::unordered_map<std::string, std::shared_ptr<const ItemDescriptor<Flag>>> + +const AllSegments() std::unordered_map<std::string, std::shared_ptr<const ItemDescriptor<Segment>>> + +const Initialized() bool + +const Description() string + } + + class ExpirationTracker{ + } + + class MemoryStore{ + } + + class PersistentStore{ + +PersistentStore(std::shared_ptr~IPersistentStoreCore~ core) + } + + class RedisPersistentStore{ + + } + + class IChangeNotifier{ + +OnChange(std::function<void(std::shared_ptr<ChangeSet>)> handler): std::unique_ptr~IConnection~ + } + + class DataStoreUpdater{ + + } +``` \ No newline at end of file diff --git a/cmake/rfc3339_timestamp.cmake b/cmake/rfc3339_timestamp.cmake new file mode 100644 index 000000000..b114f1938 --- /dev/null +++ b/cmake/rfc3339_timestamp.cmake @@ -0,0 +1,31 @@ +FetchContent_Declare(timestamp + GIT_REPOSITORY https://github.com/chansen/c-timestamp + GIT_TAG "b205c407ae6680d23d74359ac00444b80989792f" + ) + +FetchContent_GetProperties(timestamp) +if (NOT timestamp_POPULATED) + FetchContent_Populate(timestamp) +endif () + +add_library(timestamp OBJECT + ${timestamp_SOURCE_DIR}/timestamp_tm.c + ${timestamp_SOURCE_DIR}/timestamp_valid.c + ${timestamp_SOURCE_DIR}/timestamp_parse.c + ) + +if (BUILD_SHARED_LIBS) + set_target_properties(timestamp PROPERTIES + POSITION_INDEPENDENT_CODE 1 + C_VISIBILITY_PRESET hidden + ) +endif () + +target_include_directories(timestamp PUBLIC + $ + $ + ) +install( + TARGETS timestamp + EXPORT ${PROJECT_NAME}-targets +) diff --git a/contract-tests/CMakeLists.txt b/contract-tests/CMakeLists.txt index cf6a9dd44..4da668ade 100644 --- a/contract-tests/CMakeLists.txt +++ b/contract-tests/CMakeLists.txt @@ -1,2 +1,4 @@ +add_subdirectory(data-model) add_subdirectory(sse-contract-tests) -add_subdirectory(sdk-contract-tests) +add_subdirectory(client-contract-tests) +add_subdirectory(server-contract-tests) diff --git a/contract-tests/sdk-contract-tests/CMakeLists.txt b/contract-tests/client-contract-tests/CMakeLists.txt similarity index 62% rename from contract-tests/sdk-contract-tests/CMakeLists.txt rename to contract-tests/client-contract-tests/CMakeLists.txt index 9e1b0af2a..a77e15c6c 100644 --- a/contract-tests/sdk-contract-tests/CMakeLists.txt +++ b/contract-tests/client-contract-tests/CMakeLists.txt @@ -2,29 +2,29 @@ cmake_minimum_required(VERSION 3.19) project( - LaunchDarklyCPPSDKTestHarness + LaunchDarklyCPPClientSDKTestHarness VERSION 0.1 - DESCRIPTION "LaunchDarkly CPP SDK Test Harness" + DESCRIPTION "LaunchDarkly CPP Client-side SDK Test Harness" LANGUAGES CXX ) include(${CMAKE_FILES}/json.cmake) -add_executable(sdk-tests +add_executable(client-tests src/main.cpp src/server.cpp src/session.cpp - src/definitions.cpp src/entity_manager.cpp src/client_entity.cpp ) -target_link_libraries(sdk-tests PRIVATE +target_link_libraries(client-tests PRIVATE launchdarkly::client launchdarkly::internal foxy nlohmann_json::nlohmann_json Boost::coroutine + contract-test-data-model ) -target_include_directories(sdk-tests PUBLIC include) +target_include_directories(client-tests PUBLIC include) diff --git a/contract-tests/sdk-contract-tests/README.md b/contract-tests/client-contract-tests/README.md similarity index 100% rename from contract-tests/sdk-contract-tests/README.md rename to contract-tests/client-contract-tests/README.md diff --git a/contract-tests/sdk-contract-tests/include/client_entity.hpp b/contract-tests/client-contract-tests/include/client_entity.hpp similarity index 96% rename from contract-tests/sdk-contract-tests/include/client_entity.hpp rename to contract-tests/client-contract-tests/include/client_entity.hpp index 66a60d9cd..2fc40a3fd 100644 --- a/contract-tests/sdk-contract-tests/include/client_entity.hpp +++ b/contract-tests/client-contract-tests/include/client_entity.hpp @@ -1,8 +1,8 @@ #pragma once +#include #include #include -#include "definitions.hpp" class ClientEntity { public: diff --git a/contract-tests/sdk-contract-tests/include/entity_manager.hpp b/contract-tests/client-contract-tests/include/entity_manager.hpp similarity index 97% rename from contract-tests/sdk-contract-tests/include/entity_manager.hpp rename to contract-tests/client-contract-tests/include/entity_manager.hpp index e8a5802ad..92c165d62 100644 --- a/contract-tests/sdk-contract-tests/include/entity_manager.hpp +++ b/contract-tests/client-contract-tests/include/entity_manager.hpp @@ -5,8 +5,8 @@ #include #include +#include #include "client_entity.hpp" -#include "definitions.hpp" #include #include diff --git a/contract-tests/sdk-contract-tests/include/server.hpp b/contract-tests/client-contract-tests/include/server.hpp similarity index 100% rename from contract-tests/sdk-contract-tests/include/server.hpp rename to contract-tests/client-contract-tests/include/server.hpp diff --git a/contract-tests/sdk-contract-tests/include/session.hpp b/contract-tests/client-contract-tests/include/session.hpp similarity index 100% rename from contract-tests/sdk-contract-tests/include/session.hpp rename to contract-tests/client-contract-tests/include/session.hpp diff --git a/contract-tests/sdk-contract-tests/src/client_entity.cpp b/contract-tests/client-contract-tests/src/client_entity.cpp similarity index 87% rename from contract-tests/sdk-contract-tests/src/client_entity.cpp rename to contract-tests/client-contract-tests/src/client_entity.cpp index d4012c38a..15446bbfd 100644 --- a/contract-tests/sdk-contract-tests/src/client_entity.cpp +++ b/contract-tests/client-contract-tests/src/client_entity.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -65,8 +66,12 @@ static void BuildContextFromParams(launchdarkly::ContextBuilder& builder, if (single.custom) { for (auto const& [key, value] : *single.custom) { - attrs.Set(key, boost::json::value_to( - boost::json::parse(value.dump()))); + auto maybe_attr = boost::json::value_to< + tl::expected>( + boost::json::parse(value.dump())); + if (maybe_attr) { + attrs.Set(key, *maybe_attr); + } } } } @@ -126,9 +131,16 @@ tl::expected ContextConvert( tl::expected ClientEntity::Custom( CustomEventParams const& params) { - auto data = params.data ? boost::json::value_to( - boost::json::parse(params.data->dump())) - : launchdarkly::Value::Null(); + auto data = + params.data + ? boost::json::value_to< + tl::expected>( + boost::json::parse(params.data->dump())) + : launchdarkly::Value::Null(); + + if (!data) { + return tl::make_unexpected("couldn't parse custom event data"); + } if (params.omitNullData.value_or(false) && !params.metricValue && !params.data) { @@ -137,11 +149,11 @@ tl::expected ClientEntity::Custom( } if (!params.metricValue) { - client_->Track(params.eventKey, std::move(data)); + client_->Track(params.eventKey, std::move(*data)); return nlohmann::json{}; } - client_->Track(params.eventKey, std::move(data), *params.metricValue); + client_->Track(params.eventKey, std::move(*data), *params.metricValue); return nlohmann::json{}; } @@ -204,15 +216,19 @@ tl::expected ClientEntity::EvaluateDetail( } case ValueType::Any: case ValueType::Unspecified: { - auto fallback = boost::json::value_to( + auto maybe_fallback = boost::json::value_to< + tl::expected>( boost::json::parse(defaultVal.dump())); + if (!maybe_fallback) { + return tl::make_unexpected("unable to parse fallback value"); + } /* This switcharoo from nlohmann/json to boost/json to Value, then * back is because we're using nlohmann/json for the test harness * protocol, but boost::json in the SDK. We could swap over to * boost::json entirely here to remove the awkwardness. */ - auto detail = client_->JsonVariationDetail(key, fallback); + auto detail = client_->JsonVariationDetail(key, *maybe_fallback); auto serialized = boost::json::serialize(boost::json::value_from(*detail)); @@ -264,15 +280,18 @@ tl::expected ClientEntity::Evaluate( } case ValueType::Any: case ValueType::Unspecified: { - auto fallback = boost::json::value_to( + auto maybe_fallback = boost::json::value_to< + tl::expected>( boost::json::parse(defaultVal.dump())); - + if (!maybe_fallback) { + return tl::make_unexpected("unable to parse fallback value"); + } /* This switcharoo from nlohmann/json to boost/json to Value, then * back is because we're using nlohmann/json for the test harness * protocol, but boost::json in the SDK. We could swap over to * boost::json entirely here to remove the awkwardness. */ - auto evaluation = client_->JsonVariation(key, fallback); + auto evaluation = client_->JsonVariation(key, *maybe_fallback); auto serialized = boost::json::serialize(boost::json::value_from(evaluation)); diff --git a/contract-tests/sdk-contract-tests/src/entity_manager.cpp b/contract-tests/client-contract-tests/src/entity_manager.cpp similarity index 100% rename from contract-tests/sdk-contract-tests/src/entity_manager.cpp rename to contract-tests/client-contract-tests/src/entity_manager.cpp diff --git a/contract-tests/sdk-contract-tests/src/main.cpp b/contract-tests/client-contract-tests/src/main.cpp similarity index 96% rename from contract-tests/sdk-contract-tests/src/main.cpp rename to contract-tests/client-contract-tests/src/main.cpp index c886b34b8..78d2b3afe 100644 --- a/contract-tests/sdk-contract-tests/src/main.cpp +++ b/contract-tests/client-contract-tests/src/main.cpp @@ -19,7 +19,7 @@ using launchdarkly::LogLevel; int main(int argc, char* argv[]) { launchdarkly::Logger logger{ - std::make_unique("sdk-contract-tests")}; + std::make_unique("client-contract-tests")}; const std::string default_port = "8123"; std::string port = default_port; diff --git a/contract-tests/sdk-contract-tests/src/server.cpp b/contract-tests/client-contract-tests/src/server.cpp similarity index 100% rename from contract-tests/sdk-contract-tests/src/server.cpp rename to contract-tests/client-contract-tests/src/server.cpp diff --git a/contract-tests/sdk-contract-tests/src/session.cpp b/contract-tests/client-contract-tests/src/session.cpp similarity index 97% rename from contract-tests/sdk-contract-tests/src/session.cpp rename to contract-tests/client-contract-tests/src/session.cpp index 8de83db1a..5213ab6f9 100644 --- a/contract-tests/sdk-contract-tests/src/session.cpp +++ b/contract-tests/client-contract-tests/src/session.cpp @@ -1,6 +1,10 @@ #include "session.hpp" + +#include + #include #include + #include const std::string kEntityPath = "/entity/"; @@ -81,7 +85,7 @@ std::optional Session::generate_response(Request& req) { }; if (req.method() == http::verb::get && req.target() == "/") { - return capabilities_response(caps_, "c-client-sdk", "0.0.0"); + return capabilities_response(caps_, "cpp-client-sdk", launchdarkly::client_side::Client::Version()); } if (req.method() == http::verb::head && req.target() == "/") { diff --git a/contract-tests/data-model/CMakeLists.txt b/contract-tests/data-model/CMakeLists.txt new file mode 100644 index 000000000..af39cf433 --- /dev/null +++ b/contract-tests/data-model/CMakeLists.txt @@ -0,0 +1,20 @@ +# Required for Apple Silicon support. +cmake_minimum_required(VERSION 3.19) + +project( + LaunchDarklyCPPSDKTestHarnessDataModel + VERSION 0.1 + DESCRIPTION "LaunchDarkly CPP SDK Test Harness Data Model definitions" + LANGUAGES CXX +) + +include(${CMAKE_FILES}/json.cmake) + + +add_library(contract-test-data-model src/data_model.cpp) +target_link_libraries(contract-test-data-model PUBLIC nlohmann_json::nlohmann_json + ) +target_include_directories(contract-test-data-model PUBLIC + $ + $ + ) diff --git a/contract-tests/sdk-contract-tests/include/definitions.hpp b/contract-tests/data-model/include/data_model/data_model.hpp similarity index 97% rename from contract-tests/sdk-contract-tests/include/definitions.hpp rename to contract-tests/data-model/include/data_model/data_model.hpp index 7c08d367c..b4baccb9b 100644 --- a/contract-tests/sdk-contract-tests/include/definitions.hpp +++ b/contract-tests/data-model/include/data_model/data_model.hpp @@ -188,6 +188,7 @@ NLOHMANN_JSON_SERIALIZE_ENUM(ValueType, struct EvaluateFlagParams { std::string flagKey; + std::optional context; ValueType valueType; nlohmann::json defaultValue; bool detail; @@ -195,6 +196,7 @@ struct EvaluateFlagParams { }; NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(EvaluateFlagParams, flagKey, + context, valueType, defaultValue, detail); @@ -210,11 +212,13 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(EvaluateFlagResponse, reason); struct EvaluateAllFlagParams { + std::optional context; std::optional withReasons; std::optional clientSideOnly; std::optional detailsOnlyForTrackedFlags; }; NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(EvaluateAllFlagParams, + context, withReasons, clientSideOnly, detailsOnlyForTrackedFlags); @@ -226,12 +230,14 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(EvaluateAllFlagsResponse, struct CustomEventParams { std::string eventKey; + std::optional context; std::optional data; std::optional omitNullData; std::optional metricValue; }; NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(CustomEventParams, eventKey, + context, data, omitNullData, metricValue); diff --git a/contract-tests/data-model/src/data_model.cpp b/contract-tests/data-model/src/data_model.cpp new file mode 100644 index 000000000..3172a7c4b --- /dev/null +++ b/contract-tests/data-model/src/data_model.cpp @@ -0,0 +1,6 @@ +#include "data_model/data_model.hpp" + +EvaluateFlagParams::EvaluateFlagParams() + : valueType{ValueType::Unspecified}, detail{false}, context{std::nullopt} {} + +CommandParams::CommandParams() : command{Command::Unknown} {} diff --git a/contract-tests/sdk-contract-tests/src/definitions.cpp b/contract-tests/sdk-contract-tests/src/definitions.cpp deleted file mode 100644 index aba257b39..000000000 --- a/contract-tests/sdk-contract-tests/src/definitions.cpp +++ /dev/null @@ -1,6 +0,0 @@ -#include "definitions.hpp" - -EvaluateFlagParams::EvaluateFlagParams() - : valueType{ValueType::Unspecified}, detail{false} {} - -CommandParams::CommandParams() : command{Command::Unknown} {} diff --git a/contract-tests/server-contract-tests/CMakeLists.txt b/contract-tests/server-contract-tests/CMakeLists.txt new file mode 100644 index 000000000..3bd94cb96 --- /dev/null +++ b/contract-tests/server-contract-tests/CMakeLists.txt @@ -0,0 +1,30 @@ +# Required for Apple Silicon support. +cmake_minimum_required(VERSION 3.19) + +project( + LaunchDarklyCPPServerSDKTestHarness + VERSION 0.1 + DESCRIPTION "LaunchDarkly CPP Server-side SDK Test Harness" + LANGUAGES CXX +) + +include(${CMAKE_FILES}/json.cmake) + +add_executable(server-tests + src/main.cpp + src/server.cpp + src/session.cpp + src/entity_manager.cpp + src/client_entity.cpp + ) + +target_link_libraries(server-tests PRIVATE + launchdarkly::server + launchdarkly::internal + foxy + nlohmann_json::nlohmann_json + Boost::coroutine + contract-test-data-model + ) + +target_include_directories(server-tests PUBLIC include) diff --git a/contract-tests/server-contract-tests/README.md b/contract-tests/server-contract-tests/README.md new file mode 100644 index 000000000..d59cbd039 --- /dev/null +++ b/contract-tests/server-contract-tests/README.md @@ -0,0 +1,35 @@ +## SDK contract tests + +Contract tests have a "test service" on one side, and the "test harness" on +the other. + +This project implements the test service for the C++ Server-side SDK. + +**session (session.hpp)** + +This provides a simple REST API for creating/destroying +test entities. Examples: + +`GET /` - returns the capabilities of this service. + +`DELETE /` - shutdown the service. + +`POST /` - create a new test entity, and return its ID. + +`DELETE /entity/1` - delete the an entity identified by `1`. + +**entity manager (entity_manager.hpp)** + +This manages "entities", which are unique instances of the SDK client. + +**definitions (definitions.hpp)** + +Contains JSON definitions that are used to communicate with the test harness. + +**server (server.hpp)** + +Glues everything together, mainly providing the TCP acceptor that spawns new sessions. + +**session (session.hpp)** + +Prepares HTTP responses based on the results of commands sent to entities. diff --git a/contract-tests/server-contract-tests/include/client_entity.hpp b/contract-tests/server-contract-tests/include/client_entity.hpp new file mode 100644 index 000000000..39b4a3be3 --- /dev/null +++ b/contract-tests/server-contract-tests/include/client_entity.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include +#include +#include + +class ClientEntity { + public: + explicit ClientEntity( + std::unique_ptr client); + + tl::expected Command(CommandParams params); + + private: + tl::expected Evaluate( + EvaluateFlagParams const&); + + tl::expected EvaluateDetail( + EvaluateFlagParams const&, + launchdarkly::Context const&); + + tl::expected EvaluateAll( + EvaluateAllFlagParams const&); + + tl::expected Identify( + IdentifyEventParams const&); + + tl::expected Custom(CustomEventParams const&); + + std::unique_ptr client_; +}; + +static tl::expected ContextConvert( + ContextConvertParams const&); + +static tl::expected ContextBuild( + ContextBuildParams const&); diff --git a/contract-tests/server-contract-tests/include/entity_manager.hpp b/contract-tests/server-contract-tests/include/entity_manager.hpp new file mode 100644 index 000000000..b99633c32 --- /dev/null +++ b/contract-tests/server-contract-tests/include/entity_manager.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include "client_entity.hpp" + +#include + +#include +#include + +#include + +#include +#include +#include +#include +#include + +class EventOutbox; + +class EntityManager { + std::unordered_map entities_; + + std::size_t counter_; + boost::asio::any_io_executor executor_; + + launchdarkly::Logger& logger_; + + public: + /** + * Create an entity manager, which can be used to create and destroy + * entities (SSE clients + event channel back to test harness). + * @param executor Executor. + * @param logger Logger. + */ + EntityManager(boost::asio::any_io_executor executor, + launchdarkly::Logger& logger); + /** + * Create an entity with the given configuration. + * @param params Config of the entity. + * @return An ID representing the entity, or none if the entity couldn't + * be created. + */ + std::optional create(ConfigParams const& params); + /** + * Destroy an entity with the given ID. + * @param id ID of the entity. + * @return True if the entity was found and destroyed. + */ + bool destroy(std::string const& id); + + tl::expected command( + std::string const& id, + CommandParams const& params); +}; diff --git a/contract-tests/server-contract-tests/include/server.hpp b/contract-tests/server-contract-tests/include/server.hpp new file mode 100644 index 000000000..655321f04 --- /dev/null +++ b/contract-tests/server-contract-tests/include/server.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include "entity_manager.hpp" + +#include + +#include +#include +#include +#include + +#include + +#include + +namespace net = boost::asio; // from + +using tcp = boost::asio::ip::tcp; // from + +class server { + EntityManager manager_; + launchdarkly::foxy::listener listener_; + std::vector caps_; + launchdarkly::Logger& logger_; + + public: + /** + * Constructs a server, which stands up a REST API at the given + * port and address. The server is ready to accept connections upon + * construction. + * @param ioc IO context. + * @param address Address to bind. + * @param port Port to bind. + * @param logger Logger. + */ + server(net::io_context& ioc, + std::string const& address, + unsigned short port, + launchdarkly::Logger& logger); + /** + * Advertise an optional test-harness capability, such as "comments". + * @param cap + */ + void add_capability(std::string cap); + + /** + * Shuts down the server. + */ + void shutdown(); +}; diff --git a/contract-tests/server-contract-tests/include/session.hpp b/contract-tests/server-contract-tests/include/session.hpp new file mode 100644 index 000000000..029a559e0 --- /dev/null +++ b/contract-tests/server-contract-tests/include/session.hpp @@ -0,0 +1,95 @@ +#pragma once + +#include + +#include "entity_manager.hpp" + +#include +#include +#include +#include + +#include + +namespace beast = boost::beast; // from +namespace http = beast::http; // from +namespace net = boost::asio; // from +using tcp = boost::asio::ip::tcp; // from + +class Session : boost::asio::coroutine { + public: + using Request = http::request; + using Response = http::response; + + struct Frame { + Request request_; + Response resp_; + }; + + /** + * Constructs a session, which provides a REST API. + * @param session The HTTP session. + * @param manager Manager through which entities can be created/destroyed. + * @param caps Test service capabilities to advertise. + * @param logger Logger. + */ + Session(launchdarkly::foxy::server_session& session, + EntityManager& manager, + std::vector& caps, + launchdarkly::Logger& logger); + + template + auto operator()(Self& self, + boost::system::error_code ec = {}, + std::size_t const bytes_transferred = 0) -> void { + using launchdarkly::LogLevel; + auto& f = *frame_; + + reenter(*this) { + while (true) { + f.resp_ = {}; + f.request_ = {}; + + yield session_.async_read(f.request_, std::move(self)); + if (ec) { + LD_LOG(logger_, LogLevel::kWarn) + << "session: read: " << ec.what(); + break; + } + + if (auto response = generate_response(f.request_)) { + f.resp_ = *response; + } else { + LD_LOG(logger_, LogLevel::kWarn) + << "session: shutdown requested by client"; + std::exit(0); + } + + yield session_.async_write(f.resp_, std::move(self)); + + if (ec) { + LD_LOG(logger_, LogLevel::kWarn) + << "session: write: " << ec.what(); + break; + } + + if (!f.request_.keep_alive()) { + break; + } + } + + return self.complete({}, 0); + } + } + + std::optional generate_response(Request& req); + + private: + launchdarkly::foxy::server_session& session_; + EntityManager& manager_; + std::unique_ptr frame_; + std::vector& caps_; + launchdarkly::Logger& logger_; +}; + +#include diff --git a/contract-tests/server-contract-tests/src/client_entity.cpp b/contract-tests/server-contract-tests/src/client_entity.cpp new file mode 100644 index 000000000..c8fdb40ca --- /dev/null +++ b/contract-tests/server-contract-tests/src/client_entity.cpp @@ -0,0 +1,393 @@ +#include "client_entity.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +using namespace launchdarkly::server_side; + +tl::expected ParseContext( + nlohmann::json value) { + boost::system::error_code ec; + auto boost_json_val = boost::json::parse(value.dump(), ec); + if (ec) { + return tl::make_unexpected(ec.what()); + } + + auto maybe_ctx = boost::json::value_to< + tl::expected>( + boost_json_val); + if (!maybe_ctx) { + return tl::make_unexpected( + launchdarkly::ErrorToString(maybe_ctx.error())); + } + + if (!maybe_ctx->Valid()) { + return tl::make_unexpected(maybe_ctx->errors()); + } + + return *maybe_ctx; +} + +ClientEntity::ClientEntity( + std::unique_ptr client) + : client_(std::move(client)) {} + +tl::expected ClientEntity::Identify( + IdentifyEventParams const& params) { + boost::system::error_code ec; + + auto maybe_ctx = ParseContext(params.context); + if (!maybe_ctx) { + return tl::make_unexpected(maybe_ctx.error()); + } + client_->Identify(*maybe_ctx); + return nlohmann::json{}; +} + +static void BuildContextFromParams(launchdarkly::ContextBuilder& builder, + ContextSingleParams const& single) { + auto& attrs = builder.Kind(single.kind.value_or("user"), single.key); + if (single.anonymous) { + attrs.Anonymous(*single.anonymous); + } + if (single.name) { + attrs.Name(*single.name); + } + + if (single._private) { + attrs.AddPrivateAttributes(*single._private); + } + + if (single.custom) { + for (auto const& [key, value] : *single.custom) { + auto maybe_attr = boost::json::value_to< + tl::expected>( + boost::json::parse(value.dump())); + if (maybe_attr) { + attrs.Set(key, *maybe_attr); + } + } + } +} + +tl::expected ContextBuild( + ContextBuildParams const& params) { + ContextResponse resp{}; + + auto builder = launchdarkly::ContextBuilder(); + + if (params.multi) { + for (auto const& single : *params.multi) { + BuildContextFromParams(builder, single); + } + } else { + BuildContextFromParams(builder, *params.single); + } + + auto ctx = builder.Build(); + if (!ctx.Valid()) { + resp.error = ctx.errors(); + return resp; + } + + resp.output = boost::json::serialize(boost::json::value_from(ctx)); + return resp; +} + +tl::expected ContextConvert( + ContextConvertParams const& params) { + ContextResponse resp{}; + + boost::system::error_code ec; + auto json_value = boost::json::parse(params.input, ec); + if (ec) { + resp.error = ec.what(); + return resp; + } + + auto maybe_ctx = boost::json::value_to< + tl::expected>( + json_value); + + if (!maybe_ctx) { + resp.error = launchdarkly::ErrorToString(maybe_ctx.error()); + return resp; + } + + if (!maybe_ctx->Valid()) { + resp.error = maybe_ctx->errors(); + return resp; + } + + resp.output = boost::json::serialize(boost::json::value_from(*maybe_ctx)); + return resp; +} + +tl::expected ClientEntity::Custom( + CustomEventParams const& params) { + auto data = + params.data + ? boost::json::value_to< + tl::expected>( + boost::json::parse(params.data->dump())) + : launchdarkly::Value::Null(); + + if (!data) { + return tl::make_unexpected("couldn't parse custom event data"); + } + + if (!params.context) { + return tl::make_unexpected("context is required"); + } + + auto maybe_ctx = ParseContext(*params.context); + if (!maybe_ctx) { + return tl::make_unexpected(maybe_ctx.error()); + } + + if (params.omitNullData.value_or(false) && !params.metricValue && + !params.data) { + client_->Track(*maybe_ctx, params.eventKey); + return nlohmann::json{}; + } + + if (!params.metricValue) { + client_->Track(*maybe_ctx, params.eventKey, std::move(*data)); + return nlohmann::json{}; + } + + client_->Track(*maybe_ctx, params.eventKey, std::move(*data), + *params.metricValue); + return nlohmann::json{}; +} + +tl::expected ClientEntity::EvaluateAll( + EvaluateAllFlagParams const& params) { + EvaluateAllFlagsResponse resp{}; + + boost::ignore_unused(params); + + if (!params.context) { + return tl::make_unexpected("context is required"); + } + + auto maybe_ctx = ParseContext(*params.context); + if (!maybe_ctx) { + return tl::make_unexpected(maybe_ctx.error()); + } + + AllFlagsState::Options options = AllFlagsState::Options::Default; + if (params.withReasons.value_or(false)) { + options |= AllFlagsState::Options::IncludeReasons; + } + if (params.clientSideOnly.value_or(false)) { + options |= AllFlagsState::Options::ClientSideOnly; + } + if (params.detailsOnlyForTrackedFlags.value_or(false)) { + options |= AllFlagsState::Options::DetailsOnlyForTrackedFlags; + } + + auto state = client_->AllFlagsState(*maybe_ctx, options); + + resp.state = nlohmann::json::parse( + boost::json::serialize(boost::json::value_from(state))); + + return resp; +} + +tl::expected ClientEntity::EvaluateDetail( + EvaluateFlagParams const& params, + launchdarkly::Context const& ctx) { + auto const& key = params.flagKey; + + auto const& defaultVal = params.defaultValue; + + EvaluateFlagResponse result; + + std::optional reason; + + switch (params.valueType) { + case ValueType::Bool: { + auto detail = + client_->BoolVariationDetail(ctx, key, defaultVal.get()); + result.value = *detail; + reason = detail.Reason(); + result.variationIndex = detail.VariationIndex(); + break; + } + case ValueType::Int: { + auto detail = + client_->IntVariationDetail(ctx, key, defaultVal.get()); + result.value = *detail; + reason = detail.Reason(); + result.variationIndex = detail.VariationIndex(); + break; + } + case ValueType::Double: { + auto detail = client_->DoubleVariationDetail( + ctx, key, defaultVal.get()); + result.value = *detail; + reason = detail.Reason(); + result.variationIndex = detail.VariationIndex(); + break; + } + case ValueType::String: { + auto detail = client_->StringVariationDetail( + ctx, key, defaultVal.get()); + result.value = *detail; + reason = detail.Reason(); + result.variationIndex = detail.VariationIndex(); + break; + } + case ValueType::Any: + case ValueType::Unspecified: { + auto maybe_fallback = boost::json::value_to< + tl::expected>( + boost::json::parse(defaultVal.dump())); + if (!maybe_fallback) { + return tl::make_unexpected("unable to parse fallback value"); + } + + /* This switcharoo from nlohmann/json to boost/json to Value, then + * back is because we're using nlohmann/json for the test harness + * protocol, but boost::json in the SDK. We could swap over to + * boost::json entirely here to remove the awkwardness. */ + + auto detail = + client_->JsonVariationDetail(ctx, key, *maybe_fallback); + + auto serialized = + boost::json::serialize(boost::json::value_from(*detail)); + + result.value = nlohmann::json::parse(serialized); + reason = detail.Reason(); + result.variationIndex = detail.VariationIndex(); + break; + } + default: + return tl::make_unexpected("unknown variation type"); + } + + result.reason = + reason.has_value() + ? std::make_optional(nlohmann::json::parse( + boost::json::serialize(boost::json::value_from(*reason)))) + : std::nullopt; + + return result; +} +tl::expected ClientEntity::Evaluate( + EvaluateFlagParams const& params) { + auto maybe_ctx = ParseContext(params.context); + if (!maybe_ctx) { + return tl::make_unexpected(maybe_ctx.error()); + } + + if (params.detail) { + return EvaluateDetail(params, *maybe_ctx); + } + + auto const& key = params.flagKey; + + auto const& defaultVal = params.defaultValue; + + EvaluateFlagResponse result; + + switch (params.valueType) { + case ValueType::Bool: + result.value = + client_->BoolVariation(*maybe_ctx, key, defaultVal.get()); + break; + case ValueType::Int: + result.value = + client_->IntVariation(*maybe_ctx, key, defaultVal.get()); + break; + case ValueType::Double: + result.value = client_->DoubleVariation(*maybe_ctx, key, + defaultVal.get()); + break; + case ValueType::String: { + result.value = client_->StringVariation( + *maybe_ctx, key, defaultVal.get()); + break; + } + case ValueType::Any: + case ValueType::Unspecified: { + auto maybe_fallback = boost::json::value_to< + tl::expected>( + boost::json::parse(defaultVal.dump())); + if (!maybe_fallback) { + return tl::make_unexpected("unable to parse fallback value"); + } + /* This switcharoo from nlohmann/json to boost/json to Value, then + * back is because we're using nlohmann/json for the test harness + * protocol, but boost::json in the SDK. We could swap over to + * boost::json entirely here to remove the awkwardness. */ + + auto evaluation = + client_->JsonVariation(*maybe_ctx, key, *maybe_fallback); + + auto serialized = + boost::json::serialize(boost::json::value_from(evaluation)); + + result.value = nlohmann::json::parse(serialized); + break; + } + default: + return tl::make_unexpected("unknown variation type"); + } + + return result; +} +tl::expected ClientEntity::Command( + CommandParams params) { + switch (params.command) { + case Command::Unknown: + return tl::make_unexpected("unknown command"); + case Command::EvaluateFlag: + if (!params.evaluate) { + return tl::make_unexpected("evaluate params must be set"); + } + return Evaluate(*params.evaluate); + case Command::EvaluateAllFlags: + if (!params.evaluateAll) { + return tl::make_unexpected("evaluateAll params must be set"); + } + return EvaluateAll(*params.evaluateAll); + case Command::IdentifyEvent: + if (!params.identifyEvent) { + return tl::make_unexpected("identifyEvent params must be set"); + } + return Identify(*params.identifyEvent); + case Command::CustomEvent: + if (!params.customEvent) { + return tl::make_unexpected("customEvent params must be set"); + } + return Custom(*params.customEvent); + case Command::FlushEvents: + client_->FlushAsync(); + return nlohmann::json{}; + case Command::ContextBuild: + if (!params.contextBuild) { + return tl::make_unexpected("contextBuild params must be set"); + } + return ContextBuild(*params.contextBuild); + case Command::ContextConvert: + if (!params.contextConvert) { + return tl::make_unexpected("contextConvert params must be set"); + } + return ContextConvert(*params.contextConvert); + } + return tl::make_unexpected("unrecognized command"); +} diff --git a/contract-tests/server-contract-tests/src/entity_manager.cpp b/contract-tests/server-contract-tests/src/entity_manager.cpp new file mode 100644 index 000000000..8e8235036 --- /dev/null +++ b/contract-tests/server-contract-tests/src/entity_manager.cpp @@ -0,0 +1,153 @@ +#include "entity_manager.hpp" +#include + +#include +#include +#include + +using launchdarkly::LogLevel; +using namespace launchdarkly::server_side; + +EntityManager::EntityManager(boost::asio::any_io_executor executor, + launchdarkly::Logger& logger) + : counter_{0}, executor_{std::move(executor)}, logger_{logger} {} + +std::optional EntityManager::create(ConfigParams const& in) { + std::string id = std::to_string(counter_++); + + auto config_builder = ConfigBuilder(in.credential); + + auto default_endpoints = + launchdarkly::server_side::Defaults::ServiceEndpoints(); + + auto& endpoints = + config_builder.ServiceEndpoints() + .EventsBaseUrl(default_endpoints.EventsBaseUrl()) + .PollingBaseUrl(default_endpoints.PollingBaseUrl()) + .StreamingBaseUrl(default_endpoints.StreamingBaseUrl()); + + if (in.serviceEndpoints) { + if (in.serviceEndpoints->streaming) { + endpoints.StreamingBaseUrl(*in.serviceEndpoints->streaming); + } + if (in.serviceEndpoints->polling) { + endpoints.PollingBaseUrl(*in.serviceEndpoints->polling); + } + if (in.serviceEndpoints->events) { + endpoints.EventsBaseUrl(*in.serviceEndpoints->events); + } + } + + auto& datasource = config_builder.DataSource(); + + if (in.streaming) { + if (in.streaming->baseUri) { + endpoints.StreamingBaseUrl(*in.streaming->baseUri); + } + if (in.streaming->initialRetryDelayMs) { + auto streaming = DataSourceBuilder::Streaming(); + streaming.InitialReconnectDelay( + std::chrono::milliseconds(*in.streaming->initialRetryDelayMs)); + datasource.Method(std::move(streaming)); + } + } + + if (in.polling) { + if (in.polling->baseUri) { + endpoints.PollingBaseUrl(*in.polling->baseUri); + } + if (!in.streaming) { + auto method = DataSourceBuilder::Polling(); + if (in.polling->pollIntervalMs) { + method.PollInterval( + std::chrono::duration_cast( + std::chrono::milliseconds( + *in.polling->pollIntervalMs))); + } + datasource.Method(std::move(method)); + } + } + + auto& event_config = config_builder.Events(); + + if (in.events) { + ConfigEventParams const& events = *in.events; + + if (events.baseUri) { + endpoints.EventsBaseUrl(*events.baseUri); + } + + if (events.allAttributesPrivate) { + event_config.AllAttributesPrivate(*events.allAttributesPrivate); + } + + if (!events.globalPrivateAttributes.empty()) { + launchdarkly::AttributeReference::SetType attrs( + events.globalPrivateAttributes.begin(), + events.globalPrivateAttributes.end()); + event_config.PrivateAttributes(std::move(attrs)); + } + + if (events.capacity) { + event_config.Capacity(*events.capacity); + } + + if (events.flushIntervalMs) { + event_config.FlushInterval( + std::chrono::milliseconds(*events.flushIntervalMs)); + } + + } else { + event_config.Disable(); + } + + if (in.tags) { + if (in.tags->applicationId) { + config_builder.AppInfo().Identifier(*in.tags->applicationId); + } + if (in.tags->applicationVersion) { + config_builder.AppInfo().Version(*in.tags->applicationVersion); + } + } + + auto config = config_builder.Build(); + if (!config) { + LD_LOG(logger_, LogLevel::kWarn) + << "entity_manager: couldn't build config: " << config.error(); + return std::nullopt; + } + + auto client = std::make_unique(std::move(*config)); + + std::chrono::milliseconds waitForClient = std::chrono::seconds(5); + if (in.startWaitTimeMs) { + waitForClient = std::chrono::milliseconds(*in.startWaitTimeMs); + } + + auto init = client->StartAsync(); + init.wait_for(waitForClient); + + entities_.try_emplace(id, std::move(client)); + + return id; +} + +bool EntityManager::destroy(std::string const& id) { + auto it = entities_.find(id); + if (it == entities_.end()) { + return false; + } + + entities_.erase(it); + return true; +} + +tl::expected EntityManager::command( + std::string const& id, + CommandParams const& params) { + auto it = entities_.find(id); + if (it == entities_.end()) { + return tl::make_unexpected("entity not found"); + } + return it->second.Command(params); +} diff --git a/contract-tests/server-contract-tests/src/main.cpp b/contract-tests/server-contract-tests/src/main.cpp new file mode 100644 index 000000000..61245d1af --- /dev/null +++ b/contract-tests/server-contract-tests/src/main.cpp @@ -0,0 +1,66 @@ +#include "server.hpp" + +#include + +#include +#include +#include +#include +#include + +#include + +namespace net = boost::asio; +namespace beast = boost::beast; + +using launchdarkly::logging::ConsoleBackend; + +using launchdarkly::LogLevel; + +int main(int argc, char* argv[]) { + launchdarkly::Logger logger{ + std::make_unique("server-contract-tests")}; + + const std::string default_port = "8123"; + std::string port = default_port; + if (argc == 2) { + port = + argv[1]; // NOLINT(cppcoreguidelines-pro-bounds-pointer-arithmetic) + } + + try { + net::io_context ioc{1}; + + auto p = boost::lexical_cast(port); + server srv(ioc, "0.0.0.0", p, logger); + + srv.add_capability("server-side"); + srv.add_capability("strongly-typed"); + srv.add_capability("context-type"); + srv.add_capability("service-endpoints"); + srv.add_capability("tags"); + srv.add_capability("server-side-polling"); + + net::signal_set signals{ioc, SIGINT, SIGTERM}; + + boost::asio::spawn(ioc.get_executor(), [&](auto yield) mutable { + signals.async_wait(yield); + LD_LOG(logger, LogLevel::kInfo) << "shutting down.."; + srv.shutdown(); + }); + + ioc.run(); + LD_LOG(logger, LogLevel::kInfo) << "bye!"; + + } catch (boost::bad_lexical_cast&) { + LD_LOG(logger, LogLevel::kError) + << "invalid port (" << port + << "), provide a number (no arguments defaults " + "to port " + << default_port << ")"; + return EXIT_FAILURE; + } catch (std::exception const& e) { + LD_LOG(logger, LogLevel::kError) << e.what(); + return EXIT_FAILURE; + } +} diff --git a/contract-tests/server-contract-tests/src/server.cpp b/contract-tests/server-contract-tests/src/server.cpp new file mode 100644 index 000000000..b7c0a8b88 --- /dev/null +++ b/contract-tests/server-contract-tests/src/server.cpp @@ -0,0 +1,34 @@ +#include "server.hpp" +#include "session.hpp" + +#include +#include +#include +#include + +using launchdarkly::LogLevel; + +server::server(net::io_context& ioc, + std::string const& address, + unsigned short port, + launchdarkly::Logger& logger) + : manager_(ioc.get_executor(), logger), + listener_{ioc.get_executor(), + tcp::endpoint(boost::asio::ip::make_address(address), port)}, + logger_{logger} { + LD_LOG(logger_, LogLevel::kInfo) + << "server: listening on " << address << ":" << port; + listener_.async_accept([this](auto& server) { + return Session(server, manager_, caps_, logger_); + }); +} + +void server::add_capability(std::string cap) { + LD_LOG(logger_, LogLevel::kDebug) + << "server: test capability: <" << cap << ">"; + caps_.push_back(std::move(cap)); +} + +void server::shutdown() { + listener_.shutdown(); +} diff --git a/contract-tests/server-contract-tests/src/session.cpp b/contract-tests/server-contract-tests/src/session.cpp new file mode 100644 index 000000000..16a643c50 --- /dev/null +++ b/contract-tests/server-contract-tests/src/session.cpp @@ -0,0 +1,146 @@ +#include "session.hpp" + +#include + +#include +#include + +#include + +const std::string kEntityPath = "/entity/"; + +namespace net = boost::asio; + +Session::Session(launchdarkly::foxy::server_session& session, + EntityManager& manager, + std::vector& caps, + launchdarkly::Logger& logger) + : session_(session), + frame_(std::make_unique()), + manager_(manager), + caps_(caps), + logger_(logger) {} + +std::optional Session::generate_response(Request& req) { + auto const bad_request = [&req](beast::string_view why) { + Response res{http::status::bad_request, req.version()}; + res.set(http::field::server, BOOST_BEAST_VERSION_STRING); + res.set(http::field::content_type, "application/json"); + res.keep_alive(req.keep_alive()); + res.body() = nlohmann::json{"error", why}.dump(); + res.prepare_payload(); + return res; + }; + + auto const not_found = [&req](beast::string_view target) { + Response res{http::status::not_found, req.version()}; + res.set(http::field::server, BOOST_BEAST_VERSION_STRING); + res.set(http::field::content_type, "text/html"); + res.keep_alive(req.keep_alive()); + res.body() = + "The resource '" + std::string(target) + "' was not found."; + res.prepare_payload(); + return res; + }; + + auto const server_error = [&req](beast::string_view what) { + Response res{http::status::internal_server_error, req.version()}; + res.set(http::field::server, BOOST_BEAST_VERSION_STRING); + res.set(http::field::content_type, "text/html"); + res.keep_alive(req.keep_alive()); + res.body() = "An error occurred: '" + std::string(what) + "'"; + res.prepare_payload(); + return res; + }; + + auto const capabilities_response = + [&req](std::vector const& caps, std::string const& name, + std::string const& version) { + Response res{http::status::ok, req.version()}; + res.set(http::field::content_type, "application/json"); + res.keep_alive(req.keep_alive()); + res.body() = nlohmann::json{ + {"capabilities", caps}, + {"name", name}, + {"clientVersion", + version}}.dump(); + res.prepare_payload(); + return res; + }; + + auto const create_entity_response = [&req](std::string const& id) { + Response res{http::status::ok, req.version()}; + res.keep_alive(req.keep_alive()); + res.set("Location", kEntityPath + id); + res.prepare_payload(); + return res; + }; + + auto const destroy_entity_response = [&req](bool erased) { + auto status = erased ? http::status::ok : http::status::not_found; + Response res{status, req.version()}; + res.keep_alive(req.keep_alive()); + res.prepare_payload(); + return res; + }; + + if (req.method() == http::verb::get && req.target() == "/") { + return capabilities_response(caps_, "cpp-server-sdk", launchdarkly::server_side::Client::Version()); + } + + if (req.method() == http::verb::head && req.target() == "/") { + return http::response{http::status::ok, + req.version()}; + } + + if (req.method() == http::verb::delete_ && req.target() == "/") { + return std::nullopt; + } + + if (req.method() == http::verb::post && req.target() == "/") { + try { + auto json = nlohmann::json::parse(req.body()); + auto params = json.get(); + if (auto entity_id = manager_.create(params.configuration)) { + return create_entity_response(*entity_id); + } + return server_error("couldn't create client entity"); + } catch (nlohmann::json::exception& e) { + return bad_request("unable to parse config JSON"); + } + } + + if (req.method() == http::verb::post && + req.target().starts_with(kEntityPath)) { + std::string entity_id = req.target(); + boost::erase_first(entity_id, kEntityPath); + + try { + auto json = nlohmann::json::parse(req.body()); + auto params = json.get(); + tl::expected res = + manager_.command(entity_id, params); + if (res.has_value()) { + auto response = http::response{ + http::status::ok, req.version()}; + response.body() = res->dump(); + response.prepare_payload(); + return response; + } else { + return bad_request(res.error()); + } + } catch (nlohmann::json::exception& e) { + return bad_request("unable to parse config JSON"); + } + } + + if (req.method() == http::verb::delete_ && + req.target().starts_with(kEntityPath)) { + std::string entity_id = req.target(); + boost::erase_first(entity_id, kEntityPath); + bool erased = manager_.destroy(entity_id); + return destroy_entity_response(erased); + } + + return not_found(req.target()); +} diff --git a/contract-tests/server-contract-tests/test-suppressions.txt b/contract-tests/server-contract-tests/test-suppressions.txt new file mode 100644 index 000000000..460d051db --- /dev/null +++ b/contract-tests/server-contract-tests/test-suppressions.txt @@ -0,0 +1,7 @@ +# SC-204387 +streaming/validation/drop and reconnect if stream event has malformed JSON/put event +streaming/validation/drop and reconnect if stream event has malformed JSON/patch event +streaming/validation/drop and reconnect if stream event has malformed JSON/delete event +streaming/validation/drop and reconnect if stream event has well-formed JSON not matching schema/put event +streaming/validation/drop and reconnect if stream event has well-formed JSON not matching schema/patch event +streaming/validation/drop and reconnect if stream event has well-formed JSON not matching schema/delete event diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 74d58b39a..138ec4942 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -1,2 +1,5 @@ add_subdirectory(hello-c-client) add_subdirectory(hello-cpp-client) +add_subdirectory(hello-cpp-server) +add_subdirectory(hello-c-server) +add_subdirectory(client-and-server-coexistence) diff --git a/examples/client-and-server-coexistence/CMakeLists.txt b/examples/client-and-server-coexistence/CMakeLists.txt new file mode 100644 index 000000000..92b393183 --- /dev/null +++ b/examples/client-and-server-coexistence/CMakeLists.txt @@ -0,0 +1,15 @@ +# Required for Apple Silicon support. +cmake_minimum_required(VERSION 3.19) + +project( + LaunchDarklyCClientAndServerCoexistence + VERSION 0.1 + DESCRIPTION "LaunchDarkly C Client-side and Server-side SDK coexistence in same application" + LANGUAGES C +) + +set(THREADS_PREFER_PTHREAD_FLAG ON) +find_package(Threads REQUIRED) + +add_executable(c-client-and-server main.c) +target_link_libraries(c-client-and-server PRIVATE launchdarkly::client launchdarkly::server Threads::Threads) diff --git a/examples/client-and-server-coexistence/main.c b/examples/client-and-server-coexistence/main.c new file mode 100644 index 000000000..d162289c9 --- /dev/null +++ b/examples/client-and-server-coexistence/main.c @@ -0,0 +1,50 @@ +/** + * This application intends to verify that the symbols from the + * client-side and server-side SDKs do not clash, thus enabling both SDKs to be + * used within the same application. + */ + +#include +#include + +#include +#include + +#include + +#include + +int main() { + LDContextBuilder context_builder = LDContextBuilder_New(); + LDContextBuilder_AddKind(context_builder, "user", "example-user-key"); + LDContextBuilder_Attributes_SetName(context_builder, "user", "Sandy"); + LDContext context = LDContextBuilder_Build(context_builder); + + LDClientConfigBuilder client_config_builder = + LDClientConfigBuilder_New("foo"); + LDClientConfig client_config = NULL; + + LDStatus client_config_status = + LDClientConfigBuilder_Build(client_config_builder, &client_config); + + if (LDStatus_Ok(client_config_status)) { + LDClientSDK client_sdk = LDClientSDK_New(client_config, context); + printf("Created client SDK\n"); + LDClientSDK_Free(client_sdk); + } + + LDServerConfigBuilder server_config_builder = + LDServerConfigBuilder_New("foo"); + LDServerConfig server_config = NULL; + + LDStatus server_config_status = + LDServerConfigBuilder_Build(server_config_builder, &server_config); + + if (LDStatus_Ok(server_config_status)) { + LDServerSDK server_sdk = LDServerSDK_New(server_config); + printf("Created server SDK\n"); + LDServerSDK_Free(server_sdk); + } + + return 0; +} diff --git a/examples/hello-c-client/CMakeLists.txt b/examples/hello-c-client/CMakeLists.txt index c22cd5614..f8fc4ac90 100644 --- a/examples/hello-c-client/CMakeLists.txt +++ b/examples/hello-c-client/CMakeLists.txt @@ -4,12 +4,12 @@ cmake_minimum_required(VERSION 3.19) project( LaunchDarklyHelloCClient VERSION 0.1 - DESCRIPTION "LaunchDarkly Hello C Client" + DESCRIPTION "LaunchDarkly Hello C Client-side SDK" LANGUAGES C ) set(THREADS_PREFER_PTHREAD_FLAG ON) find_package(Threads REQUIRED) -add_executable(hello-c main.c) -target_link_libraries(hello-c PRIVATE launchdarkly::client Threads::Threads) +add_executable(hello-c-client main.c) +target_link_libraries(hello-c-client PRIVATE launchdarkly::client Threads::Threads) diff --git a/examples/hello-c-client/main.c b/examples/hello-c-client/main.c index b826ffe27..4915ff191 100644 --- a/examples/hello-c-client/main.c +++ b/examples/hello-c-client/main.c @@ -29,7 +29,7 @@ int main() { LDClientConfigBuilder config_builder = LDClientConfigBuilder_New(MOBILE_KEY); - LDClientConfig config; + LDClientConfig config = NULL; LDStatus config_status = LDClientConfigBuilder_Build(config_builder, &config); if (!LDStatus_Ok(config_status)) { @@ -44,7 +44,7 @@ int main() { LDClientSDK client = LDClientSDK_New(config, context); - bool initialized_successfully; + bool initialized_successfully = false; if (LDClientSDK_Start(client, INIT_TIMEOUT_MILLISECONDS, &initialized_successfully)) { if (initialized_successfully) { diff --git a/examples/hello-c-server/CMakeLists.txt b/examples/hello-c-server/CMakeLists.txt new file mode 100644 index 000000000..c3ea8e824 --- /dev/null +++ b/examples/hello-c-server/CMakeLists.txt @@ -0,0 +1,15 @@ +# Required for Apple Silicon support. +cmake_minimum_required(VERSION 3.19) + +project( + LaunchDarklyHelloCServer + VERSION 0.1 + DESCRIPTION "LaunchDarkly Hello C Server-side SDK" + LANGUAGES C +) + +set(THREADS_PREFER_PTHREAD_FLAG ON) +find_package(Threads REQUIRED) + +add_executable(hello-c-server main.c) +target_link_libraries(hello-c-server PRIVATE launchdarkly::server Threads::Threads) diff --git a/examples/hello-c-server/main.c b/examples/hello-c-server/main.c new file mode 100644 index 000000000..4b3f68fcd --- /dev/null +++ b/examples/hello-c-server/main.c @@ -0,0 +1,78 @@ +#include +#include + +#include + +#include +#include +#include +#include + +// Set SDK_KEY to your LaunchDarkly SKD key. +#define SDK_KEY "" + +// Set FEATURE_FLAG_KEY to the feature flag key you want to evaluate. +#define FEATURE_FLAG_KEY "my-boolean-flag" + +// Set INIT_TIMEOUT_MILLISECONDS to the amount of time you will wait for +// the client to become initialized. +#define INIT_TIMEOUT_MILLISECONDS 3000 + +int main() { + if (!strlen(SDK_KEY)) { + printf( + "*** Please edit main.c to set SDK_KEY to your LaunchDarkly " + "SDK key first\n\n"); + return 1; + } + + LDServerConfigBuilder config_builder = LDServerConfigBuilder_New(SDK_KEY); + + LDServerConfig config = NULL; + LDStatus config_status = + LDServerConfigBuilder_Build(config_builder, &config); + if (!LDStatus_Ok(config_status)) { + printf("error: config is invalid: %s", LDStatus_Error(config_status)); + return 1; + } + + LDServerSDK client = LDServerSDK_New(config); + + bool initialized_successfully = false; + if (LDServerSDK_Start(client, INIT_TIMEOUT_MILLISECONDS, + &initialized_successfully)) { + if (initialized_successfully) { + printf("*** SDK successfully initialized!\n\n"); + } else { + printf("*** SDK failed to initialize\n"); + return 1; + } + } else { + printf("SDK initialization didn't complete in %dms\n", + INIT_TIMEOUT_MILLISECONDS); + return 1; + } + + LDContextBuilder context_builder = LDContextBuilder_New(); + LDContextBuilder_AddKind(context_builder, "user", "example-user-key"); + LDContextBuilder_Attributes_SetName(context_builder, "user", "Sandy"); + LDContext context = LDContextBuilder_Build(context_builder); + + bool flag_value = + LDServerSDK_BoolVariation(client, context, FEATURE_FLAG_KEY, false); + + printf("*** Feature flag '%s' is %s for this user\n\n", FEATURE_FLAG_KEY, + flag_value ? "true" : "false"); + + // Here we ensure that the SDK shuts down cleanly and has a chance to + // deliver analytics events to LaunchDarkly before the program exits. If + // analytics events are not delivered, the user properties and flag usage + // statistics will not appear on your dashboard. In a normal long-running + // application, the SDK would continue running and events would be delivered + // automatically in the background. + + LDContext_Free(context); + LDServerSDK_Free(client); + + return 0; +} diff --git a/examples/hello-cpp-client/CMakeLists.txt b/examples/hello-cpp-client/CMakeLists.txt index 0b96fae34..c99ab3215 100644 --- a/examples/hello-cpp-client/CMakeLists.txt +++ b/examples/hello-cpp-client/CMakeLists.txt @@ -4,12 +4,12 @@ cmake_minimum_required(VERSION 3.19) project( LaunchDarklyHelloCPPClient VERSION 0.1 - DESCRIPTION "LaunchDarkly Hello CPP Client" + DESCRIPTION "LaunchDarkly Hello CPP Client-side SDK" LANGUAGES CXX ) set(THREADS_PREFER_PTHREAD_FLAG ON) find_package(Threads REQUIRED) -add_executable(hello-cpp main.cpp) -target_link_libraries(hello-cpp PRIVATE launchdarkly::client Threads::Threads) +add_executable(hello-cpp-client main.cpp) +target_link_libraries(hello-cpp-client PRIVATE launchdarkly::client Threads::Threads) diff --git a/examples/hello-cpp-client/main.cpp b/examples/hello-cpp-client/main.cpp index 5e0a2c4b7..99ed3c849 100644 --- a/examples/hello-cpp-client/main.cpp +++ b/examples/hello-cpp-client/main.cpp @@ -1,8 +1,8 @@ #include #include -#include #include +#include // Set MOBILE_KEY to your LaunchDarkly mobile key. #define MOBILE_KEY "" @@ -18,7 +18,7 @@ using namespace launchdarkly; int main() { if (!strlen(MOBILE_KEY)) { printf( - "*** Please edit main.c to set MOBILE_KEY to your LaunchDarkly " + "*** Please edit main.cpp to set MOBILE_KEY to your LaunchDarkly " "mobile key first\n\n"); return 1; } diff --git a/examples/hello-cpp-server/CMakeLists.txt b/examples/hello-cpp-server/CMakeLists.txt new file mode 100644 index 000000000..3c2d37cc0 --- /dev/null +++ b/examples/hello-cpp-server/CMakeLists.txt @@ -0,0 +1,15 @@ +# Required for Apple Silicon support. +cmake_minimum_required(VERSION 3.19) + +project( + LaunchDarklyHelloCPPServer + VERSION 0.1 + DESCRIPTION "LaunchDarkly Hello CPP Server-side SDK" + LANGUAGES CXX +) + +set(THREADS_PREFER_PTHREAD_FLAG ON) +find_package(Threads REQUIRED) + +add_executable(hello-cpp-server main.cpp) +target_link_libraries(hello-cpp-server PRIVATE launchdarkly::server Threads::Threads) diff --git a/examples/hello-cpp-server/main.cpp b/examples/hello-cpp-server/main.cpp new file mode 100644 index 000000000..1f975ded5 --- /dev/null +++ b/examples/hello-cpp-server/main.cpp @@ -0,0 +1,59 @@ +#include +#include + +#include +#include + +// Set SDK_KEY to your LaunchDarkly SDK key. +#define SDK_KEY "" + +// Set FEATURE_FLAG_KEY to the feature flag key you want to evaluate. +#define FEATURE_FLAG_KEY "my-boolean-flag" + +// Set INIT_TIMEOUT_MILLISECONDS to the amount of time you will wait for +// the client to become initialized. +#define INIT_TIMEOUT_MILLISECONDS 3000 + +using namespace launchdarkly; +int main() { + if (!strlen(SDK_KEY)) { + printf( + "*** Please edit main.cpp to set SDK_KEY to your LaunchDarkly " + "SDK key first\n\n"); + return 1; + } + + auto config = server_side::ConfigBuilder(SDK_KEY).Build(); + if (!config) { + std::cout << "error: config is invalid: " << config.error() << '\n'; + return 1; + } + + auto client = server_side::Client(std::move(*config)); + + auto start_result = client.StartAsync(); + auto status = start_result.wait_for( + std::chrono::milliseconds(INIT_TIMEOUT_MILLISECONDS)); + if (status == std::future_status::ready) { + if (start_result.get()) { + std::cout << "*** SDK successfully initialized!\n\n"; + } else { + std::cout << "*** SDK failed to initialize\n"; + return 1; + } + } else { + std::cout << "*** SDK initialization didn't complete in " + << INIT_TIMEOUT_MILLISECONDS << "ms\n"; + return 1; + } + + auto context = + ContextBuilder().Kind("user", "example-user-key").Name("Sandy").Build(); + + bool flag_value = client.BoolVariation(context, FEATURE_FLAG_KEY, false); + + std::cout << "*** Feature flag '" << FEATURE_FLAG_KEY << "' is " + << (flag_value ? "true" : "false") << " for this user\n\n"; + + return 0; +} diff --git a/libs/client-sdk/README.md b/libs/client-sdk/README.md index 4ec15f1c4..da2948e26 100644 --- a/libs/client-sdk/README.md +++ b/libs/client-sdk/README.md @@ -8,6 +8,8 @@ The LaunchDarkly Client-Side SDK for C/C++ is designed primarily for use in desk It follows the client-side LaunchDarkly model for single-user contexts (much like our mobile or JavaScript SDKs). It is not intended for use in multi-user systems such as web servers and applications. +For using LaunchDarkly in server-side C/C++ applications, refer to our [Server-Side C/C++ SDK](../server-sdk/README.md). + LaunchDarkly overview ------------------------- [LaunchDarkly](https://www.launchdarkly.com) is a feature management platform that serves trillions of feature flags @@ -85,28 +87,6 @@ This will expose the `launchdarkly::client` target. Next, link the target to you target_link_libraries(my-target PRIVATE launchdarkly::client) ``` -Various CMake options are available to customize the SDK build. - -| Option | Description | Default | Requires | -|---------------------------|----------------------------------------------------------------------------------------|--------------------|-------------------------------------------| -| `BUILD_TESTING` | Coarse-grained switch; turn off to disable all testing and only build the SDK targets. | On | N/A | -| `LD_BUILD_UNIT_TESTS` | Whether C++ unit tests are built. | On | `BUILD_TESTING; NOT LD_BUILD_SHARED_LIBS` | -| `LD_TESTING_SANITIZERS` | Whether sanitizers should be enabled. | On | `LD_BUILD_UNIT_TESTS` | -| `LD_BUILD_CONTRACT_TESTS` | Whether the contract test service (used in CI) is built. | Off | `BUILD_TESTING` | -| `LD_BUILD_EXAMPLES` | Whether example apps (hello world) are built. | On | N/A | -| `LD_BUILD_SHARED_LIBS` | Whether the SDK is built as a static or shared library. | Off (static lib) | N/A | -| `LD_DYNAMIC_LINK_OPENSSL` | Whether OpenSSL be dynamically linked. | Off (static link) | N/A | - -**Note:** _if building the SDK as a shared library, then unit tests won't be able to link correctly since the SDK's C++ -symbols aren't exposed. To run unit tests, build the SDK as a static library._ - -Example usage: - -```bash -# Build the SDK as a shared library -cmake -GNinja .. -DLD_BUILD_SHARED_LIBS=On -``` - Learn more ----------- diff --git a/libs/client-sdk/include/launchdarkly/client_side/bindings/c/config/config.h b/libs/client-sdk/include/launchdarkly/client_side/bindings/c/config/config.h index bb13b4dcd..1b8c154e4 100644 --- a/libs/client-sdk/include/launchdarkly/client_side/bindings/c/config/config.h +++ b/libs/client-sdk/include/launchdarkly/client_side/bindings/c/config/config.h @@ -14,8 +14,8 @@ extern "C" { // only need to export C interface if typedef struct _LDClientConfig* LDClientConfig; /** - * Frees an unused configuration. Configurations passed into an LDClient must - * not be be freed. + * Free an unused configuration. Configurations used to construct an LDClientSDK + * must not be be freed. * * @param config Config to free. */ diff --git a/libs/client-sdk/include/launchdarkly/client_side/bindings/c/sdk.h b/libs/client-sdk/include/launchdarkly/client_side/bindings/c/sdk.h index 91762e570..82c886603 100644 --- a/libs/client-sdk/include/launchdarkly/client_side/bindings/c/sdk.h +++ b/libs/client-sdk/include/launchdarkly/client_side/bindings/c/sdk.h @@ -8,10 +8,12 @@ #include #include +#include #include #include #include #include +#include #include #include @@ -26,9 +28,6 @@ extern "C" { // only need to export C interface if typedef struct _LDClientSDK* LDClientSDK; -#define LD_NONBLOCKING 0 -#define LD_DISCARD_DETAIL NULL - /** * Constructs a new client-side LaunchDarkly SDK from a configuration and * context. @@ -447,7 +446,6 @@ LDClientSDK_FlagNotifier_OnFlagChange(LDClientSDK sdk, struct LDFlagListener listener); typedef struct _LDDataSourceStatus* LDDataSourceStatus; -typedef struct _LDDataSourceStatus_ErrorInfo* LDDataSourceStatus_ErrorInfo; /** * Enumeration of possible data source states. @@ -503,40 +501,6 @@ enum LDDataSourceStatus_State { LD_DATASOURCESTATUS_STATE_SHUTDOWN = 4 }; -/** - * A description of an error condition that the data source encountered. - */ -enum LDDataSourceStatus_ErrorKind { - /** - * An unexpected error, such as an uncaught exception, further - * described by the error message. - */ - LD_DATASOURCESTATUS_ERRORKIND_UNKNOWN = 0, - - /** - * An I/O error such as a dropped connection. - */ - LD_DATASOURCESTATUS_ERRORKIND_NETWORK_ERROR = 1, - - /** - * The LaunchDarkly service returned an HTTP response with an error - * status, available in the status code. - */ - LD_DATASOURCESTATUS_ERRORKIND_ERROR_RESPONSE = 2, - - /** - * The SDK received malformed data from the LaunchDarkly service. - */ - LD_DATASOURCESTATUS_ERRORKIND_INVALID_DATA = 3, - - /** - * The data source itself is working, but when it tried to put an - * update into the data store, the data store failed (so the SDK may - * not have the latest data). - */ - LD_DATASOURCESTATUS_ERRORKIND_STORE_ERROR = 4, -}; - /** * Get an enumerated value representing the overall current state of the data * source. @@ -583,34 +547,6 @@ LDDataSourceStatus_GetLastError(LDDataSourceStatus status); */ LD_EXPORT(time_t) LDDataSourceStatus_StateSince(LDDataSourceStatus status); -/** - * Get an enumerated value representing the general category of the error. - */ -LD_EXPORT(enum LDDataSourceStatus_ErrorKind) -LDDataSourceStatus_ErrorInfo_GetKind(LDDataSourceStatus_ErrorInfo info); - -/** - * The HTTP status code if the error was - * LD_DATASOURCESTATUS_ERRORKIND_ERROR_RESPONSE. - */ -LD_EXPORT(uint64_t) -LDDataSourceStatus_ErrorInfo_StatusCode(LDDataSourceStatus_ErrorInfo info); - -/** - * Any additional human-readable information relevant to the error. - * - * The format is subject to change and should not be relied on - * programmatically. - */ -LD_EXPORT(char const*) -LDDataSourceStatus_ErrorInfo_Message(LDDataSourceStatus_ErrorInfo info); - -/** - * The date/time that the error occurred, in seconds since epoch. - */ -LD_EXPORT(time_t) -LDDataSourceStatus_ErrorInfo_Time(LDDataSourceStatus_ErrorInfo info); - typedef void (*DataSourceStatusCallbackFn)(LDDataSourceStatus status, void* user_data); @@ -685,13 +621,6 @@ LDClientSDK_DataSourceStatus_Status(LDClientSDK sdk); */ LD_EXPORT(void) LDDataSourceStatus_Free(LDDataSourceStatus status); -/** - * Frees the data source status error information. - * @param status The error information to free. - */ -LD_EXPORT(void) -LDDataSourceStatus_ErrorInfo_Free(LDDataSourceStatus_ErrorInfo info); - #ifdef __cplusplus } #endif diff --git a/libs/client-sdk/include/launchdarkly/client_side/data_source_status.hpp b/libs/client-sdk/include/launchdarkly/client_side/data_source_status.hpp index c1591509e..4c14572c5 100644 --- a/libs/client-sdk/include/launchdarkly/client_side/data_source_status.hpp +++ b/libs/client-sdk/include/launchdarkly/client_side/data_source_status.hpp @@ -5,202 +5,75 @@ #include #include #include -#include -#include +#include #include +#include namespace launchdarkly::client_side::data_sources { -class DataSourceStatus { - public: - using DateTime = std::chrono::time_point; - +/** + * Enumeration of possible data source states. + */ +enum class DataSourceState { /** - * Enumeration of possible data source states. + * The initial state of the data source when the SDK is being + * initialized. + * + * If it encounters an error that requires it to retry initialization, + * the state will remain at kInitializing until it either succeeds and + * becomes kValid, or permanently fails and becomes kShutdown. */ - enum class DataSourceState { - /** - * The initial state of the data source when the SDK is being - * initialized. - * - * If it encounters an error that requires it to retry initialization, - * the state will remain at kInitializing until it either succeeds and - * becomes kValid, or permanently fails and becomes kShutdown. - */ - kInitializing = 0, - - /** - * Indicates that the data source is currently operational and has not - * had any problems since the last time it received data. - * - * In streaming mode, this means that there is currently an open stream - * connection and that at least one initial message has been received on - * the stream. In polling mode, it means that the last poll request - * succeeded. - */ - kValid = 1, - - /** - * Indicates that the data source encountered an error that it will - * attempt to recover from. - * - * In streaming mode, this means that the stream connection failed, or - * had to be dropped due to some other error, and will be retried after - * a backoff delay. In polling mode, it means that the last poll request - * failed, and a new poll request will be made after the configured - * polling interval. - */ - kInterrupted = 2, - - /** - * Indicates that the application has told the SDK to stay offline. - */ - kSetOffline = 3, - - /** - * Indicates that the data source has been permanently shut down. - * - * This could be because it encountered an unrecoverable error (for - * instance, the LaunchDarkly service rejected the SDK key; an invalid - * SDK key will never become valid), or because the SDK client was - * explicitly shut down. - */ - kShutdown = 4, - - // BackgroundDisabled, - // TODO: A plugin of sorts would likely be required for some - // functionality like this. kNetworkUnavailable, - }; + kInitializing = 0, /** - * A description of an error condition that the data source encountered. + * Indicates that the data source is currently operational and has not + * had any problems since the last time it received data. + * + * In streaming mode, this means that there is currently an open stream + * connection and that at least one initial message has been received on + * the stream. In polling mode, it means that the last poll request + * succeeded. */ - class ErrorInfo { - public: - using StatusCodeType = uint64_t; - - /** - * An enumeration describing the general type of an error. - */ - enum class ErrorKind { - /** - * An unexpected error, such as an uncaught exception, further - * described by the error message. - */ - kUnknown = 0, - - /** - * An I/O error such as a dropped connection. - */ - kNetworkError = 1, - - /** - * The LaunchDarkly service returned an HTTP response with an error - * status, available in the status code. - */ - kErrorResponse = 2, - - /** - * The SDK received malformed data from the LaunchDarkly service. - */ - kInvalidData = 3, - - /** - * The data source itself is working, but when it tried to put an - * update into the data store, the data store failed (so the SDK may - * not have the latest data). - */ - kStoreError = 4 - }; - - /** - * An enumerated value representing the general category of the error. - */ - [[nodiscard]] ErrorKind Kind() const; - - /** - * The HTTP status code if the error was ErrorKind::kErrorResponse. - */ - [[nodiscard]] StatusCodeType StatusCode() const; - - /** - * Any additional human-readable information relevant to the error. - * - * The format is subject to change and should not be relied on - * programmatically. - */ - [[nodiscard]] std::string const& Message() const; - - /** - * The date/time that the error occurred. - */ - [[nodiscard]] DateTime Time() const; - - ErrorInfo(ErrorKind kind, - StatusCodeType status_code, - std::string message, - DateTime time); - - private: - ErrorKind kind_; - StatusCodeType status_code_; - std::string message_; - DateTime time_; - }; + kValid = 1, /** - * An enumerated value representing the overall current state of the data - * source. + * Indicates that the data source encountered an error that it will + * attempt to recover from. + * + * In streaming mode, this means that the stream connection failed, or + * had to be dropped due to some other error, and will be retried after + * a backoff delay. In polling mode, it means that the last poll request + * failed, and a new poll request will be made after the configured + * polling interval. */ - [[nodiscard]] DataSourceState State() const; + kInterrupted = 2, /** - * The date/time that the value of State most recently changed. - * - * The meaning of this depends on the current state: - * - For DataSourceState::kInitializing, it is the time that the SDK started - * initializing. - * - For DataSourceState::kValid, it is the time that the data - * source most recently entered a valid state, after previously having been - * DataSourceState::kInitializing or an invalid state such as - * DataSourceState::kInterrupted. - * - For DataSourceState::kInterrupted, it is the time that the data source - * most recently entered an error state, after previously having been - * DataSourceState::kValid. - * - For DataSourceState::kShutdown, it is the time that the data source - * encountered an unrecoverable error or that the SDK was explicitly shut - * down. + * Indicates that the application has told the SDK to stay offline. */ - [[nodiscard]] DateTime StateSince() const; + kSetOffline = 3, /** - * Information about the last error that the data source encountered, if - * any. + * Indicates that the data source has been permanently shut down. * - * This property should be updated whenever the data source encounters a - * problem, even if it does not cause the state to change. For instance, if - * a stream connection fails and the state changes to - * DataSourceState::kInterrupted, and then subsequent attempts to restart - * the connection also fail, the state will remain - * DataSourceState::kInterrupted but the error information will be updated - * each time-- and the last error will still be reported in this property - * even if the state later becomes DataSourceState::kValid. + * This could be because it encountered an unrecoverable error (for + * instance, the LaunchDarkly service rejected the SDK key; an invalid + * SDK key will never become valid), or because the SDK client was + * explicitly shut down. */ - [[nodiscard]] std::optional LastError() const; - - DataSourceStatus(DataSourceState state, - DateTime state_since, - std::optional last_error); + kShutdown = 4, - DataSourceStatus(DataSourceStatus const& status); + // BackgroundDisabled, - private: - DataSourceState state_; - DateTime state_since_; - std::optional last_error_; + // TODO: A plugin of sorts would likely be required to implement + // network availability. + // kNetworkUnavailable, }; +using DataSourceStatus = + common::data_sources::DataSourceStatusBase; + /** * Interface for accessing and listening to the data source status. */ @@ -210,7 +83,7 @@ class IDataSourceStatusProvider { * The current status of the data source. Suitable for broadcast to * data source status listeners. */ - virtual DataSourceStatus Status() const = 0; + [[nodiscard]] virtual DataSourceStatus Status() const = 0; /** * Listen to changes to the data source status. @@ -246,12 +119,6 @@ class IDataSourceStatusProvider { std::ostream& operator<<(std::ostream& out, DataSourceStatus::DataSourceState const& state); -std::ostream& operator<<(std::ostream& out, - DataSourceStatus::ErrorInfo::ErrorKind const& kind); - std::ostream& operator<<(std::ostream& out, DataSourceStatus const& status); -std::ostream& operator<<(std::ostream& out, - DataSourceStatus::ErrorInfo const& error); - } // namespace launchdarkly::client_side::data_sources diff --git a/libs/client-sdk/src/CMakeLists.txt b/libs/client-sdk/src/CMakeLists.txt index da88dac4f..7e1b894ce 100644 --- a/libs/client-sdk/src/CMakeLists.txt +++ b/libs/client-sdk/src/CMakeLists.txt @@ -15,38 +15,26 @@ target_sources(${LIBNAME} PRIVATE ${HEADER_LIST} data_sources/streaming_data_source.cpp data_sources/data_source_event_handler.cpp - data_sources/data_source_update_sink.cpp data_sources/polling_data_source.cpp flag_manager/flag_store.cpp flag_manager/flag_updater.cpp flag_manager/flag_change_event.cpp data_sources/data_source_status.cpp - data_sources/data_source_status_manager.cpp - event_processor/event_processor.cpp - event_processor/null_event_processor.cpp - boost_signal_connection.cpp client_impl.cpp client.cpp - boost_signal_connection.hpp client_impl.hpp - data_sources/data_source.hpp data_sources/data_source_event_handler.hpp data_sources/data_source_status_manager.hpp data_sources/data_source_update_sink.hpp data_sources/polling_data_source.hpp data_sources/streaming_data_source.hpp - event_processor/event_processor.hpp - event_processor/null_event_processor.hpp flag_manager/flag_store.hpp flag_manager/flag_updater.hpp - event_processor.hpp bindings/c/sdk.cpp bindings/c/builder.cpp bindings/c/config.cpp data_sources/null_data_source.cpp flag_manager/context_index.cpp - serialization/json_all_flags.hpp - serialization/json_all_flags.cpp flag_manager/flag_manager.cpp flag_manager/flag_persistence.cpp bindings/c/sdk.cpp diff --git a/libs/client-sdk/src/bindings/c/sdk.cpp b/libs/client-sdk/src/bindings/c/sdk.cpp index cb4f7c0e6..da070b98e 100644 --- a/libs/client-sdk/src/bindings/c/sdk.cpp +++ b/libs/client-sdk/src/bindings/c/sdk.cpp @@ -24,9 +24,6 @@ struct Detail; launchdarkly::client_side::data_sources::DataSourceStatus*>(ptr)) #define FROM_DATASOURCESTATUS(ptr) (reinterpret_cast(ptr)) -#define TO_DATASOURCESTATUS_ERRORINFO(ptr) \ - (reinterpret_cast(ptr)) #define FROM_DATASOURCESTATUS_ERRORINFO(ptr) \ (reinterpret_cast(ptr)) @@ -206,7 +203,6 @@ LDClientSDK_StringVariation(LDClientSDK sdk, LD_ASSERT_NOT_NULL(flag_key); LD_ASSERT_NOT_NULL(default_value); - // TODO: custom allocation / free routines return strdup( TO_SDK(sdk)->StringVariation(flag_key, default_value).c_str()); } @@ -377,37 +373,6 @@ LD_EXPORT(time_t) LDDataSourceStatus_StateSince(LDDataSourceStatus status) { .count(); } -LD_EXPORT(LDDataSourceStatus_ErrorKind) -LDDataSourceStatus_ErrorInfo_GetKind(LDDataSourceStatus_ErrorInfo info) { - LD_ASSERT_NOT_NULL(info); - - return static_cast( - TO_DATASOURCESTATUS_ERRORINFO(info)->Kind()); -} - -LD_EXPORT(uint64_t) -LDDataSourceStatus_ErrorInfo_StatusCode(LDDataSourceStatus_ErrorInfo info) { - LD_ASSERT_NOT_NULL(info); - - return TO_DATASOURCESTATUS_ERRORINFO(info)->StatusCode(); -} - -LD_EXPORT(char const*) -LDDataSourceStatus_ErrorInfo_Message(LDDataSourceStatus_ErrorInfo info) { - LD_ASSERT_NOT_NULL(info); - - return TO_DATASOURCESTATUS_ERRORINFO(info)->Message().c_str(); -} - -LD_EXPORT(time_t) -LDDataSourceStatus_ErrorInfo_Time(LDDataSourceStatus_ErrorInfo info) { - LD_ASSERT_NOT_NULL(info); - - return std::chrono::duration_cast( - TO_DATASOURCESTATUS_ERRORINFO(info)->Time().time_since_epoch()) - .count(); -} - LD_EXPORT(void) LDDataSourceStatusListener_Init(struct LDDataSourceStatusListener* listener) { listener->StatusChanged = nullptr; @@ -445,10 +410,5 @@ LD_EXPORT(void) LDDataSourceStatus_Free(LDDataSourceStatus status) { delete TO_DATASOURCESTATUS(status); } -LD_EXPORT(void) -LDDataSourceStatus_ErrorInfo_Free(LDDataSourceStatus_ErrorInfo info) { - delete TO_DATASOURCESTATUS_ERRORINFO(info); -} - // NOLINTEND cppcoreguidelines-pro-type-reinterpret-cast // NOLINTEND OCInconsistentNamingInspection diff --git a/libs/client-sdk/src/client_impl.cpp b/libs/client-sdk/src/client_impl.cpp index ed16cbe32..9d464673a 100644 --- a/libs/client-sdk/src/client_impl.cpp +++ b/libs/client-sdk/src/client_impl.cpp @@ -1,21 +1,19 @@ - -#include - -#include -#include - #include "client_impl.hpp" #include "data_sources/null_data_source.hpp" #include "data_sources/polling_data_source.hpp" #include "data_sources/streaming_data_source.hpp" -#include "event_processor/event_processor.hpp" -#include "event_processor/null_event_processor.hpp" +#include +#include #include #include #include +#include +#include +#include + namespace launchdarkly::client_side { // The ASIO implementation assumes that the io_context will be run from a @@ -32,14 +30,14 @@ using launchdarkly::client_side::data_sources::DataSourceStatus; using launchdarkly::config::shared::built::DataSourceConfig; using launchdarkly::config::shared::built::HttpProperties; -static std::shared_ptr MakeDataSource( - HttpProperties const& http_properties, - Config const& config, - Context const& context, - boost::asio::any_io_executor const& executor, - IDataSourceUpdateSink& flag_updater, - data_sources::DataSourceStatusManager& status_manager, - Logger& logger) { +static std::shared_ptr<::launchdarkly::data_sources::IDataSource> +MakeDataSource(HttpProperties const& http_properties, + Config const& config, + Context const& context, + boost::asio::any_io_executor const& executor, + IDataSourceUpdateSink& flag_updater, + data_sources::DataSourceStatusManager& status_manager, + Logger& logger) { if (config.Offline()) { return std::make_shared(executor, status_manager); @@ -50,7 +48,6 @@ static std::shared_ptr MakeDataSource( auto data_source_properties = builder.Build(); if (config.DataSourceConfig().method.index() == 0) { - // TODO: use initial reconnect delay. return std::make_shared< launchdarkly::client_side::data_sources::StreamingDataSource>( config.ServiceEndpoints(), config.DataSourceConfig(), @@ -112,14 +109,15 @@ ClientImpl::ClientImpl(Config config, flag_manager_.LoadCache(context_); if (config.Events().Enabled() && !config.Offline()) { - event_processor_ = std::make_unique( - ioc_.get_executor(), config.ServiceEndpoints(), config.Events(), - http_properties_, logger_); + event_processor_ = + std::make_unique>( + ioc_.get_executor(), config.ServiceEndpoints(), config.Events(), + http_properties_, logger_); } else { - event_processor_ = std::make_unique(); + event_processor_ = std::make_unique(); } - event_processor_->SendAsync(events::client::IdentifyEventParams{ + event_processor_->SendAsync(events::IdentifyEventParams{ std::chrono::system_clock::now(), context_}); run_thread_ = std::move(std::thread([&]() { ioc_.run(); })); @@ -143,7 +141,7 @@ static bool IsInitialized(DataSourceStatus::DataSourceState state) { std::future ClientImpl::IdentifyAsync(Context context) { UpdateContextSynchronized(context); flag_manager_.LoadCache(context); - event_processor_->SendAsync(events::client::IdentifyEventParams{ + event_processor_->SendAsync(events::IdentifyEventParams{ std::chrono::system_clock::now(), std::move(context)}); return StartAsyncInternal(IsInitializedSuccessfully); @@ -192,8 +190,8 @@ bool ClientImpl::Initialized() const { std::unordered_map ClientImpl::AllFlags() const { std::unordered_map result; for (auto& [key, descriptor] : flag_manager_.Store().GetAll()) { - if (descriptor->flag) { - result.try_emplace(key, descriptor->flag->Detail().Value()); + if (descriptor->item) { + result.try_emplace(key, descriptor->item->Detail().Value()); } } return result; @@ -234,7 +232,7 @@ EvaluationDetail ClientImpl::VariationInternal(FlagKey const& key, bool detailed) { auto desc = flag_manager_.Store().Get(key); - events::client::FeatureEventParams event = { + events::FeatureEventParams event = { std::chrono::system_clock::now(), key, ReadContextSynchronized([](Context const& c) { return c; }), @@ -247,13 +245,12 @@ EvaluationDetail ClientImpl::VariationInternal(FlagKey const& key, std::nullopt, }; - if (!desc || !desc->flag) { + if (!desc || !desc->item) { if (!Initialized()) { LD_LOG(logger_, LogLevel::kWarn) << "LaunchDarkly client has not yet been initialized. " "Returning default value"; - // TODO: SC-199918 auto error_reason = EvaluationReason(EvaluationReason::ErrorKind::kClientNotReady); if (eval_reasons_available_) { @@ -282,9 +279,9 @@ EvaluationDetail ClientImpl::VariationInternal(FlagKey const& key, "Returning cached value"; } - assert(desc->flag); + assert(desc->item); - auto const& flag = *(desc->flag); + auto const& flag = *(desc->item); auto const& detail = flag.Detail(); if (check_type && default_value.Type() != Value::Type::kNull && @@ -389,7 +386,7 @@ void ClientImpl::UpdateContextSynchronized(Context context) { ClientImpl::~ClientImpl() { ioc_.stop(); - // TODO: Probably not the best. + // TODO(SC-219101) run_thread_.join(); } diff --git a/libs/client-sdk/src/client_impl.hpp b/libs/client-sdk/src/client_impl.hpp index 20ab4cf5d..c273e2ad1 100644 --- a/libs/client-sdk/src/client_impl.hpp +++ b/libs/client-sdk/src/client_impl.hpp @@ -11,7 +11,7 @@ #include #include -#include "tl/expected.hpp" +#include #include #include @@ -19,13 +19,14 @@ #include #include #include +#include #include #include #include -#include "data_sources/data_source.hpp" +#include + #include "data_sources/data_source_status_manager.hpp" -#include "event_processor.hpp" #include "flag_manager/flag_manager.hpp" namespace launchdarkly::client_side { @@ -37,7 +38,7 @@ class ClientImpl : public IClient { ClientImpl(ClientImpl const&) = delete; ClientImpl& operator=(ClientImpl) = delete; ClientImpl& operator=(ClientImpl&& other) = delete; - + bool Initialized() const override; using FlagKey = std::string; @@ -129,11 +130,12 @@ class ClientImpl : public IClient { mutable std::shared_mutex context_mutex_; flag_manager::FlagManager flag_manager_; - std::function()> data_source_factory_; + std::function()> + data_source_factory_; - std::shared_ptr data_source_; + std::shared_ptr<::launchdarkly::data_sources::IDataSource> data_source_; - std::unique_ptr event_processor_; + std::unique_ptr event_processor_; mutable std::mutex init_mutex_; std::condition_variable init_waiter_; diff --git a/libs/client-sdk/src/data_sources/data_source_event_handler.cpp b/libs/client-sdk/src/data_sources/data_source_event_handler.cpp index a2a13a4fb..fe182d81d 100644 --- a/libs/client-sdk/src/data_sources/data_source_event_handler.cpp +++ b/libs/client-sdk/src/data_sources/data_source_event_handler.cpp @@ -1,8 +1,9 @@ #include "data_source_event_handler.hpp" -#include "../serialization/json_all_flags.hpp" #include #include +#include +#include #include #include @@ -35,13 +36,14 @@ static tl::expected tag_invoke( auto const& obj = json_value.as_object(); auto const* key_iter = obj.find("key"); auto key = ValueAsOpt(key_iter, obj.end()); - auto result = - boost::json::value_to>( - json_value); + auto result = boost::json::value_to< + tl::expected, JsonError>>( + json_value); - if (result.has_value() && key.has_value()) { + if (result.has_value() && result.value().has_value() && + key.has_value()) { return DataSourceEventHandler::PatchData{key.value(), - result.value()}; + result.value().value()}; } } return tl::unexpected(JsonError::kSchemaFailure); @@ -93,11 +95,15 @@ DataSourceEventHandler::MessageStatus DataSourceEventHandler::HandleMessage( return DataSourceEventHandler::MessageStatus::kInvalidMessage; } auto res = boost::json::value_to, JsonError>>( - parsed); + std::optional>, + JsonError>>(parsed); if (res.has_value()) { - handler_.Init(context_, res.value()); + // If the map was null or omitted, treat it like an empty data set. + auto map = res.value().value_or( + std::unordered_map{}); + + handler_.Init(context_, std::move(map)); status_manager_.SetState(DataSourceStatus::DataSourceState::kValid); return DataSourceEventHandler::MessageStatus::kMessageHandled; } diff --git a/libs/client-sdk/src/data_sources/data_source_event_handler.hpp b/libs/client-sdk/src/data_sources/data_source_event_handler.hpp index a7115f2aa..90866d480 100644 --- a/libs/client-sdk/src/data_sources/data_source_event_handler.hpp +++ b/libs/client-sdk/src/data_sources/data_source_event_handler.hpp @@ -2,13 +2,13 @@ #include -#include "data_source.hpp" #include "data_source_status_manager.hpp" #include "data_source_update_sink.hpp" #include #include #include +#include #include namespace launchdarkly::client_side::data_sources { diff --git a/libs/client-sdk/src/data_sources/data_source_status.cpp b/libs/client-sdk/src/data_sources/data_source_status.cpp index 56770c61d..437b5b357 100644 --- a/libs/client-sdk/src/data_sources/data_source_status.cpp +++ b/libs/client-sdk/src/data_sources/data_source_status.cpp @@ -1,59 +1,9 @@ - #include -#include #include namespace launchdarkly::client_side::data_sources { -DataSourceStatus::ErrorInfo::ErrorKind DataSourceStatus::ErrorInfo::Kind() - const { - return kind_; -} -DataSourceStatus::ErrorInfo::StatusCodeType -DataSourceStatus::ErrorInfo::StatusCode() const { - return status_code_; -} -std::string const& DataSourceStatus::ErrorInfo::Message() const { - return message_; -} -DataSourceStatus::DateTime DataSourceStatus::ErrorInfo::Time() const { - return time_; -} - -DataSourceStatus::ErrorInfo::ErrorInfo(ErrorInfo::ErrorKind kind, - ErrorInfo::StatusCodeType status_code, - std::string message, - DataSourceStatus::DateTime time) - : kind_(kind), - status_code_(status_code), - message_(std::move(message)), - time_(time) {} - -DataSourceStatus::DataSourceState DataSourceStatus::State() const { - return state_; -} - -DataSourceStatus::DateTime DataSourceStatus::StateSince() const { - return state_since_; -} - -std::optional DataSourceStatus::LastError() const { - return last_error_; -} - -DataSourceStatus::DataSourceStatus(DataSourceStatus const& status) - : state_(status.State()), - state_since_(status.StateSince()), - last_error_(status.LastError()) {} - -DataSourceStatus::DataSourceStatus(DataSourceState state, - DataSourceStatus::DateTime state_since, - std::optional last_error) - : state_(state), - state_since_(state_since), - last_error_(std::move(last_error)) {} - std::ostream& operator<<(std::ostream& out, DataSourceStatus::DataSourceState const& state) { switch (state) { @@ -77,28 +27,6 @@ std::ostream& operator<<(std::ostream& out, return out; } -std::ostream& operator<<(std::ostream& out, - DataSourceStatus::ErrorInfo::ErrorKind const& kind) { - switch (kind) { - case DataSourceStatus::ErrorInfo::ErrorKind::kUnknown: - out << "UNKNOWN"; - break; - case DataSourceStatus::ErrorInfo::ErrorKind::kNetworkError: - out << "NETWORK_ERROR"; - break; - case DataSourceStatus::ErrorInfo::ErrorKind::kErrorResponse: - out << "ERROR_RESPONSE"; - break; - case DataSourceStatus::ErrorInfo::ErrorKind::kInvalidData: - out << "INVALID_DATA"; - break; - case DataSourceStatus::ErrorInfo::ErrorKind::kStoreError: - out << "STORE_ERROR"; - break; - } - return out; -} - std::ostream& operator<<(std::ostream& out, DataSourceStatus const& status) { std::time_t as_time_t = std::chrono::system_clock::to_time_t(status.StateSince()); @@ -111,13 +39,4 @@ std::ostream& operator<<(std::ostream& out, DataSourceStatus const& status) { return out; } -std::ostream& operator<<(std::ostream& out, - DataSourceStatus::ErrorInfo const& error) { - std::time_t as_time_t = std::chrono::system_clock::to_time_t(error.Time()); - out << "Error(" << error.Kind() << ", " << error.Message() - << ", StatusCode(" << error.StatusCode() << "), Since(" - << std::put_time(std::gmtime(&as_time_t), "%Y-%m-%d %H:%M:%S") << "))"; - return out; -} - } // namespace launchdarkly::client_side::data_sources diff --git a/libs/client-sdk/src/data_sources/data_source_status_manager.cpp b/libs/client-sdk/src/data_sources/data_source_status_manager.cpp deleted file mode 100644 index c7f196868..000000000 --- a/libs/client-sdk/src/data_sources/data_source_status_manager.cpp +++ /dev/null @@ -1,128 +0,0 @@ -#include -#include -#include -#include - -#include - -#include "../boost_signal_connection.hpp" -#include "data_source_status_manager.hpp" - -namespace launchdarkly::client_side::data_sources { - -void DataSourceStatusManager::SetState( - DataSourceStatus::DataSourceState state) { - bool changed = UpdateState(state); - if (changed) { - data_source_status_signal_(std::move(Status())); - } -} - -void DataSourceStatusManager::SetState( - DataSourceStatus::DataSourceState state, - DataSourceStatus::ErrorInfo::StatusCodeType code, - std::string message) { - { - std::lock_guard lock(status_mutex_); - - UpdateState(state); - - last_error_ = DataSourceStatus::ErrorInfo( - DataSourceStatus::ErrorInfo::ErrorKind::kErrorResponse, code, - message, std::chrono::system_clock::now()); - } - - data_source_status_signal_(std::move(Status())); -} -bool DataSourceStatusManager::UpdateState( - DataSourceStatus::DataSourceState const& requested_state) { - std::lock_guard lock(status_mutex_); - - // If initializing, then interruptions remain initializing. - auto new_state = - (requested_state == DataSourceStatus::DataSourceState::kInterrupted && - state_ == DataSourceStatus::DataSourceState::kInitializing) - ? DataSourceStatus::DataSourceState:: - kInitializing // see comment on - // IDataSourceUpdateSink.UpdateStatus - : requested_state; - auto changed = state_ != new_state; - if (changed) { - state_ = new_state; - state_since_ = std::chrono::system_clock::now(); - } - return changed; -} - -void DataSourceStatusManager::SetState( - DataSourceStatus::DataSourceState state, - DataSourceStatus::ErrorInfo::ErrorKind kind, - std::string message) { - { - std::lock_guard lock(status_mutex_); - - UpdateState(state); - - last_error_ = DataSourceStatus::ErrorInfo( - kind, 0, std::move(message), std::chrono::system_clock::now()); - } - - data_source_status_signal_(Status()); -} - -void DataSourceStatusManager::SetError( - DataSourceStatus::ErrorInfo::ErrorKind kind, - std::string message) { - { - std::lock_guard lock(status_mutex_); - last_error_ = DataSourceStatus::ErrorInfo( - kind, 0, std::move(message), std::chrono::system_clock::now()); - state_since_ = std::chrono::system_clock::now(); - } - - data_source_status_signal_(Status()); -} - -void DataSourceStatusManager::SetError( - DataSourceStatus::ErrorInfo::StatusCodeType code, - std::string message) { - { - std::lock_guard lock(status_mutex_); - last_error_ = DataSourceStatus::ErrorInfo( - DataSourceStatus::ErrorInfo::ErrorKind::kErrorResponse, code, - message, std::chrono::system_clock::now()); - state_since_ = std::chrono::system_clock::now(); - } - data_source_status_signal_(Status()); -} - -DataSourceStatus DataSourceStatusManager::Status() const { - std::lock_guard lock(status_mutex_); - return {state_, state_since_, last_error_}; -} - -std::unique_ptr DataSourceStatusManager::OnDataSourceStatusChange( - std::function handler) { - std::lock_guard lock{status_mutex_}; - return std::make_unique< ::launchdarkly::client_side::SignalConnection>( - data_source_status_signal_.connect(handler)); -} - -std::unique_ptr -DataSourceStatusManager::OnDataSourceStatusChangeEx( - std::function handler) { - std::lock_guard lock{status_mutex_}; - return std::make_unique< ::launchdarkly::client_side::SignalConnection>( - data_source_status_signal_.connect_extended( - [handler](boost::signals2::connection const& conn, - data_sources::DataSourceStatus status) { - if (handler(status)) { - conn.disconnect(); - } - })); -} -DataSourceStatusManager::DataSourceStatusManager() - : state_(DataSourceStatus::DataSourceState::kInitializing), - state_since_(std::chrono::system_clock::now()) {} - -} // namespace launchdarkly::client_side::data_sources diff --git a/libs/client-sdk/src/data_sources/data_source_status_manager.hpp b/libs/client-sdk/src/data_sources/data_source_status_manager.hpp index 62bab885c..b2b52eb4e 100644 --- a/libs/client-sdk/src/data_sources/data_source_status_manager.hpp +++ b/libs/client-sdk/src/data_sources/data_source_status_manager.hpp @@ -7,89 +7,22 @@ #include #include +#include namespace launchdarkly::client_side::data_sources { -/** - * Class that manages updates to the data source status and implements an - * interface to get the current status and listen to status changes. - */ -class DataSourceStatusManager : public IDataSourceStatusProvider { +class DataSourceStatusManager + : public internal::data_sources::DataSourceStatusManagerBase< + DataSourceStatus, + IDataSourceStatusProvider> { public: - using DataSourceStatusHandler = - std::function; + DataSourceStatusManager() = default; - /** - * Set the state. - * - * @param state The new state. - */ - void SetState(DataSourceStatus::DataSourceState state); - - /** - * If an error and state change happen simultaneously, then they should - * be updated simultaneously. - * - * @param state The new state. - * @param code Status code for an http error. - * @param message The message to associate with the error. - */ - void SetState(DataSourceStatus::DataSourceState state, - DataSourceStatus::ErrorInfo::StatusCodeType code, - std::string message); - - /** - * If an error and state change happen simultaneously, then they should - * be updated simultaneously. - * - * @param state The new state. - * @param kind The error kind. - * @param message The message to associate with the error. - */ - void SetState(DataSourceStatus::DataSourceState state, - DataSourceStatus::ErrorInfo::ErrorKind kind, - std::string message); - - /** - * Set an error with the given kind and message. - * - * For ErrorInfo::ErrorKind::kErrorResponse use the - * SetError(ErrorInfo::StatusCodeType) method. - * @param kind The kind of the error. - * @param message A message for the error. - */ - void SetError(DataSourceStatus::ErrorInfo::ErrorKind kind, - std::string message); - - /** - * Set an error based on the given status code. - * @param code The status code of the error. - */ - void SetError(DataSourceStatus::ErrorInfo::StatusCodeType code, - std::string message); - // TODO: Handle error codes once the EventSource supports it. sc-204392 - - DataSourceStatus Status() const override; - - std::unique_ptr OnDataSourceStatusChange( - std::function handler) - override; - - std::unique_ptr OnDataSourceStatusChangeEx( - std::function handler) - override; - - DataSourceStatusManager(); - - private: - DataSourceStatus::DataSourceState state_; - DataSourceStatus::DateTime state_since_; - std::optional last_error_; - - boost::signals2::signal - data_source_status_signal_; - mutable std::recursive_mutex status_mutex_; - bool UpdateState(DataSourceStatus::DataSourceState const& requested_state); + ~DataSourceStatusManager() override = default; + DataSourceStatusManager(DataSourceStatusManager const& item) = delete; + DataSourceStatusManager(DataSourceStatusManager&& item) = delete; + DataSourceStatusManager& operator=(DataSourceStatusManager const&) = delete; + DataSourceStatusManager& operator=(DataSourceStatusManager&&) = delete; }; } // namespace launchdarkly::client_side::data_sources diff --git a/libs/client-sdk/src/data_sources/data_source_update_sink.cpp b/libs/client-sdk/src/data_sources/data_source_update_sink.cpp deleted file mode 100644 index cff337160..000000000 --- a/libs/client-sdk/src/data_sources/data_source_update_sink.cpp +++ /dev/null @@ -1,23 +0,0 @@ -#include "data_source_update_sink.hpp" - -namespace launchdarkly::client_side { - -bool operator==(ItemDescriptor const& lhs, ItemDescriptor const& rhs) { - return lhs.version == rhs.version && lhs.flag == rhs.flag; -} - -std::ostream& operator<<(std::ostream& out, ItemDescriptor const& descriptor) { - out << "{"; - out << " version: " << descriptor.version; - if (descriptor.flag.has_value()) { - out << " flag: " << descriptor.flag.value(); - } else { - out << " flag: "; - } - return out; -} -ItemDescriptor::ItemDescriptor(uint64_t version) : version(version) {} - -ItemDescriptor::ItemDescriptor(EvaluationResult flag) - : version(flag.Version()), flag(std::move(flag)) {} -} // namespace launchdarkly::client_side diff --git a/libs/client-sdk/src/data_sources/data_source_update_sink.hpp b/libs/client-sdk/src/data_sources/data_source_update_sink.hpp index b08d9daca..1149618c9 100644 --- a/libs/client-sdk/src/data_sources/data_source_update_sink.hpp +++ b/libs/client-sdk/src/data_sources/data_source_update_sink.hpp @@ -9,37 +9,11 @@ #include #include #include +#include namespace launchdarkly::client_side { -/** - * An item descriptor is an abstraction that allows for Flag data to be - * handled using the same type in both a put or a patch. - */ -struct ItemDescriptor { - /** - * The version number of this data, provided by the SDK. - */ - uint64_t version; - - /** - * The data item, or nullopt if this is a deleted item placeholder. - */ - std::optional flag; - - explicit ItemDescriptor(uint64_t version); - - explicit ItemDescriptor(EvaluationResult flag); - - ItemDescriptor(ItemDescriptor const& item) = default; - ItemDescriptor(ItemDescriptor&& item) = default; - ItemDescriptor& operator=(ItemDescriptor const&) = default; - ItemDescriptor& operator=(ItemDescriptor&&) = default; - ~ItemDescriptor() = default; - - friend std::ostream& operator<<(std::ostream& out, - ItemDescriptor const& descriptor); -}; +using ItemDescriptor = data_model::ItemDescriptor; /** * Interface for handling updates from LaunchDarkly. @@ -62,6 +36,4 @@ class IDataSourceUpdateSink { IDataSourceUpdateSink() = default; }; -bool operator==(ItemDescriptor const& lhs, ItemDescriptor const& rhs); - } // namespace launchdarkly::client_side diff --git a/libs/client-sdk/src/data_sources/null_data_source.hpp b/libs/client-sdk/src/data_sources/null_data_source.hpp index b99ec07f0..e2fa7cc22 100644 --- a/libs/client-sdk/src/data_sources/null_data_source.hpp +++ b/libs/client-sdk/src/data_sources/null_data_source.hpp @@ -2,12 +2,13 @@ #include -#include "data_source.hpp" #include "data_source_status_manager.hpp" +#include + namespace launchdarkly::client_side::data_sources { -class NullDataSource : public IDataSource { +class NullDataSource : public ::launchdarkly::data_sources::IDataSource { public: explicit NullDataSource(boost::asio::any_io_executor exec, DataSourceStatusManager& status_manager); diff --git a/libs/client-sdk/src/data_sources/polling_data_source.hpp b/libs/client-sdk/src/data_sources/polling_data_source.hpp index 53e7656c1..62874b1f5 100644 --- a/libs/client-sdk/src/data_sources/polling_data_source.hpp +++ b/libs/client-sdk/src/data_sources/polling_data_source.hpp @@ -4,7 +4,6 @@ #include -#include "data_source.hpp" #include "data_source_event_handler.hpp" #include "data_source_status_manager.hpp" #include "data_source_update_sink.hpp" @@ -12,13 +11,14 @@ #include #include #include +#include #include #include namespace launchdarkly::client_side::data_sources { class PollingDataSource - : public IDataSource, + : public ::launchdarkly::data_sources::IDataSource, public std::enable_shared_from_this { public: PollingDataSource( diff --git a/libs/client-sdk/src/data_sources/streaming_data_source.cpp b/libs/client-sdk/src/data_sources/streaming_data_source.cpp index 45de5a41c..fd8c4e29a 100644 --- a/libs/client-sdk/src/data_sources/streaming_data_source.cpp +++ b/libs/client-sdk/src/data_sources/streaming_data_source.cpp @@ -127,8 +127,6 @@ void StreamingDataSource::Start() { client_builder.header(header.first, header.second); } - // TODO: Handle proxy support. sc-204386 - auto weak_self = weak_from_this(); client_builder.receiver([weak_self](launchdarkly::sse::Event const& event) { diff --git a/libs/client-sdk/src/data_sources/streaming_data_source.hpp b/libs/client-sdk/src/data_sources/streaming_data_source.hpp index a8eec90af..430e77888 100644 --- a/libs/client-sdk/src/data_sources/streaming_data_source.hpp +++ b/libs/client-sdk/src/data_sources/streaming_data_source.hpp @@ -5,7 +5,6 @@ using namespace std::chrono_literals; #include -#include "data_source.hpp" #include "data_source_event_handler.hpp" #include "data_source_status_manager.hpp" #include "data_source_update_sink.hpp" @@ -16,13 +15,14 @@ using namespace std::chrono_literals; #include #include #include +#include #include #include namespace launchdarkly::client_side::data_sources { class StreamingDataSource final - : public IDataSource, + : public ::launchdarkly::data_sources::IDataSource, public std::enable_shared_from_this { public: StreamingDataSource( diff --git a/libs/client-sdk/src/event_processor/event_processor.cpp b/libs/client-sdk/src/event_processor/event_processor.cpp deleted file mode 100644 index ac78fa1c6..000000000 --- a/libs/client-sdk/src/event_processor/event_processor.cpp +++ /dev/null @@ -1,25 +0,0 @@ -#include "event_processor.hpp" - -namespace launchdarkly::client_side { - -EventProcessor::EventProcessor( - boost::asio::any_io_executor const& io, - config::shared::built::ServiceEndpoints const& endpoints, - config::shared::built::Events const& events_config, - config::shared::built::HttpProperties const& http_properties, - Logger& logger) - : impl_(io, endpoints, events_config, http_properties, logger) {} - -void EventProcessor::SendAsync(events::InputEvent event) { - impl_.AsyncSend(std::move(event)); -} - -void EventProcessor::FlushAsync() { - impl_.AsyncFlush(); -} - -void EventProcessor::ShutdownAsync() { - impl_.AsyncClose(); -} - -} // namespace launchdarkly::client_side diff --git a/libs/client-sdk/src/event_processor/event_processor.hpp b/libs/client-sdk/src/event_processor/event_processor.hpp deleted file mode 100644 index dd8d7b06d..000000000 --- a/libs/client-sdk/src/event_processor/event_processor.hpp +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once - -#include - -#include -#include -#include -#include - -#include "../event_processor.hpp" - -namespace launchdarkly::client_side { - -class EventProcessor : public IEventProcessor { - public: - EventProcessor(boost::asio::any_io_executor const& io, - config::shared::built::ServiceEndpoints const& endpoints, - config::shared::built::Events const& events_config, - config::shared::built::HttpProperties const& http_properties, - Logger& logger); - void SendAsync(events::InputEvent event) override; - void FlushAsync() override; - void ShutdownAsync() override; - - private: - events::AsioEventProcessor impl_; -}; - -} // namespace launchdarkly::client_side diff --git a/libs/client-sdk/src/flag_manager/flag_persistence.cpp b/libs/client-sdk/src/flag_manager/flag_persistence.cpp index 2ad273435..5423c7028 100644 --- a/libs/client-sdk/src/flag_manager/flag_persistence.cpp +++ b/libs/client-sdk/src/flag_manager/flag_persistence.cpp @@ -1,9 +1,12 @@ #include "flag_persistence.hpp" -#include "../serialization/json_all_flags.hpp" #include #include +#include +#include +#include + #include namespace launchdarkly::client_side::flag_manager { @@ -72,8 +75,7 @@ void FlagPersistence::LoadCached(Context const& context) { } auto res = boost::json::value_to, + std::optional>, JsonError>>(parsed); if (!res) { LD_LOG(logger_, LogLevel::kError) @@ -81,7 +83,12 @@ void FlagPersistence::LoadCached(Context const& context) { << error_code.message(); return; } - sink_.Init(context, *res); + + // If the map was null or omitted, treat it like an empty data set. + auto map = + res.value().value_or(std::unordered_map{}); + + sink_.Init(context, std::move(map)); } void FlagPersistence::StoreCache(std::string const& context_id) { @@ -99,9 +106,10 @@ void FlagPersistence::StoreCache(std::string const& context_id) { persistence_->Set(environment_namespace_, index_key_, boost::json::serialize(boost::json::value_from(index))); - persistence_->Set( - environment_namespace_, context_id, - boost::json::serialize(boost::json::value_from(flag_store_.GetAll()))); + boost::json::value v = boost::json::value_from(flag_store_.GetAll()); + + persistence_->Set(environment_namespace_, context_id, + boost::json::serialize(v)); } ContextIndex FlagPersistence::GetIndex() { diff --git a/libs/client-sdk/src/flag_manager/flag_store.cpp b/libs/client-sdk/src/flag_manager/flag_store.cpp index acd7a43e8..dfcdb8da2 100644 --- a/libs/client-sdk/src/flag_manager/flag_store.cpp +++ b/libs/client-sdk/src/flag_manager/flag_store.cpp @@ -1,6 +1,5 @@ #include -#include "../serialization/json_all_flags.hpp" #include "flag_store.hpp" #include diff --git a/libs/client-sdk/src/flag_manager/flag_updater.cpp b/libs/client-sdk/src/flag_manager/flag_updater.cpp index 9e98bd27f..4a316f8d8 100644 --- a/libs/client-sdk/src/flag_manager/flag_updater.cpp +++ b/libs/client-sdk/src/flag_manager/flag_updater.cpp @@ -1,6 +1,7 @@ #include -#include "../boost_signal_connection.hpp" +#include + #include "flag_updater.hpp" namespace launchdarkly::client_side::flag_manager { @@ -8,10 +9,10 @@ namespace launchdarkly::client_side::flag_manager { FlagUpdater::FlagUpdater(FlagStore& flag_store) : flag_store_(flag_store) {} Value GetValue(ItemDescriptor& descriptor) { - if (descriptor.flag) { + if (descriptor.item) { // `flag->` unwraps the first optional we know is present. // The second `value()` is not an optional. - return descriptor.flag->Detail().Value(); + return descriptor.item->Detail().Value(); } return {}; } @@ -31,7 +32,7 @@ void FlagUpdater::Init(Context const& context, auto existing = old_flags.find(new_pair.first); if (existing != old_flags.end()) { // The flag changed. - auto& evaluation_result = new_pair.second.flag; + auto& evaluation_result = new_pair.second.item; if (evaluation_result) { auto new_value = GetValue(new_pair.second); auto old_value = GetValue(*existing->second); @@ -74,7 +75,7 @@ void FlagUpdater::DispatchEvent(FlagValueChangeEvent event) { auto handler = signals_.find(event.FlagName()); if (handler != signals_.end()) { if (handler->second.empty()) { - // Empty, remove it from the map so it doesn't count toward + // Empty, remove it from the map, so it doesn't count toward // future calculations. signals_.erase(event.FlagName()); } else { @@ -86,24 +87,24 @@ void FlagUpdater::DispatchEvent(FlagValueChangeEvent event) { void FlagUpdater::Upsert(Context const& context, std::string key, - ItemDescriptor item) { + ItemDescriptor descriptor) { // Check the version. auto existing = flag_store_.Get(key); - if (existing && (existing->version >= item.version)) { + if (existing && (existing->version >= descriptor.version)) { // Out of order update, ignore it. return; } if (HasListeners()) { // Existed and updated. - if (existing && item.flag) { - DispatchEvent( - FlagValueChangeEvent(key, GetValue(item), GetValue(*existing))); - } else if (item.flag) { + if (existing && descriptor.item) { + DispatchEvent(FlagValueChangeEvent(key, GetValue(descriptor), + GetValue(*existing))); + } else if (descriptor.item) { DispatchEvent(FlagValueChangeEvent( - key, item.flag.value().Detail().Value(), Value())); + key, descriptor.item.value().Detail().Value(), Value())); // new flag - } else if (existing && existing->flag.has_value()) { + } else if (existing && existing->item.has_value()) { // Existed and deleted. DispatchEvent(FlagValueChangeEvent(key, GetValue(*existing))); } else { @@ -111,7 +112,7 @@ void FlagUpdater::Upsert(Context const& context, // Do nothing. } } - flag_store_.Upsert(key, item); + flag_store_.Upsert(key, descriptor); } bool FlagUpdater::HasListeners() const { @@ -123,7 +124,7 @@ std::unique_ptr FlagUpdater::OnFlagChange( std::string const& key, std::function)> handler) { std::lock_guard lock{signal_mutex_}; - return std::make_unique< ::launchdarkly::client_side::SignalConnection>( + return std::make_unique( signals_[key].connect(handler)); } diff --git a/libs/client-sdk/src/serialization/json_all_flags.cpp b/libs/client-sdk/src/serialization/json_all_flags.cpp deleted file mode 100644 index 663b05374..000000000 --- a/libs/client-sdk/src/serialization/json_all_flags.cpp +++ /dev/null @@ -1,54 +0,0 @@ -#include - -#include "json_all_flags.hpp" - -#include - -namespace launchdarkly::client_side { -// This tag_invoke needs to be in the same namespace as the -// ItemDescriptor. - -tl::expected, JsonError> -tag_invoke(boost::json::value_to_tag< - tl::expected, - JsonError>> const& unused, - boost::json::value const& json_value) { - boost::ignore_unused(unused); - - if (!json_value.is_object()) { - return tl::unexpected(JsonError::kSchemaFailure); - } - auto const& obj = json_value.as_object(); - std::unordered_map descriptors; - for (auto const& pair : obj) { - auto eval_result = - boost::json::value_to>( - pair.value()); - if (!eval_result.has_value()) { - return tl::unexpected(JsonError::kSchemaFailure); - } - descriptors.emplace(pair.key(), - ItemDescriptor(std::move(eval_result.value()))); - } - return descriptors; -} - -void tag_invoke( - boost::json::value_from_tag const& unused, - boost::json::value& json_value, - std::unordered_map> const& - all_flags) { - boost::ignore_unused(unused); - - auto& obj = json_value.emplace_object(); - for (auto descriptor : all_flags) { - // Only serialize non-deleted flags. - if (descriptor.second->flag) { - auto eval_result_json = - boost::json::value_from(*descriptor.second->flag); - obj.emplace(descriptor.first, eval_result_json); - } - } -} - -} // namespace launchdarkly::client_side diff --git a/libs/client-sdk/src/serialization/json_all_flags.hpp b/libs/client-sdk/src/serialization/json_all_flags.hpp deleted file mode 100644 index 18150002e..000000000 --- a/libs/client-sdk/src/serialization/json_all_flags.hpp +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once - -#include - -#include - -#include - -#include "../data_sources/data_source_update_sink.hpp" - -#include - -namespace launchdarkly::client_side { - -tl::expected, JsonError> -tag_invoke(boost::json::value_to_tag< - tl::expected, - JsonError>> const& unused, - boost::json::value const& json_value); - -void tag_invoke( - boost::json::value_from_tag const& unused, - boost::json::value& json_value, - std::unordered_map> const& - evaluation_result); - -} // namespace launchdarkly::client_side diff --git a/libs/client-sdk/tests/client_c_bindings_test.cpp b/libs/client-sdk/tests/client_c_bindings_test.cpp index 8991044ff..ed594f4d8 100644 --- a/libs/client-sdk/tests/client_c_bindings_test.cpp +++ b/libs/client-sdk/tests/client_c_bindings_test.cpp @@ -49,7 +49,7 @@ TEST(ClientBindings, RegisterFlagListener) { ASSERT_TRUE(LDStatus_Ok(status)); LDContextBuilder ctx_builder = LDContextBuilder_New(); - LDContextBuilder_AddKind(ctx_builder, "`user", "shadow"); + LDContextBuilder_AddKind(ctx_builder, "user", "shadow"); LDContext context = LDContextBuilder_Build(ctx_builder); diff --git a/libs/client-sdk/tests/data_source_status_test.cpp b/libs/client-sdk/tests/data_source_status_test.cpp new file mode 100644 index 000000000..109778287 --- /dev/null +++ b/libs/client-sdk/tests/data_source_status_test.cpp @@ -0,0 +1,36 @@ +#include + +#include + +using launchdarkly::client_side::data_sources::DataSourceStatus; + +TEST(DataSourceStatusTest, OstreamBasicStatus) { + auto time = + std::chrono::system_clock::time_point{std::chrono::milliseconds{0}}; + + DataSourceStatus status(DataSourceStatus::DataSourceState::kInitializing, + time, std::nullopt); + + std::stringstream ss; + ss << status; + EXPECT_EQ("Status(INITIALIZING, Since(1970-01-01 00:00:00))", ss.str()); +} + +TEST(DataSourceStatusTest, OStreamErrorInfo) { + auto time = + std::chrono::system_clock::time_point{std::chrono::milliseconds{0}}; + + DataSourceStatus status( + DataSourceStatus::DataSourceState::kInterrupted, time, + launchdarkly::common::data_sources::DataSourceStatusErrorInfo( + launchdarkly::common::data_sources::DataSourceStatusErrorKind:: + kInvalidData, + 404, "Bad times", time)); + + std::stringstream ss; + ss << status; + EXPECT_EQ( + "Status(INTERRUPTED, Since(1970-01-01 00:00:00), Error(INVALID_DATA, " + "Bad times, StatusCode(404), Since(1970-01-01 00:00:00)))", + ss.str()); +} diff --git a/libs/client-sdk/tests/flag_persistence_test.cpp b/libs/client-sdk/tests/flag_persistence_test.cpp index 4d61b843e..0ff1edb17 100644 --- a/libs/client-sdk/tests/flag_persistence_test.cpp +++ b/libs/client-sdk/tests/flag_persistence_test.cpp @@ -108,7 +108,7 @@ TEST(FlagPersistenceTests, CanLoadCache) { flag_persistence.LoadCached(context); // The store contains the flag loaded from the persistence. - EXPECT_EQ("test", store.Get("flagA")->flag->Detail().Value().AsString()); + EXPECT_EQ("test", store.Get("flagA")->item->Detail().Value().AsString()); } TEST(FlagPersistenceTests, EvictsContextsBeyondMax) { diff --git a/libs/client-sdk/tests/flag_store_test.cpp b/libs/client-sdk/tests/flag_store_test.cpp index 26096fe8c..818c06fe8 100644 --- a/libs/client-sdk/tests/flag_store_test.cpp +++ b/libs/client-sdk/tests/flag_store_test.cpp @@ -32,7 +32,7 @@ TEST(FlagstoreTests, HandlesInitWithData) { std::nullopt}}}}}}); EXPECT_FALSE(store.GetAll().empty()); - EXPECT_EQ("test", store.Get("flagA")->flag->Detail().Value()); + EXPECT_EQ("test", store.Get("flagA")->item->Detail().Value()); } TEST(FlagstoreTests, HandlesSecondInit) { @@ -53,7 +53,7 @@ TEST(FlagstoreTests, HandlesSecondInit) { std::nullopt}}}}}}); EXPECT_FALSE(store.GetAll().empty()); - EXPECT_EQ("test", store.Get("flagB")->flag->Detail().Value()); + EXPECT_EQ("test", store.Get("flagB")->item->Detail().Value()); EXPECT_FALSE(store.Get("flagA")); } @@ -74,8 +74,8 @@ TEST(FlagstoreTests, HandlePatchNewFlag) { std::nullopt}}}); EXPECT_FALSE(store.GetAll().empty()); - EXPECT_EQ("test", store.Get("flagA")->flag->Detail().Value()); - EXPECT_EQ("second", store.Get("flagB")->flag->Detail().Value()); + EXPECT_EQ("test", store.Get("flagA")->item->Detail().Value()); + EXPECT_EQ("second", store.Get("flagB")->item->Detail().Value()); } TEST(FlagstoreTests, HandlePatchUpdateFlag) { @@ -95,7 +95,7 @@ TEST(FlagstoreTests, HandlePatchUpdateFlag) { std::nullopt}}}); EXPECT_FALSE(store.GetAll().empty()); - EXPECT_EQ("second", store.Get("flagA")->flag->Detail().Value()); + EXPECT_EQ("second", store.Get("flagA")->item->Detail().Value()); } TEST(FlagstoreTests, HandleDelete) { @@ -111,7 +111,7 @@ TEST(FlagstoreTests, HandleDelete) { store.Upsert("flagA", ItemDescriptor{2}); EXPECT_FALSE(store.GetAll().empty()); - EXPECT_FALSE(store.Get("flagA")->flag.has_value()); + EXPECT_FALSE(store.Get("flagA")->item.has_value()); } TEST(FlagstoreTests, GetItemWhichDoesNotExist) { diff --git a/libs/client-sdk/tests/flag_updater_test.cpp b/libs/client-sdk/tests/flag_updater_test.cpp index eb58ad36a..50b1067c4 100644 --- a/libs/client-sdk/tests/flag_updater_test.cpp +++ b/libs/client-sdk/tests/flag_updater_test.cpp @@ -44,7 +44,7 @@ TEST(FlagUpdaterDataTests, HandlesInitWithData) { std::nullopt}}}}}}); EXPECT_FALSE(manager.GetAll().empty()); - EXPECT_EQ("test", manager.Get("flagA")->flag.value().Detail().Value()); + EXPECT_EQ("test", manager.Get("flagA")->item.value().Detail().Value()); } TEST(FlagUpdaterDataTests, HandlesSecondInit) { @@ -70,7 +70,7 @@ TEST(FlagUpdaterDataTests, HandlesSecondInit) { std::nullopt}}}}}}); EXPECT_FALSE(manager.GetAll().empty()); - EXPECT_EQ("test", manager.Get("flagB")->flag.value().Detail().Value()); + EXPECT_EQ("test", manager.Get("flagB")->item.value().Detail().Value()); EXPECT_FALSE(manager.Get("flagA")); } @@ -94,8 +94,8 @@ TEST(FlagUpdaterDataTests, HandlePatchNewFlag) { std::nullopt}}}); EXPECT_FALSE(manager.GetAll().empty()); - EXPECT_EQ("test", manager.Get("flagA")->flag.value().Detail().Value()); - EXPECT_EQ("second", manager.Get("flagB")->flag.value().Detail().Value()); + EXPECT_EQ("test", manager.Get("flagA")->item.value().Detail().Value()); + EXPECT_EQ("second", manager.Get("flagB")->item.value().Detail().Value()); } TEST(FlagUpdaterDataTests, HandlePatchUpdateFlag) { @@ -118,7 +118,7 @@ TEST(FlagUpdaterDataTests, HandlePatchUpdateFlag) { std::nullopt}}}); EXPECT_FALSE(manager.GetAll().empty()); - EXPECT_EQ("second", manager.Get("flagA")->flag.value().Detail().Value()); + EXPECT_EQ("second", manager.Get("flagA")->item.value().Detail().Value()); } TEST(FlagUpdaterDataTests, HandlePatchOutOfOrder) { @@ -141,7 +141,7 @@ TEST(FlagUpdaterDataTests, HandlePatchOutOfOrder) { std::nullopt}}}); EXPECT_FALSE(manager.GetAll().empty()); - EXPECT_EQ("test", manager.Get("flagA")->flag.value().Detail().Value()); + EXPECT_EQ("test", manager.Get("flagA")->item.value().Detail().Value()); } TEST(FlagUpdaterDataTests, HandleDelete) { @@ -161,7 +161,7 @@ TEST(FlagUpdaterDataTests, HandleDelete) { ItemDescriptor{2}); EXPECT_FALSE(manager.GetAll().empty()); - EXPECT_FALSE(manager.Get("flagA")->flag.has_value()); + EXPECT_FALSE(manager.Get("flagA")->item.has_value()); } TEST(FlagUpdaterDataTests, HandleDeleteOutOfOrder) { @@ -181,7 +181,7 @@ TEST(FlagUpdaterDataTests, HandleDeleteOutOfOrder) { ItemDescriptor{0}); EXPECT_FALSE(manager.GetAll().empty()); - EXPECT_EQ("test", manager.Get("flagA")->flag.value().Detail().Value()); + EXPECT_EQ("test", manager.Get("flagA")->item.value().Detail().Value()); } TEST(FlagUpdaterEventTests, InitialInitProducesNoEvents) { diff --git a/libs/common/include/launchdarkly/attribute_reference.hpp b/libs/common/include/launchdarkly/attribute_reference.hpp index 6f252f9e9..13ff74043 100644 --- a/libs/common/include/launchdarkly/attribute_reference.hpp +++ b/libs/common/include/launchdarkly/attribute_reference.hpp @@ -15,7 +15,7 @@ namespace launchdarkly { * launchdarkly::Context::Get, or to identify an attribute or nested value that * should be considered private * with launchdarkly::AttributesBuilder::SetPrivate or - * launchdarkly::AttributesBuilder::AddPrivateAttribute + * launchdarkly::AttributesBuilder::AddPrivateAttribute * (the SDK configuration can also have a list of private attribute references). * * This is represented as a separate type, rather than just a string, so that @@ -123,6 +123,11 @@ class AttributeReference { */ AttributeReference(char const* ref_str); + /** + * Default constructs an invalid attribute reference. + */ + AttributeReference(); + bool operator==(AttributeReference const& other) const { return components_ == other.components_; } diff --git a/libs/common/include/launchdarkly/bindings/c/data_source/error_info.h b/libs/common/include/launchdarkly/bindings/c/data_source/error_info.h new file mode 100644 index 000000000..acc51f12d --- /dev/null +++ b/libs/common/include/launchdarkly/bindings/c/data_source/error_info.h @@ -0,0 +1,59 @@ +/** @file error_info.h + * @brief LaunchDarkly Server-side C Bindings for Data Source Error Info. + */ +// NOLINTBEGIN modernize-use-using +#pragma once + +#include +#include + +#include +#include + +#ifdef __cplusplus +extern "C" { // only need to export C interface if +// used by C++ source code +#endif + +typedef struct _LDDataSourceStatus_ErrorInfo* LDDataSourceStatus_ErrorInfo; + +/** + * Get an enumerated value representing the general category of the error. + */ +LD_EXPORT(enum LDDataSourceStatus_ErrorKind) +LDDataSourceStatus_ErrorInfo_GetKind(LDDataSourceStatus_ErrorInfo info); + +/** + * The HTTP status code if the error was + * LD_DATASOURCESTATUS_ERRORKIND_ERROR_RESPONSE. + */ +LD_EXPORT(uint64_t) +LDDataSourceStatus_ErrorInfo_StatusCode(LDDataSourceStatus_ErrorInfo info); + +/** + * Any additional human-readable information relevant to the error. + * + * The format is subject to change and should not be relied on + * programmatically. + */ +LD_EXPORT(char const*) +LDDataSourceStatus_ErrorInfo_Message(LDDataSourceStatus_ErrorInfo info); + +/** + * The date/time that the error occurred, in seconds since epoch. + */ +LD_EXPORT(time_t) +LDDataSourceStatus_ErrorInfo_Time(LDDataSourceStatus_ErrorInfo info); + +/** + * Frees the data source status error information. + * @param status The error information to free. + */ +LD_EXPORT(void) +LDDataSourceStatus_ErrorInfo_Free(LDDataSourceStatus_ErrorInfo info); + +#ifdef __cplusplus +} +#endif + +// NOLINTEND modernize-use-using diff --git a/libs/common/include/launchdarkly/bindings/c/data_source/error_kind.h b/libs/common/include/launchdarkly/bindings/c/data_source/error_kind.h new file mode 100644 index 000000000..29524b862 --- /dev/null +++ b/libs/common/include/launchdarkly/bindings/c/data_source/error_kind.h @@ -0,0 +1,52 @@ +/** @file error_kind.h + * @brief LaunchDarkly Server-side C Bindings for Data Source Error Kinds. + */ +// NOLINTBEGIN modernize-use-using +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { // only need to export C interface if +// used by C++ source code +#endif + +/** + * A description of an error condition that the data source encountered. + */ +enum LDDataSourceStatus_ErrorKind { + /** + * An unexpected error, such as an uncaught exception, further + * described by the error message. + */ + LD_DATASOURCESTATUS_ERRORKIND_UNKNOWN = 0, + + /** + * An I/O error such as a dropped connection. + */ + LD_DATASOURCESTATUS_ERRORKIND_NETWORK_ERROR = 1, + + /** + * The LaunchDarkly service returned an HTTP response with an error + * status, available in the status code. + */ + LD_DATASOURCESTATUS_ERRORKIND_ERROR_RESPONSE = 2, + + /** + * The SDK received malformed data from the LaunchDarkly service. + */ + LD_DATASOURCESTATUS_ERRORKIND_INVALID_DATA = 3, + + /** + * The data source itself is working, but when it tried to put an + * update into the data store, the data store failed (so the SDK may + * not have the latest data). + */ + LD_DATASOURCESTATUS_ERRORKIND_STORE_ERROR = 4, +}; + +#ifdef __cplusplus +} +#endif + +// NOLINTEND modernize-use-using diff --git a/libs/common/include/launchdarkly/bindings/c/shared_function_argument_macro_definitions.h b/libs/common/include/launchdarkly/bindings/c/shared_function_argument_macro_definitions.h new file mode 100644 index 000000000..c97662584 --- /dev/null +++ b/libs/common/include/launchdarkly/bindings/c/shared_function_argument_macro_definitions.h @@ -0,0 +1,8 @@ +/** @file */ +#pragma once + +/* Function should operate asynchronously. */ +#define LD_NONBLOCKING 0 + +/* Function should discard evaluation details. */ +#define LD_DISCARD_DETAIL NULL diff --git a/libs/common/include/launchdarkly/config/client.hpp b/libs/common/include/launchdarkly/config/client.hpp index f218569d3..516c4b85b 100644 --- a/libs/common/include/launchdarkly/config/client.hpp +++ b/libs/common/include/launchdarkly/config/client.hpp @@ -15,9 +15,7 @@ using SDK = config::shared::ClientSDK; using Defaults = config::shared::Defaults; using AppInfoBuilder = config::shared::builders::AppInfoBuilder; using EndpointsBuilder = config::shared::builders::EndpointsBuilder; - using ConfigBuilder = config::shared::builders::ConfigBuilder; - using EventsBuilder = config::shared::builders::EventsBuilder; using HttpPropertiesBuilder = config::shared::builders::HttpPropertiesBuilder; diff --git a/libs/common/include/launchdarkly/config/server.hpp b/libs/common/include/launchdarkly/config/server.hpp index 960d103cc..d284983f1 100644 --- a/libs/common/include/launchdarkly/config/server.hpp +++ b/libs/common/include/launchdarkly/config/server.hpp @@ -19,6 +19,7 @@ using EventsBuilder = config::shared::builders::EventsBuilder; using HttpPropertiesBuilder = config::shared::builders::HttpPropertiesBuilder; using DataSourceBuilder = config::shared::builders::DataSourceBuilder; +using LoggingBuilder = config::shared::builders::LoggingBuilder; using PersistenceBuilder = config::shared::builders::PersistenceBuilder; using Config = config::Config; diff --git a/libs/common/include/launchdarkly/config/shared/built/events.hpp b/libs/common/include/launchdarkly/config/shared/built/events.hpp index 0db3adf6b..2b3e76c35 100644 --- a/libs/common/include/launchdarkly/config/shared/built/events.hpp +++ b/libs/common/include/launchdarkly/config/shared/built/events.hpp @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -36,6 +37,9 @@ class Events final { * should be made. * @param flush_workers How many workers to use for concurrent event * delivery. + * @param context_keys_cache_capacity Max number of unique context keys to + * hold in LRU cache used for context deduplication when generating index + * events. */ Events(bool enabled, std::size_t capacity, @@ -44,7 +48,8 @@ class Events final { bool all_attributes_private, AttributeReference::SetType private_attrs, std::chrono::milliseconds delivery_retry_delay, - std::size_t flush_workers); + std::size_t flush_workers, + std::optional context_keys_cache_capacity); /** * Returns true if event-sending is enabled. @@ -87,6 +92,13 @@ class Events final { */ [[nodiscard]] std::size_t FlushWorkers() const; + /** + * Max number of unique context keys to hold in LRU cache used for context + * deduplication when generating index events. + * @return Max, or std::nullopt if not applicable. + */ + [[nodiscard]] std::optional ContextKeysCacheCapacity() const; + private: bool enabled_; std::size_t capacity_; @@ -96,6 +108,7 @@ class Events final { AttributeReference::SetType private_attributes_; std::chrono::milliseconds delivery_retry_delay_; std::size_t flush_workers_; + std::optional context_keys_cache_capacity_; }; bool operator==(Events const& lhs, Events const& rhs); diff --git a/libs/common/include/launchdarkly/config/shared/defaults.hpp b/libs/common/include/launchdarkly/config/shared/defaults.hpp index 8ab68e73e..bd62ae2c4 100644 --- a/libs/common/include/launchdarkly/config/shared/defaults.hpp +++ b/libs/common/include/launchdarkly/config/shared/defaults.hpp @@ -44,7 +44,8 @@ struct Defaults { false, AttributeReference::SetType(), std::chrono::seconds(1), - 5}; + 5, + std::nullopt}; } static auto HttpProperties() -> shared::built::HttpProperties { @@ -88,7 +89,8 @@ struct Defaults { false, AttributeReference::SetType(), std::chrono::seconds(1), - 5}; + 5, + 1000}; } static auto HttpProperties() -> shared::built::HttpProperties { diff --git a/libs/common/include/launchdarkly/context.hpp b/libs/common/include/launchdarkly/context.hpp index 9c63229e8..81a3cad88 100644 --- a/libs/common/include/launchdarkly/context.hpp +++ b/libs/common/include/launchdarkly/context.hpp @@ -57,8 +57,9 @@ class Context final { * @param ref The reference to the desired attribute. * @return The attribute Value or a Value representing null. */ - Value const& Get(std::string const& kind, - launchdarkly::AttributeReference const& ref); + [[nodiscard]] Value const& Get( + std::string const& kind, + launchdarkly::AttributeReference const& ref) const; /** * Check if a context is valid. diff --git a/libs/common/include/launchdarkly/data/evaluation_detail.hpp b/libs/common/include/launchdarkly/data/evaluation_detail.hpp index 7263f17d9..f8b3dbfc0 100644 --- a/libs/common/include/launchdarkly/data/evaluation_detail.hpp +++ b/libs/common/include/launchdarkly/data/evaluation_detail.hpp @@ -33,7 +33,15 @@ class EvaluationDetail { * @param error_kind Kind of the error. * @param default_value Default value. */ - EvaluationDetail(enum EvaluationReason::ErrorKind error_kind, T default_value); + EvaluationDetail(enum EvaluationReason::ErrorKind error_kind, + T default_value); + + /** + * Constructs an EvaluationDetail consisting of a reason but no value. + * This is used when a flag has no appropriate fallback value. + * @param reason The reason. + */ + EvaluationDetail(EvaluationReason reason); /** * @return A reference to the variation value. For convenience, the * @@ -47,6 +55,11 @@ class EvaluationDetail { */ [[nodiscard]] std::optional VariationIndex() const; + /** + * @return True if the evaluation resulted in an error. + */ + [[nodiscard]] bool IsError() const; + /** * @return A reference to the reason for the results. */ @@ -64,9 +77,10 @@ class EvaluationDetail { }; /* - * Holds details for the C bindings, omitting the generic type parameter that is - * needed for EvaluationDetail. Instead, the bindings will directly return - * the evaluation result, and fill in a detail structure using an out parameter. + * Holds details for the C bindings, omitting the generic type parameter + * that is needed for EvaluationDetail. Instead, the bindings will + * directly return the evaluation result, and fill in a detail structure + * using an out parameter. */ struct CEvaluationDetail { template diff --git a/libs/common/include/launchdarkly/data/evaluation_reason.hpp b/libs/common/include/launchdarkly/data/evaluation_reason.hpp index 5238af9a8..75a6fa282 100644 --- a/libs/common/include/launchdarkly/data/evaluation_reason.hpp +++ b/libs/common/include/launchdarkly/data/evaluation_reason.hpp @@ -119,6 +119,42 @@ class EvaluationReason { explicit EvaluationReason(enum ErrorKind error_kind); + /** + * The flag was off. + */ + static EvaluationReason Off(); + + /** + * The flag didn't return a variation due to a prerequisite failing. + */ + static EvaluationReason PrerequisiteFailed(std::string prerequisite_key); + + /** + * The flag evaluated to a particular variation due to a target match. + */ + static EvaluationReason TargetMatch(); + + /** + * The flag evaluated to its fallthrough value. + * @param in_experiment Whether the flag is part of an experiment. + */ + static EvaluationReason Fallthrough(bool in_experiment); + + /** + * The flag evaluated to a particular variation because it matched a rule. + * @param rule_index Index of the rule. + * @param rule_id ID of the rule. + * @param in_experiment Whether the flag is part of an experiment. + */ + static EvaluationReason RuleMatch(std::size_t rule_index, + std::optional rule_id, + bool in_experiment); + + /** + * The flag data was malformed. + */ + static EvaluationReason MalformedFlag(); + friend std::ostream& operator<<(std::ostream& out, EvaluationReason const& reason); diff --git a/libs/common/include/launchdarkly/data/evaluation_result.hpp b/libs/common/include/launchdarkly/data/evaluation_result.hpp index 127589c54..52aa598ca 100644 --- a/libs/common/include/launchdarkly/data/evaluation_result.hpp +++ b/libs/common/include/launchdarkly/data/evaluation_result.hpp @@ -57,9 +57,6 @@ class EvaluationResult { debug_events_until_date, EvaluationDetailInternal detail); - friend std::ostream& operator<<(std::ostream& out, - EvaluationResult const& result); - private: uint64_t version_; std::optional flag_version_; @@ -70,6 +67,8 @@ class EvaluationResult { EvaluationDetailInternal detail_; }; +std::ostream& operator<<(std::ostream& out, EvaluationResult const& result); + bool operator==(EvaluationResult const& lhs, EvaluationResult const& rhs); bool operator!=(EvaluationResult const& lhs, EvaluationResult const& rhs); diff --git a/libs/common/include/launchdarkly/data_sources/data_source_status_base.hpp b/libs/common/include/launchdarkly/data_sources/data_source_status_base.hpp new file mode 100644 index 000000000..3f758f2e0 --- /dev/null +++ b/libs/common/include/launchdarkly/data_sources/data_source_status_base.hpp @@ -0,0 +1,80 @@ +#include +#include + +#include +#include + +// Common is included in the namespace to disambiguate from client/server +// for backward compatibility. +namespace launchdarkly::common::data_sources { + +template +class DataSourceStatusBase { + public: + using ErrorKind = DataSourceStatusErrorKind; + using ErrorInfo = DataSourceStatusErrorInfo; + using DateTime = std::chrono::time_point; + using DataSourceState = TDataSourceState; + + /** + * An enumerated value representing the overall current state of the data + * source. + */ + [[nodiscard]] DataSourceState State() const { return state_; } + + /** + * The date/time that the value of State most recently changed. + * + * The meaning of this depends on the current state: + * - For DataSourceState::kInitializing, it is the time that the SDK started + * initializing. + * - For DataSourceState::kValid, it is the time that the data + * source most recently entered a valid state, after previously having been + * DataSourceState::kInitializing or an invalid state such as + * DataSourceState::kInterrupted. + * - For DataSourceState::kInterrupted, it is the time that the data source + * most recently entered an error state, after previously having been + * DataSourceState::kValid. + * - For DataSourceState::kShutdown (client-side) or DataSourceState::kOff + * (server-side), it is the time that the data source encountered an + * unrecoverable error or that the SDK was explicitly shut down. + */ + [[nodiscard]] DateTime StateSince() const { return state_since_; } + + /** + * Information about the last error that the data source encountered, if + * any. + * + * This property should be updated whenever the data source encounters a + * problem, even if it does not cause the state to change. For instance, if + * a stream connection fails and the state changes to + * DataSourceState::kInterrupted, and then subsequent attempts to restart + * the connection also fail, the state will remain + * DataSourceState::kInterrupted but the error information will be updated + * each time-- and the last error will still be reported in this property + * even if the state later becomes DataSourceState::kValid. + */ + [[nodiscard]] std::optional LastError() const { + return last_error_; + } + + DataSourceStatusBase(DataSourceState state, + DateTime state_since, + std::optional last_error) + : state_(state), + state_since_(state_since), + last_error_(std::move(last_error)) {} + + ~DataSourceStatusBase() = default; + DataSourceStatusBase(DataSourceStatusBase const& item) = default; + DataSourceStatusBase(DataSourceStatusBase&& item) noexcept = default; + DataSourceStatusBase& operator=(DataSourceStatusBase const&) = delete; + DataSourceStatusBase& operator=(DataSourceStatusBase&&) = delete; + + private: + DataSourceState state_; + DateTime state_since_; + std::optional last_error_; +}; + +} // namespace launchdarkly::common::data_sources diff --git a/libs/common/include/launchdarkly/data_sources/data_source_status_error_info.hpp b/libs/common/include/launchdarkly/data_sources/data_source_status_error_info.hpp new file mode 100644 index 000000000..c1d6813c7 --- /dev/null +++ b/libs/common/include/launchdarkly/data_sources/data_source_status_error_info.hpp @@ -0,0 +1,62 @@ +#pragma once + +#include + +#include +#include +#include + +namespace launchdarkly::common::data_sources { + +/** + * A description of an error condition that the data source encountered. + */ +class DataSourceStatusErrorInfo { + public: + using StatusCodeType = std::uint64_t; + using ErrorKind = DataSourceStatusErrorKind; + using DateTime = std::chrono::time_point; + + /** + * An enumerated value representing the general category of the error. + */ + [[nodiscard]] ErrorKind Kind() const { return kind_; } + + /** + * The HTTP status code if the error was ErrorKind::kErrorResponse. + */ + [[nodiscard]] StatusCodeType StatusCode() const { return status_code_; } + + /** + * Any additional human-readable information relevant to the error. + * + * The format is subject to change and should not be relied on + * programmatically. + */ + [[nodiscard]] std::string const& Message() const { return message_; } + + /** + * The date/time that the error occurred. + */ + [[nodiscard]] DateTime Time() const { return time_; } + + DataSourceStatusErrorInfo(ErrorKind kind, + StatusCodeType status_code, + std::string message, + DateTime time) + : kind_(kind), + status_code_(status_code), + message_(std::move(message)), + time_(time) {} + + private: + ErrorKind kind_; + StatusCodeType status_code_; + std::string message_; + DateTime time_; +}; + +std::ostream& operator<<(std::ostream& out, + DataSourceStatusErrorInfo const& error); + +} // namespace launchdarkly::common::data_sources diff --git a/libs/common/include/launchdarkly/data_sources/data_source_status_error_kind.hpp b/libs/common/include/launchdarkly/data_sources/data_source_status_error_kind.hpp new file mode 100644 index 000000000..6adc7d87e --- /dev/null +++ b/libs/common/include/launchdarkly/data_sources/data_source_status_error_kind.hpp @@ -0,0 +1,44 @@ +#pragma once + +#include + +namespace launchdarkly::common::data_sources { + +/** + * An enumeration describing the general type of an error. + */ +enum class DataSourceStatusErrorKind { + /** + * An unexpected error, such as an uncaught exception, further + * described by the error message. + */ + kUnknown = 0, + + /** + * An I/O error such as a dropped connection. + */ + kNetworkError = 1, + + /** + * The LaunchDarkly service returned an HTTP response with an error + * status, available in the status code. + */ + kErrorResponse = 2, + + /** + * The SDK received malformed data from the LaunchDarkly service. + */ + kInvalidData = 3, + + /** + * The data source itself is working, but when it tried to put an + * update into the data store, the data store failed (so the SDK may + * not have the latest data). + */ + kStoreError = 4 +}; + +std::ostream& operator<<(std::ostream& out, + DataSourceStatusErrorKind const& kind); + +} // namespace launchdarkly::common::data_sources diff --git a/libs/common/include/launchdarkly/detail/c_binding_helpers.hpp b/libs/common/include/launchdarkly/detail/c_binding_helpers.hpp index 6a800e1fe..2f170ec9b 100644 --- a/libs/common/include/launchdarkly/detail/c_binding_helpers.hpp +++ b/libs/common/include/launchdarkly/detail/c_binding_helpers.hpp @@ -1,11 +1,12 @@ #include -#include -#include #include -#include #include +#include +#include +#include + namespace launchdarkly::detail { template struct has_result_type : std::false_type {}; diff --git a/libs/common/include/launchdarkly/logging/log_level.hpp b/libs/common/include/launchdarkly/logging/log_level.hpp index 46977cbfc..5338b2908 100644 --- a/libs/common/include/launchdarkly/logging/log_level.hpp +++ b/libs/common/include/launchdarkly/logging/log_level.hpp @@ -1,5 +1,7 @@ #pragma once +#include + namespace launchdarkly { /** * Log levels with kDebug being lowest severity and kError being highest @@ -28,4 +30,6 @@ char const* GetLogLevelName(LogLevel level, char const* default_); */ LogLevel GetLogLevelEnum(char const* name, LogLevel default_); +std::ostream& operator<<(std::ostream& out, LogLevel const& level); + } // namespace launchdarkly diff --git a/libs/common/include/launchdarkly/value.hpp b/libs/common/include/launchdarkly/value.hpp index 1b386b7b6..845453451 100644 --- a/libs/common/include/launchdarkly/value.hpp +++ b/libs/common/include/launchdarkly/value.hpp @@ -431,4 +431,24 @@ bool operator!=(Value::Array const& lhs, Value::Array const& rhs); bool operator==(Value::Object const& lhs, Value::Object const& rhs); bool operator!=(Value::Object const& lhs, Value::Object const& rhs); +/* Returns true if both values are numbers and lhs < rhs. Returns false if + * either value is not a number. + */ +bool operator<(Value const& lhs, Value const& rhs); + +/* Returns true if both values are numbers and lhs > rhs. Returns false if + * either value is not a number. + */ +bool operator>(Value const& lhs, Value const& rhs); + +/* Returns true if both values are numbers and lhs <= rhs. Returns false if + * either value is not a number. + */ +bool operator<=(Value const& lhs, Value const& rhs); + +/* Returns true if both values are numbers and lhs >= rhs. Returns false if + * either value is not a number. + */ +bool operator>=(Value const& lhs, Value const& rhs); + } // namespace launchdarkly diff --git a/libs/common/src/CMakeLists.txt b/libs/common/src/CMakeLists.txt index 790f86516..0130ec962 100644 --- a/libs/common/src/CMakeLists.txt +++ b/libs/common/src/CMakeLists.txt @@ -11,6 +11,7 @@ file(GLOB HEADER_LIST CONFIGURE_DEPENDS "${LaunchDarklyCommonSdk_SOURCE_DIR}/include/launchdarkly/config/shared/built/*.hpp" "${LaunchDarklyCommonSdk_SOURCE_DIR}/include/launchdarkly/data/*.hpp" "${LaunchDarklyCommonSdk_SOURCE_DIR}/include/launchdarkly/logging/*.hpp" + "${LaunchDarklyCommonSdk_SOURCE_DIR}/include/launchdarkly/data_sources/*.hpp" ) # Automatic library: static or dynamic based on user config. @@ -48,9 +49,12 @@ add_library(${LIBNAME} OBJECT bindings/c/listener_connection.cpp bindings/c/flag_listener.cpp bindings/c/memory_routines.cpp + bindings/c/data_source/error_info.cpp log_level.cpp config/persistence_builder.cpp config/logging_builder.cpp + data_sources/data_source_status_error_kind.cpp + data_sources/data_source_status_error_info.cpp ) add_library(launchdarkly::common ALIAS ${LIBNAME}) diff --git a/libs/common/src/attribute_reference.cpp b/libs/common/src/attribute_reference.cpp index d6ccec765..3a8fcf095 100644 --- a/libs/common/src/attribute_reference.cpp +++ b/libs/common/src/attribute_reference.cpp @@ -170,6 +170,10 @@ std::string EscapeLiteral(std::string const& literal, } AttributeReference::AttributeReference(std::string str, bool literal) { + if (str.empty()) { + valid_ = false; + return; + } if (literal) { components_.push_back(str); // Literal starting with a '/' needs to be converted to an attribute @@ -226,6 +230,8 @@ AttributeReference::AttributeReference(std::string ref_str) AttributeReference::AttributeReference(char const* ref_str) : AttributeReference(std::string(ref_str)) {} +AttributeReference::AttributeReference() : AttributeReference("") {} + std::string AttributeReference::PathToStringReference( std::vector path) { // Approximate size to reduce resizes. diff --git a/libs/common/src/bindings/c/data_source/error_info.cpp b/libs/common/src/bindings/c/data_source/error_info.cpp new file mode 100644 index 000000000..9776de89b --- /dev/null +++ b/libs/common/src/bindings/c/data_source/error_info.cpp @@ -0,0 +1,46 @@ +#include + +#include +#include + +using namespace launchdarkly::common; + +#define TO_DATASOURCESTATUS_ERRORINFO(ptr) \ + (reinterpret_cast< \ + launchdarkly::common::data_sources::DataSourceStatusErrorInfo*>(ptr)) + +LD_EXPORT(LDDataSourceStatus_ErrorKind) +LDDataSourceStatus_ErrorInfo_GetKind(LDDataSourceStatus_ErrorInfo info) { + LD_ASSERT_NOT_NULL(info); + + return static_cast( + TO_DATASOURCESTATUS_ERRORINFO(info)->Kind()); +} + +LD_EXPORT(uint64_t) +LDDataSourceStatus_ErrorInfo_StatusCode(LDDataSourceStatus_ErrorInfo info) { + LD_ASSERT_NOT_NULL(info); + + return TO_DATASOURCESTATUS_ERRORINFO(info)->StatusCode(); +} + +LD_EXPORT(char const*) +LDDataSourceStatus_ErrorInfo_Message(LDDataSourceStatus_ErrorInfo info) { + LD_ASSERT_NOT_NULL(info); + + return TO_DATASOURCESTATUS_ERRORINFO(info)->Message().c_str(); +} + +LD_EXPORT(time_t) +LDDataSourceStatus_ErrorInfo_Time(LDDataSourceStatus_ErrorInfo info) { + LD_ASSERT_NOT_NULL(info); + + return std::chrono::duration_cast( + TO_DATASOURCESTATUS_ERRORINFO(info)->Time().time_since_epoch()) + .count(); +} + +LD_EXPORT(void) +LDDataSourceStatus_ErrorInfo_Free(LDDataSourceStatus_ErrorInfo info) { + delete TO_DATASOURCESTATUS_ERRORINFO(info); +} diff --git a/libs/common/src/config/events.cpp b/libs/common/src/config/events.cpp index 4147639ed..7f24b7c59 100644 --- a/libs/common/src/config/events.cpp +++ b/libs/common/src/config/events.cpp @@ -9,7 +9,8 @@ Events::Events(bool enabled, bool all_attributes_private, AttributeReference::SetType private_attrs, std::chrono::milliseconds delivery_retry_delay, - std::size_t flush_workers) + std::size_t flush_workers, + std::optional context_keys_cache_capacity) : enabled_(enabled), capacity_(capacity), flush_interval_(flush_interval), @@ -17,7 +18,8 @@ Events::Events(bool enabled, all_attributes_private_(all_attributes_private), private_attributes_(std::move(private_attrs)), delivery_retry_delay_(delivery_retry_delay), - flush_workers_(flush_workers) {} + flush_workers_(flush_workers), + context_keys_cache_capacity_(context_keys_cache_capacity) {} bool Events::Enabled() const { return enabled_; @@ -51,6 +53,10 @@ std::size_t Events::FlushWorkers() const { return flush_workers_; } +std::optional Events::ContextKeysCacheCapacity() const { + return context_keys_cache_capacity_; +} + bool operator==(Events const& lhs, Events const& rhs) { return lhs.Path() == rhs.Path() && lhs.FlushInterval() == rhs.FlushInterval() && @@ -58,6 +64,7 @@ bool operator==(Events const& lhs, Events const& rhs) { lhs.AllAttributesPrivate() == rhs.AllAttributesPrivate() && lhs.PrivateAttributes() == rhs.PrivateAttributes() && lhs.DeliveryRetryDelay() == rhs.DeliveryRetryDelay() && - lhs.FlushWorkers() == rhs.FlushWorkers(); + lhs.FlushWorkers() == rhs.FlushWorkers() && + lhs.ContextKeysCacheCapacity() == rhs.ContextKeysCacheCapacity(); } } // namespace launchdarkly::config::shared::built diff --git a/libs/common/src/context.cpp b/libs/common/src/context.cpp index 42d734be6..ad301a0e5 100644 --- a/libs/common/src/context.cpp +++ b/libs/common/src/context.cpp @@ -40,7 +40,7 @@ Context::Context(std::map attributes) } Value const& Context::Get(std::string const& kind, - AttributeReference const& ref) { + AttributeReference const& ref) const { auto found = attributes_.find(kind); if (found != attributes_.end()) { return found->second.Get(ref); @@ -64,7 +64,7 @@ std::string Context::make_canonical_key() { if (kinds_to_keys_.size() == 1) { if (auto iterator = kinds_to_keys_.find("user"); iterator != kinds_to_keys_.end()) { - return std::string(iterator->second); + return iterator->second; } } std::stringstream stream; diff --git a/libs/common/src/data/evaluation_detail.cpp b/libs/common/src/data/evaluation_detail.cpp index 72093b267..22e383f03 100644 --- a/libs/common/src/data/evaluation_detail.cpp +++ b/libs/common/src/data/evaluation_detail.cpp @@ -14,12 +14,17 @@ EvaluationDetail::EvaluationDetail( reason_(std::move(reason)) {} template -EvaluationDetail::EvaluationDetail(enum EvaluationReason::ErrorKind error_kind, - T default_value) +EvaluationDetail::EvaluationDetail( + enum EvaluationReason::ErrorKind error_kind, + T default_value) : value_(std::move(default_value)), variation_index_(std::nullopt), reason_(error_kind) {} +template +EvaluationDetail::EvaluationDetail(EvaluationReason reason) + : value_(), variation_index_(std::nullopt), reason_(std::move(reason)) {} + template T const& EvaluationDetail::Value() const { return value_; @@ -39,6 +44,11 @@ T const& EvaluationDetail::operator*() const { return value_; } +template +[[nodiscard]] bool EvaluationDetail::IsError() const { + return reason_.has_value() && reason_->ErrorKind().has_value(); +} + template class EvaluationDetail; template class EvaluationDetail; template class EvaluationDetail; diff --git a/libs/common/src/data/evaluation_reason.cpp b/libs/common/src/data/evaluation_reason.cpp index a626ace5e..7fb433007 100644 --- a/libs/common/src/data/evaluation_reason.cpp +++ b/libs/common/src/data/evaluation_reason.cpp @@ -56,6 +56,39 @@ EvaluationReason::EvaluationReason(enum ErrorKind error_kind) false, std::nullopt) {} +EvaluationReason EvaluationReason::Off() { + return {Kind::kOff, std::nullopt, std::nullopt, std::nullopt, + std::nullopt, false, std::nullopt}; +} + +EvaluationReason EvaluationReason::PrerequisiteFailed( + std::string prerequisite_key) { + return { + Kind::kPrerequisiteFailed, std::nullopt, std::nullopt, std::nullopt, + std::move(prerequisite_key), false, std::nullopt}; +} + +EvaluationReason EvaluationReason::TargetMatch() { + return {Kind::kTargetMatch, std::nullopt, std::nullopt, std::nullopt, + std::nullopt, false, std::nullopt}; +} + +EvaluationReason EvaluationReason::Fallthrough(bool in_experiment) { + return {Kind::kFallthrough, std::nullopt, std::nullopt, std::nullopt, + std::nullopt, in_experiment, std::nullopt}; +} + +EvaluationReason EvaluationReason::RuleMatch(std::size_t rule_index, + std::optional rule_id, + bool in_experiment) { + return {Kind::kRuleMatch, std::nullopt, rule_index, std::move(rule_id), + std::nullopt, in_experiment, std::nullopt}; +} + +EvaluationReason EvaluationReason::MalformedFlag() { + return EvaluationReason{ErrorKind::kMalformedFlag}; +} + std::ostream& operator<<(std::ostream& out, EvaluationReason const& reason) { out << "{"; out << " kind: " << reason.kind_; diff --git a/libs/common/src/data/evaluation_result.cpp b/libs/common/src/data/evaluation_result.cpp index a6b1bdbbb..298b49b0b 100644 --- a/libs/common/src/data/evaluation_result.cpp +++ b/libs/common/src/data/evaluation_result.cpp @@ -48,17 +48,17 @@ EvaluationResult::EvaluationResult( std::ostream& operator<<(std::ostream& out, EvaluationResult const& result) { out << "{"; - out << " version: " << result.version_; - out << " trackEvents: " << result.track_events_; - out << " trackReason: " << result.track_reason_; + out << " version: " << result.Version(); + out << " trackEvents: " << result.TrackEvents(); + out << " trackReason: " << result.TrackReason(); - if (result.debug_events_until_date_.has_value()) { + if (result.DebugEventsUntilDate().has_value()) { std::time_t as_time_t = std::chrono::system_clock::to_time_t( - result.debug_events_until_date_.value()); + result.DebugEventsUntilDate().value()); out << " debugEventsUntilDate: " << std::put_time(std::gmtime(&as_time_t), "%Y-%m-%d %H:%M:%S"); } - out << " detail: " << result.detail_; + out << " detail: " << result.Detail(); out << "}"; return out; } diff --git a/libs/common/src/data_sources/data_source_status_error_info.cpp b/libs/common/src/data_sources/data_source_status_error_info.cpp new file mode 100644 index 000000000..b0299af5f --- /dev/null +++ b/libs/common/src/data_sources/data_source_status_error_info.cpp @@ -0,0 +1,19 @@ +#include +#include +#include +#include + +#include + +namespace launchdarkly::common::data_sources { + +std::ostream& operator<<(std::ostream& out, + DataSourceStatusErrorInfo const& error) { + std::time_t as_time_t = std::chrono::system_clock::to_time_t(error.Time()); + out << "Error(" << error.Kind() << ", " << error.Message() + << ", StatusCode(" << error.StatusCode() << "), Since(" + << std::put_time(std::gmtime(&as_time_t), "%Y-%m-%d %H:%M:%S") << "))"; + return out; +} + +} // namespace launchdarkly::common::data_sources diff --git a/libs/common/src/data_sources/data_source_status_error_kind.cpp b/libs/common/src/data_sources/data_source_status_error_kind.cpp new file mode 100644 index 000000000..69545eb16 --- /dev/null +++ b/libs/common/src/data_sources/data_source_status_error_kind.cpp @@ -0,0 +1,30 @@ +#include +#include + +#include + +namespace launchdarkly::common::data_sources { + +std::ostream& operator<<(std::ostream& out, + DataSourceStatusErrorKind const& kind) { + switch (kind) { + case DataSourceStatusErrorKind::kUnknown: + out << "UNKNOWN"; + break; + case DataSourceStatusErrorKind::kNetworkError: + out << "NETWORK_ERROR"; + break; + case DataSourceStatusErrorKind::kErrorResponse: + out << "ERROR_RESPONSE"; + break; + case DataSourceStatusErrorKind::kInvalidData: + out << "INVALID_DATA"; + break; + case DataSourceStatusErrorKind::kStoreError: + out << "STORE_ERROR"; + break; + } + return out; +} + +} // namespace launchdarkly::common::data_sources diff --git a/libs/common/src/log_level.cpp b/libs/common/src/log_level.cpp index e4c6b29b0..d708d056f 100644 --- a/libs/common/src/log_level.cpp +++ b/libs/common/src/log_level.cpp @@ -44,4 +44,9 @@ LogLevel GetLogLevelEnum(char const* level, LogLevel default_) { return default_; } +std::ostream& operator<<(std::ostream& out, LogLevel const& level) { + out << GetLogLevelName(level, "unknown"); + return out; +} + } // namespace launchdarkly diff --git a/libs/common/src/value.cpp b/libs/common/src/value.cpp index d2abf9c9a..780d1f352 100644 --- a/libs/common/src/value.cpp +++ b/libs/common/src/value.cpp @@ -282,4 +282,23 @@ bool operator!=(Value::Object const& lhs, Value::Object const& rhs) { return !(lhs == rhs); } +inline bool BothNumbers(Value const& lhs, Value const& rhs) { + return lhs.IsNumber() && rhs.IsNumber(); +} + +bool operator<(Value const& lhs, Value const& rhs) { + return BothNumbers(lhs, rhs) && lhs.AsDouble() < rhs.AsDouble(); +} + +bool operator>(Value const& lhs, Value const& rhs) { + return BothNumbers(lhs, rhs) && rhs < lhs; +} + +bool operator<=(Value const& lhs, Value const& rhs) { + return BothNumbers(lhs, rhs) && !(lhs > rhs); +} + +bool operator>=(Value const& lhs, Value const& rhs) { + return BothNumbers(lhs, rhs) && !(lhs < rhs); +} } // namespace launchdarkly diff --git a/libs/common/tests/data_source_status_test.cpp b/libs/common/tests/data_source_status_test.cpp new file mode 100644 index 000000000..c3b165803 --- /dev/null +++ b/libs/common/tests/data_source_status_test.cpp @@ -0,0 +1,71 @@ +#include + +#include + +namespace test_things { +enum class TestDataSourceStates { kStateA = 0, kStateB = 1, kStateC = 2 }; + +using DataSourceStatus = + launchdarkly::common::data_sources::DataSourceStatusBase< + TestDataSourceStates>; + +std::ostream& operator<<(std::ostream& out, TestDataSourceStates const& state) { + switch (state) { + case TestDataSourceStates::kStateA: + out << "kStateA"; + break; + case TestDataSourceStates::kStateB: + out << "kStateB"; + break; + case TestDataSourceStates::kStateC: + out << "kStateC"; + break; + } + + return out; +} + +std::ostream& operator<<(std::ostream& out, DataSourceStatus const& status) { + std::time_t as_time_t = + std::chrono::system_clock::to_time_t(status.StateSince()); + out << "Status(" << status.State() << ", Since(" + << std::put_time(std::gmtime(&as_time_t), "%Y-%m-%d %H:%M:%S") << ")"; + if (status.LastError()) { + out << ", " << status.LastError().value(); + } + out << ")"; + return out; +} +} // namespace test_things + +TEST(DataSourceStatusTest, OstreamBasicStatus) { + auto time = + std::chrono::system_clock::time_point{std::chrono::milliseconds{0}}; + ; + test_things::DataSourceStatus status( + test_things::DataSourceStatus::DataSourceState::kStateA, time, + std::nullopt); + + std::stringstream ss; + ss << status; + EXPECT_EQ("Status(kStateA, Since(1970-01-01 00:00:00))", ss.str()); +} + +TEST(DataSourceStatusTest, OStreamErrorInfo) { + auto time = + std::chrono::system_clock::time_point{std::chrono::milliseconds{0}}; + ; + test_things::DataSourceStatus status( + test_things::DataSourceStatus::DataSourceState::kStateC, time, + launchdarkly::common::data_sources::DataSourceStatusErrorInfo( + launchdarkly::common::data_sources::DataSourceStatusErrorKind:: + kInvalidData, + 404, "Bad times", time)); + + std::stringstream ss; + ss << status; + EXPECT_EQ( + "Status(kStateC, Since(1970-01-01 00:00:00), Error(INVALID_DATA, Bad " + "times, StatusCode(404), Since(1970-01-01 00:00:00)))", + ss.str()); +} diff --git a/libs/common/tests/value_test.cpp b/libs/common/tests/value_test.cpp index c4f3255d4..f9ad35fb9 100644 --- a/libs/common/tests/value_test.cpp +++ b/libs/common/tests/value_test.cpp @@ -1,14 +1,14 @@ #include -#include -#include -#include -#include #include -#include #include +#include +#include +#include +#include + // NOLINTBEGIN cppcoreguidelines-avoid-magic-numbers using BoostValue = boost::json::value; diff --git a/libs/internal/include/launchdarkly/context_filter.hpp b/libs/internal/include/launchdarkly/context_filter.hpp index 104e15cce..a4bbddb9a 100644 --- a/libs/internal/include/launchdarkly/context_filter.hpp +++ b/libs/internal/include/launchdarkly/context_filter.hpp @@ -1,13 +1,13 @@ #pragma once -#include -#include -#include +#include +#include #include -#include -#include +#include +#include +#include namespace launchdarkly { diff --git a/libs/internal/include/launchdarkly/data_model/context_aware_reference.hpp b/libs/internal/include/launchdarkly/data_model/context_aware_reference.hpp new file mode 100644 index 000000000..f176cf403 --- /dev/null +++ b/libs/internal/include/launchdarkly/data_model/context_aware_reference.hpp @@ -0,0 +1,65 @@ +#pragma once + +#include +#include + +#include + +namespace launchdarkly::data_model { + +/** + * The JSON data conditionally contains Attribute References (which are capable + * of addressing arbitrarily nested attributes in contexts) or Attribute Names, + * which are names of top-level attributes in contexts. + * + * In order to distinguish these two cases, inspection of a context kind field + * is necessary. The presence or absence of that field determines whether the + * data is an Attribute Reference or Attribute Name. + * + * Because this logic is needed in (3) places, it is factored out into this + * type. To use it, call + * boost::json::value_from, + * JsonError>>(json_value), where T is any type that defines the following: + * - kContextFieldName: name of the field containing the context kind + * - kReferenceFieldName: name of the field containing the attribute reference + * or attribute name' + * + * To ensure the field names don't go out of sync with the declared member + * variables, use the two macros defined below. + * @tparam Fields + */ +template +struct ContextAwareReference { + static_assert( + std::is_same::value && + std::is_same::value, + "T must define kContextFieldName and kReferenceFieldName as constexpr " + "static const char*"); +}; + +template +struct ContextAwareReference< + FieldNames, + typename std::enable_if< + std::is_same::value && + std::is_same::value>::type> { + using fields = FieldNames; + ContextKind contextKind; + AttributeReference reference; +}; + +// NOLINTBEGIN cppcoreguidelines-macro-usage +#define DEFINE_CONTEXT_KIND_FIELD(name) \ + ContextKind name; \ + constexpr static const char* kContextFieldName = #name; + +#define DEFINE_ATTRIBUTE_REFERENCE_FIELD(name) \ + AttributeReference name; \ + constexpr static const char* kReferenceFieldName = #name; +// NOLINTEND cppcoreguidelines-macro-usage + +} // namespace launchdarkly::data_model diff --git a/libs/internal/include/launchdarkly/data_model/context_kind.hpp b/libs/internal/include/launchdarkly/data_model/context_kind.hpp new file mode 100644 index 000000000..6d4690311 --- /dev/null +++ b/libs/internal/include/launchdarkly/data_model/context_kind.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include + +#include + +namespace launchdarkly::data_model { + +BOOST_STRONG_TYPEDEF(std::string, ContextKind); + +inline bool IsUser(ContextKind const& kind) noexcept { + return kind.t == "user"; +} + +} // namespace launchdarkly::data_model diff --git a/libs/internal/include/launchdarkly/data_model/flag.hpp b/libs/internal/include/launchdarkly/data_model/flag.hpp new file mode 100644 index 000000000..ebf9b24a9 --- /dev/null +++ b/libs/internal/include/launchdarkly/data_model/flag.hpp @@ -0,0 +1,112 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +namespace launchdarkly::data_model { + +struct Flag { + using Variation = std::int64_t; + using Weight = std::int64_t; + using FlagVersion = std::uint64_t; + using Date = std::uint64_t; + + struct Rollout { + enum class Kind { + kUnrecognized = 0, + kExperiment = 1, + kRollout = 2, + }; + + struct WeightedVariation { + Variation variation; + Weight weight; + bool untracked; + + WeightedVariation(); + + WeightedVariation(Variation index, Weight weight); + static WeightedVariation Untracked(Variation index, Weight weight); + + private: + WeightedVariation(Variation index, Weight weight, bool untracked); + }; + + std::vector variations; + + Kind kind; + std::optional seed; + + DEFINE_ATTRIBUTE_REFERENCE_FIELD(bucketBy) + DEFINE_CONTEXT_KIND_FIELD(contextKind) + + Rollout() = default; + explicit Rollout(std::vector); + }; + + using VariationOrRollout = std::variant, Rollout>; + + struct Prerequisite { + std::string key; + Variation variation; + }; + + struct Target { + std::vector values; + Variation variation; + ContextKind contextKind; + }; + + struct Rule { + std::vector clauses; + VariationOrRollout variationOrRollout; + + bool trackEvents; + std::optional id; + }; + + struct ClientSideAvailability { + bool usingMobileKey; + bool usingEnvironmentId; + }; + + std::string key; + FlagVersion version; + bool on; + VariationOrRollout fallthrough; + std::vector variations; + + std::vector prerequisites; + std::vector targets; + std::vector contextTargets; + std::vector rules; + std::optional offVariation; + bool clientSide; + ClientSideAvailability clientSideAvailability; + std::optional salt; + bool trackEvents; + bool trackEventsFallthrough; + std::optional debugEventsUntilDate; + + /** + * Returns the flag's version. Satisfies ItemDescriptor template + * constraints. + * @return Version of this flag. + */ + [[nodiscard]] inline std::uint64_t Version() const { return version; } + + [[nodiscard]] bool IsExperimentationEnabled( + std::optional const& reason) const; +}; +} // namespace launchdarkly::data_model diff --git a/libs/internal/include/launchdarkly/data_model/item_descriptor.hpp b/libs/internal/include/launchdarkly/data_model/item_descriptor.hpp new file mode 100644 index 000000000..db6f443bf --- /dev/null +++ b/libs/internal/include/launchdarkly/data_model/item_descriptor.hpp @@ -0,0 +1,66 @@ +#pragma once + +#include + +#include +#include + +#include +#include +#include +#include + +namespace launchdarkly::data_model { +/** + * An item descriptor is an abstraction that allows for Flag data to be + * handled using the same type in both a put or a patch. + */ +template +struct ItemDescriptor { + /** + * The version number of this data, provided by the SDK. + */ + uint64_t version; + + /** + * The data item, or nullopt if this is a deleted item placeholder. + */ + std::optional item; + + explicit ItemDescriptor(uint64_t version); + + explicit ItemDescriptor(T item); + + ItemDescriptor(ItemDescriptor const&) = default; + ItemDescriptor(ItemDescriptor&&) = default; + ItemDescriptor& operator=(ItemDescriptor const&) = default; + ItemDescriptor& operator=(ItemDescriptor&&) = default; + ~ItemDescriptor() = default; +}; + +template +bool operator==(ItemDescriptor const& lhs, ItemDescriptor const& rhs) { + return lhs.version == rhs.version && lhs.item == rhs.item; +} + +template +std::ostream& operator<<(std::ostream& out, + ItemDescriptor const& descriptor) { + out << "{"; + out << " version: " << descriptor.version; + if (descriptor.item.has_value()) { + out << " item: " << descriptor.item.value(); + } else { + out << " item: "; + } + return out; +} + +template +ItemDescriptor::ItemDescriptor(uint64_t version) : version(version) {} + +template +ItemDescriptor::ItemDescriptor(T item) + : version(item.Version()), item(std::move(item)) {} + +} // namespace launchdarkly::data_model diff --git a/libs/internal/include/launchdarkly/data_model/rule_clause.hpp b/libs/internal/include/launchdarkly/data_model/rule_clause.hpp new file mode 100644 index 000000000..fd11916cf --- /dev/null +++ b/libs/internal/include/launchdarkly/data_model/rule_clause.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include +#include + +#include "context_aware_reference.hpp" + +#include +#include +#include + +namespace launchdarkly::data_model { +struct Clause { + enum class Op { + kUnrecognized, /* didn't match any known operators */ + kIn, + kStartsWith, + kEndsWith, + kMatches, + kContains, + kLessThan, + kLessThanOrEqual, + kGreaterThan, + kGreaterThanOrEqual, + kBefore, + kAfter, + kSemVerEqual, + kSemVerLessThan, + kSemVerGreaterThan, + kSegmentMatch + }; + + Op op; + std::vector values; + bool negate; + + DEFINE_CONTEXT_KIND_FIELD(contextKind) + DEFINE_ATTRIBUTE_REFERENCE_FIELD(attribute) +}; + +std::ostream& operator<<(std::ostream& os, Clause::Op operator_); + +} // namespace launchdarkly::data_model diff --git a/libs/internal/include/launchdarkly/data_model/sdk_data_set.hpp b/libs/internal/include/launchdarkly/data_model/sdk_data_set.hpp new file mode 100644 index 000000000..6fb24a228 --- /dev/null +++ b/libs/internal/include/launchdarkly/data_model/sdk_data_set.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include +#include +#include + +#include +#include + +#include +#include + +namespace launchdarkly::data_model { + +struct SDKDataSet { + template + using Collection = std::unordered_map>; + using FlagKey = std::string; + using SegmentKey = std::string; + using Flags = Collection; + using Segments = Collection; + + Flags flags; + Segments segments; +}; + +} // namespace launchdarkly::data_model diff --git a/libs/internal/include/launchdarkly/data_model/segment.hpp b/libs/internal/include/launchdarkly/data_model/segment.hpp new file mode 100644 index 000000000..74405c3df --- /dev/null +++ b/libs/internal/include/launchdarkly/data_model/segment.hpp @@ -0,0 +1,56 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +namespace launchdarkly::data_model { + +struct Segment { + struct Target { + ContextKind contextKind; + std::vector values; + }; + + struct Rule { + using ReferenceType = ContextAwareReference; + + std::vector clauses; + std::optional id; + std::optional weight; + + DEFINE_CONTEXT_KIND_FIELD(rolloutContextKind) + DEFINE_ATTRIBUTE_REFERENCE_FIELD(bucketBy) + }; + + std::string key; + std::uint64_t version; + + std::vector included; + std::vector excluded; + std::vector includedContexts; + std::vector excludedContexts; + std::vector rules; + std::optional salt; + bool unbounded; + std::optional unboundedContextKind; + std::optional generation; + + /** + * Returns the segment's version. Satisfies ItemDescriptor template + * constraints. + * @return Version of this segment. + */ + [[nodiscard]] inline std::uint64_t Version() const { return version; } +}; +} // namespace launchdarkly::data_model diff --git a/libs/client-sdk/src/data_sources/data_source.hpp b/libs/internal/include/launchdarkly/data_sources/data_source.hpp similarity index 84% rename from libs/client-sdk/src/data_sources/data_source.hpp rename to libs/internal/include/launchdarkly/data_sources/data_source.hpp index 368f684ed..6ec7fe06f 100644 --- a/libs/client-sdk/src/data_sources/data_source.hpp +++ b/libs/internal/include/launchdarkly/data_sources/data_source.hpp @@ -1,6 +1,6 @@ #pragma once #include -namespace launchdarkly::client_side { +namespace launchdarkly::data_sources { class IDataSource { public: @@ -16,4 +16,4 @@ class IDataSource { IDataSource() = default; }; -} // namespace launchdarkly::client_side +} // namespace launchdarkly::data_sources diff --git a/libs/internal/include/launchdarkly/data_sources/data_source_status_manager_base.hpp b/libs/internal/include/launchdarkly/data_sources/data_source_status_manager_base.hpp new file mode 100644 index 000000000..b791d4e0c --- /dev/null +++ b/libs/internal/include/launchdarkly/data_sources/data_source_status_manager_base.hpp @@ -0,0 +1,189 @@ +#pragma once + +#include +#include +#include + +#include + +#include +#include + +namespace launchdarkly::internal::data_sources { + +/** + * Class that manages updates to the data source status and implements an + * interface to get the current status and listen to status changes. + */ +template +class DataSourceStatusManagerBase : public TInterface { + public: + /** + * Set the state. + * + * @param state The new state. + */ + void SetState(typename TDataSourceStatus::DataSourceState state) { + bool changed = UpdateState(state); + if (changed) { + data_source_status_signal_(Status()); + } + } + + /** + * If an error and state change happen simultaneously, then they should + * be updated simultaneously. + * + * @param state The new state. + * @param code Status code for an http error. + * @param message The message to associate with the error. + */ + void SetState(typename TDataSourceStatus::DataSourceState state, + typename TDataSourceStatus::ErrorInfo::StatusCodeType code, + std::string message) { + { + std::lock_guard lock(status_mutex_); + + UpdateState(state); + + last_error_ = typename TDataSourceStatus::ErrorInfo( + TDataSourceStatus::ErrorInfo::ErrorKind::kErrorResponse, code, + message, std::chrono::system_clock::now()); + } + + data_source_status_signal_(Status()); + } + + /** + * If an error and state change happen simultaneously, then they should + * be updated simultaneously. + * + * @param state The new state. + * @param kind The error kind. + * @param message The message to associate with the error. + */ + void SetState(typename TDataSourceStatus::DataSourceState state, + typename TDataSourceStatus::ErrorInfo::ErrorKind kind, + std::string message) { + { + std::lock_guard lock(status_mutex_); + + UpdateState(state); + + last_error_ = typename TDataSourceStatus::ErrorInfo( + kind, 0, std::move(message), std::chrono::system_clock::now()); + } + + data_source_status_signal_(Status()); + } + + /** + * Set an error with the given kind and message. + * + * For ErrorInfo::ErrorKind::kErrorResponse use the + * SetError(ErrorInfo::StatusCodeType) method. + * @param kind The kind of the error. + * @param message A message for the error. + */ + void SetError(typename TDataSourceStatus::ErrorInfo::ErrorKind kind, + std::string message) { + { + std::lock_guard lock(status_mutex_); + last_error_ = typename TDataSourceStatus::ErrorInfo( + kind, 0, std::move(message), std::chrono::system_clock::now()); + state_since_ = std::chrono::system_clock::now(); + } + + data_source_status_signal_(Status()); + } + + /** + * Set an error based on the given status code. + * @param code The status code of the error. + */ + void SetError(typename TDataSourceStatus::ErrorInfo::StatusCodeType code, + std::string message) { + { + std::lock_guard lock(status_mutex_); + last_error_ = typename TDataSourceStatus::ErrorInfo( + TDataSourceStatus::ErrorInfo::ErrorKind::kErrorResponse, code, + message, std::chrono::system_clock::now()); + state_since_ = std::chrono::system_clock::now(); + } + data_source_status_signal_(Status()); + } + + TDataSourceStatus Status() const override { + return {state_, state_since_, last_error_}; + } + + std::unique_ptr OnDataSourceStatusChange( + std::function handler) override { + std::lock_guard lock{status_mutex_}; + return std::make_unique< + ::launchdarkly::internal::signals::SignalConnection>( + data_source_status_signal_.connect(handler)); + } + + std::unique_ptr OnDataSourceStatusChangeEx( + std::function handler) override { + return std::make_unique< + launchdarkly::internal::signals::SignalConnection>( + data_source_status_signal_.connect_extended( + [handler](boost::signals2::connection const& conn, + TDataSourceStatus status) { + if (handler(status)) { + conn.disconnect(); + } + })); + } + DataSourceStatusManagerBase() + : state_(TDataSourceStatus::DataSourceState::kInitializing), + state_since_(std::chrono::system_clock::now()) {} + + virtual ~DataSourceStatusManagerBase() = default; + DataSourceStatusManagerBase(DataSourceStatusManagerBase const& item) = + delete; + DataSourceStatusManagerBase(DataSourceStatusManagerBase&& item) = delete; + DataSourceStatusManagerBase& operator=(DataSourceStatusManagerBase const&) = + delete; + DataSourceStatusManagerBase& operator=(DataSourceStatusManagerBase&&) = + delete; + + private: + typename TDataSourceStatus::DataSourceState state_; + typename TDataSourceStatus::DateTime state_since_; + std::optional last_error_; + + boost::signals2::signal + data_source_status_signal_; + mutable std::recursive_mutex status_mutex_; + + bool UpdateState( + typename TDataSourceStatus::DataSourceState const& requested_state) { + std::lock_guard lock(status_mutex_); + + // Interrupted and initializing are common to server and client. + // If logic specific to client or server states was needed, then + // the implementation would need to be re-organized to allow overriding + // the method. + + // If initializing, then interruptions remain initializing. + auto new_state = + (requested_state == + TDataSourceStatus::DataSourceState::kInterrupted && + state_ == TDataSourceStatus::DataSourceState::kInitializing) + ? TDataSourceStatus::DataSourceState:: + kInitializing // see comment on + // IDataSourceUpdateSink.UpdateStatus + : requested_state; + auto changed = state_ != new_state; + if (changed) { + state_ = new_state; + state_since_ = std::chrono::system_clock::now(); + } + return changed; + } +}; + +} // namespace launchdarkly::internal::data_sources diff --git a/libs/internal/include/launchdarkly/encoding/base_16.hpp b/libs/internal/include/launchdarkly/encoding/base_16.hpp new file mode 100644 index 000000000..0ffae71b4 --- /dev/null +++ b/libs/internal/include/launchdarkly/encoding/base_16.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include +#include +#include +#include + +namespace launchdarkly::encoding { + +template +std::string Base16Encode(std::array arr) { + std::stringstream output_stream; + output_stream << std::hex << std::noshowbase; + for (unsigned char byte : arr) { + output_stream << std::setw(2) << std::setfill('0') + << static_cast(byte); + } + return output_stream.str(); +} + +} // namespace launchdarkly::encoding diff --git a/libs/internal/include/launchdarkly/encoding/sha_1.hpp b/libs/internal/include/launchdarkly/encoding/sha_1.hpp new file mode 100644 index 000000000..e233757c3 --- /dev/null +++ b/libs/internal/include/launchdarkly/encoding/sha_1.hpp @@ -0,0 +1,10 @@ +#pragma once +#include +#include + +#include + +namespace launchdarkly::encoding { +std::array Sha1String( + std::string const& input); +} diff --git a/libs/internal/include/launchdarkly/encoding/sha_256.hpp b/libs/internal/include/launchdarkly/encoding/sha_256.hpp index f4b354346..d9d71d5b4 100644 --- a/libs/internal/include/launchdarkly/encoding/sha_256.hpp +++ b/libs/internal/include/launchdarkly/encoding/sha_256.hpp @@ -3,7 +3,7 @@ #include #include -#include "openssl/sha.h" +#include namespace launchdarkly::encoding { diff --git a/libs/internal/include/launchdarkly/events/asio_event_processor.hpp b/libs/internal/include/launchdarkly/events/asio_event_processor.hpp index 6e8cc23f2..a531877e8 100644 --- a/libs/internal/include/launchdarkly/events/asio_event_processor.hpp +++ b/libs/internal/include/launchdarkly/events/asio_event_processor.hpp @@ -1,5 +1,19 @@ #pragma once +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + #include #include #include @@ -10,22 +24,10 @@ #include #include -#include -#include -#include -#include -#include - -#include "event_batch.hpp" -#include "events.hpp" -#include "outbox.hpp" -#include "summarizer.hpp" -#include "worker_pool.hpp" - namespace launchdarkly::events { template -class AsioEventProcessor { +class AsioEventProcessor : public IEventProcessor { public: AsioEventProcessor( boost::asio::any_io_executor const& io, @@ -34,11 +36,11 @@ class AsioEventProcessor { config::shared::built::HttpProperties const& http_properties, Logger& logger); - void AsyncFlush(); + virtual void FlushAsync() override; - void AsyncSend(InputEvent event); + virtual void SendAsync(events::InputEvent event) override; - void AsyncClose(); + virtual void ShutdownAsync() override; private: using Clock = std::chrono::system_clock; @@ -48,8 +50,8 @@ class AsioEventProcessor { }; boost::asio::any_io_executor io_; - Outbox outbox_; - Summarizer summarizer_; + detail::Outbox outbox_; + detail::Summarizer summarizer_; std::chrono::milliseconds flush_interval_; boost::asio::steady_timer timer_; @@ -60,7 +62,7 @@ class AsioEventProcessor { boost::uuids::random_generator uuids_; - WorkerPool workers_; + detail::WorkerPool workers_; std::size_t inbox_capacity_; std::size_t inbox_size_; @@ -74,11 +76,13 @@ class AsioEventProcessor { launchdarkly::ContextFilter filter_; + detail::LRUCache context_key_cache_; + Logger& logger_; void HandleSend(InputEvent event); - std::optional CreateBatch(); + std::optional CreateBatch(); void Flush(FlushTrigger flush_type); @@ -90,7 +94,7 @@ class AsioEventProcessor { void InboxDecrement(); void OnEventDeliveryResult(std::size_t count, - RequestWorker::DeliveryResult); + detail::RequestWorker::DeliveryResult); }; } // namespace launchdarkly::events diff --git a/libs/internal/include/launchdarkly/events/client_events.hpp b/libs/internal/include/launchdarkly/events/client_events.hpp deleted file mode 100644 index 595a6ae0d..000000000 --- a/libs/internal/include/launchdarkly/events/client_events.hpp +++ /dev/null @@ -1,50 +0,0 @@ -#pragma once - -#include "common_events.hpp" - -namespace launchdarkly::events::client { - -struct IdentifyEventParams { - Date creation_date; - Context context; -}; - -struct IdentifyEvent { - Date creation_date; - EventContext context; -}; - -struct FeatureEventParams { - Date creation_date; - std::string key; - Context context; - Value value; - Value default_; - std::optional version; - std::optional variation; - std::optional reason; - bool require_full_event; - std::optional debug_events_until_date; -}; - -struct FeatureEventBase { - Date creation_date; - std::string key; - std::optional version; - std::optional variation; - Value value; - std::optional reason; - Value default_; - - explicit FeatureEventBase(FeatureEventParams const& params); -}; - -struct FeatureEvent : public FeatureEventBase { - ContextKeys context_keys; -}; - -struct DebugEvent : public FeatureEventBase { - EventContext context; -}; - -} // namespace launchdarkly::events::client diff --git a/libs/internal/include/launchdarkly/events/common_events.hpp b/libs/internal/include/launchdarkly/events/common_events.hpp deleted file mode 100644 index a2f95c8be..000000000 --- a/libs/internal/include/launchdarkly/events/common_events.hpp +++ /dev/null @@ -1,39 +0,0 @@ -#pragma once - -#include -#include -#include - -#include - -#include -#include -#include - -namespace launchdarkly::events { - -using Value = launchdarkly::Value; -using VariationIndex = size_t; -using Reason = EvaluationReason; -using Result = EvaluationResult; -using Context = launchdarkly::Context; -using EventContext = boost::json::value; -using Version = std::uint64_t; -using ContextKeys = std::map; - -struct Date { - std::chrono::system_clock::time_point t; -}; - -struct TrackEventParams { - Date creation_date; - std::string key; - ContextKeys context_keys; - std::optional data; - std::optional metric_value; -}; - -// Track (custom) events are directly serialized from their parameters. -using TrackEvent = TrackEventParams; - -} // namespace launchdarkly::events diff --git a/libs/internal/include/launchdarkly/events/data/common_events.hpp b/libs/internal/include/launchdarkly/events/data/common_events.hpp new file mode 100644 index 000000000..c9670df30 --- /dev/null +++ b/libs/internal/include/launchdarkly/events/data/common_events.hpp @@ -0,0 +1,92 @@ +#pragma once + +#include +#include +#include + +#include + +#include +#include +#include + +namespace launchdarkly::events { + +using Value = launchdarkly::Value; +using VariationIndex = size_t; +using Reason = EvaluationReason; +using Result = EvaluationResult; +using Context = launchdarkly::Context; +using EventContext = boost::json::value; +using Version = std::uint64_t; +using ContextKeys = std::map; + +struct Date { + std::chrono::system_clock::time_point t; +}; + +struct TrackEventParams { + Date creation_date; + std::string key; + ContextKeys context_keys; + std::optional data; + std::optional metric_value; +}; + +struct ServerTrackEventParams { + TrackEventParams base; + Context context; +}; + +using ClientTrackEventParams = TrackEventParams; + +using TrackEvent = TrackEventParams; + +struct IdentifyEventParams { + Date creation_date; + Context context; +}; + +struct IdentifyEvent { + Date creation_date; + EventContext context; +}; + +struct FeatureEventParams { + Date creation_date; + std::string key; + Context context; + Value value; + Value default_; + std::optional version; + std::optional variation; + std::optional reason; + bool require_full_event; + std::optional debug_events_until_date; + std::optional prereq_of; +}; + +struct FeatureEventBase { + Date creation_date; + std::string key; + std::optional version; + std::optional variation; + Value value; + std::optional reason; + Value default_; + std::optional prereq_of; + + explicit FeatureEventBase(FeatureEventParams const& params); +}; + +struct FeatureEvent { + FeatureEventBase base; + ContextKeys context_keys; +}; + +struct DebugEvent { + FeatureEventBase base; + EventContext context; +}; + +} // namespace launchdarkly::events diff --git a/libs/internal/include/launchdarkly/events/data/events.hpp b/libs/internal/include/launchdarkly/events/data/events.hpp new file mode 100644 index 000000000..a0e00eae6 --- /dev/null +++ b/libs/internal/include/launchdarkly/events/data/events.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + +namespace launchdarkly::events { + +using InputEvent = std::variant; + +using OutputEvent = std::variant; + +} // namespace launchdarkly::events diff --git a/libs/internal/include/launchdarkly/events/data/server_events.hpp b/libs/internal/include/launchdarkly/events/data/server_events.hpp new file mode 100644 index 000000000..393f0ff0f --- /dev/null +++ b/libs/internal/include/launchdarkly/events/data/server_events.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include + +namespace launchdarkly::events::server_side { + +struct IndexEvent { + Date creation_date; + EventContext context; +}; + +} // namespace launchdarkly::events::server_side diff --git a/libs/internal/include/launchdarkly/events/event_batch.hpp b/libs/internal/include/launchdarkly/events/detail/event_batch.hpp similarity index 92% rename from libs/internal/include/launchdarkly/events/event_batch.hpp rename to libs/internal/include/launchdarkly/events/detail/event_batch.hpp index 10af02fce..71a866cdf 100644 --- a/libs/internal/include/launchdarkly/events/event_batch.hpp +++ b/libs/internal/include/launchdarkly/events/detail/event_batch.hpp @@ -1,11 +1,13 @@ #pragma once -#include #include #include + +#include + #include -namespace launchdarkly::events { +namespace launchdarkly::events::detail { /** * EventBatch represents a batch of events being sent to LaunchDarkly as @@ -43,4 +45,4 @@ class EventBatch { network::HttpRequest request_; }; -} // namespace launchdarkly::events +} // namespace launchdarkly::events::detail diff --git a/libs/internal/include/launchdarkly/events/detail/lru_cache.hpp b/libs/internal/include/launchdarkly/events/detail/lru_cache.hpp new file mode 100644 index 000000000..423831dfe --- /dev/null +++ b/libs/internal/include/launchdarkly/events/detail/lru_cache.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include + +namespace launchdarkly::events::detail { + +class LRUCache { + public: + /** + * Constructs a new cache with a given capacity. When capacity is exceeded, + * entries are evicted from the cache in LRU order. + * @param capacity + */ + explicit LRUCache(std::size_t capacity); + + /** + * Adds a value to the cache; returns true if it was already there. + * @param value Value to add. + * @return True if the value was already in the cache. + */ + bool Notice(std::string const& value); + + /** + * Returns the current size of the cache. + * @return Number of unique entries in cache. + */ + std::size_t Size() const; + + /** + * Clears all cache entries. + */ + void Clear(); + + private: + using KeyList = std::list; + std::size_t capacity_; + std::unordered_map map_; + KeyList list_; +}; + +} // namespace launchdarkly::events::detail diff --git a/libs/internal/include/launchdarkly/events/outbox.hpp b/libs/internal/include/launchdarkly/events/detail/outbox.hpp similarity index 79% rename from libs/internal/include/launchdarkly/events/outbox.hpp rename to libs/internal/include/launchdarkly/events/detail/outbox.hpp index b51dc0fcf..50e09422a 100644 --- a/libs/internal/include/launchdarkly/events/outbox.hpp +++ b/libs/internal/include/launchdarkly/events/detail/outbox.hpp @@ -1,11 +1,12 @@ #pragma once +#include + #include #include #include -#include "events.hpp" -namespace launchdarkly::events { +namespace launchdarkly::events::detail { /** * Represents a fixed-size queue for holding output events, which are events @@ -27,18 +28,18 @@ class Outbox { * @return True if all events were accepted; false if >= 1 events were * dropped. */ - bool PushDiscardingOverflow(std::vector events); + [[nodiscard]] bool PushDiscardingOverflow(std::vector events); /** * Consumes all events in the outbox. * @return All events in the outbox, in the order they were pushed. */ - std::vector Consume(); + [[nodiscard]] std::vector Consume(); /** * True if the outbox is empty. */ - bool Empty(); + [[nodiscard]] bool Empty() const; private: std::queue items_; @@ -49,4 +50,4 @@ class Outbox { bool Push(OutputEvent item); }; -} // namespace launchdarkly::events +} // namespace launchdarkly::events::detail diff --git a/libs/internal/include/launchdarkly/events/parse_date_header.hpp b/libs/internal/include/launchdarkly/events/detail/parse_date_header.hpp similarity index 93% rename from libs/internal/include/launchdarkly/events/parse_date_header.hpp rename to libs/internal/include/launchdarkly/events/detail/parse_date_header.hpp index 4e009e1d6..65c91151e 100644 --- a/libs/internal/include/launchdarkly/events/parse_date_header.hpp +++ b/libs/internal/include/launchdarkly/events/detail/parse_date_header.hpp @@ -5,7 +5,7 @@ #include #include -namespace launchdarkly::events { +namespace launchdarkly::events::detail { template @@ -43,4 +43,4 @@ static std::optional ParseDateHeader( return Clock::from_time_t(real_gm_t); } -} // namespace launchdarkly::events +} // namespace launchdarkly::events::detail diff --git a/libs/internal/include/launchdarkly/events/request_worker.hpp b/libs/internal/include/launchdarkly/events/detail/request_worker.hpp similarity index 97% rename from libs/internal/include/launchdarkly/events/request_worker.hpp rename to libs/internal/include/launchdarkly/events/detail/request_worker.hpp index a7e176c44..9046d49eb 100644 --- a/libs/internal/include/launchdarkly/events/request_worker.hpp +++ b/libs/internal/include/launchdarkly/events/detail/request_worker.hpp @@ -9,9 +9,9 @@ #include #include -#include "event_batch.hpp" +#include -namespace launchdarkly::events { +namespace launchdarkly::events::detail { enum class State { /* Worker is ready for a new job. */ @@ -174,4 +174,4 @@ class RequestWorker { ResultCallback cb); }; -} // namespace launchdarkly::events +} // namespace launchdarkly::events::detail diff --git a/libs/internal/include/launchdarkly/events/summarizer.hpp b/libs/internal/include/launchdarkly/events/detail/summarizer.hpp similarity index 89% rename from libs/internal/include/launchdarkly/events/summarizer.hpp rename to libs/internal/include/launchdarkly/events/detail/summarizer.hpp index ce6a553fd..e87a6055c 100644 --- a/libs/internal/include/launchdarkly/events/summarizer.hpp +++ b/libs/internal/include/launchdarkly/events/detail/summarizer.hpp @@ -7,11 +7,11 @@ #include #include -#include +#include "launchdarkly/value.hpp" -#include "events.hpp" +#include "launchdarkly/events/data/events.hpp" -namespace launchdarkly::events { +namespace launchdarkly::events::detail { /** * Summarizer is responsible for accepting FeatureEventParams (the context @@ -39,7 +39,7 @@ class Summarizer { * Updates the summary with a feature event. * @param event Feature event. */ - void Update(client::FeatureEventParams const& event); + void Update(events::FeatureEventParams const& event); /** * Marks the summary as finished at a given timestamp. @@ -55,12 +55,12 @@ class Summarizer { /** * Returns the summary's start time as given in the constructor. */ - [[nodiscard]] Time start_time() const; + [[nodiscard]] Time StartTime() const; /** * Returns the summary's end time as specified using Finish. */ - [[nodiscard]] Time end_time() const; + [[nodiscard]] Time EndTime() const; struct VariationSummary { public: @@ -111,4 +111,4 @@ class Summarizer { std::unordered_map features_; }; -} // namespace launchdarkly::events +} // namespace launchdarkly::events::detail diff --git a/libs/internal/include/launchdarkly/events/worker_pool.hpp b/libs/internal/include/launchdarkly/events/detail/worker_pool.hpp similarity index 93% rename from libs/internal/include/launchdarkly/events/worker_pool.hpp rename to libs/internal/include/launchdarkly/events/detail/worker_pool.hpp index 418936d56..a6a08441d 100644 --- a/libs/internal/include/launchdarkly/events/worker_pool.hpp +++ b/libs/internal/include/launchdarkly/events/detail/worker_pool.hpp @@ -1,5 +1,10 @@ #pragma once +#include +#include +#include +#include + #include #include @@ -7,13 +12,7 @@ #include #include -#include -#include -#include - -#include "request_worker.hpp" - -namespace launchdarkly::events { +namespace launchdarkly::events::detail { /** * WorkerPool represents a pool of workers capable of delivering event payloads @@ -71,4 +70,4 @@ class WorkerPool { std::vector> workers_; }; -} // namespace launchdarkly::events +} // namespace launchdarkly::events::detail diff --git a/libs/client-sdk/src/event_processor.hpp b/libs/internal/include/launchdarkly/events/event_processor_interface.hpp similarity index 89% rename from libs/client-sdk/src/event_processor.hpp rename to libs/internal/include/launchdarkly/events/event_processor_interface.hpp index 9b6b25d0c..4e1a9e631 100644 --- a/libs/client-sdk/src/event_processor.hpp +++ b/libs/internal/include/launchdarkly/events/event_processor_interface.hpp @@ -1,8 +1,8 @@ #pragma once -#include +#include -namespace launchdarkly::client_side { +namespace launchdarkly::events { class IEventProcessor { public: @@ -34,4 +34,4 @@ class IEventProcessor { IEventProcessor() = default; }; -} // namespace launchdarkly::client_side +} // namespace launchdarkly::events diff --git a/libs/internal/include/launchdarkly/events/events.hpp b/libs/internal/include/launchdarkly/events/events.hpp deleted file mode 100644 index e3770c13d..000000000 --- a/libs/internal/include/launchdarkly/events/events.hpp +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once - -#include "client_events.hpp" -namespace launchdarkly::events { - -using InputEvent = std::variant; - -using OutputEvent = std::variant; - -} // namespace launchdarkly::events diff --git a/libs/client-sdk/src/event_processor/null_event_processor.hpp b/libs/internal/include/launchdarkly/events/null_event_processor.hpp similarity index 64% rename from libs/client-sdk/src/event_processor/null_event_processor.hpp rename to libs/internal/include/launchdarkly/events/null_event_processor.hpp index 1cb615302..8f228fb54 100644 --- a/libs/client-sdk/src/event_processor/null_event_processor.hpp +++ b/libs/internal/include/launchdarkly/events/null_event_processor.hpp @@ -1,8 +1,8 @@ #pragma once -#include "../event_processor.hpp" +#include -namespace launchdarkly::client_side { +namespace launchdarkly::events { class NullEventProcessor : public IEventProcessor { public: @@ -11,4 +11,4 @@ class NullEventProcessor : public IEventProcessor { void FlushAsync() override; void ShutdownAsync() override; }; -} // namespace launchdarkly::client_side +} // namespace launchdarkly::events diff --git a/libs/internal/include/launchdarkly/serialization/events/json_events.hpp b/libs/internal/include/launchdarkly/serialization/events/json_events.hpp index cdb6acf2a..1578eed07 100644 --- a/libs/internal/include/launchdarkly/serialization/events/json_events.hpp +++ b/libs/internal/include/launchdarkly/serialization/events/json_events.hpp @@ -1,11 +1,19 @@ #pragma once +#include +#include + #include -#include -#include +namespace launchdarkly::events::server_side { + +void tag_invoke(boost::json::value_from_tag const&, + boost::json::value& json_value, + IndexEvent const& event); +} // namespace launchdarkly::events::server_side + +namespace launchdarkly::events { -namespace launchdarkly::events::client { void tag_invoke(boost::json::value_from_tag const&, boost::json::value& json_value, FeatureEvent const& event); @@ -21,9 +29,6 @@ void tag_invoke(boost::json::value_from_tag const&, void tag_invoke(boost::json::value_from_tag const&, boost::json::value& json_value, DebugEvent const& event); -} // namespace launchdarkly::events::client - -namespace launchdarkly::events { void tag_invoke(boost::json::value_from_tag const&, boost::json::value& json_value, @@ -39,7 +44,7 @@ void tag_invoke(boost::json::value_from_tag const&, } // namespace launchdarkly::events -namespace launchdarkly::events { +namespace launchdarkly::events::detail { void tag_invoke(boost::json::value_from_tag const&, boost::json::value& json_value, @@ -47,4 +52,4 @@ void tag_invoke(boost::json::value_from_tag const&, void tag_invoke(boost::json::value_from_tag const&, boost::json::value& json_value, Summarizer const& summary); -} // namespace launchdarkly::events +} // namespace launchdarkly::events::detail diff --git a/libs/internal/include/launchdarkly/serialization/json_attributes.hpp b/libs/internal/include/launchdarkly/serialization/json_attributes.hpp index 5dbd20c36..c7be6612b 100644 --- a/libs/internal/include/launchdarkly/serialization/json_attributes.hpp +++ b/libs/internal/include/launchdarkly/serialization/json_attributes.hpp @@ -1,9 +1,9 @@ #pragma once -#include - #include +#include + namespace launchdarkly { /** * Method used by boost::json for converting launchdarkly::Attributes into a diff --git a/libs/internal/include/launchdarkly/serialization/json_context.hpp b/libs/internal/include/launchdarkly/serialization/json_context.hpp index 491b7ebde..28c03b51c 100644 --- a/libs/internal/include/launchdarkly/serialization/json_context.hpp +++ b/libs/internal/include/launchdarkly/serialization/json_context.hpp @@ -1,12 +1,11 @@ #pragma once -#include - -#include - #include #include +#include +#include + namespace launchdarkly { /** * Method used by boost::json for converting a launchdarkly::Context into a diff --git a/libs/internal/include/launchdarkly/serialization/json_context_aware_reference.hpp b/libs/internal/include/launchdarkly/serialization/json_context_aware_reference.hpp new file mode 100644 index 000000000..c9eac9038 --- /dev/null +++ b/libs/internal/include/launchdarkly/serialization/json_context_aware_reference.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +namespace launchdarkly { + +template +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, + JsonError>> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + + using Type = data_model::ContextAwareReference; + + auto const& obj = json_value.as_object(); + + std::optional kind; + + PARSE_CONDITIONAL_FIELD(kind, obj, Type::fields::kContextFieldName); + + std::string attr_ref_or_name; + PARSE_FIELD_DEFAULT(attr_ref_or_name, obj, + Type::fields::kReferenceFieldName, "key"); + + if (kind) { + return Type{*kind, + AttributeReference::FromReferenceStr(attr_ref_or_name)}; + } + + return Type{data_model::ContextKind("user"), + AttributeReference::FromLiteralStr(attr_ref_or_name)}; +} + +} // namespace launchdarkly diff --git a/libs/internal/include/launchdarkly/serialization/json_context_kind.hpp b/libs/internal/include/launchdarkly/serialization/json_context_kind.hpp new file mode 100644 index 000000000..2551a65c9 --- /dev/null +++ b/libs/internal/include/launchdarkly/serialization/json_context_kind.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include +#include + +#include +#include + +#include + +namespace launchdarkly { + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& + unused, + boost::json::value const& json_value); + +// Serializers need to be in launchdarkly::data_model for ADL. +namespace data_model { + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::ContextKind const& segment); + +} + +} // namespace launchdarkly diff --git a/libs/internal/include/launchdarkly/serialization/json_evaluation_reason.hpp b/libs/internal/include/launchdarkly/serialization/json_evaluation_reason.hpp index cdb1734a4..deb3d9dfe 100644 --- a/libs/internal/include/launchdarkly/serialization/json_evaluation_reason.hpp +++ b/libs/internal/include/launchdarkly/serialization/json_evaluation_reason.hpp @@ -1,11 +1,10 @@ #pragma once -#include "tl/expected.hpp" - -#include - #include -#include "json_errors.hpp" +#include + +#include +#include namespace launchdarkly { /** @@ -24,8 +23,8 @@ tl::expected tag_invoke( boost::json::value const& json_value); tl::expected tag_invoke( - boost::json::value_to_tag< - tl::expected> const& unused, + boost::json::value_to_tag> const& unused, boost::json::value const& json_value); void tag_invoke(boost::json::value_from_tag const& unused, diff --git a/libs/internal/include/launchdarkly/serialization/json_evaluation_result.hpp b/libs/internal/include/launchdarkly/serialization/json_evaluation_result.hpp index d03617cfa..71f3f37c2 100644 --- a/libs/internal/include/launchdarkly/serialization/json_evaluation_result.hpp +++ b/libs/internal/include/launchdarkly/serialization/json_evaluation_result.hpp @@ -1,21 +1,16 @@ #pragma once -#include "tl/expected.hpp" - -#include - #include -#include "json_errors.hpp" +#include + +#include +#include namespace launchdarkly { -/** - * Method used by boost::json for converting a boost::json::value into a - * launchdarkly::EvaluationResult. - * @return A EvaluationResult representation of the boost::json::value. - */ -tl::expected tag_invoke( - boost::json::value_to_tag> const& - unused, + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& unused, boost::json::value const& json_value); void tag_invoke(boost::json::value_from_tag const& unused, diff --git a/libs/internal/include/launchdarkly/serialization/json_flag.hpp b/libs/internal/include/launchdarkly/serialization/json_flag.hpp new file mode 100644 index 000000000..775e2cb15 --- /dev/null +++ b/libs/internal/include/launchdarkly/serialization/json_flag.hpp @@ -0,0 +1,107 @@ +#pragma once + +#include +#include + +#include + +namespace launchdarkly { + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, + JsonError>> const& unused, + boost::json::value const& json_value); + +tl::expected, JsonError> +tag_invoke(boost::json::value_to_tag< + tl::expected, + JsonError>> const& unused, + boost::json::value const& json_value); + +tl::expected, + JsonError> +tag_invoke(boost::json::value_to_tag, + JsonError>> const& unused, + boost::json::value const& json_value); + +tl::expected, JsonError> +tag_invoke(boost::json::value_to_tag< + tl::expected, + JsonError>> const& unused, + boost::json::value const& json_value); + +tl::expected, JsonError> +tag_invoke(boost::json::value_to_tag< + tl::expected, + JsonError>> const& unused, + boost::json::value const& json_value); + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& + unused, + boost::json::value const& json_value); + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& + unused, + boost::json::value const& json_value); + +tl::expected, JsonError> +tag_invoke( + boost::json::value_to_tag< + tl::expected, + JsonError>> const& unused, + boost::json::value const& json_value); + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& unused, + boost::json::value const& json_value); + +// Serializers need to be in launchdarkly::data_model for ADL. +namespace data_model { + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Flag::Rollout const& rollout); + +void tag_invoke( + boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Flag::VariationOrRollout const& variation_or_rollout); + +void tag_invoke( + boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Flag::Rollout::WeightedVariation const& weighted_variation); + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Flag::Rollout::Kind const& kind); + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Flag::Prerequisite const& prerequisite); + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Flag::Target const& target); + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Flag::Rule const& rule); + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Flag::ClientSideAvailability const& availability); + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Flag const& flag); + +} // namespace data_model +} // namespace launchdarkly diff --git a/libs/internal/include/launchdarkly/serialization/json_item_descriptor.hpp b/libs/internal/include/launchdarkly/serialization/json_item_descriptor.hpp new file mode 100644 index 000000000..a04ffe378 --- /dev/null +++ b/libs/internal/include/launchdarkly/serialization/json_item_descriptor.hpp @@ -0,0 +1,57 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include + +namespace launchdarkly { + +template +tl::expected>, JsonError> +tag_invoke(boost::json::value_to_tag< + tl::expected>, + JsonError>> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + + auto maybe_item = + boost::json::value_to, JsonError>>( + json_value); + + if (!maybe_item) { + return tl::unexpected(maybe_item.error()); + } + + auto const& item = maybe_item.value(); + + if (!item) { + return std::nullopt; + } + + return data_model::ItemDescriptor(std::move(item.value())); +} + +template +void tag_invoke( + boost::json::value_from_tag const& unused, + boost::json::value& json_value, + std::unordered_map>> const& + all_flags) { + boost::ignore_unused(unused); + + auto& obj = json_value.emplace_object(); + for (auto descriptor : all_flags) { + // Only serialize non-deleted items.. + if (descriptor.second->item) { + auto eval_result_json = + boost::json::value_from(*descriptor.second->item); + obj.emplace(descriptor.first, eval_result_json); + } + } +} +} // namespace launchdarkly diff --git a/libs/internal/include/launchdarkly/serialization/json_primitives.hpp b/libs/internal/include/launchdarkly/serialization/json_primitives.hpp new file mode 100644 index 000000000..268b9596a --- /dev/null +++ b/libs/internal/include/launchdarkly/serialization/json_primitives.hpp @@ -0,0 +1,121 @@ +#pragma once + +#include +#include + +#include +#include + +#include +#include + +namespace launchdarkly { + +template +tl::expected>, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected>, JsonError>> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + + if (json_value.is_null()) { + return std::nullopt; + } + + if (!json_value.is_array()) { + return tl::unexpected(JsonError::kSchemaFailure); + } + + auto const& arr = json_value.as_array(); + std::vector items; + items.reserve(arr.size()); + for (auto const& item : arr) { + auto eval_result = + boost::json::value_to, JsonError>>( + item); + if (!eval_result.has_value()) { + return tl::unexpected(eval_result.error()); + } + auto maybe_val = eval_result.value(); + if (maybe_val) { + items.emplace_back(std::move(maybe_val.value())); + } + } + return items; +} + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& unused, + boost::json::value const& json_value); + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& unused, + boost::json::value const& json_value); + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& unused, + boost::json::value const& json_value); + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& unused, + boost::json::value const& json_value); + +template +tl::expected>, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected>, JsonError>> const& + unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + + if (json_value.is_null()) { + return std::nullopt; + } + if (!json_value.is_object()) { + return tl::unexpected(JsonError::kSchemaFailure); + } + auto const& obj = json_value.as_object(); + std::unordered_map descriptors; + for (auto const& pair : obj) { + auto eval_result = + boost::json::value_to, JsonError>>( + pair.value()); + if (!eval_result) { + return tl::unexpected(eval_result.error()); + } + auto const& maybe_val = eval_result.value(); + if (maybe_val) { + descriptors.emplace(pair.key(), std::move(maybe_val.value())); + } + } + return descriptors; +} + +/** + * Convenience implementation that deserializes a T via the tag_invoke overload + * for std::optional. + * + * If that overload returns std::nullopt, this returns + * a default-constructed T. + * + * Json errors are propagated. + */ +template +tl::expected tag_invoke( + boost::json::value_to_tag> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + auto maybe_val = + boost::json::value_to, JsonError>>( + json_value); + if (!maybe_val.has_value()) { + return tl::unexpected(maybe_val.error()); + } + return maybe_val.value().value_or(T()); +} + +} // namespace launchdarkly diff --git a/libs/internal/include/launchdarkly/serialization/json_rule_clause.hpp b/libs/internal/include/launchdarkly/serialization/json_rule_clause.hpp new file mode 100644 index 000000000..a66d85606 --- /dev/null +++ b/libs/internal/include/launchdarkly/serialization/json_rule_clause.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include +#include + +#include +#include + +namespace launchdarkly { +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag, + JsonError>> const& unused, + boost::json::value const& json_value); + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& + unused, + boost::json::value const& json_value); + +tl::expected tag_invoke( + boost::json::value_to_tag< + tl::expected> const& unused, + boost::json::value const& json_value); + +// Serialization needs to be in launchdarkly::data_model for ADL. +namespace data_model { + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Clause const& clause); + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Clause::Op const& op); + +} // namespace data_model + +} // namespace launchdarkly diff --git a/libs/internal/include/launchdarkly/serialization/json_sdk_data_set.hpp b/libs/internal/include/launchdarkly/serialization/json_sdk_data_set.hpp new file mode 100644 index 000000000..f2501bfa3 --- /dev/null +++ b/libs/internal/include/launchdarkly/serialization/json_sdk_data_set.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include +#include + +#include + +namespace launchdarkly { +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& + unused, + boost::json::value const& json_value); + +} // namespace launchdarkly diff --git a/libs/internal/include/launchdarkly/serialization/json_segment.hpp b/libs/internal/include/launchdarkly/serialization/json_segment.hpp new file mode 100644 index 000000000..83987903e --- /dev/null +++ b/libs/internal/include/launchdarkly/serialization/json_segment.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include +#include + +#include + +namespace launchdarkly { +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag, + JsonError>> const& unused, + boost::json::value const& json_value); + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, + JsonError>> const& unused, + boost::json::value const& json_value); + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, + JsonError>> const& unused, + boost::json::value const& json_value); + +// Serializers need to be in launchdarkly::data_model for ADL. +namespace data_model { + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Segment const& segment); + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Segment::Target const& target); + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Segment::Rule const& rule); + +} // namespace data_model + +} // namespace launchdarkly diff --git a/libs/internal/include/launchdarkly/serialization/json_value.hpp b/libs/internal/include/launchdarkly/serialization/json_value.hpp index 5c02047d9..c99b1f5bb 100644 --- a/libs/internal/include/launchdarkly/serialization/json_value.hpp +++ b/libs/internal/include/launchdarkly/serialization/json_value.hpp @@ -1,17 +1,24 @@ #pragma once -#include - +#include #include +#include +#include + namespace launchdarkly { /** * Method used by boost::json for converting a boost::json::value into a * launchdarkly::Value. * @return A Value representation of the boost::json::value. */ +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const&, + boost::json::value const&); + Value tag_invoke(boost::json::value_to_tag const&, - boost::json::value const&); + boost::json::value const& json_value); /** * Method used by boost::json for converting a launchdarkly::Value into a diff --git a/libs/internal/include/launchdarkly/serialization/value_mapping.hpp b/libs/internal/include/launchdarkly/serialization/value_mapping.hpp index b7cc5aa3d..09d90b4e2 100644 --- a/libs/internal/include/launchdarkly/serialization/value_mapping.hpp +++ b/libs/internal/include/launchdarkly/serialization/value_mapping.hpp @@ -1,10 +1,108 @@ #pragma once +#include +#include + #include #include +#include -namespace launchdarkly { +#include +#include + +// Parses a field, propagating an error if the field's value is of the wrong +// type. If the field was null or omitted in the data, it is set to +// default_value. +#define PARSE_FIELD_DEFAULT(field, obj, key, default_value) \ + do { \ + std::optional maybe_val; \ + PARSE_CONDITIONAL_FIELD(maybe_val, obj, key); \ + field = maybe_val.value_or(default_value); \ + } while (0) + +// Parses a field, propagating an error if the field's value is of the wrong +// type. Intended for fields where the "zero value" of that field is a valid +// member of the domain of that field. If the "zero value" of the field is meant +// to denote absence of that field, rather than a valid member of the domain, +// then use PARSE_CONDITIONAL_FIELD in order to avoid discarding the information +// of whether that field was present or not. +#define PARSE_FIELD(field, obj, key) \ + do { \ + static_assert(std::is_default_constructible_v && \ + "field must be default-constructible"); \ + PARSE_FIELD_DEFAULT(field, obj, key, decltype(field){}); \ + } while (0) + +// Parses a field that is conditional and/or has no valid default value. +// This would be the case for fields that depend on the existence of some other +// field. Another scenario would be a string field representing enum values, +// where there's no default defined/empty string is meaningless. It will +// propagate an error if the field's value is of the wrong type. Intended to be +// called on fields of type std::optional. +#define PARSE_CONDITIONAL_FIELD(field, obj, key) \ + do { \ + auto const& it = obj.find(key); \ + if (it != obj.end()) { \ + if (auto result = boost::json::value_to< \ + tl::expected>(it->value())) { \ + field = result.value(); \ + } else { \ + /* Field was of wrong type. */ \ + return tl::make_unexpected(result.error()); \ + } \ + } \ + } while (0) + +// Parses a field, propagating an error if it is omitted/null or if the field's +// value is of the wrong type. Use only if the field *must* be present in the +// JSON document. Think twice; this is unlikely - most fields have a +// well-defined default value that can be used if not present. +#define PARSE_REQUIRED_FIELD(field, obj, key) \ + do { \ + auto const& it = obj.find(key); \ + if (it == obj.end()) { \ + /* Ideally report that field is missing, instead of generic \ + * failure */ \ + return tl::make_unexpected(JsonError::kSchemaFailure); \ + } \ + auto result = boost::json::value_to< \ + tl::expected, JsonError>>( \ + it->value()); \ + if (!result) { \ + /* The field's value is of the wrong type. */ \ + return tl::make_unexpected(result.error()); \ + } \ + /* We have the field, but its value might be null. */ \ + auto const& maybe_val = result.value(); \ + if (!maybe_val) { \ + /* Ideally report that the field was null, instead of generic \ + * failure. */ \ + return tl::make_unexpected(JsonError::kSchemaFailure); \ + } \ + field = std::move(*maybe_val); \ + } while (0) + +#define REQUIRE_OBJECT(value) \ + do { \ + if (json_value.is_null()) { \ + return std::nullopt; \ + } \ + if (!json_value.is_object()) { \ + return tl::make_unexpected(JsonError::kSchemaFailure); \ + } \ + } while (0) +#define REQUIRE_STRING(value) \ + do { \ + if (json_value.is_null()) { \ + return std::nullopt; \ + } \ + if (!json_value.is_string()) { \ + return tl::make_unexpected(JsonError::kSchemaFailure); \ + } \ + } while (0) + +namespace launchdarkly { template std::optional ValueAsOpt(boost::json::object::const_iterator iterator, boost::json::object::const_iterator end) { @@ -58,4 +156,18 @@ template <> std::string ValueOrDefault(boost::json::object::const_iterator iterator, boost::json::object::const_iterator end, std::string default_value); + +template +void WriteMinimal(boost::json::object& obj, + std::string const& key, // No copy when not used. + std::optional val) { + if (val.has_value()) { + obj.emplace(key, boost::json::value_from(val.value())); + } +} + +void WriteMinimal(boost::json::object& obj, + std::string const& key, // No copy when not used. + bool val); + } // namespace launchdarkly diff --git a/libs/client-sdk/src/boost_signal_connection.hpp b/libs/internal/include/launchdarkly/signals/boost_signal_connection.hpp similarity index 78% rename from libs/client-sdk/src/boost_signal_connection.hpp rename to libs/internal/include/launchdarkly/signals/boost_signal_connection.hpp index 16eeda9f9..14e92348a 100644 --- a/libs/client-sdk/src/boost_signal_connection.hpp +++ b/libs/internal/include/launchdarkly/signals/boost_signal_connection.hpp @@ -4,7 +4,7 @@ #include -namespace launchdarkly::client_side { +namespace launchdarkly::internal::signals { class SignalConnection : public IConnection { public: @@ -16,4 +16,4 @@ class SignalConnection : public IConnection { boost::signals2::connection connection_; }; -} // namespace launchdarkly::client_side +} // namespace launchdarkly::internal::signals diff --git a/libs/internal/src/CMakeLists.txt b/libs/internal/src/CMakeLists.txt index 7c0f69c29..03d499c20 100644 --- a/libs/internal/src/CMakeLists.txt +++ b/libs/internal/src/CMakeLists.txt @@ -5,19 +5,23 @@ file(GLOB HEADER_LIST CONFIGURE_DEPENDS "${LaunchDarklyInternalSdk_SOURCE_DIR}/include/launchdarkly/network/*.hpp" "${LaunchDarklyInternalSdk_SOURCE_DIR}/include/launchdarkly/serialization/*.hpp" "${LaunchDarklyInternalSdk_SOURCE_DIR}/include/launchdarkly/serialization/events/*.hpp" -) + "${LaunchDarklyInternalSdk_SOURCE_DIR}/include/launchdarkly/signals/*.hpp" + "${LaunchDarklyInternalSdk_SOURCE_DIR}/include/launchdarkly/data_sources/*.hpp" + ) # Automatic library: static or dynamic based on user config. add_library(${LIBNAME} OBJECT ${HEADER_LIST} context_filter.cpp events/asio_event_processor.cpp - events/client_events.cpp + events/null_event_processor.cpp + events/common_events.cpp events/event_batch.cpp events/outbox.cpp events/request_worker.cpp events/summarizer.cpp events/worker_pool.cpp + events/lru_cache.cpp logging/console_backend.cpp logging/null_logger.cpp logging/logger.cpp @@ -31,8 +35,18 @@ add_library(${LIBNAME} OBJECT serialization/json_value.cpp serialization/value_mapping.cpp serialization/json_evaluation_result.cpp + serialization/json_sdk_data_set.cpp + serialization/json_segment.cpp + serialization/json_primitives.cpp + serialization/json_rule_clause.cpp + serialization/json_flag.cpp + serialization/json_context_kind.cpp + data_model/rule_clause.cpp + data_model/flag.cpp encoding/base_64.cpp - encoding/sha_256.cpp) + encoding/sha_256.cpp + encoding/sha_1.cpp + signals/boost_signal_connection.cpp) add_library(launchdarkly::internal ALIAS ${LIBNAME}) diff --git a/libs/internal/src/data_model/flag.cpp b/libs/internal/src/data_model/flag.cpp new file mode 100644 index 000000000..2a03074a7 --- /dev/null +++ b/libs/internal/src/data_model/flag.cpp @@ -0,0 +1,51 @@ +#include + +namespace launchdarkly::data_model { + +Flag::Rollout::WeightedVariation::WeightedVariation(Flag::Variation variation_, + Flag::Weight weight_) + : WeightedVariation(variation_, weight_, false) {} + +Flag::Rollout::WeightedVariation::WeightedVariation(Flag::Variation variation_, + Flag::Weight weight_, + bool untracked_) + : variation(variation_), weight(weight_), untracked(untracked_) {} + +Flag::Rollout::WeightedVariation Flag::Rollout::WeightedVariation::Untracked( + Flag::Variation variation_, + Flag::Weight weight_) { + return {variation_, weight_, true}; +} +Flag::Rollout::WeightedVariation::WeightedVariation() + : variation(0), weight(0), untracked(false) {} + +Flag::Rollout::Rollout(std::vector variations_) + : variations(std::move(variations_)), + kind(Kind::kRollout), + seed(std::nullopt), + bucketBy("key"), + contextKind("user") {} + +bool Flag::IsExperimentationEnabled( + std::optional const& reason) const { + if (!reason) { + return false; + } + if (reason->InExperiment()) { + return true; + } + switch (reason->Kind()) { + case EvaluationReason::Kind::kFallthrough: + return this->trackEventsFallthrough; + case EvaluationReason::Kind::kRuleMatch: + if (!reason->RuleIndex() || + reason->RuleIndex() >= this->rules.size()) { + return false; + } + return this->rules.at(*reason->RuleIndex()).trackEvents; + default: + return false; + } +} + +} // namespace launchdarkly::data_model diff --git a/libs/internal/src/data_model/rule_clause.cpp b/libs/internal/src/data_model/rule_clause.cpp new file mode 100644 index 000000000..664fa14aa --- /dev/null +++ b/libs/internal/src/data_model/rule_clause.cpp @@ -0,0 +1,62 @@ +#include + +namespace launchdarkly::data_model { + +std::ostream& operator<<(std::ostream& os, Clause::Op operator_) { + switch (operator_) { + case Clause::Op::kUnrecognized: + os << "unrecognized"; + break; + case Clause::Op::kIn: + os << "in"; + break; + case Clause::Op::kStartsWith: + os << "startsWith"; + break; + case Clause::Op::kEndsWith: + os << "endsWith"; + break; + case Clause::Op::kMatches: + os << "matches"; + break; + case Clause::Op::kContains: + os << "contains"; + break; + case Clause::Op::kLessThan: + os << "lessThan"; + break; + case Clause::Op::kLessThanOrEqual: + os << "lessThanOrEqual"; + break; + case Clause::Op::kGreaterThan: + os << "greaterThan"; + break; + case Clause::Op::kGreaterThanOrEqual: + os << "greaterThanOrEqual"; + break; + case Clause::Op::kBefore: + os << "before"; + break; + case Clause::Op::kAfter: + os << "after"; + break; + case Clause::Op::kSemVerEqual: + os << "semVerEqual"; + break; + case Clause::Op::kSemVerLessThan: + os << "semVerLessThan"; + break; + case Clause::Op::kSemVerGreaterThan: + os << "semVerGreaterThan"; + break; + case Clause::Op::kSegmentMatch: + os << "segmentMatch"; + break; + default: + os << "unknown"; + break; + } + return os; +} + +} // namespace launchdarkly::data_model diff --git a/libs/internal/src/encoding/sha_1.cpp b/libs/internal/src/encoding/sha_1.cpp new file mode 100644 index 000000000..c516995bb --- /dev/null +++ b/libs/internal/src/encoding/sha_1.cpp @@ -0,0 +1,18 @@ +#include + +#include + +namespace launchdarkly::encoding { + +std::array Sha1String( + std::string const& input) { + std::array hash{}; + + SHA_CTX sha; + SHA1_Init(&sha); + SHA1_Update(&sha, input.c_str(), input.size()); + SHA1_Final(hash.data(), &sha); + + return hash; +} +} // namespace launchdarkly::encoding diff --git a/libs/internal/src/encoding/sha_256.cpp b/libs/internal/src/encoding/sha_256.cpp index f968a8a98..a9c208979 100644 --- a/libs/internal/src/encoding/sha_256.cpp +++ b/libs/internal/src/encoding/sha_256.cpp @@ -1,24 +1,19 @@ -#include "openssl/sha.h" +#include #include -#include -#include -#include - namespace launchdarkly::encoding { std::array Sha256String( std::string const& input) { - unsigned char hash[SHA256_DIGEST_LENGTH]; + std::array hash{}; + SHA256_CTX sha256; SHA256_Init(&sha256); SHA256_Update(&sha256, input.c_str(), input.size()); - SHA256_Final(hash, &sha256); + SHA256_Final(hash.data(), &sha256); - std::array out; - std::copy(std::begin(hash), std::end(hash), out.begin()); - return out; + return hash; } } // namespace launchdarkly::encoding diff --git a/libs/internal/src/events/asio_event_processor.cpp b/libs/internal/src/events/asio_event_processor.cpp index 547f79638..d8512af47 100644 --- a/libs/internal/src/events/asio_event_processor.cpp +++ b/libs/internal/src/events/asio_event_processor.cpp @@ -11,6 +11,8 @@ #include #include +#include + namespace http = boost::beast::http; namespace launchdarkly::events { @@ -54,6 +56,7 @@ AsioEventProcessor::AsioEventProcessor( last_known_past_time_(std::nullopt), filter_(events_config.AllAttributesPrivate(), events_config.PrivateAttributes()), + context_key_cache_(events_config.ContextKeysCacheCapacity().value_or(0)), logger_(logger) { ScheduleFlush(); } @@ -87,7 +90,7 @@ void AsioEventProcessor::InboxDecrement() { } template -void AsioEventProcessor::AsyncSend(InputEvent input_event) { +void AsioEventProcessor::SendAsync(InputEvent input_event) { if (!InboxIncrement()) { return; } @@ -112,7 +115,7 @@ void AsioEventProcessor::HandleSend(InputEvent event) { template void AsioEventProcessor::Flush(FlushTrigger flush_type) { - workers_.Get([this](RequestWorker* worker) { + workers_.Get([this](detail::RequestWorker* worker) { if (worker == nullptr) { LD_LOG(logger_, LogLevel::kDebug) << "event-processor: no flush workers available; skipping " @@ -127,10 +130,11 @@ void AsioEventProcessor::Flush(FlushTrigger flush_type) { } worker->AsyncDeliver( std::move(*batch), - [this](std::size_t count, RequestWorker::DeliveryResult result) { + [this](std::size_t count, + detail::RequestWorker::DeliveryResult result) { OnEventDeliveryResult(count, result); }); - summarizer_ = Summarizer(Clock::now()); + summarizer_ = detail::Summarizer(Clock::now()); }); if (flush_type == FlushTrigger::Automatic) { @@ -141,7 +145,7 @@ void AsioEventProcessor::Flush(FlushTrigger flush_type) { template void AsioEventProcessor::OnEventDeliveryResult( std::size_t event_count, - RequestWorker::DeliveryResult result) { + detail::RequestWorker::DeliveryResult result) { boost::ignore_unused(event_count); std::visit( @@ -176,17 +180,17 @@ void AsioEventProcessor::ScheduleFlush() { } template -void AsioEventProcessor::AsyncFlush() { +void AsioEventProcessor::FlushAsync() { boost::asio::post(io_, [=] { Flush(FlushTrigger::Manual); }); } template -void AsioEventProcessor::AsyncClose() { +void AsioEventProcessor::ShutdownAsync() { timer_.cancel(); } template -std::optional AsioEventProcessor::CreateBatch() { +std::optional AsioEventProcessor::CreateBatch() { auto events = boost::json::value_from(outbox_.Consume()).as_array(); bool has_summary = @@ -205,7 +209,7 @@ std::optional AsioEventProcessor::CreateBatch() { props.Header(kPayloadIdHeader, boost::lexical_cast(uuids_())); props.Header(to_string(http::field::content_type), "application/json"); - return EventBatch(url_, props.Build(), events); + return detail::EventBatch(url_, props.Build(), events); } template @@ -213,52 +217,82 @@ std::vector AsioEventProcessor::Process( InputEvent input_event) { std::vector out; std::visit( - overloaded{[&](client::FeatureEventParams&& event) { - summarizer_.Update(event); - - client::FeatureEventBase base{event}; - - auto debug_until_date = event.debug_events_until_date; - - // To be conservative, use as the current time the - // maximum of the actual current time and the server's - // time. This way if the local host is running behind, we - // won't accidentally keep emitting events. - - auto conservative_now = std::max( - std::chrono::system_clock::now(), - last_known_past_time_.value_or( - std::chrono::system_clock::from_time_t(0))); - - bool emit_debug_event = - debug_until_date && - conservative_now < debug_until_date->t; - - if (emit_debug_event) { - out.emplace_back(client::DebugEvent{ - base, filter_.filter(event.context)}); - } - - if (event.require_full_event) { - out.emplace_back(client::FeatureEvent{ - std::move(base), event.context.KindsToKeys()}); - } - }, - [&](client::IdentifyEventParams&& event) { - // Contexts should already have been checked for - // validity by this point. - assert(event.context.Valid()); - out.emplace_back(client::IdentifyEvent{ - event.creation_date, filter_.filter(event.context)}); - }, - [&](TrackEventParams&& event) { - out.emplace_back(std::move(event)); - }}, + overloaded{ + [&](FeatureEventParams&& event) { + summarizer_.Update(event); + + if constexpr (std::is_same::value) { + if (!context_key_cache_.Notice( + event.context.CanonicalKey())) { + out.emplace_back(server_side::IndexEvent{ + event.creation_date, + filter_.filter(event.context)}); + } + } + + FeatureEventBase base{event}; + + auto debug_until_date = event.debug_events_until_date; + + // To be conservative, use as the current time the + // maximum of the actual current time and the server's + // time. This way if the local host is running behind, we + // won't accidentally keep emitting events. + + auto conservative_now = + std::max(std::chrono::system_clock::now(), + last_known_past_time_.value_or( + std::chrono::system_clock::from_time_t(0))); + + bool emit_debug_event = + debug_until_date && conservative_now < debug_until_date->t; + + if (emit_debug_event) { + out.emplace_back( + DebugEvent{base, filter_.filter(event.context)}); + } + + if (event.require_full_event) { + out.emplace_back(FeatureEvent{std::move(base), + event.context.KindsToKeys()}); + } + }, + [&](IdentifyEventParams&& event) { + // Contexts should already have been checked for + // validity by this point. + assert(event.context.Valid()); + + if constexpr (std::is_same::value) { + context_key_cache_.Notice(event.context.CanonicalKey()); + } + + out.emplace_back(IdentifyEvent{event.creation_date, + filter_.filter(event.context)}); + }, + [&](ClientTrackEventParams&& event) { + out.emplace_back(std::move(event)); + }, + [&](ServerTrackEventParams&& event) { + if constexpr (std::is_same::value) { + if (!context_key_cache_.Notice( + event.context.CanonicalKey())) { + out.emplace_back(server_side::IndexEvent{ + event.base.creation_date, + filter_.filter(event.context)}); + } + } + + out.emplace_back(std::move(event.base)); + }}, std::move(input_event)); return out; } template class AsioEventProcessor; +template class AsioEventProcessor; } // namespace launchdarkly::events diff --git a/libs/internal/src/events/client_events.cpp b/libs/internal/src/events/common_events.cpp similarity index 57% rename from libs/internal/src/events/client_events.cpp rename to libs/internal/src/events/common_events.cpp index 33c4dcce4..e6fdbc096 100644 --- a/libs/internal/src/events/client_events.cpp +++ b/libs/internal/src/events/common_events.cpp @@ -1,6 +1,6 @@ -#include +#include -namespace launchdarkly::events::client { +namespace launchdarkly::events { FeatureEventBase::FeatureEventBase(FeatureEventParams const& params) : creation_date(params.creation_date), key(params.key), @@ -8,6 +8,6 @@ FeatureEventBase::FeatureEventBase(FeatureEventParams const& params) variation(params.variation), value(params.value), reason(params.reason), - default_(params.default_) {} - -} // namespace launchdarkly::events::client + default_(params.default_), + prereq_of(params.prereq_of) {} +} // namespace launchdarkly::events diff --git a/libs/internal/src/events/event_batch.cpp b/libs/internal/src/events/event_batch.cpp index ade9ad4ae..710affba7 100644 --- a/libs/internal/src/events/event_batch.cpp +++ b/libs/internal/src/events/event_batch.cpp @@ -1,8 +1,8 @@ -#include +#include #include -namespace launchdarkly::events { +namespace launchdarkly::events::detail { EventBatch::EventBatch(std::string url, config::shared::built::HttpProperties http_props, boost::json::value const& events) @@ -24,4 +24,4 @@ std::string EventBatch::Target() const { return request_.Url(); } -} // namespace launchdarkly::events +} // namespace launchdarkly::events::detail diff --git a/libs/internal/src/events/lru_cache.cpp b/libs/internal/src/events/lru_cache.cpp new file mode 100644 index 000000000..4b92375e9 --- /dev/null +++ b/libs/internal/src/events/lru_cache.cpp @@ -0,0 +1,32 @@ +#include + +namespace launchdarkly::events::detail { +LRUCache::LRUCache(std::size_t capacity) + : capacity_(capacity), map_(), list_() {} + +bool LRUCache::Notice(std::string const& value) { + auto it = map_.find(value); + if (it != map_.end()) { + list_.remove(value); + list_.push_front(value); + return true; + } + while (map_.size() >= capacity_) { + map_.erase(list_.back()); + list_.pop_back(); + } + list_.push_front(value); + map_.emplace(value, list_.front()); + return false; +} + +void LRUCache::Clear() { + map_.clear(); + list_.clear(); +} + +std::size_t LRUCache::Size() const { + return list_.size(); +} + +} // namespace launchdarkly::events::detail diff --git a/libs/client-sdk/src/event_processor/null_event_processor.cpp b/libs/internal/src/events/null_event_processor.cpp similarity index 54% rename from libs/client-sdk/src/event_processor/null_event_processor.cpp rename to libs/internal/src/events/null_event_processor.cpp index b12e710ca..3f6352515 100644 --- a/libs/client-sdk/src/event_processor/null_event_processor.cpp +++ b/libs/internal/src/events/null_event_processor.cpp @@ -1,10 +1,10 @@ -#include "null_event_processor.hpp" +#include -namespace launchdarkly::client_side { +namespace launchdarkly::events { void NullEventProcessor::SendAsync(events::InputEvent event) {} void NullEventProcessor::FlushAsync() {} void NullEventProcessor::ShutdownAsync() {} -} // namespace launchdarkly::client_side +} // namespace launchdarkly::events diff --git a/libs/internal/src/events/outbox.cpp b/libs/internal/src/events/outbox.cpp index 233e1e0f9..33e95d008 100644 --- a/libs/internal/src/events/outbox.cpp +++ b/libs/internal/src/events/outbox.cpp @@ -1,6 +1,6 @@ -#include +#include -namespace launchdarkly::events { +namespace launchdarkly::events::detail { Outbox::Outbox(std::size_t capacity) : items_(), capacity_(capacity) {} @@ -34,8 +34,8 @@ std::vector Outbox::Consume() { return out; } -bool Outbox::Empty() { +bool Outbox::Empty() const { return items_.empty(); } -} // namespace launchdarkly::events +} // namespace launchdarkly::events::detail diff --git a/libs/internal/src/events/request_worker.cpp b/libs/internal/src/events/request_worker.cpp index cd632fe6d..b39090294 100644 --- a/libs/internal/src/events/request_worker.cpp +++ b/libs/internal/src/events/request_worker.cpp @@ -1,7 +1,7 @@ -#include -#include +#include +#include -namespace launchdarkly::events { +namespace launchdarkly::events::detail { RequestWorker::RequestWorker(boost::asio::any_io_executor io, std::chrono::milliseconds retry_after, @@ -213,4 +213,4 @@ std::ostream& operator<<(std::ostream& out, Action const& s) { return out; } -} // namespace launchdarkly::events +} // namespace launchdarkly::events::detail diff --git a/libs/internal/src/events/summarizer.cpp b/libs/internal/src/events/summarizer.cpp index a196b5473..3ca0ff236 100644 --- a/libs/internal/src/events/summarizer.cpp +++ b/libs/internal/src/events/summarizer.cpp @@ -1,6 +1,6 @@ -#include +#include -namespace launchdarkly::events { +namespace launchdarkly::events::detail { Summarizer::Summarizer(std::chrono::system_clock::time_point start) : start_time_(start) {} @@ -14,7 +14,7 @@ Summarizer::Features() const { return features_; } -void Summarizer::Update(client::FeatureEventParams const& event) { +void Summarizer::Update(events::FeatureEventParams const& event) { auto const& kinds = event.context.Kinds(); auto feature_state_iterator = @@ -37,11 +37,11 @@ Summarizer& Summarizer::Finish(Time end_time) { return *this; } -Summarizer::Time Summarizer::start_time() const { +Summarizer::Time Summarizer::StartTime() const { return start_time_; } -Summarizer::Time Summarizer::end_time() const { +Summarizer::Time Summarizer::EndTime() const { return end_time_; } @@ -69,4 +69,4 @@ std::int32_t Summarizer::VariationSummary::Count() const { Summarizer::State::State(Value default_value) : default_(std::move(default_value)) {} -} // namespace launchdarkly::events +} // namespace launchdarkly::events::detail diff --git a/libs/internal/src/events/worker_pool.cpp b/libs/internal/src/events/worker_pool.cpp index 370b60bed..caa4a2b70 100644 --- a/libs/internal/src/events/worker_pool.cpp +++ b/libs/internal/src/events/worker_pool.cpp @@ -1,9 +1,9 @@ #include -#include -#include +#include +#include -namespace launchdarkly::events { +namespace launchdarkly::events::detail { std::optional GetLocale(std::string const& locale, std::string const& tag, @@ -39,4 +39,4 @@ WorkerPool::WorkerPool(boost::asio::any_io_executor io, } } -} // namespace launchdarkly::events +} // namespace launchdarkly::events::detail diff --git a/libs/internal/src/serialization/events/json_events.cpp b/libs/internal/src/serialization/events/json_events.cpp index c3d821e15..a7ccaf303 100644 --- a/libs/internal/src/serialization/events/json_events.cpp +++ b/libs/internal/src/serialization/events/json_events.cpp @@ -2,11 +2,11 @@ #include #include -namespace launchdarkly::events::client { +namespace launchdarkly::events { void tag_invoke(boost::json::value_from_tag const& tag, boost::json::value& json_value, FeatureEvent const& event) { - auto base = boost::json::value_from(event); + auto base = boost::json::value_from(event.base); base.as_object().emplace("kind", "feature"); base.as_object().emplace("contextKeys", boost::json::value_from(event.context_keys)); @@ -16,7 +16,7 @@ void tag_invoke(boost::json::value_from_tag const& tag, void tag_invoke(boost::json::value_from_tag const& tag, boost::json::value& json_value, DebugEvent const& event) { - auto base = boost::json::value_from(event); + auto base = boost::json::value_from(event.base); base.as_object().emplace("kind", "debug"); base.as_object().emplace("context", boost::json::value_from(event.context)); json_value = std::move(base); @@ -39,6 +39,9 @@ void tag_invoke(boost::json::value_from_tag const& tag, obj.emplace("reason", boost::json::value_from(*event.reason)); } obj.emplace("default", boost::json::value_from(event.default_)); + if (event.prereq_of) { + obj.emplace("prereqOf", *event.prereq_of); + } } void tag_invoke(boost::json::value_from_tag const& tag, @@ -49,7 +52,19 @@ void tag_invoke(boost::json::value_from_tag const& tag, obj.emplace("creationDate", boost::json::value_from(event.creation_date)); obj.emplace("context", event.context); } -} // namespace launchdarkly::events::client +} // namespace launchdarkly::events + +namespace launchdarkly::events::server_side { + +void tag_invoke(boost::json::value_from_tag const&, + boost::json::value& json_value, + IndexEvent const& event) { + auto& obj = json_value.emplace_object(); + obj.emplace("kind", "index"); + obj.emplace("creationDate", boost::json::value_from(event.creation_date)); + obj.emplace("context", event.context); +} +} // namespace launchdarkly::events::server_side namespace launchdarkly::events { @@ -88,7 +103,7 @@ void tag_invoke(boost::json::value_from_tag const& tag, } // namespace launchdarkly::events -namespace launchdarkly::events { +namespace launchdarkly::events::detail { void tag_invoke(boost::json::value_from_tag const& tag, boost::json::value& json_value, @@ -119,8 +134,8 @@ void tag_invoke(boost::json::value_from_tag const& tag, auto& obj = json_value.emplace_object(); obj.emplace("kind", "summary"); obj.emplace("startDate", - boost::json::value_from(Date{summary.start_time()})); - obj.emplace("endDate", boost::json::value_from(Date{summary.end_time()})); + boost::json::value_from(Date{summary.StartTime()})); + obj.emplace("endDate", boost::json::value_from(Date{summary.EndTime()})); obj.emplace("features", boost::json::value_from(summary.Features())); } -} // namespace launchdarkly::events +} // namespace launchdarkly::events::detail diff --git a/libs/internal/src/serialization/json_attributes.cpp b/libs/internal/src/serialization/json_attributes.cpp index 763433889..f9ba610a3 100644 --- a/libs/internal/src/serialization/json_attributes.cpp +++ b/libs/internal/src/serialization/json_attributes.cpp @@ -2,6 +2,7 @@ #include #include +#include namespace launchdarkly { void tag_invoke(boost::json::value_from_tag const& unused, diff --git a/libs/internal/src/serialization/json_context.cpp b/libs/internal/src/serialization/json_context.cpp index 3c667ff8a..f61810196 100644 --- a/libs/internal/src/serialization/json_context.cpp +++ b/libs/internal/src/serialization/json_context.cpp @@ -1,11 +1,13 @@ #include #include #include +#include #include -#include - #include +#include + +#include namespace launchdarkly { void tag_invoke(boost::json::value_from_tag const&, @@ -103,7 +105,12 @@ std::optional ParseSingle(ContextBuilder& builder, attr == meta_iter || attr->value().is_null()) { continue; } - attrs.Set(attr->key(), boost::json::value_to(attr->value())); + auto maybe_unmarshalled_attr = + boost::json::value_to>( + attr->value()); + if (maybe_unmarshalled_attr) { + attrs.Set(attr->key(), maybe_unmarshalled_attr.value()); + } } return std::nullopt; diff --git a/libs/internal/src/serialization/json_context_kind.cpp b/libs/internal/src/serialization/json_context_kind.cpp new file mode 100644 index 000000000..fa00891fb --- /dev/null +++ b/libs/internal/src/serialization/json_context_kind.cpp @@ -0,0 +1,33 @@ +#include +#include + +#include + +namespace launchdarkly { +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& + unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + + REQUIRE_STRING(json_value); + auto const& str = json_value.as_string(); + + if (str.empty()) { + /* Empty string is not a valid context kind. */ + return tl::make_unexpected(JsonError::kSchemaFailure); + } + + return data_model::ContextKind(str.c_str()); +} + +namespace data_model { +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::ContextKind const& context_kind) { + boost::ignore_unused(unused); + json_value.emplace_string() = context_kind.t; +} +} // namespace data_model +} // namespace launchdarkly diff --git a/libs/internal/src/serialization/json_evaluation_reason.cpp b/libs/internal/src/serialization/json_evaluation_reason.cpp index 517281dd4..1ae824144 100644 --- a/libs/internal/src/serialization/json_evaluation_reason.cpp +++ b/libs/internal/src/serialization/json_evaluation_reason.cpp @@ -1,7 +1,9 @@ +#include #include #include #include +#include #include @@ -11,6 +13,7 @@ tl::expected tag_invoke( boost::json::value_to_tag< tl::expected> const& unused, boost::json::value const& json_value) { + boost::ignore_unused(unused); if (!json_value.is_string()) { return tl::unexpected(JsonError::kSchemaFailure); } @@ -39,6 +42,7 @@ tl::expected tag_invoke( void tag_invoke(boost::json::value_from_tag const& unused, boost::json::value& json_value, enum EvaluationReason::Kind const& kind) { + boost::ignore_unused(unused); auto& str = json_value.emplace_string(); switch (kind) { case EvaluationReason::Kind::kOff: @@ -66,6 +70,8 @@ tl::expected tag_invoke( boost::json::value_to_tag> const& unused, boost::json::value const& json_value) { + boost::ignore_unused(unused); + if (!json_value.is_string()) { return tl::unexpected(JsonError::kSchemaFailure); } @@ -94,6 +100,8 @@ tl::expected tag_invoke( void tag_invoke(boost::json::value_from_tag const& unused, boost::json::value& json_value, enum EvaluationReason::ErrorKind const& kind) { + boost::ignore_unused(unused); + auto& str = json_value.emplace_string(); switch (kind) { case EvaluationReason::ErrorKind::kClientNotReady: @@ -182,6 +190,8 @@ tl::expected tag_invoke( void tag_invoke(boost::json::value_from_tag const& unused, boost::json::value& json_value, EvaluationReason const& reason) { + boost::ignore_unused(unused); + auto& obj = json_value.emplace_object(); obj.emplace("kind", boost::json::value_from(reason.Kind())); if (auto error_kind = reason.ErrorKind()) { diff --git a/libs/internal/src/serialization/json_evaluation_result.cpp b/libs/internal/src/serialization/json_evaluation_result.cpp index 00a9e2ed7..7b7b8a335 100644 --- a/libs/internal/src/serialization/json_evaluation_result.cpp +++ b/libs/internal/src/serialization/json_evaluation_result.cpp @@ -1,97 +1,105 @@ +#include #include #include #include #include #include +#include namespace launchdarkly { -tl::expected tag_invoke( - boost::json::value_to_tag> const& - unused, + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& unused, boost::json::value const& json_value) { boost::ignore_unused(unused); - if (json_value.is_object()) { - auto& json_obj = json_value.as_object(); - auto* version_iter = json_obj.find("version"); - auto version_opt = ValueAsOpt(version_iter, json_obj.end()); - if (!version_opt.has_value()) { - return tl::unexpected(JsonError::kSchemaFailure); - } - auto version = version_opt.value(); - - auto* flag_version_iter = json_obj.find("flagVersion"); - auto flag_version = - ValueAsOpt(flag_version_iter, json_obj.end()); - - auto* track_events_iter = json_obj.find("trackEvents"); - auto track_events = - ValueOrDefault(track_events_iter, json_obj.end(), false); - - auto* track_reason_iter = json_obj.find("trackReason"); - auto track_reason = - ValueOrDefault(track_reason_iter, json_obj.end(), false); - - auto* debug_events_until_date_iter = - json_obj.find("debugEventsUntilDate"); - - auto debug_events_until_date = - MapOpt, - uint64_t>(ValueAsOpt(debug_events_until_date_iter, - json_obj.end()), - [](auto value) { - return std::chrono::system_clock::time_point{ - std::chrono::milliseconds{value}}; - }); - - // Evaluation detail is directly de-serialized inline here. - // This is because the shape of the evaluation detail is different - // when deserializing FlagMeta. Primarily `variation` not - // `variationIndex`. - - auto* value_iter = json_obj.find("value"); - if (value_iter == json_obj.end()) { - return tl::unexpected(JsonError::kSchemaFailure); - } - auto value = boost::json::value_to(value_iter->value()); + if (json_value.is_null()) { + return std::nullopt; + } + if (!json_value.is_object()) { + return tl::unexpected(JsonError::kSchemaFailure); + } + auto const& json_obj = json_value.as_object(); + + auto* version_iter = json_obj.find("version"); + auto version_opt = ValueAsOpt(version_iter, json_obj.end()); + if (!version_opt.has_value()) { + return tl::unexpected(JsonError::kSchemaFailure); + } + auto version = version_opt.value(); + + auto* flag_version_iter = json_obj.find("flagVersion"); + auto flag_version = ValueAsOpt(flag_version_iter, json_obj.end()); + + auto* track_events_iter = json_obj.find("trackEvents"); + auto track_events = + ValueOrDefault(track_events_iter, json_obj.end(), false); - auto* variation_iter = json_obj.find("variation"); - auto variation = ValueAsOpt(variation_iter, json_obj.end()); + auto* track_reason_iter = json_obj.find("trackReason"); + auto track_reason = + ValueOrDefault(track_reason_iter, json_obj.end(), false); - auto* reason_iter = json_obj.find("reason"); + auto* debug_events_until_date_iter = json_obj.find("debugEventsUntilDate"); - // There is a reason. - if (reason_iter != json_obj.end() && !reason_iter->value().is_null()) { - auto reason = boost::json::value_to< - tl::expected>( + auto debug_events_until_date = + MapOpt, uint64_t>( + ValueAsOpt(debug_events_until_date_iter, json_obj.end()), + [](auto value) { + return std::chrono::system_clock::time_point{ + std::chrono::milliseconds{value}}; + }); + + // Evaluation detail is directly de-serialized inline here. + // This is because the shape of the evaluation detail is different + // when deserializing FlagMeta. Primarily `variation` not + // `variationIndex`. + + auto* value_iter = json_obj.find("value"); + if (value_iter == json_obj.end()) { + return tl::unexpected(JsonError::kSchemaFailure); + } + + auto maybe_value = boost::json::value_to>( + value_iter->value()); + if (!maybe_value) { + return tl::unexpected(maybe_value.error()); + } + + auto* variation_iter = json_obj.find("variation"); + auto variation = ValueAsOpt(variation_iter, json_obj.end()); + + auto* reason_iter = json_obj.find("reason"); + + // There is a reason. + if (reason_iter != json_obj.end() && !reason_iter->value().is_null()) { + auto reason = + boost::json::value_to>( reason_iter->value()); - if (reason.has_value()) { - return EvaluationResult{ - version, - flag_version, - track_events, - track_reason, - debug_events_until_date, - EvaluationDetailInternal( - value, variation, std::make_optional(reason.value()))}; - } - // We could not parse the reason. - return tl::unexpected(JsonError::kSchemaFailure); + if (reason.has_value()) { + return EvaluationResult{ + version, + flag_version, + track_events, + track_reason, + debug_events_until_date, + EvaluationDetailInternal(*maybe_value, variation, + std::make_optional(reason.value()))}; } - - // There was no reason. - return EvaluationResult{ - version, - flag_version, - track_events, - track_reason, - debug_events_until_date, - EvaluationDetailInternal(value, variation, std::nullopt)}; + // We could not parse the reason. + return tl::unexpected(JsonError::kSchemaFailure); } - return tl::unexpected(JsonError::kSchemaFailure); + // There was no reason. + return EvaluationResult{ + version, + flag_version, + track_events, + track_reason, + debug_events_until_date, + EvaluationDetailInternal(*maybe_value, variation, std::nullopt)}; } void tag_invoke(boost::json::value_from_tag const& unused, diff --git a/libs/internal/src/serialization/json_flag.cpp b/libs/internal/src/serialization/json_flag.cpp new file mode 100644 index 000000000..1d040c1fa --- /dev/null +++ b/libs/internal/src/serialization/json_flag.cpp @@ -0,0 +1,369 @@ +#include +#include +#include +#include +#include +#include + +namespace launchdarkly { + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, + JsonError>> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + + REQUIRE_OBJECT(json_value); + auto const& obj = json_value.as_object(); + + data_model::Flag::Rollout rollout{}; + + PARSE_FIELD(rollout.variations, obj, "variations"); + PARSE_FIELD_DEFAULT(rollout.kind, obj, "kind", + data_model::Flag::Rollout::Kind::kRollout); + PARSE_CONDITIONAL_FIELD(rollout.seed, obj, "seed"); + + auto kind_and_bucket_by = boost::json::value_to, + JsonError>>(json_value); + if (!kind_and_bucket_by) { + return tl::make_unexpected(kind_and_bucket_by.error()); + } + + rollout.contextKind = kind_and_bucket_by->contextKind; + rollout.bucketBy = kind_and_bucket_by->reference; + + return rollout; +} + +tl::expected, + JsonError> +tag_invoke(boost::json::value_to_tag, + JsonError>> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + REQUIRE_OBJECT(json_value); + auto const& obj = json_value.as_object(); + + data_model::Flag::Rollout::WeightedVariation weighted_variation{}; + PARSE_FIELD(weighted_variation.variation, obj, "variation"); + PARSE_FIELD(weighted_variation.weight, obj, "weight"); + PARSE_FIELD(weighted_variation.untracked, obj, "untracked"); + return weighted_variation; +} + +tl::expected, JsonError> +tag_invoke(boost::json::value_to_tag< + tl::expected, + JsonError>> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + REQUIRE_STRING(json_value); + + auto const& str = json_value.as_string(); + if (str == "experiment") { + return data_model::Flag::Rollout::Kind::kExperiment; + } else if (str == "rollout") { + return data_model::Flag::Rollout::Kind::kRollout; + } else { + return data_model::Flag::Rollout::Kind::kUnrecognized; + } +} + +tl::expected, JsonError> +tag_invoke(boost::json::value_to_tag< + tl::expected, + JsonError>> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + REQUIRE_OBJECT(json_value); + auto const& obj = json_value.as_object(); + + data_model::Flag::Prerequisite prerequisite{}; + PARSE_REQUIRED_FIELD(prerequisite.key, obj, "key"); + PARSE_FIELD(prerequisite.variation, obj, "variation"); + return prerequisite; +} + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& + unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + REQUIRE_OBJECT(json_value); + auto const& obj = json_value.as_object(); + + data_model::Flag::Target target{}; + PARSE_FIELD(target.values, obj, "values"); + PARSE_FIELD(target.variation, obj, "variation"); + PARSE_FIELD_DEFAULT(target.contextKind, obj, "contextKind", + data_model::ContextKind("user")); + return target; +} + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& + unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + REQUIRE_OBJECT(json_value); + auto const& obj = json_value.as_object(); + + data_model::Flag::Rule rule{}; + + PARSE_FIELD(rule.trackEvents, obj, "trackEvents"); + PARSE_FIELD(rule.clauses, obj, "clauses"); + PARSE_CONDITIONAL_FIELD(rule.id, obj, "id"); + + auto variation_or_rollout = boost::json::value_to, JsonError>>( + json_value); + if (!variation_or_rollout) { + return tl::make_unexpected(variation_or_rollout.error()); + } + + rule.variationOrRollout = + variation_or_rollout->value_or(data_model::Flag::Variation(0)); + + return rule; +} + +tl::expected, JsonError> +tag_invoke( + boost::json::value_to_tag< + tl::expected, + JsonError>> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + REQUIRE_OBJECT(json_value); + auto const& obj = json_value.as_object(); + + data_model::Flag::ClientSideAvailability client_side_availability{}; + PARSE_FIELD(client_side_availability.usingEnvironmentId, obj, + "usingEnvironmentId"); + PARSE_FIELD(client_side_availability.usingMobileKey, obj, "usingMobileKey"); + return client_side_availability; +} + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + + REQUIRE_OBJECT(json_value); + + auto const& obj = json_value.as_object(); + + data_model::Flag flag{}; + + PARSE_REQUIRED_FIELD(flag.key, obj, "key"); + + PARSE_CONDITIONAL_FIELD(flag.debugEventsUntilDate, obj, + "debugEventsUntilDate"); + PARSE_CONDITIONAL_FIELD(flag.salt, obj, "salt"); + PARSE_CONDITIONAL_FIELD(flag.offVariation, obj, "offVariation"); + + PARSE_FIELD(flag.version, obj, "version"); + PARSE_FIELD(flag.on, obj, "on"); + PARSE_FIELD(flag.variations, obj, "variations"); + + PARSE_FIELD(flag.prerequisites, obj, "prerequisites"); + PARSE_FIELD(flag.targets, obj, "targets"); + PARSE_FIELD(flag.contextTargets, obj, "contextTargets"); + PARSE_FIELD(flag.rules, obj, "rules"); + PARSE_FIELD(flag.clientSide, obj, "clientSide"); + PARSE_FIELD(flag.clientSideAvailability, obj, "clientSideAvailability"); + PARSE_FIELD(flag.trackEvents, obj, "trackEvents"); + PARSE_FIELD(flag.trackEventsFallthrough, obj, "trackEventsFallthrough"); + PARSE_FIELD(flag.fallthrough, obj, "fallthrough"); + return flag; +} + +tl::expected, JsonError> +tag_invoke(boost::json::value_to_tag< + tl::expected, + JsonError>> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + REQUIRE_OBJECT(json_value); + auto const& obj = json_value.as_object(); + + std::optional rollout{}; + PARSE_CONDITIONAL_FIELD(rollout, obj, "rollout"); + + if (rollout) { + return std::make_optional(*rollout); + } + + std::optional variation; + + /* If there's no rollout, this must be a variation. If there's no variation, + * then this will be detected as a malformed flag at evaluation time. */ + PARSE_CONDITIONAL_FIELD(variation, obj, "variation"); + + return variation; +} + +namespace data_model { +void tag_invoke( + boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Flag::Rollout::WeightedVariation const& weighted_variation) { + auto& obj = json_value.emplace_object(); + obj.emplace("variation", weighted_variation.variation); + obj.emplace("weight", weighted_variation.weight); + + WriteMinimal(obj, "untracked", weighted_variation.untracked); +} + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Flag::Rollout::Kind const& kind) { + switch (kind) { + case Flag::Rollout::Kind::kUnrecognized: + // TODO(SC-222050) + break; + case Flag::Rollout::Kind::kExperiment: + json_value.emplace_string() = "experiment"; + break; + case Flag::Rollout::Kind::kRollout: + json_value.emplace_string() = "rollout"; + break; + } +} + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Flag::Rollout const& rollout) { + auto& obj = json_value.emplace_object(); + + obj.emplace("variations", boost::json::value_from(rollout.variations)); + if (rollout.kind != Flag::Rollout::Kind::kUnrecognized) { + // TODO(SC-222050) + obj.emplace("kind", boost::json::value_from(rollout.kind)); + } + WriteMinimal(obj, "seed", rollout.seed); + obj.emplace("bucketBy", rollout.bucketBy.RedactionName()); + obj.emplace("contextKind", rollout.contextKind.t); +} + +void tag_invoke( + boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Flag::VariationOrRollout const& variation_or_rollout) { + auto& obj = json_value.emplace_object(); + std::visit( + [&obj](auto&& arg) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + obj.emplace("rollout", boost::json::value_from(arg)); + } else if constexpr (std::is_same_v< + T, std::optional< + data_model::Flag::Variation>>) { + if (arg) { + obj.emplace("variation", *arg); + } + } + }, + variation_or_rollout); +} + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Flag::Prerequisite const& prerequisite) { + auto& obj = json_value.emplace_object(); + obj.emplace("key", prerequisite.key); + obj.emplace("variation", prerequisite.variation); +} + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Flag::Target const& target) { + auto& obj = json_value.emplace_object(); + obj.emplace("values", boost::json::value_from(target.values)); + obj.emplace("variation", target.variation); + obj.emplace("contextKind", target.contextKind.t); +} + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Flag::ClientSideAvailability const& availability) { + auto& obj = json_value.emplace_object(); + WriteMinimal(obj, "usingEnvironmentId", availability.usingEnvironmentId); + WriteMinimal(obj, "usingMobileKey", availability.usingMobileKey); +} + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Flag::Rule const& rule) { + auto& obj = json_value.emplace_object(); + WriteMinimal(obj, "trackEvents", rule.trackEvents); + WriteMinimal(obj, "id", rule.id); + std::visit( + [&obj](auto&& arg) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + obj.emplace("rollout", boost::json::value_from(arg)); + } else if constexpr (std::is_same_v) { + obj.emplace("variation", arg); + } + }, + rule.variationOrRollout); + obj.emplace("clauses", boost::json::value_from(rule.clauses)); +} + +// The "targets" array in a flag cannot have a contextKind, so this intermediate +// representation allows the flag data model to use Flag::Target, but still +// serialize a user target correctly. +struct UserTarget { + std::vector values; + std::uint64_t variation; + UserTarget(data_model::Flag::Target const& target) + : values(target.values), variation(target.variation) {} +}; + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + UserTarget const& target) { + auto& obj = json_value.emplace_object(); + obj.emplace("values", boost::json::value_from(target.values)); + obj.emplace("variation", target.variation); +} + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Flag const& flag) { + auto& obj = json_value.emplace_object(); + WriteMinimal(obj, "trackEvents", flag.trackEvents); + WriteMinimal(obj, "clientSide", flag.clientSide); + WriteMinimal(obj, "on", flag.on); + WriteMinimal(obj, "trackEventsFallthrough", flag.trackEventsFallthrough); + WriteMinimal(obj, "debugEventsUntilDate", flag.debugEventsUntilDate); + WriteMinimal(obj, "salt", flag.salt); + WriteMinimal(obj, "offVariation", flag.offVariation); + obj.emplace("key", flag.key); + obj.emplace("version", flag.version); + obj.emplace("variations", boost::json::value_from(flag.variations)); + obj.emplace("rules", boost::json::value_from(flag.rules)); + obj.emplace("prerequisites", boost::json::value_from(flag.prerequisites)); + obj.emplace("fallthrough", boost::json::value_from(flag.fallthrough)); + obj.emplace("clientSideAvailability", + boost::json::value_from(flag.clientSideAvailability)); + obj.emplace("contextTargets", boost::json::value_from(flag.contextTargets)); + + std::vector user_targets; + for (auto const& target : flag.targets) { + user_targets.emplace_back(target); + } + obj.emplace("targets", boost::json::value_from(user_targets)); +} + +} // namespace data_model + +} // namespace launchdarkly diff --git a/libs/internal/src/serialization/json_primitives.cpp b/libs/internal/src/serialization/json_primitives.cpp new file mode 100644 index 000000000..ea38767bf --- /dev/null +++ b/libs/internal/src/serialization/json_primitives.cpp @@ -0,0 +1,69 @@ +#include + +namespace launchdarkly { +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + if (json_value.is_null()) { + return std::nullopt; + } + if (!json_value.is_bool()) { + return tl::unexpected(JsonError::kSchemaFailure); + } + return json_value.as_bool(); +} + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + if (json_value.is_null()) { + return std::nullopt; + } + if (!json_value.is_number()) { + return tl::unexpected(JsonError::kSchemaFailure); + } + boost::json::error_code ec; + auto val = json_value.to_number(ec); + if (ec) { + return tl::unexpected(JsonError::kSchemaFailure); + } + return val; +} + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + if (json_value.is_null()) { + return std::nullopt; + } + if (!json_value.is_number()) { + return tl::unexpected(JsonError::kSchemaFailure); + } + boost::json::error_code ec; + auto val = json_value.to_number(ec); + if (ec) { + return tl::unexpected(JsonError::kSchemaFailure); + } + return val; +} + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + if (json_value.is_null()) { + return std::nullopt; + } + if (!json_value.is_string()) { + return tl::unexpected(JsonError::kSchemaFailure); + } + return std::string(json_value.as_string()); +} +} // namespace launchdarkly diff --git a/libs/internal/src/serialization/json_rule_clause.cpp b/libs/internal/src/serialization/json_rule_clause.cpp new file mode 100644 index 000000000..febc96f28 --- /dev/null +++ b/libs/internal/src/serialization/json_rule_clause.cpp @@ -0,0 +1,178 @@ +#include +#include +#include +#include +#include +#include + +namespace launchdarkly { + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag, + JsonError>> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + + REQUIRE_OBJECT(json_value); + auto const& obj = json_value.as_object(); + + data_model::Clause clause{}; + + PARSE_REQUIRED_FIELD(clause.op, obj, "op"); + PARSE_FIELD(clause.values, obj, "values"); + PARSE_FIELD(clause.negate, obj, "negate"); + + auto kind_and_attr = boost::json::value_to, JsonError>>( + json_value); + if (!kind_and_attr) { + return tl::make_unexpected(kind_and_attr.error()); + } + + clause.contextKind = kind_and_attr->contextKind; + clause.attribute = kind_and_attr->reference; + return clause; +} + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& + unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + + REQUIRE_STRING(json_value); + + auto const& str = json_value.as_string(); + + if (str == "") { + // Treating empty string as indicating the field is absent, but could + // also treat it as a valid but unknown value (like kUnrecognized.) + return std::nullopt; + } else if (str == "in") { + return data_model::Clause::Op::kIn; + } else if (str == "endsWith") { + return data_model::Clause::Op::kEndsWith; + } else if (str == "startsWith") { + return data_model::Clause::Op::kStartsWith; + } else if (str == "matches") { + return data_model::Clause::Op::kMatches; + } else if (str == "contains") { + return data_model::Clause::Op::kContains; + } else if (str == "lessThan") { + return data_model::Clause::Op::kLessThan; + } else if (str == "lessThanOrEqual") { + return data_model::Clause::Op::kLessThanOrEqual; + } else if (str == "greaterThan") { + return data_model::Clause::Op::kGreaterThan; + } else if (str == "greaterThanOrEqual") { + return data_model::Clause::Op::kGreaterThanOrEqual; + } else if (str == "before") { + return data_model::Clause::Op::kBefore; + } else if (str == "after") { + return data_model::Clause::Op::kAfter; + } else if (str == "semVerEqual") { + return data_model::Clause::Op::kSemVerEqual; + } else if (str == "semVerLessThan") { + return data_model::Clause::Op::kSemVerLessThan; + } else if (str == "semVerGreaterThan") { + return data_model::Clause::Op::kSemVerGreaterThan; + } else if (str == "segmentMatch") { + return data_model::Clause::Op::kSegmentMatch; + } else { + return data_model::Clause::Op::kUnrecognized; + } +} + +tl::expected tag_invoke( + boost::json::value_to_tag< + tl::expected> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + auto maybe_op = boost::json::value_to< + tl::expected, JsonError>>( + json_value); + if (!maybe_op) { + return tl::unexpected(maybe_op.error()); + } + return maybe_op.value().value_or(data_model::Clause::Op::kUnrecognized); +} + +namespace data_model { + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Clause const& clause) { + auto& obj = json_value.emplace_object(); + + obj.emplace("values", boost::json::value_from(clause.values)); + + WriteMinimal(obj, "negate", clause.negate); + + if (clause.op != data_model::Clause::Op::kUnrecognized) { + // TODO: Should we store the original value? + obj.emplace("op", boost::json::value_from(clause.op)); + } + if (clause.attribute.Valid()) { + obj.emplace("attribute", clause.attribute.RedactionName()); + } + obj.emplace("contextKind", clause.contextKind.t); +} + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Clause::Op const& op) { + switch (op) { + case data_model::Clause::Op::kUnrecognized: + // TODO: Should we do anything? + break; + case data_model::Clause::Op::kIn: + json_value.emplace_string() = "in"; + break; + case data_model::Clause::Op::kStartsWith: + json_value.emplace_string() = "startsWith"; + break; + case data_model::Clause::Op::kEndsWith: + json_value.emplace_string() = "endsWith"; + break; + case data_model::Clause::Op::kMatches: + json_value.emplace_string() = "matches"; + break; + case data_model::Clause::Op::kContains: + json_value.emplace_string() = "contains"; + break; + case data_model::Clause::Op::kLessThan: + json_value.emplace_string() = "lessThan"; + break; + case data_model::Clause::Op::kLessThanOrEqual: + json_value.emplace_string() = "lessThanOrEqual"; + break; + case data_model::Clause::Op::kGreaterThan: + json_value.emplace_string() = "greaterThan"; + break; + case data_model::Clause::Op::kGreaterThanOrEqual: + json_value.emplace_string() = "greaterThanOrEqual"; + break; + case data_model::Clause::Op::kBefore: + json_value.emplace_string() = "before"; + break; + case data_model::Clause::Op::kAfter: + json_value.emplace_string() = "after"; + break; + case data_model::Clause::Op::kSemVerEqual: + json_value.emplace_string() = "semVerEqual"; + break; + case data_model::Clause::Op::kSemVerLessThan: + json_value.emplace_string() = "semVerLessThan"; + break; + case data_model::Clause::Op::kSemVerGreaterThan: + json_value.emplace_string() = "semVerGreaterThan"; + break; + case data_model::Clause::Op::kSegmentMatch: + json_value.emplace_string() = "segmentMatch"; + break; + } +} +} // namespace data_model + +} // namespace launchdarkly diff --git a/libs/internal/src/serialization/json_sdk_data_set.cpp b/libs/internal/src/serialization/json_sdk_data_set.cpp new file mode 100644 index 000000000..e7b894a0a --- /dev/null +++ b/libs/internal/src/serialization/json_sdk_data_set.cpp @@ -0,0 +1,28 @@ +#include +#include +#include +#include +#include +#include +#include + +namespace launchdarkly { +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& + unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + + REQUIRE_OBJECT(json_value); + + auto const& obj = json_value.as_object(); + + data_model::SDKDataSet data_set{}; + + PARSE_FIELD(data_set.flags, obj, "flags"); + PARSE_FIELD(data_set.segments, obj, "segments"); + + return data_set; +} +} // namespace launchdarkly diff --git a/libs/internal/src/serialization/json_segment.cpp b/libs/internal/src/serialization/json_segment.cpp new file mode 100644 index 000000000..fa927b57a --- /dev/null +++ b/libs/internal/src/serialization/json_segment.cpp @@ -0,0 +1,147 @@ +#include +#include +#include +#include +#include +#include +#include + +namespace launchdarkly { + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, + JsonError>> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + + REQUIRE_OBJECT(json_value); + auto const& obj = json_value.as_object(); + + data_model::Segment::Target target{}; + + // The zero value of a ContextKind ("" - empty string) is not valid in the + // domain of possible contexts. This field is parsed as REQUIRED to + // specify that fact. + PARSE_REQUIRED_FIELD(target.contextKind, obj, "contextKind"); + + PARSE_FIELD(target.values, obj, "values"); + + return target; +} + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, + JsonError>> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + + REQUIRE_OBJECT(json_value); + auto const& obj = json_value.as_object(); + + data_model::Segment::Rule rule{}; + + PARSE_FIELD(rule.clauses, obj, "clauses"); + + PARSE_CONDITIONAL_FIELD(rule.weight, obj, "weight"); + PARSE_CONDITIONAL_FIELD(rule.id, obj, "id"); + + auto kind_and_bucket_by = boost::json::value_to, + JsonError>>(json_value); + if (!kind_and_bucket_by) { + return tl::make_unexpected(kind_and_bucket_by.error()); + } + + rule.bucketBy = kind_and_bucket_by->reference; + rule.rolloutContextKind = kind_and_bucket_by->contextKind; + + return rule; +} + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag, + JsonError>> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + + if (json_value.is_null()) { + return std::nullopt; + } + + if (!json_value.is_object()) { + return tl::unexpected(JsonError::kSchemaFailure); + } + + if (json_value.as_object().empty()) { + return std::nullopt; + } + + auto const& obj = json_value.as_object(); + + data_model::Segment segment{}; + + PARSE_REQUIRED_FIELD(segment.key, obj, "key"); + PARSE_REQUIRED_FIELD(segment.version, obj, "version"); + + PARSE_CONDITIONAL_FIELD(segment.generation, obj, "generation"); + PARSE_CONDITIONAL_FIELD(segment.salt, obj, "salt"); + PARSE_CONDITIONAL_FIELD(segment.unboundedContextKind, obj, + "unboundedContextKind"); + + PARSE_FIELD(segment.excluded, obj, "excluded"); + PARSE_FIELD(segment.included, obj, "included"); + PARSE_FIELD(segment.unbounded, obj, "unbounded"); + PARSE_FIELD(segment.includedContexts, obj, "includedContexts"); + PARSE_FIELD(segment.excludedContexts, obj, "excludedContexts"); + PARSE_FIELD(segment.rules, obj, "rules"); + + return segment; +} + +// Serializers need to be in launchdarkly::data_model for ADL. +namespace data_model { + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Segment const& segment) { + auto& obj = json_value.emplace_object(); + obj.emplace("key", segment.key); + obj.emplace("version", segment.version); + WriteMinimal(obj, "salt", segment.salt); + WriteMinimal(obj, "generation", segment.generation); + WriteMinimal(obj, "unboundedContextKind", segment.unboundedContextKind); + WriteMinimal(obj, "unbounded", segment.unbounded); + + obj.emplace("rules", boost::json::value_from(segment.rules)); + obj.emplace("excluded", boost::json::value_from(segment.excluded)); + obj.emplace("excludedContexts", + boost::json::value_from(segment.excludedContexts)); + obj.emplace("included", boost::json::value_from(segment.included)); + obj.emplace("includedContexts", + boost::json::value_from(segment.includedContexts)); +} + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Segment::Target const& target) { + auto& obj = json_value.emplace_object(); + obj.emplace("values", boost::json::value_from(target.values)); + obj.emplace("contextKind", boost::json::value_from(target.contextKind)); +} + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Segment::Rule const& rule) { + auto& obj = json_value.emplace_object(); + WriteMinimal(obj, "weight", rule.weight); + WriteMinimal(obj, "id", rule.id); + obj.emplace("clauses", boost::json::value_from(rule.clauses)); + obj.emplace("bucketBy", rule.bucketBy.RedactionName()); + obj.emplace("rolloutContextKind", rule.rolloutContextKind.t); +} + +} // namespace data_model + +} // namespace launchdarkly diff --git a/libs/internal/src/serialization/json_value.cpp b/libs/internal/src/serialization/json_value.cpp index be80e6622..2e0b76ca0 100644 --- a/libs/internal/src/serialization/json_value.cpp +++ b/libs/internal/src/serialization/json_value.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -9,9 +10,12 @@ namespace launchdarkly { // constructors. Replacing them with braced init lists would result in all types // being lists. -Value tag_invoke(boost::json::value_to_tag const& unused, - boost::json::value const& json_value) { +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& unused, + boost::json::value const& json_value) { boost::ignore_unused(unused); + // The name of the function needs to be tag_invoke for boost::json. // The conditions in these switches explicitly use the constructors, because @@ -33,7 +37,12 @@ Value tag_invoke(boost::json::value_to_tag const& unused, auto vec = json_value.as_array(); std::vector values; for (auto const& item : vec) { - values.push_back(boost::json::value_to(item)); + auto value = + boost::json::value_to>(item); + if (!value) { + return tl::make_unexpected(value.error()); + } + values.emplace_back(std::move(*value)); } return Value(values); } @@ -41,8 +50,13 @@ Value tag_invoke(boost::json::value_to_tag const& unused, auto& map = json_value.as_object(); std::map values; for (auto const& pair : map) { - auto value = boost::json::value_to(pair.value()); - values.emplace(pair.key().data(), std::move(value)); + auto value = + boost::json::value_to>( + pair.value()); + if (!value) { + return tl::make_unexpected(value.error()); + } + values.emplace(pair.key().data(), std::move(*value)); } return Value(std::move(values)); } @@ -52,6 +66,13 @@ Value tag_invoke(boost::json::value_to_tag const& unused, launchdarkly::detail::unreachable(); } +Value tag_invoke(boost::json::value_to_tag const&, + boost::json::value const& json_value) { + auto val = + boost::json::value_to>(json_value); + return val ? std::move(*val) : Value(); +} + void tag_invoke(boost::json::value_from_tag const&, boost::json::value& json_value, Value const& ld_value) { diff --git a/libs/internal/src/serialization/value_mapping.cpp b/libs/internal/src/serialization/value_mapping.cpp index d8344efdb..7142d8bf7 100644 --- a/libs/internal/src/serialization/value_mapping.cpp +++ b/libs/internal/src/serialization/value_mapping.cpp @@ -1,5 +1,7 @@ #include +#include + namespace launchdarkly { template <> @@ -21,6 +23,23 @@ std::optional ValueAsOpt( return std::nullopt; } +template <> +std::optional> ValueAsOpt( + boost::json::object::const_iterator iterator, + boost::json::object::const_iterator end) { + if (iterator != end && iterator->value().is_array()) { + std::vector result; + for (auto const& item : iterator->value().as_array()) { + if (!item.is_string()) { + return std::nullopt; + } + result.emplace_back(item.as_string()); + } + return result; + } + return std::nullopt; +} + template <> bool ValueOrDefault(boost::json::object::const_iterator iterator, boost::json::object::const_iterator end, @@ -51,4 +70,9 @@ uint64_t ValueOrDefault(boost::json::object::const_iterator iterator, return default_value; } +void WriteMinimal(boost::json::object& obj, std::string const& key, bool val) { + if (val) { + obj.emplace(key, val); + } +} } // namespace launchdarkly diff --git a/libs/client-sdk/src/boost_signal_connection.cpp b/libs/internal/src/signals/boost_signal_connection.cpp similarity index 55% rename from libs/client-sdk/src/boost_signal_connection.cpp rename to libs/internal/src/signals/boost_signal_connection.cpp index 30cb90604..29f4323d1 100644 --- a/libs/client-sdk/src/boost_signal_connection.cpp +++ b/libs/internal/src/signals/boost_signal_connection.cpp @@ -1,6 +1,6 @@ -#include "boost_signal_connection.hpp" +#include -namespace launchdarkly::client_side { +namespace launchdarkly::internal::signals { SignalConnection::SignalConnection(boost::signals2::connection connection) : connection_(std::move(connection)) {} @@ -9,4 +9,4 @@ void SignalConnection::Disconnect() { connection_.disconnect(); } -} // namespace launchdarkly::client_side +} // namespace launchdarkly::internal::signals diff --git a/libs/internal/tests/data_model_serialization_test.cpp b/libs/internal/tests/data_model_serialization_test.cpp new file mode 100644 index 000000000..c8965e16d --- /dev/null +++ b/libs/internal/tests/data_model_serialization_test.cpp @@ -0,0 +1,753 @@ +#include + +#include + +#include +#include +#include +#include +#include + +using namespace launchdarkly; +using launchdarkly::data_model::ContextKind; + +TEST(SDKDataSetTests, DeserializesEmptyDataSet) { + auto result = + boost::json::value_to>( + boost::json::parse("{}")); + ASSERT_TRUE(result); + ASSERT_TRUE(result->segments.empty()); + ASSERT_TRUE(result->flags.empty()); +} + +TEST(SDKDataSetTests, ErrorOnInvalidSchema) { + auto result = + boost::json::value_to>( + boost::json::parse("[]")); + ASSERT_FALSE(result); + ASSERT_EQ(result.error(), JsonError::kSchemaFailure); +} + +TEST(SDKDataSetTests, DeserializesZeroSegments) { + auto result = + boost::json::value_to>( + boost::json::parse(R"({"segments":{}})")); + ASSERT_TRUE(result); + ASSERT_TRUE(result->segments.empty()); +} + +TEST(SDKDataSetTests, DeserializesZeroFlags) { + auto result = + boost::json::value_to>( + boost::json::parse(R"({"flags":{}})")); + ASSERT_TRUE(result); + ASSERT_TRUE(result->flags.empty()); +} + +TEST(SegmentTests, DeserializesMinimumValid) { + auto result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse(R"({"key":"foo", "version": 42})")); + ASSERT_TRUE(result); + ASSERT_TRUE(result.value()); + + ASSERT_EQ(result.value()->version, 42); + ASSERT_EQ(result.value()->key, "foo"); +} + +TEST(SegmentTests, TolerantOfUnrecognizedFields) { + auto result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse( + R"({"key":"foo", "version": 42, "somethingRandom" : true})")); + ASSERT_TRUE(result); + ASSERT_TRUE(result.value()); +} + +TEST(SegmentRuleTests, DeserializesMinimumValid) { + auto result = boost::json::value_to< + tl::expected>(boost::json::parse( + R"({"clauses": [{"attribute": "", "op": "in", "values": ["a"]}]})")); + ASSERT_TRUE(result); + + auto const& clauses = result->clauses; + ASSERT_EQ(clauses.size(), 1); + + auto const& clause = clauses.at(0); + ASSERT_EQ(clause.op, data_model::Clause::Op::kIn); +} + +TEST(SegmentRuleTests, TolerantOfUnrecognizedFields) { + auto result = boost::json::value_to< + tl::expected>(boost::json::parse( + R"({"somethingRandom": true, "clauses": [{"attribute": "", "op": "in", "values": ["a"]}]})")); + + ASSERT_TRUE(result); +} + +TEST(SegmentRuleTests, DeserializesSimpleAttributeReference) { + auto result = boost::json::value_to< + tl::expected>(boost::json::parse( + R"({"rolloutContextKind" : "foo", "bucketBy" : "bar", "clauses": []})")); + ASSERT_TRUE(result); + ASSERT_EQ(result->rolloutContextKind, ContextKind("foo")); + ASSERT_EQ(result->bucketBy, AttributeReference("bar")); +} + +TEST(SegmentRuleTests, DeserializesPointerAttributeReference) { + auto result = boost::json::value_to< + tl::expected>(boost::json::parse( + R"({"rolloutContextKind" : "foo", "bucketBy" : "/foo/bar", "clauses": []})")); + ASSERT_TRUE(result); + ASSERT_EQ(result->rolloutContextKind, ContextKind("foo")); + ASSERT_EQ(result->bucketBy, AttributeReference("/foo/bar")); +} + +TEST(SegmentRuleTests, DeserializesEscapedReference) { + auto result = boost::json::value_to< + tl::expected>(boost::json::parse( + R"({"rolloutContextKind" : "foo", "bucketBy" : "/~1foo~1bar", "clauses": []})")); + ASSERT_TRUE(result); + ASSERT_EQ(result->rolloutContextKind, ContextKind("foo")); + ASSERT_EQ(result->bucketBy, AttributeReference("/~1foo~1bar")); +} + +TEST(SegmentRuleTests, RejectsEmptyContextKind) { + auto result = boost::json::value_to< + tl::expected>(boost::json::parse( + R"({"rolloutContextKind" : "", "bucketBy" : "/~1foo~1bar", "clauses": []})")); + ASSERT_FALSE(result); +} + +TEST(SegmentRuleTests, DeserializesLiteralAttributeName) { + auto result = boost::json::value_to< + tl::expected>( + boost::json::parse(R"({"bucketBy" : "/~1foo~1bar", "clauses": []})")); + ASSERT_TRUE(result); + ASSERT_EQ(result->bucketBy, + AttributeReference::FromLiteralStr("/~1foo~1bar")); +} + +TEST(ClauseTests, DeserializesMinimumValid) { + auto result = + boost::json::value_to>( + boost::json::parse(R"({"op": "segmentMatch", "values": []})")); + ASSERT_TRUE(result); + + ASSERT_EQ(result->op, data_model::Clause::Op::kSegmentMatch); + ASSERT_TRUE(result->values.empty()); +} + +TEST(ClauseTests, TolerantOfUnrecognizedFields) { + auto result = boost::json::value_to< + tl::expected>(boost::json::parse( + R"({"somethingRandom": true, "attribute": "", "op": "in", "values": ["a"]})")); + ASSERT_TRUE(result); +} + +TEST(ClauseTests, TolerantOfEmptyAttribute) { + auto result = + boost::json::value_to>( + boost::json::parse( + R"({"attribute": "", "op": "segmentMatch", "values": ["a"]})")); + ASSERT_TRUE(result); + ASSERT_FALSE(result->attribute.Valid()); +} + +TEST(ClauseTests, TolerantOfUnrecognizedOperator) { + auto result = boost::json::value_to< + tl::expected>(boost::json::parse( + R"({"attribute": "", "op": "notAnActualOperator", "values": ["a"]})")); + ASSERT_TRUE(result); + ASSERT_EQ(result->op, data_model::Clause::Op::kUnrecognized); +} + +TEST(ClauseTests, DeserializesSimpleAttributeReference) { + auto result = boost::json::value_to< + tl::expected>(boost::json::parse( + R"({"attribute": "foo", "op": "in", "values": ["a"], "contextKind" : "user"})")); + ASSERT_TRUE(result); + ASSERT_EQ(result->attribute, AttributeReference("foo")); +} + +TEST(ClauseTests, DeserializesPointerAttributeReference) { + auto result = boost::json::value_to< + tl::expected>(boost::json::parse( + R"({"attribute": "/foo/bar", "op": "in", "values": ["a"], "contextKind" : "user"})")); + ASSERT_TRUE(result); + ASSERT_EQ(result->attribute, AttributeReference("/foo/bar")); +} + +TEST(ClauseTests, DeserializesEscapedReference) { + auto result = boost::json::value_to< + tl::expected>(boost::json::parse( + R"({"attribute": "/~1foo~1bar", "op": "in", "values": ["a"], "contextKind" : "user"})")); + ASSERT_TRUE(result); + ASSERT_EQ(result->attribute, AttributeReference("/~1foo~1bar")); +} + +TEST(ClauseTests, RejectsEmptyContextKind) { + auto result = boost::json::value_to< + tl::expected>(boost::json::parse( + R"({"attribute": "/~1foo~1bar", "op": "in", "values": ["a"], "contextKind" : ""})")); + ASSERT_FALSE(result); +} + +TEST(ClauseTests, DeserializesLiteralAttributeName) { + auto result = + boost::json::value_to>( + boost::json::parse( + R"({"attribute": "/foo/bar", "op": "in", "values": ["a"]})")); + ASSERT_TRUE(result); + ASSERT_EQ(result->attribute, + AttributeReference::FromLiteralStr("/foo/bar")); +} + +TEST(RolloutTests, DeserializesMinimumValid) { + auto result = boost::json::value_to< + tl::expected>( + boost::json::parse(R"({})")); + ASSERT_TRUE(result); + ASSERT_EQ(result->kind, data_model::Flag::Rollout::Kind::kRollout); + ASSERT_EQ(result->contextKind, ContextKind("user")); + ASSERT_EQ(result->bucketBy, "key"); +} + +TEST(RolloutTests, DeserializesAllFieldsWithAttributeReference) { + auto result = boost::json::value_to< + tl::expected>(boost::json::parse( + R"({"kind": "experiment", "contextKind": "org", "bucketBy": "/foo/bar", "seed" : 123, "variations" : []})")); + ASSERT_TRUE(result); + ASSERT_EQ(result->kind, data_model::Flag::Rollout::Kind::kExperiment); + ASSERT_EQ(result->contextKind, ContextKind("org")); + ASSERT_EQ(result->bucketBy, "/foo/bar"); + ASSERT_EQ(result->seed, 123); + ASSERT_TRUE(result->variations.empty()); +} + +TEST(RolloutTests, DeserializesAllFieldsWithLiteralAttributeName) { + auto result = boost::json::value_to< + tl::expected>(boost::json::parse( + R"({"kind": "experiment", "bucketBy": "/foo/bar", "seed" : 123, "variations" : []})")); + ASSERT_TRUE(result); + ASSERT_EQ(result->kind, data_model::Flag::Rollout::Kind::kExperiment); + ASSERT_EQ(result->contextKind, ContextKind("user")); + ASSERT_EQ(result->bucketBy, "/~1foo~1bar"); + ASSERT_EQ(result->seed, 123); + ASSERT_TRUE(result->variations.empty()); +} + +TEST(WeightedVariationTests, DeserializesMinimumValid) { + auto result = boost::json::value_to< + tl::expected>( + boost::json::parse(R"({})")); + ASSERT_TRUE(result); + ASSERT_EQ(result->variation, 0); + ASSERT_EQ(result->weight, 0); + ASSERT_FALSE(result->untracked); +} + +TEST(WeightedVariationTests, DeserializesAllFields) { + auto result = boost::json::value_to< + tl::expected>( + boost::json::parse( + R"({"variation" : 2, "weight" : 123, "untracked" : true})")); + ASSERT_TRUE(result); + ASSERT_EQ(result->variation, 2); + ASSERT_EQ(result->weight, 123); + ASSERT_TRUE(result->untracked); +} + +TEST(PrerequisiteTests, DeserializeFailsWithoutKey) { + auto result = boost::json::value_to< + tl::expected>( + boost::json::parse(R"({})")); + ASSERT_FALSE(result); +} + +TEST(PrerequisiteTests, DeserializesMinimumValid) { + auto result = boost::json::value_to< + tl::expected>( + boost::json::parse(R"({"key" : "foo"})")); + ASSERT_TRUE(result); + ASSERT_EQ(result->variation, 0); + ASSERT_EQ(result->key, "foo"); +} + +TEST(PrerequisiteTests, DeserializesAllFields) { + auto result = boost::json::value_to< + tl::expected>( + boost::json::parse(R"({"key" : "foo", "variation" : 123})")); + ASSERT_TRUE(result); + ASSERT_EQ(result->key, "foo"); + ASSERT_EQ(result->variation, 123); +} + +TEST(PrerequisiteTests, DeserializeSucceedsWithNegativeVariation) { + auto result = boost::json::value_to< + tl::expected>( + boost::json::parse(R"({"key" : "foo", "variation" : -123})")); + ASSERT_TRUE(result); +} + +TEST(TargetTests, DeserializesMinimumValid) { + auto result = boost::json::value_to< + tl::expected>( + boost::json::parse(R"({})")); + ASSERT_TRUE(result); + ASSERT_EQ(result->contextKind, ContextKind("user")); + ASSERT_EQ(result->variation, 0); + ASSERT_TRUE(result->values.empty()); +} + +TEST(TargetTests, DeserializesSucceedsWithNegativeVariation) { + auto result = boost::json::value_to< + tl::expected>( + boost::json::parse(R"({"variation" : -123})")); + ASSERT_TRUE(result); +} + +TEST(TargetTests, DeserializesAllFields) { + auto result = boost::json::value_to< + tl::expected>(boost::json::parse( + R"({"variation" : 123, "values" : ["a"], "contextKind" : "org"})")); + ASSERT_TRUE(result); + ASSERT_EQ(result->contextKind, ContextKind("org")); + ASSERT_EQ(result->variation, 123); + ASSERT_EQ(result->values.size(), 1); + ASSERT_EQ(result->values[0], "a"); +} + +TEST(FlagRuleTests, DeserializesMinimumValid) { + auto result = + boost::json::value_to>( + boost::json::parse(R"({"variation" : 123})")); + ASSERT_TRUE(result); + ASSERT_FALSE(result->trackEvents); + ASSERT_TRUE(result->clauses.empty()); + ASSERT_FALSE(result->id); + ASSERT_EQ(std::get>( + result->variationOrRollout), + data_model::Flag::Variation(123)); +} + +TEST(FlagRuleTests, DeserializesRollout) { + auto result = + boost::json::value_to>( + boost::json::parse(R"({"rollout" : {}})")); + ASSERT_TRUE(result); + ASSERT_EQ( + std::get(result->variationOrRollout).kind, + data_model::Flag::Rollout::Kind::kRollout); +} + +TEST(FlagRuleTests, DeserializesAllFields) { + auto result = boost::json::value_to< + tl::expected>(boost::json::parse( + R"({"id" : "foo", "variation" : 123, "trackEvents" : true, "clauses" : []})")); + ASSERT_TRUE(result); + ASSERT_TRUE(result->trackEvents); + ASSERT_TRUE(result->clauses.empty()); + ASSERT_EQ(result->id, "foo"); + ASSERT_EQ(std::get>( + result->variationOrRollout), + data_model::Flag::Variation(123)); +} + +TEST(ClientSideAvailabilityTests, DeserializesMinimumValid) { + auto result = boost::json::value_to< + tl::expected>( + boost::json::parse(R"({})")); + ASSERT_TRUE(result); + ASSERT_FALSE(result->usingMobileKey); + ASSERT_FALSE(result->usingEnvironmentId); +} + +TEST(ClientSideAvailabilityTests, DeserializesAllFields) { + auto result = boost::json::value_to< + tl::expected>( + boost::json::parse( + R"({"usingMobileKey" : true, "usingEnvironmentId" : true})")); + ASSERT_TRUE(result); + ASSERT_TRUE(result->usingMobileKey); + ASSERT_TRUE(result->usingEnvironmentId); +} + +TEST(WeightedVariationTests, SerializeAllFields) { + data_model::Flag::Rollout::WeightedVariation variation(1, 2); + variation.untracked = true; + auto json = boost::json::value_from(variation); + + auto expected = boost::json::parse( + R"({"variation": 1, "weight": 2, "untracked": true})"); + + EXPECT_EQ(expected, json); +} + +TEST(WeightedVariationTests, SerializeUntrackedOnlyTrue) { + data_model::Flag::Rollout::WeightedVariation variation(1, 2); + variation.untracked = false; + auto json = boost::json::value_from(variation); + + auto expected = boost::json::parse(R"({"variation": 1, "weight": 2})"); + + EXPECT_EQ(expected, json); +} + +TEST(RolloutTests, SerializeAllFields) { + using Rollout = data_model::Flag::Rollout; + Rollout rollout; + rollout.kind = Rollout::Kind::kExperiment; + rollout.contextKind = "user"; + rollout.bucketBy = AttributeReference("ham"); + rollout.seed = 42; + rollout.variations = { + data_model::Flag::Rollout::WeightedVariation::Untracked(1, 2), {3, 4}}; + + auto json = boost::json::value_from(rollout); + + auto expected = boost::json::parse(R"({ + "kind": "experiment", + "contextKind": "user", + "bucketBy": "ham", + "seed": 42, + "variations": [ + {"variation": 1, "weight": 2, "untracked": true}, + {"variation": 3, "weight": 4} + ] + })"); + + EXPECT_EQ(expected, json); +} + +TEST(VariationOrRolloutTests, SerializeVariation) { + data_model::Flag::VariationOrRollout variation = 5; + + auto json = boost::json::value_from(variation); + + auto expected = boost::json::parse(R"({"variation":5})"); + EXPECT_EQ(expected, json); +} + +TEST(VariationOrRolloutTests, SerializeRollout) { + using Rollout = data_model::Flag::Rollout; + Rollout rollout; + rollout.kind = Rollout::Kind::kExperiment; + rollout.contextKind = "user"; + rollout.bucketBy = AttributeReference("ham"); + rollout.seed = 42; + rollout.variations = { + data_model::Flag::Rollout::WeightedVariation::Untracked(1, 2), {3, 4}}; + data_model::Flag::VariationOrRollout var_or_roll = rollout; + auto json = boost::json::value_from(var_or_roll); + + auto expected = boost::json::parse(R"({ + "rollout":{ + "kind": "experiment", + "contextKind": "user", + "bucketBy": "ham", + "seed": 42, + "variations": [ + {"variation": 1, "weight": 2, "untracked": true}, + {"variation": 3, "weight": 4} + ] + }})"); + EXPECT_EQ(expected, json); +} + +TEST(PrerequisiteTests, SerializeAll) { + data_model::Flag::Prerequisite prerequisite{"potato", 6}; + auto json = boost::json::value_from(prerequisite); + + auto expected = boost::json::parse(R"({"key":"potato","variation":6})"); + EXPECT_EQ(expected, json); +} + +TEST(TargetTests, SerializeAll) { + data_model::Flag::Target target{{"a", "b"}, 42, ContextKind("taco_stand")}; + auto json = boost::json::value_from(target); + + auto expected = boost::json::parse( + R"({"values":["a", "b"], "variation": 42, "contextKind":"taco_stand"})"); + EXPECT_EQ(expected, json); +} + +TEST(ClientSideAvailabilityTests, SerializeAll) { + data_model::Flag::ClientSideAvailability availability{true, true}; + + auto json = boost::json::value_from(availability); + + auto expected = boost::json::parse( + R"({"usingMobileKey": true, "usingEnvironmentId": true})"); + EXPECT_EQ(expected, json); +} + +class ClauseOperatorsFixture + : public ::testing::TestWithParam {}; + +INSTANTIATE_TEST_SUITE_P( + ClauseTests, + ClauseOperatorsFixture, + testing::Values(data_model::Clause::Op::kSegmentMatch, + data_model::Clause::Op::kAfter, + data_model::Clause::Op::kBefore, + data_model::Clause::Op::kContains, + data_model::Clause::Op::kEndsWith, + data_model::Clause::Op::kGreaterThan, + data_model::Clause::Op::kGreaterThanOrEqual, + data_model::Clause::Op::kIn, + data_model::Clause::Op::kLessThan, + data_model::Clause::Op::kLessThanOrEqual, + data_model::Clause::Op::kMatches, + data_model::Clause::Op::kSemVerEqual, + data_model::Clause::Op::kSemVerGreaterThan, + data_model::Clause::Op::kSemVerLessThan, + data_model::Clause::Op::kStartsWith)); + +TEST_P(ClauseOperatorsFixture, AllOperatorsSerializeDeserialize) { + auto op = GetParam(); + + auto serialized = boost::json::serialize(boost::json::value_from(op)); + auto parsed = boost::json::parse(serialized); + auto deserialized = boost::json::value_to< + tl::expected, JsonError>>(parsed); + + EXPECT_EQ(op, **deserialized); +} + +TEST(ClauseTests, SerializeAll) { + data_model::Clause clause{data_model::Clause::Op::kIn, + {"a", "b"}, + true, + ContextKind("bob"), + "/potato"}; + + auto json = boost::json::value_from(clause); + auto expected = boost::json::parse( + R"({ + "op": "in", + "negate": true, + "values": ["a", "b"], + "contextKind": "bob", + "attribute": "/potato" + })"); + EXPECT_EQ(expected, json); +} + +TEST(FlagRuleTests, SerializeAllRollout) { + using Rollout = data_model::Flag::Rollout; + Rollout rollout; + rollout.kind = Rollout::Kind::kExperiment; + rollout.contextKind = "user"; + rollout.bucketBy = AttributeReference("ham"); + rollout.seed = 42; + rollout.variations = { + data_model::Flag::Rollout::WeightedVariation::Untracked(1, 2), {3, 4}}; + data_model::Flag::Rule rule{{{data_model::Clause::Op::kIn, + {"a", "b"}, + true, + ContextKind("bob"), + "/potato"}}, + rollout, + true, + "therule"}; + + auto json = boost::json::value_from(rule); + auto expected = boost::json::parse( + R"({ + "clauses":[{ + "op": "in", + "negate": true, + "values": ["a", "b"], + "contextKind": "bob", + "attribute": "/potato" + }], + "rollout": { + "kind": "experiment", + "contextKind": "user", + "bucketBy": "ham", + "seed": 42, + "variations": [ + {"variation": 1, "weight": 2, "untracked": true}, + {"variation": 3, "weight": 4} + ] + }, + "trackEvents": true, + "id": "therule" + })"); + EXPECT_EQ(expected, json); +} + +TEST(FlagTests, SerializeAll) { + data_model::Flag flag{ + "the-key", + 21, // version + true, // on + 42, // fallthrough + {"a", "b"}, // variations + {{"prereqA", 2}, {"prereqB", 3}}, // prerequisites + {{{ + "1", + "2", + "3", + }, + 12, + ContextKind("user")}}, // targets + {{{ + "4", + "5", + "6", + }, + 24, + ContextKind("bob")}}, // contextTargets + {}, // rules + 84, // offVariation + true, // clientSide + data_model::Flag::ClientSideAvailability{true, true}, + "4242", // salt + true, // trackEvents + true, // trackEventsFalltrhough + 900 // debugEventsUntilDate + }; + + auto json = boost::json::value_from(flag); + auto expected = boost::json::parse( + R"({ + "trackEvents":true, + "clientSide":true, + "on":true, + "trackEventsFallthrough":true, + "debugEventsUntilDate":900, + "salt":"4242", + "offVariation":84, + "key":"the-key", + "version":21, + "variations":["a","b"], + "rules":[], + "prerequisites":[{"key":"prereqA","variation":2}, + {"key":"prereqB","variation":3}], + "fallthrough":{"variation":42}, + "clientSideAvailability": + {"usingEnvironmentId":true,"usingMobileKey":true}, + "contextTargets": + [{"values":["4","5","6"],"variation":24,"contextKind":"bob"}], + "targets":[{"values":["1","2","3"],"variation":12}] + })"); + EXPECT_EQ(expected, json); +} + +TEST(SegmentTargetTests, SerializeAll) { + data_model::Segment::Target target{ContextKind("bob"), {"bill", "sam"}}; + + auto json = boost::json::value_from(target); + auto expected = boost::json::parse( + R"({ + "contextKind": "bob", + "values": ["bill", "sam"] + })"); + EXPECT_EQ(expected, json); +} + +TEST(SegmentRuleTests, SerializeAll) { + data_model::Segment::Rule rule{{{data_model::Clause::Op::kIn, + {"a", "b"}, + true, + ContextKind("bob"), + "/potato"}}, + "ididid", + 300, + ContextKind("bob"), + "/happy"}; + + auto json = boost::json::value_from(rule); + auto expected = boost::json::parse( + R"({ + "clauses": [{ + "op": "in", + "negate": true, + "values": ["a", "b"], + "contextKind": "bob", + "attribute": "/potato" + }], + "id": "ididid", + "weight": 300, + "rolloutContextKind": "bob", + "bucketBy": "/happy" + })"); + EXPECT_EQ(expected, json); +} + +TEST(SegmentTests, SerializeBasicAll) { + data_model::Segment segment{ + "my-segment", + 87, + {"bob", "sam"}, + {"sally", "johan"}, + {{ContextKind("vegetable"), {"potato", "yam"}}}, + {{ContextKind("material"), {"cardboard", "plastic"}}}, + {{{{data_model::Clause::Op::kIn, + {"a", "b"}, + true, + ContextKind("bob"), + "/potato"}}, + "ididid", + 300, + ContextKind("bob"), + "/happy"}}, + "salty", + false, + std::nullopt, + std::nullopt, + }; + + auto json = boost::json::value_from(segment); + auto expected = boost::json::parse( + R"({ + "key": "my-segment", + "version": 87, + "included": ["bob", "sam"], + "excluded": ["sally", "johan"], + "includedContexts": + [{"contextKind": "vegetable", "values":["potato", "yam"]}], + "excludedContexts": + [{"contextKind": "material", "values":["cardboard", "plastic"]}], + "salt": "salty", + "rules":[{ + "clauses": [{ + "op": "in", + "negate": true, + "values": ["a", "b"], + "contextKind": "bob", + "attribute": "/potato" + }], + "id": "ididid", + "weight": 300, + "rolloutContextKind": "bob", + "bucketBy": "/happy" + }] + })"); + EXPECT_EQ(expected, json); +} + +TEST(SegmentTests, SerializeUnbounded) { + data_model::Segment segment{ + "my-segment", 87, {}, {}, {}, {}, {}, "salty", true, + ContextKind("company"), 12}; + + auto json = boost::json::value_from(segment); + auto expected = boost::json::parse( + R"({ + "key": "my-segment", + "version": 87, + "included": [], + "excluded": [], + "includedContexts": [], + "excludedContexts": [], + "salt": "salty", + "rules":[], + "unbounded": true, + "unboundedContextKind": "company", + "generation": 12 + })"); + EXPECT_EQ(expected, json); +} diff --git a/libs/internal/tests/evaluation_result_test.cpp b/libs/internal/tests/evaluation_result_test.cpp index 7db22aa92..48956a3af 100644 --- a/libs/internal/tests/evaluation_result_test.cpp +++ b/libs/internal/tests/evaluation_result_test.cpp @@ -15,54 +15,49 @@ using launchdarkly::JsonError; using launchdarkly::Value; TEST(EvaluationResultTests, FromJsonAllFields) { - auto evaluation_result = - boost::json::value_to>( - boost::json::parse("{" - "\"version\": 12," - "\"flagVersion\": 24," - "\"trackEvents\": true," - "\"trackReason\": true," - "\"debugEventsUntilDate\": 1680555761," - "\"value\": {\"item\": 42}," - "\"variation\": 84," - "\"reason\": {" - "\"kind\":\"OFF\"," - "\"errorKind\":\"MALFORMED_FLAG\"," - "\"ruleIndex\":12," - "\"ruleId\":\"RULE_ID\"," - "\"prerequisiteKey\":\"PREREQ_KEY\"," - "\"inExperiment\":true," - "\"bigSegmentStatus\":\"STORE_ERROR\"" - "}" - "}")); + auto evaluation_result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse("{" + "\"version\": 12," + "\"flagVersion\": 24," + "\"trackEvents\": true," + "\"trackReason\": true," + "\"debugEventsUntilDate\": 1680555761," + "\"value\": {\"item\": 42}," + "\"variation\": 84," + "\"reason\": {" + "\"kind\":\"OFF\"," + "\"errorKind\":\"MALFORMED_FLAG\"," + "\"ruleIndex\":12," + "\"ruleId\":\"RULE_ID\"," + "\"prerequisiteKey\":\"PREREQ_KEY\"," + "\"inExperiment\":true," + "\"bigSegmentStatus\":\"STORE_ERROR\"" + "}" + "}")); EXPECT_TRUE(evaluation_result.has_value()); - EXPECT_EQ(12, evaluation_result.value().Version()); - EXPECT_EQ(24, evaluation_result.value().FlagVersion()); - EXPECT_TRUE(evaluation_result.value().TrackEvents()); - EXPECT_TRUE(evaluation_result.value().TrackReason()); + auto const& val = evaluation_result.value(); + EXPECT_TRUE(val.has_value()); + + EXPECT_EQ(12, val->Version()); + EXPECT_EQ(24, val->FlagVersion()); + EXPECT_TRUE(val->TrackEvents()); + EXPECT_TRUE(val->TrackReason()); EXPECT_EQ(std::chrono::system_clock::time_point{std::chrono::milliseconds{ 1680555761}}, - evaluation_result.value().DebugEventsUntilDate()); - EXPECT_EQ( - 42, - evaluation_result.value().Detail().Value().AsObject()["item"].AsInt()); - EXPECT_EQ(84, evaluation_result.value().Detail().VariationIndex()); + val->DebugEventsUntilDate()); + EXPECT_EQ(42, val->Detail().Value().AsObject()["item"].AsInt()); + EXPECT_EQ(84, val->Detail().VariationIndex()); EXPECT_EQ(EvaluationReason::Kind::kOff, - evaluation_result.value().Detail().Reason()->get().Kind()); + val->Detail().Reason()->get().Kind()); EXPECT_EQ(EvaluationReason::ErrorKind::kMalformedFlag, - evaluation_result.value().Detail().Reason()->get().ErrorKind()); - EXPECT_EQ(12, - evaluation_result.value().Detail().Reason()->get().RuleIndex()); - EXPECT_EQ("RULE_ID", - evaluation_result.value().Detail().Reason()->get().RuleId()); - EXPECT_EQ( - "PREREQ_KEY", - evaluation_result.value().Detail().Reason()->get().PrerequisiteKey()); - EXPECT_EQ("STORE_ERROR", - evaluation_result.value().Detail().Reason()->get().BigSegmentStatus()); - EXPECT_TRUE( - evaluation_result.value().Detail().Reason()->get().InExperiment()); + val->Detail().Reason()->get().ErrorKind()); + EXPECT_EQ(12, val->Detail().Reason()->get().RuleIndex()); + EXPECT_EQ("RULE_ID", val->Detail().Reason()->get().RuleId()); + EXPECT_EQ("PREREQ_KEY", val->Detail().Reason()->get().PrerequisiteKey()); + EXPECT_EQ("STORE_ERROR", val->Detail().Reason()->get().BigSegmentStatus()); + EXPECT_TRUE(val->Detail().Reason()->get().InExperiment()); } TEST(EvaluationResultTests, ToJsonAllFields) { @@ -91,29 +86,27 @@ TEST(EvaluationResultTests, ToJsonAllFields) { } TEST(EvaluationResultTests, FromJsonMinimalFields) { - auto evaluation_result = - boost::json::value_to>( - boost::json::parse("{" - "\"version\": 12," - "\"value\": {\"item\": 42}" - "}")); - - EXPECT_EQ(12, evaluation_result.value().Version()); - EXPECT_EQ(std::nullopt, evaluation_result.value().FlagVersion()); - EXPECT_FALSE(evaluation_result.value().TrackEvents()); - EXPECT_FALSE(evaluation_result.value().TrackReason()); - EXPECT_EQ(std::nullopt, evaluation_result.value().DebugEventsUntilDate()); - EXPECT_EQ( - 42, - evaluation_result.value().Detail().Value().AsObject()["item"].AsInt()); - EXPECT_EQ(std::nullopt, - evaluation_result.value().Detail().VariationIndex()); - EXPECT_EQ(std::nullopt, evaluation_result.value().Detail().Reason()); + auto evaluation_result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse("{" + "\"version\": 12," + "\"value\": {\"item\": 42}" + "}")); + + auto const& value = evaluation_result.value(); + EXPECT_EQ(12, value->Version()); + EXPECT_EQ(std::nullopt, value->FlagVersion()); + EXPECT_FALSE(value->TrackEvents()); + EXPECT_FALSE(value->TrackReason()); + EXPECT_EQ(std::nullopt, value->DebugEventsUntilDate()); + EXPECT_EQ(42, value->Detail().Value().AsObject()["item"].AsInt()); + EXPECT_EQ(std::nullopt, value->Detail().VariationIndex()); + EXPECT_EQ(std::nullopt, value->Detail().Reason()); } TEST(EvaluationResultTests, FromMapOfResults) { - auto results = boost::json::value_to< - std::map>>( + auto results = boost::json::value_to, JsonError>>>( boost::json::parse("{" "\"flagA\":{" "\"version\": 12," @@ -124,26 +117,25 @@ TEST(EvaluationResultTests, FromMapOfResults) { "\"value\": false" "}" "}")); - - EXPECT_TRUE(results.at("flagA").value().Detail().Value().AsBool()); - EXPECT_FALSE(results.at("flagB").value().Detail().Value().AsBool()); + EXPECT_TRUE(results.at("flagA").value().value().Detail().Value().AsBool()); + EXPECT_FALSE(results.at("flagB").value().value().Detail().Value().AsBool()); } TEST(EvaluationResultTests, NoResultFieldsJson) { - auto results = - boost::json::value_to>( - boost::json::parse("{}")); + auto results = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse("{}")); - EXPECT_FALSE(results.has_value()); + EXPECT_FALSE(results); EXPECT_EQ(JsonError::kSchemaFailure, results.error()); } TEST(EvaluationResultTests, VersionWrongTypeJson) { - auto results = - boost::json::value_to>( - boost::json::parse("{\"version\": \"apple\"}")); + auto results = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse("{\"version\": \"apple\"}")); - EXPECT_FALSE(results.has_value()); + EXPECT_FALSE(results); EXPECT_EQ(JsonError::kSchemaFailure, results.error()); } diff --git a/libs/internal/tests/event_processor_test.cpp b/libs/internal/tests/event_processor_test.cpp index b1b788598..226ebb3ef 100644 --- a/libs/internal/tests/event_processor_test.cpp +++ b/libs/internal/tests/event_processor_test.cpp @@ -7,12 +7,11 @@ #include #include #include -#include -#include -#include +#include #include using namespace launchdarkly::events; +using namespace launchdarkly::events::detail; using namespace launchdarkly::network; static std::chrono::system_clock::time_point TimeZero() { @@ -88,16 +87,16 @@ TEST_F(EventProcessorTests, ProcessorCompiles) { auto context = launchdarkly::ContextBuilder().Kind("org", "ld").Build(); ASSERT_TRUE(context.Valid()); - auto identify_event = events::client::IdentifyEventParams{ + auto identify_event = events::IdentifyEventParams{ std::chrono::system_clock::now(), context, }; for (std::size_t i = 0; i < 10; i++) { - processor.AsyncSend(identify_event); + processor.SendAsync(identify_event); } - processor.AsyncClose(); + processor.ShutdownAsync(); ioc_thread.join(); } @@ -105,8 +104,8 @@ TEST_F(EventProcessorTests, ParseValidDateHeader) { using namespace launchdarkly; using Clock = std::chrono::system_clock; - auto date = - events::ParseDateHeader("Wed, 21 Oct 2015 07:28:00 GMT", locale); + auto date = events::detail::ParseDateHeader( + "Wed, 21 Oct 2015 07:28:00 GMT", locale); ASSERT_TRUE(date); @@ -117,18 +116,20 @@ TEST_F(EventProcessorTests, ParseValidDateHeader) { TEST_F(EventProcessorTests, ParseInvalidDateHeader) { using namespace launchdarkly; - auto not_a_date = events::ParseDateHeader( - "this is definitely not a date", locale); + auto not_a_date = + events::detail::ParseDateHeader( + "this is definitely not a date", locale); ASSERT_FALSE(not_a_date); - auto not_gmt = events::ParseDateHeader( + auto not_gmt = events::detail::ParseDateHeader( "Wed, 21 Oct 2015 07:28:00 PST", locale); ASSERT_FALSE(not_gmt); - auto missing_year = events::ParseDateHeader( - "Wed, 21 Oct 07:28:00 GMT", locale); + auto missing_year = + events::detail::ParseDateHeader( + "Wed, 21 Oct 07:28:00 GMT", locale); ASSERT_FALSE(missing_year); } diff --git a/libs/internal/tests/event_serialization_test.cpp b/libs/internal/tests/event_serialization_test.cpp index 00e274946..f1f1cddfc 100644 --- a/libs/internal/tests/event_serialization_test.cpp +++ b/libs/internal/tests/event_serialization_test.cpp @@ -3,18 +3,16 @@ #include #include -#include - #include +#include #include -#include namespace launchdarkly::events { TEST(EventSerialization, FeatureEvent) { auto creation_date = std::chrono::system_clock::from_time_t({}); - auto event = events::client::FeatureEvent{ - client::FeatureEventBase(client::FeatureEventParams{ + auto event = events::FeatureEvent{ + events::FeatureEventBase(events::FeatureEventParams{ creation_date, "key", ContextBuilder().Kind("foo", "bar").Build(), @@ -42,9 +40,9 @@ TEST(EventSerialization, DebugEvent) { AttributeReference::SetType attrs; ContextFilter filter(false, attrs); auto context = ContextBuilder().Kind("foo", "bar").Build(); - auto event = events::client::DebugEvent{ - client::FeatureEventBase( - client::FeatureEventBase(client::FeatureEventParams{ + auto event = events::DebugEvent{ + events::FeatureEventBase( + events::FeatureEventBase(events::FeatureEventParams{ creation_date, "key", ContextBuilder().Kind("foo", "bar").Build(), @@ -71,7 +69,7 @@ TEST(EventSerialization, IdentifyEvent) { auto creation_date = std::chrono::system_clock::from_time_t({}); AttributeReference::SetType attrs; ContextFilter filter(false, attrs); - auto event = events::client::IdentifyEvent{ + auto event = events::IdentifyEvent{ creation_date, filter.filter(ContextBuilder().Kind("foo", "bar").Build())}; @@ -82,4 +80,19 @@ TEST(EventSerialization, IdentifyEvent) { ASSERT_EQ(result, event_json); } +TEST(EventSerialization, IndexEvent) { + auto creation_date = std::chrono::system_clock::from_time_t({}); + AttributeReference::SetType attrs; + ContextFilter filter(false, attrs); + auto event = events::server_side::IndexEvent{ + creation_date, + filter.filter(ContextBuilder().Kind("foo", "bar").Build())}; + + auto event_json = boost::json::value_from(event); + + auto result = boost::json::parse( + R"({"kind":"index","creationDate":0,"context":{"key":"bar","kind":"foo"}})"); + ASSERT_EQ(result, event_json); +} + } // namespace launchdarkly::events diff --git a/libs/internal/tests/event_summarizer_test.cpp b/libs/internal/tests/event_summarizer_test.cpp index 2922f9b2d..9e657ef8c 100644 --- a/libs/internal/tests/event_summarizer_test.cpp +++ b/libs/internal/tests/event_summarizer_test.cpp @@ -2,15 +2,14 @@ #include #include #include -#include -#include +#include #include #include #include +#include "launchdarkly/events/detail/summarizer.hpp" using namespace launchdarkly::events; -using namespace launchdarkly::events::client; -using namespace launchdarkly::events; +using namespace launchdarkly::events::detail; static std::chrono::system_clock::time_point TimeZero() { return std::chrono::system_clock::time_point{}; @@ -27,13 +26,13 @@ TEST(SummarizerTests, IsEmptyOnConstruction) { TEST(SummarizerTests, DefaultConstructionUsesZeroStartTime) { Summarizer summarizer; - ASSERT_EQ(summarizer.start_time(), TimeZero()); + ASSERT_EQ(summarizer.StartTime(), TimeZero()); } TEST(SummarizerTests, ExplicitStartTimeIsCorrect) { auto start = std::chrono::system_clock::from_time_t(12345); Summarizer summarizer(start); - ASSERT_EQ(summarizer.start_time(), start); + ASSERT_EQ(summarizer.StartTime(), start); } struct EvaluationParams { @@ -292,7 +291,7 @@ INSTANTIATE_TEST_SUITE_P( {Summarizer::VariationKey(1, 1), 1}}}}})); TEST(SummarizerTests, MissingFlagCreatesCounterUsingDefaultValue) { - using namespace launchdarkly::events::client; + using namespace launchdarkly::events; using namespace launchdarkly; Summarizer summarizer; @@ -340,7 +339,7 @@ TEST(SummarizerTests, MissingFlagCreatesCounterUsingDefaultValue) { } TEST(SummarizerTests, JsonSerialization) { - using namespace launchdarkly::events::client; + using namespace launchdarkly::events; using namespace launchdarkly; Summarizer summarizer; diff --git a/libs/internal/tests/ld_logger_test.cpp b/libs/internal/tests/ld_logger_test.cpp index cf393d332..d0072dcc1 100644 --- a/libs/internal/tests/ld_logger_test.cpp +++ b/libs/internal/tests/ld_logger_test.cpp @@ -86,11 +86,7 @@ INSTANTIATE_TEST_SUITE_P(LDLoggerTest, testing::Values(LogLevel::kDebug, LogLevel::kInfo, LogLevel::kWarn, - LogLevel::kError), - [](testing::TestParamInfo const& info) { - return launchdarkly::GetLogLevelName(info.param, - "unknown"); - }); + LogLevel::kError)); TEST(LDLoggerTest, UsesOstreamForEnabledLevel) { Messages messages; diff --git a/libs/internal/tests/lru_cache_test.cpp b/libs/internal/tests/lru_cache_test.cpp new file mode 100644 index 000000000..e1cf64ee0 --- /dev/null +++ b/libs/internal/tests/lru_cache_test.cpp @@ -0,0 +1,65 @@ +#include "launchdarkly/events/detail/lru_cache.hpp" +#include + +using namespace launchdarkly::events::detail; + +TEST(ContextKeyCacheTests, CacheSizeOne) { + LRUCache cache(1); + + auto keys = {"foo", "bar", "baz", "qux"}; + for (auto const& k : keys) { + ASSERT_FALSE(cache.Notice(k)); + ASSERT_EQ(cache.Size(), 1); + } +} + +TEST(ContextKeyCacheTests, CacheIsCleared) { + LRUCache cache(3); + auto keys = {"foo", "bar", "baz"}; + for (auto const& k : keys) { + cache.Notice(k); + } + ASSERT_EQ(cache.Size(), 3); + cache.Clear(); + ASSERT_EQ(cache.Size(), 0); +} + +TEST(ContextKeyCacheTests, LRUProperty) { + LRUCache cache(3); + auto keys = {"foo", "bar", "baz"}; + for (auto const& k : keys) { + cache.Notice(k); + } + + for (auto const& k : keys) { + ASSERT_TRUE(cache.Notice(k)); + } + + // Evict foo. + cache.Notice("qux"); + ASSERT_TRUE(cache.Notice("bar")); + ASSERT_TRUE(cache.Notice("baz")); + ASSERT_TRUE(cache.Notice("qux")); + + // Evict bar. + cache.Notice("foo"); + ASSERT_TRUE(cache.Notice("baz")); + ASSERT_TRUE(cache.Notice("qux")); + ASSERT_TRUE(cache.Notice("foo")); +} + +TEST(ContextKeyCacheTests, DoesNotExceedCapacity) { + const std::size_t CAP = 100; + const std::size_t N = 100000; + LRUCache cache(CAP); + + for (int i = 0; i < N; ++i) { + cache.Notice(std::to_string(i)); + } + + for (int i = N - CAP; i < N; ++i) { + ASSERT_TRUE(cache.Notice(std::to_string(i))); + } + + ASSERT_EQ(cache.Size(), CAP); +} diff --git a/libs/internal/tests/request_worker_test.cpp b/libs/internal/tests/request_worker_test.cpp index d38b665e9..645769fad 100644 --- a/libs/internal/tests/request_worker_test.cpp +++ b/libs/internal/tests/request_worker_test.cpp @@ -1,8 +1,9 @@ +#include "launchdarkly/events/detail/request_worker.hpp" #include -#include #include using namespace launchdarkly::events; +using namespace launchdarkly::events::detail; using namespace launchdarkly::network; struct TestCase { diff --git a/libs/internal/tests/sha_1_test.cpp b/libs/internal/tests/sha_1_test.cpp new file mode 100644 index 000000000..a491de37e --- /dev/null +++ b/libs/internal/tests/sha_1_test.cpp @@ -0,0 +1,20 @@ +#include + +#include "launchdarkly/encoding/base_16.hpp" +#include "launchdarkly/encoding/sha_1.hpp" + +using namespace launchdarkly::encoding; + +TEST(Sha1, CanEncodeString) { + // Test vectors from + // https://www.di-mgt.com.au/sha_testvectors.html + EXPECT_EQ(std::string("da39a3ee5e6b4b0d3255bfef95601890afd80709"), + Base16Encode(Sha1String(""))); + + EXPECT_EQ(std::string("a9993e364706816aba3e25717850c26c9cd0d89d"), + Base16Encode(Sha1String("abc"))); + + EXPECT_EQ(std::string("84983e441c3bd26ebaae4aa1f95129e5e54670f1"), + Base16Encode(Sha1String( + "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"))); +} diff --git a/libs/internal/tests/sha_256_test.cpp b/libs/internal/tests/sha_256_test.cpp index 99105a755..1e417aef8 100644 --- a/libs/internal/tests/sha_256_test.cpp +++ b/libs/internal/tests/sha_256_test.cpp @@ -1,18 +1,9 @@ #include +#include "launchdarkly/encoding/base_16.hpp" #include "launchdarkly/encoding/sha_256.hpp" -using launchdarkly::encoding::Sha256String; - -static std::string HexEncode(std::array arr) { - std::stringstream output_stream; - output_stream << std::hex << std::noshowbase; - for (unsigned char byte : arr) { - output_stream << std::setw(2) << std::setfill('0') - << static_cast(byte); - } - return output_stream.str(); -} +using namespace launchdarkly::encoding; TEST(Sha256, CanEncodeString) { // Test vectors from @@ -20,16 +11,16 @@ TEST(Sha256, CanEncodeString) { EXPECT_EQ( std::string( "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"), - HexEncode(Sha256String(""))); + Base16Encode(Sha256String(""))); EXPECT_EQ( std::string( "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"), - HexEncode(Sha256String("abc"))); + Base16Encode(Sha256String("abc"))); EXPECT_EQ( std::string( "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1"), - HexEncode(Sha256String( + Base16Encode(Sha256String( "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"))); } diff --git a/libs/server-sdk/CMakeLists.txt b/libs/server-sdk/CMakeLists.txt new file mode 100644 index 000000000..c1de8c375 --- /dev/null +++ b/libs/server-sdk/CMakeLists.txt @@ -0,0 +1,38 @@ +# This project aims to follow modern cmake guidelines, e.g. +# https://cliutils.gitlab.io/modern-cmake + +# Required for Apple Silicon support. +cmake_minimum_required(VERSION 3.19) + +project( + LaunchDarklyCPPServer + VERSION 0.1 + DESCRIPTION "LaunchDarkly C++ Server SDK" + LANGUAGES CXX C +) + +set(LIBNAME "launchdarkly-cpp-server") + +# If this project is the main CMake project (as opposed to being included via add_subdirectory) +if (CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) + # Disable C++ extensions for portability. + set(CMAKE_CXX_EXTENSIONS OFF) + # Enable folder support in IDEs. + set_property(GLOBAL PROPERTY USE_FOLDERS ON) +endif () + +#set(CMAKE_FILES "${CMAKE_CURRENT_SOURCE_DIR}/cmake") +#set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_FILES}) + +# Needed to fetch external dependencies. +include(FetchContent) + +# Needed to parse RFC3339 dates in flag rules. +include(${CMAKE_FILES}/rfc3339_timestamp.cmake) + +# Add main SDK sources. +add_subdirectory(src) + +if (LD_BUILD_UNIT_TESTS) + add_subdirectory(tests) +endif () diff --git a/libs/server-sdk/Doxyfile b/libs/server-sdk/Doxyfile new file mode 100644 index 000000000..c8ba8bd90 --- /dev/null +++ b/libs/server-sdk/Doxyfile @@ -0,0 +1,94 @@ +# Doxyfile 1.8.17 + +# This file describes the settings to be used by the documentation system +# doxygen (www.doxygen.org) for a project. +# +# All text after a double hash (##) is considered a comment and is placed in +# front of the TAG it is preceding. +# +# All text after a single hash (#) is considered a comment and will be ignored. +# The format is: +# TAG = value [value, ...] +# For lists, items can also be appended using: +# TAG += value [value, ...] +# Values that contain spaces should be placed between quotes (\" \"). + +#--------------------------------------------------------------------------- +# Project related configuration options +#--------------------------------------------------------------------------- + +# This tag specifies the encoding used for all characters in the configuration +# file that follow. The default is UTF-8 which is also the encoding used for all +# text before the first occurrence of this tag. Doxygen uses libiconv (or the +# iconv built into libc) for the transcoding. See +# https://www.gnu.org/software/libiconv/ for the list of possible encodings. +# The default value is: UTF-8. + +DOXYFILE_ENCODING = UTF-8 + +# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by +# double-quotes, unless you are using Doxywizard) that should identify the +# project for which the documentation is generated. This name is used in the +# title of most generated pages and in a few other places. +# The default value is: My Project. + +PROJECT_NAME = "C++ Server-Side SDK" + +# Using the PROJECT_BRIEF tag one can provide an optional one line description +# for a project that appears at the top of each page and should give viewer a +# quick idea about the purpose of the project. Keep the description short. + +PROJECT_BRIEF = "LaunchDarkly SDK" + +# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path +# into which the generated documentation will be written. If a relative path is +# entered, it will be relative to the location where doxygen was started. If +# left blank the current directory will be used. + +OUTPUT_DIRECTORY = docs + +# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want +# to include (a tag file for) the STL sources as input, then you should set this +# tag to YES in order to let doxygen match functions declarations and +# definitions whose arguments contain STL classes (e.g. func(std::string); +# versus func(std::string) {}). This also make the inheritance and collaboration +# diagrams that involve STL classes more complete and accurate. +# The default value is: NO. + +BUILTIN_STL_SUPPORT = YES + +# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or +# enum is documented as struct, union, or enum with the name of the typedef. So +# typedef struct TypeS {} TypeT, will appear in the documentation as a struct +# with name TypeT. When disabled the typedef will appear as a member of a file, +# namespace, or class. And the struct will be named TypeS. This can typically be +# useful for C code in case the coding convention dictates that all compound +# types are typedef'ed and only the typedef is referenced, never the tag name. +# The default value is: NO. + +TYPEDEF_HIDES_STRUCT = YES + +#--------------------------------------------------------------------------- +# Configuration options related to the input files +#--------------------------------------------------------------------------- + +# The INPUT tag is used to specify the files and/or directories that contain +# documented source files. You may enter file names like myfile.cpp or +# directories like /usr/src/myproject. Separate the files or directories with +# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING +# Note: If this tag is empty the current directory is searched. + +INPUT = include src docs ../common/include ../common/src + +# The RECURSIVE tag can be used to specify whether or not subdirectories should +# be searched for input files as well. +# The default value is: NO. + +RECURSIVE = YES + +# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that +# is part of the input, its contents will be placed on the main page +# (index.html). This can be useful if you have a project on for instance GitHub +# and want to reuse the introduction page also for the doxygen output. + +USE_MDFILE_AS_MAINPAGE = ./docs/doc.md diff --git a/libs/server-sdk/README.md b/libs/server-sdk/README.md new file mode 100644 index 000000000..a758b532c --- /dev/null +++ b/libs/server-sdk/README.md @@ -0,0 +1,137 @@ +LaunchDarkly Server-Side SDK for C/C++ +=================================== + +[![Actions Status](https://github.com/launchdarkly/cpp-sdks/actions/workflows/server.yml/badge.svg)](https://github.com/launchdarkly/cpp-sdks/actions/workflows/server.yml) +[![Documentation](https://img.shields.io/static/v1?label=GitHub+Pages&message=API+reference&color=00add8)](https://launchdarkly.github.io/cpp-sdks/libs/server-sdk/docs/html/) + +The LaunchDarkly Server-Side SDK for C/C++ is designed primarily for use in multi-user systems such as web servers +and applications. It follows the server-side LaunchDarkly model for multi-user contexts. +It is not intended for use in desktop and embedded systems applications. + +For using LaunchDarkly in client-side C/C++ applications, refer to our [Client-Side C/C++ SDK](../client-sdk/README.md). + +LaunchDarkly overview +------------------------- +[LaunchDarkly](https://www.launchdarkly.com) is a feature management platform that serves trillions of feature flags +daily to help teams build better software, faster. [Get started](https://docs.launchdarkly.com/docs/getting-started) +using LaunchDarkly today! + +[![Twitter Follow](https://img.shields.io/twitter/follow/launchdarkly.svg?style=social&label=Follow&maxAge=2592000)](https://twitter.com/intent/follow?screen_name=launchdarkly) + +Compatibility +------------------------- + +This version of the LaunchDarkly SDK is compatible with POSIX environments (Linux, OS X, BSD) and Windows. + +Getting started +--------------- + +Download a release archive from +the [Github releases](https://github.com/launchdarkly/cpp-sdks/releases?q=cpp-server&expanded=true) for use in your +project. + +Refer to the [SDK documentation][reference-guide] for complete instructions on +installing and using the SDK. + +### Incorporating the SDK + +The SDK can be used via a C++ or C interface and can be incorporated via a static library or shared object. The static +library and shared object each have their own use cases and limitations. + +The static library supports both the C++ and C interface. When using the static library, you should ensure that it is +compiled using a compatible configuration and toolchain. For instance, when using MSVC, it needs to be using the same +runtime library. + +Using the static library also requires that you have OpenSSL and Boost available at the time of compilation for your +project. + +The C++ API does not have a stable ABI, so if this is important to you, consider using the shared object with the C API. + +Example of basic compilation using the C++ API with a static library using gcc: + +```shell +g++ -I path_to_the_sdk_install/include -O3 -std=c++17 -Llib -fPIE -g main.cpp path_to_the_sdk_install/lib/liblaunchdarkly-cpp-server.a -lpthread -lstdc++ -lcrypto -lssl -lboost_json -lboost_url +``` + +Example of basic compilation using the C API with a static library using msvc: + +```shell +cl /I include /Fe: hello.exe main.cpp /link lib/launchdarkly-cpp-server.lib +``` + +The shared library (so, DLL, dylib), only supports the C interface. The shared object does not require you to have Boost +or OpenSSL available when linking the shared object to your project. + +Example of basic compilation using the C API with a shared library using gcc: + +```shell +gcc -I $(pwd)/include -Llib -fPIE -g main.c liblaunchdarkly-cpp-server.so +``` + +The examples here are to help with getting started, but generally speaking the SDK should be incorporated using your +build system (CMake for instance). + +### CMake Usage + +First, add the SDK to your project: + +```cmake +add_subdirectory(path-to-sdk-repo) +``` + +Currently `find_package` is not yet supported. + +This will expose the `launchdarkly::server` target. Next, link the target to your executable or library: + +```cmake +target_link_libraries(my-target PRIVATE launchdarkly::server) +``` + +Learn more +----------- + +Read our [documentation](https://docs.launchdarkly.com) for in-depth instructions on configuring and using LaunchDarkly. +You can also head straight to +the [complete reference guide for this SDK][reference-guide]. + +Testing +------- + +We run integration tests for all our SDKs using a centralized test harness. This approach gives us the ability to test +for consistency across SDKs, as well as test networking behavior in a long-running application. These tests cover each +method in the SDK, and verify that event sending, flag evaluation, stream reconnection, and other aspects of the SDK all +behave correctly. + +Contributing +------------ + +We encourage pull requests and other contributions from the community. Read +our [contributing guidelines](../../CONTRIBUTING.md) for instructions on how to contribute to this SDK. + +About LaunchDarkly +----------- + +* LaunchDarkly is a continuous delivery platform that provides feature flags as a service and allows developers to + iterate quickly and safely. We allow you to easily flag your features and manage them from the LaunchDarkly dashboard. + With LaunchDarkly, you can: + * Roll out a new feature to a subset of your users (like a group of users who opt-in to a beta tester group), + gathering feedback and bug reports from real-world use cases. + * Gradually roll out a feature to an increasing percentage of users, and track the effect that the feature has on + key metrics (for instance, how likely is a user to complete a purchase if they have feature A versus feature B?). + * Turn off a feature that you realize is causing performance problems in production, without needing to re-deploy, + or even restart the application with a changed configuration file. + * Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get + access to more features than users in the ‘silver’ plan). Disable parts of your application to facilitate + maintenance, without taking everything offline. +* LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. + Read [our documentation](https://docs.launchdarkly.com/docs) for a complete list. +* Explore LaunchDarkly + * [launchdarkly.com](https://www.launchdarkly.com/ "LaunchDarkly Main Website") for more information + * [docs.launchdarkly.com](https://docs.launchdarkly.com/ "LaunchDarkly Documentation") for our documentation and + SDK reference guides + * [apidocs.launchdarkly.com](https://apidocs.launchdarkly.com/ "LaunchDarkly API Documentation") for our API + documentation + * [blog.launchdarkly.com](https://blog.launchdarkly.com/ "LaunchDarkly Blog Documentation") for the latest product + updates + +[reference-guide]: https://docs.launchdarkly.com/sdk/server-side/c-c-- diff --git a/libs/server-sdk/docs/doc.md b/libs/server-sdk/docs/doc.md new file mode 100644 index 000000000..fbc1d6a9f --- /dev/null +++ b/libs/server-sdk/docs/doc.md @@ -0,0 +1,9 @@ +# SDK Layout and Overview + +## Basic Functionality + +The following pages document the core of the API, every application will use these portions of the SDK: + +- [Client](@ref launchdarkly::server_side::Client) +- [Config Builder](@ref launchdarkly::config::shared::builders::ConfigBuilder) +- [Context Builder](@ref launchdarkly::ContextBuilder) diff --git a/libs/server-sdk/include/launchdarkly/server_side/all_flags_state.hpp b/libs/server-sdk/include/launchdarkly/server_side/all_flags_state.hpp new file mode 100644 index 000000000..6f4b50b63 --- /dev/null +++ b/libs/server-sdk/include/launchdarkly/server_side/all_flags_state.hpp @@ -0,0 +1,172 @@ +#pragma once + +#include +#include + +#include +#include +#include + +namespace launchdarkly::server_side { + +/** + * AllFlagsState is a snapshot of the state of multiple feature flags with + * regard to a specific evaluation context. + * + * Serializing this object to JSON using boost::json::value_from will produce + * the appropriate data structure for bootstrapping the LaunchDarkly JavaScript + * client. + * + * To do this, the header + * must be + * included to make the appropriate `tag_invoke` implementations available to + * boost. + */ +class AllFlagsState { + public: + enum class Options : std::uint8_t { + /** + * Default behavior. + */ + Default = 0, + /** + * Include evaluation reasons in the state object. By default, they + * are not. + */ + IncludeReasons = (1 << 0), + /** + * Include detailed flag metadata only for flags with event tracking + * or debugging turned on. + * + * This reduces the size of the JSON data if you are + * passing the flag state to the front end. + */ + DetailsOnlyForTrackedFlags = (1 << 1), + /** + * Include only flags marked for use with the client-side SDK. + * By default, all flags are included. + */ + ClientSideOnly = (1 << 2) + }; + + /** + * State contains information pertaining to a single feature flag. + */ + class State { + public: + State(std::uint64_t version, + std::optional variation, + std::optional reason, + bool track_events, + bool track_reason, + std::optional debug_events_until_date); + + /** + * @return The flag's version number when it was evaluated. + */ + [[nodiscard]] std::uint64_t Version() const; + + /** + * @return The variation index that was selected for the specified + * evaluation context. + */ + [[nodiscard]] std::optional Variation() const; + + /** + * @return The reason that the flag evaluation produced the specified + * variation. + */ + [[nodiscard]] std::optional const& Reason() const; + + /** + * @return True if a full feature event must be sent when evaluating + * this flag. This will be true if tracking was explicitly enabled for + * this flag for data export, or if the evaluation involved an + * experiment, or both. + */ + [[nodiscard]] bool TrackEvents() const; + + /** + * @return True if the evaluation reason should always be included in + * any full feature event created for this flag, regardless of whether a + * VariationDetail method was called. This will be true if the + * evaluation involved an experiment. + */ + [[nodiscard]] bool TrackReason() const; + + /** + * @return The date on which debug mode expires for this flag, if + * enabled. + */ + [[nodiscard]] std::optional const& DebugEventsUntilDate() + const; + + /** + * + * @return True if the options passed to AllFlagsState, combined with + * the obtained flag state, indicate that some metadata can be left out + * of the JSON serialization. + */ + [[nodiscard]] bool OmitDetails() const; + + friend class AllFlagsStateBuilder; + + private: + std::uint64_t version_; + std::optional variation_; + std::optional reason_; + bool track_events_; + bool track_reason_; + std::optional debug_events_until_date_; + bool omit_details_; + }; + + /** + * @return True if the call to AllFlagsState succeeded. False if there was + * an error, such as the data store being unavailable. When false, the other + * accessors will return empty maps. + */ + [[nodiscard]] bool Valid() const; + + /** + * @return A map of metadata for each flag. + */ + [[nodiscard]] std::unordered_map const& States() const; + + /** + * @return A map of evaluation results for each flag. + */ + [[nodiscard]] std::unordered_map const& Values() const; + + /** + * Constructs an invalid instance of AllFlagsState. + */ + AllFlagsState(); + + /** + * Constructs a valid instance of AllFlagsState. + * @param evaluations A map of evaluation results for each flag. + * @param flags_state A map of metadata for each flag. + */ + AllFlagsState(std::unordered_map evaluations, + std::unordered_map flags_state); + + private: + bool const valid_; + const std::unordered_map flags_state_; + const std::unordered_map evaluations_; +}; + +void operator|=(AllFlagsState::Options& lhs, AllFlagsState::Options rhs); +AllFlagsState::Options operator|(AllFlagsState::Options lhs, + AllFlagsState::Options rhs); + +AllFlagsState::Options operator&(AllFlagsState::Options lhs, + AllFlagsState::Options rhs); + +bool operator==(class AllFlagsState::State const& lhs, + class AllFlagsState::State const& rhs); + +bool operator==(AllFlagsState const& lhs, AllFlagsState const& rhs); + +} // namespace launchdarkly::server_side diff --git a/libs/server-sdk/include/launchdarkly/server_side/bindings/c/all_flags_state/all_flags_state.h b/libs/server-sdk/include/launchdarkly/server_side/bindings/c/all_flags_state/all_flags_state.h new file mode 100644 index 000000000..ab8d42af9 --- /dev/null +++ b/libs/server-sdk/include/launchdarkly/server_side/bindings/c/all_flags_state/all_flags_state.h @@ -0,0 +1,116 @@ +/** @file */ +// NOLINTBEGIN modernize-use-using + +#pragma once + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { // only need to export C interface if +// used by C++ source code +#endif + +typedef struct _LDAllFlagsState* LDAllFlagsState; + +/** + * Frees an AllFlagsState. + * @param state The AllFlagState to free. + */ +LD_EXPORT(void) LDAllFlagsState_Free(LDAllFlagsState state); + +/** + * True if the LDAllFlagsState is valid. False if there was + * an error, such as the data store being unavailable. + * + * An invalid LDAllFlagsState can still be serialized successfully to a JSON + * string. + * + * @param state The LDAllFlagState to check for validity. Must not be NULL. + * @return True if the state is valid, false otherwise. + */ +LD_EXPORT(bool) LDAllFlagsState_Valid(LDAllFlagsState state); + +/** + * Serializes the LDAllFlagsState to a JSON string. + * + * This JSON is suitable for bootstrapping a client-side SDK. + * + * @param state The LDAllFlagState to serialize. Must not be NULL. + * @return A JSON string representing the LDAllFlagsState. The caller must free + * the string using LDMemory_FreeString. + */ +LD_EXPORT(char*) +LDAllFlagsState_SerializeJSON(LDAllFlagsState state); + +/** + * Returns the flag value for the context used to generate this + * LDAllFlagsState. + * + * In order to avoid copying when a large value is accessed, + * the returned LDValue is a reference and NOT DIRECTLY OWNED by the caller. Its + * lifetime is managed by the parent LDAllFlagsState object. + * + * WARNING! + * Do not free the returned LDValue. + * Do not in any way access the returned LDValue after the LDAllFlagsState has + * been freed. + * + * If the flag has no value, returns an LDValue of type LDValueType_Null. + * + * To obtain a caller-owned copy of the LDValue not subject to these + * restrictions, call LDValue_NewValue on the result. + * + * @param state An LDAllFlagsState. Must not be NULL. + * @param flag_key Key of the flag. Must not be NULL. + * @return The evaluation result of the flag. The caller MUST NOT free this + * value and MUST NOT access this value after the LDAllFlagsState has been + * freed. + */ +LD_EXPORT(LDValue) +LDAllFlagsState_Value(LDAllFlagsState state, char const* flag_key); + +/** + * Defines options that may be used with LDServerSDK_AllFlagsState. To + * obtain default behavior, pass LD_ALLFLAGSSTATE_DEFAULT. + * + * It is possible to combine multiple options by ORing them together. + * + * Example: + * @code + * LDAllFlagsState state = LDServerSDK_AllFlagsState(sdk, context, + * LD_ALLFLAGSSTATE_INCLUDE_REASONS | LD_ALLFLAGSSTATE_CLIENT_SIDE_ONLY + * ); + * @endcode + */ +enum LDAllFlagsState_Options { + /** + * Default behavior. + */ + LD_ALLFLAGSSTATE_DEFAULT = 0, + /** + * Include evaluation reasons in the state object. By default, they + * are not. + */ + LD_ALLFLAGSSTATE_INCLUDE_REASONS = (1 << 0), + /** + * Include detailed flag metadata only for flags with event tracking + * or debugging turned on. + * + * This reduces the size of the JSON data if you are + * passing the flag state to the front end. + */ + LD_ALLFLAGSSTATE_DETAILS_ONLY_FOR_TRACKED_FLAGS = (1 << 1), + /** + * Include only flags marked for use with the client-side SDK. + * By default, all flags are included. + */ + LD_ALLFLAGSSTATE_CLIENT_SIDE_ONLY = (1 << 2) +}; + +#ifdef __cplusplus +} +#endif + +// NOLINTEND modernize-use-using diff --git a/libs/server-sdk/include/launchdarkly/server_side/bindings/c/config/builder.h b/libs/server-sdk/include/launchdarkly/server_side/bindings/c/config/builder.h new file mode 100644 index 000000000..6e7b3e8ca --- /dev/null +++ b/libs/server-sdk/include/launchdarkly/server_side/bindings/c/config/builder.h @@ -0,0 +1,346 @@ +/** @file */ +// NOLINTBEGIN modernize-use-using + +#pragma once + +#include + +#include +#include +#include + +#include +#include + +#ifdef __cplusplus +extern "C" { // only need to export C interface if +// used by C++ source code +#endif + +typedef struct _LDServerConfigBuilder* LDServerConfigBuilder; +typedef struct _LDServerDataSourceStreamBuilder* + LDServerDataSourceStreamBuilder; +typedef struct _LDServerDataSourcePollBuilder* LDServerDataSourcePollBuilder; + +/** + * Constructs a client-side config builder. + */ +LD_EXPORT(LDServerConfigBuilder) LDServerConfigBuilder_New(char const* sdk_key); + +/** + * Sets a custom URL for the polling service. + * @param b Client config builder. Must not be NULL. + * @param url Target URL. Must not be NULL. + */ +LD_EXPORT(void) +LDServerConfigBuilder_ServiceEndpoints_PollingBaseURL(LDServerConfigBuilder b, + char const* url); +/** + * Sets a custom URL for the streaming service. + * @param b Client config builder. Must not be NULL. + * @param url Target URL. Must not be NULL. + */ +LD_EXPORT(void) +LDServerConfigBuilder_ServiceEndpoints_StreamingBaseURL(LDServerConfigBuilder b, + char const* url); +/** + * Sets a custom URL for the events service. + * @param b Client config builder. Must not be NULL. + * @param url Target URL. Must not be NULL. + */ +LD_EXPORT(void) +LDServerConfigBuilder_ServiceEndpoints_EventsBaseURL(LDServerConfigBuilder b, + char const* url); +/** + * Sets a custom URL for a Relay Proxy instance. The streaming, + * polling, and events URLs are set automatically. + * @param b Client config builder. Must not be NULL. + * @param url Target URL. Must not be NULL. + */ +LD_EXPORT(void) +LDServerConfigBuilder_ServiceEndpoints_RelayProxyBaseURL( + LDServerConfigBuilder b, + char const* url); + +/** + * Sets an identifier for the application. + * @param b Client config builder. Must not be NULL. + * @param app_id Non-empty string. Must be <= 64 chars. Must be alphanumeric, + * '-', '.', or '_'. Must not be NULL. + */ +LD_EXPORT(void) +LDServerConfigBuilder_AppInfo_Identifier(LDServerConfigBuilder b, + char const* app_id); + +/** + * Sets a version for the application. + * @param b Client config builder. Must not be NULL. + * @param app_version Non-empty string. Must be <= 64 chars. Must be + * alphanumeric, + * '-', '.', or '_'. Must not be NULL. + */ +LD_EXPORT(void) +LDServerConfigBuilder_AppInfo_Version(LDServerConfigBuilder b, + char const* app_version); + +/** + * Enables or disables "Offline" mode. True means + * Offline mode is enabled. + * @param b Client config builder. Must not be NULL. + * @param offline True if offline. + */ +LD_EXPORT(void) +LDServerConfigBuilder_Offline(LDServerConfigBuilder b, bool offline); + +/** + * Specify if event-sending should be enabled or not. By default, + * events are enabled. + * @param b Client config builder. Must not be NULL. + * @param enabled True to enable event-sending. + */ +LD_EXPORT(void) +LDServerConfigBuilder_Events_Enabled(LDServerConfigBuilder b, bool enabled); + +/** + * Sets the capacity of the event processor. When more events are generated + * within the processor's flush interval than this value, events will be + * dropped. + * @param b Client config builder. Must not be NULL. + * @param capacity Event queue capacity. + */ +LD_EXPORT(void) +LDServerConfigBuilder_Events_Capacity(LDServerConfigBuilder b, size_t capacity); + +/** + * Sets the flush interval of the event processor. The processor queues + * outgoing events based on the capacity parameter; these events are then + * delivered based on the flush interval. + * @param b Client config builder. Must not be NULL. + * @param milliseconds Interval between automatic flushes. + */ +LD_EXPORT(void) +LDServerConfigBuilder_Events_FlushIntervalMs(LDServerConfigBuilder b, + unsigned int milliseconds); + +/** + * Attribute privacy indicates whether or not attributes should be + * retained by LaunchDarkly after being sent upon initialization, + * and if attributes should later be sent in events. + * + * Attribute privacy may be specified in 3 ways: + * + * (1) To specify that all attributes should be considered private - not + * just those designated private on a per-context basis - call this method + * with true as the parameter. + * + * (2) To specify that a specific set of attributes should be considered + * private - in addition to those designated private on a per-context basis + * - call @ref PrivateAttribute. + * + * (3) To specify private attributes on a per-context basis, it is not + * necessary to call either of these methods, as the default behavior is to + * treat all attributes as non-private unless otherwise specified. + * + * @param b Client config builder. Must not be NULL. + * @param all_attributes_private True for behavior of (1), false for default + * behavior of (2) or (3). + */ +LD_EXPORT(void) +LDServerConfigBuilder_Events_AllAttributesPrivate(LDServerConfigBuilder b, + bool all_attributes_private); + +/** + * Specifies a single private attribute. May be called multiple times + * with additional private attributes. + * @param b Client config builder. Must not be NULL. + * @param attribute_reference Attribute to mark private. + */ +LD_EXPORT(void) +LDServerConfigBuilder_Events_PrivateAttribute(LDServerConfigBuilder b, + char const* attribute_reference); + +/** + * Set the streaming configuration for the builder. + * + * A data source may either be streaming or polling. Setting a streaming + * builder indicates the data source will use streaming. Setting a polling + * builder will indicate the use of polling. + * + * @param b Client config builder. Must not be NULL. + * @param stream_builder The streaming builder. The builder is consumed; do not + * free it. + */ +LD_EXPORT(void) +LDServerConfigBuilder_DataSource_MethodStream( + LDServerConfigBuilder b, + LDServerDataSourceStreamBuilder stream_builder); + +/** + * Set the polling configuration for the builder. + * + * A data source may either be streaming or polling. Setting a stream + * builder indicates the data source will use streaming. Setting a polling + * builder will indicate the use of polling. + * + * @param b Client config builder. Must not be NULL. + * @param poll_builder The polling builder. The builder is consumed; do not free + * it. + */ +LD_EXPORT(void) +LDServerConfigBuilder_DataSource_MethodPoll( + LDServerConfigBuilder b, + LDServerDataSourcePollBuilder poll_builder); + +/** + * Creates a new DataSource builder for the Streaming method. + * + * If not passed into the config + * builder, must be manually freed with LDServerDataSourceStreamBuilder_Free. + * + * @return New builder for Streaming method. + */ +LD_EXPORT(LDServerDataSourceStreamBuilder) +LDServerDataSourceStreamBuilder_New(); + +/** + * Sets the initial reconnect delay for the streaming connection. + * + * The streaming service uses a backoff algorithm (with jitter) every time + * the connection needs to be reestablished.The delay for the first + * reconnection will start near this value, and then increase exponentially + * for any subsequent connection failures. + * + * @param b Streaming method builder. Must not be NULL. + * @param milliseconds Initial delay for a reconnection attempt. + */ +LD_EXPORT(void) +LDServerDataSourceStreamBuilder_InitialReconnectDelayMs( + LDServerDataSourceStreamBuilder b, + unsigned int milliseconds); + +/** + * Frees a Streaming method builder. Do not call if the builder was consumed by + * the config builder. + * + * @param b Builder to free. + */ +LD_EXPORT(void) +LDServerDataSourceStreamBuilder_Free(LDServerDataSourceStreamBuilder b); + +/** + * Creates a new DataSource builder for the Polling method. + * + * If not passed into the config + * builder, must be manually freed with LDServerDataSourcePollBuilder_Free. + * + * @return New builder for Polling method. + */ + +LD_EXPORT(LDServerDataSourcePollBuilder) +LDServerDataSourcePollBuilder_New(); + +/** + * Sets the interval at which the SDK will poll for feature flag updates. + * @param b Polling method builder. Must not be NULL. + * @param milliseconds Polling interval. + */ +LD_EXPORT(void) +LDServerDataSourcePollBuilder_IntervalS(LDServerDataSourcePollBuilder b, + unsigned int seconds); + +/** + * Frees a Polling method builder. Do not call if the builder was consumed by + * the config builder. + * + * @param b Builder to free. + */ +LD_EXPORT(void) +LDServerDataSourcePollBuilder_Free(LDServerDataSourcePollBuilder b); + +/** + * This should be used for wrapper SDKs to set the wrapper name. + * + * Wrapper information will be included in request headers. + * @param b Client config builder. Must not be NULL. + * @param wrapper_name Name of the wrapper. + */ +LD_EXPORT(void) +LDServerConfigBuilder_HttpProperties_WrapperName(LDServerConfigBuilder b, + char const* wrapper_name); + +/** + * This should be used for wrapper SDKs to set the wrapper version. + * + * Wrapper information will be included in request headers. + * @param b Client config builder. Must not be NULL. + * @param wrapper_version Version of the wrapper. + */ +LD_EXPORT(void) +LDServerConfigBuilder_HttpProperties_WrapperVersion( + LDServerConfigBuilder b, + char const* wrapper_version); + +/** + * Set a custom header value. May be called more than once with additional + * headers. + * + * @param b Client config builder. Must not be NULL. + * @param key Name of the header. Must not be NULL. + * @param value Value of the header. Must not be NULL. + */ +LD_EXPORT(void) +LDServerConfigBuilder_HttpProperties_Header(LDServerConfigBuilder b, + char const* key, + char const* value); + +/** + * Disables the default SDK logging. + * @param b Client config builder. Must not be NULL. + */ +LD_EXPORT(void) +LDServerConfigBuilder_Logging_Disable(LDServerConfigBuilder b); + +/** + * Configures the SDK with basic logging. + * @param b Client config builder. Must not be NULL. + * @param basic_builder The basic logging builder. Must not be NULL. + */ +LD_EXPORT(void) +LDServerConfigBuilder_Logging_Basic(LDServerConfigBuilder b, + LDLoggingBasicBuilder basic_builder); + +/** + * Configures the SDK with custom logging. + * @param b Client config builder. Must not be NULL. + * @param custom_builder The custom logging builder. Must not be NULL. + */ +LD_EXPORT(void) +LDServerConfigBuilder_Logging_Custom(LDServerConfigBuilder b, + LDLoggingCustomBuilder custom_builder); + +/** + * Creates an LDClientConfig. The LDServerConfigBuilder is consumed. + * On success, the config will be stored in out_config; otherwise, + * out_config will be set to NULL and the returned LDStatus will indicate + * the error. + * @param builder Builder to consume. Must not be NULL. + * @param out_config Pointer to where the built config will be + * stored. Must not be NULL. + * @return Error status on failure. + */ +LD_EXPORT(LDStatus) +LDServerConfigBuilder_Build(LDServerConfigBuilder builder, + LDServerConfig* out_config); + +/** + * Frees the builder; only necessary if not calling Build. + * @param builder Builder to free. + */ +LD_EXPORT(void) +LDServerConfigBuilder_Free(LDServerConfigBuilder builder); + +#ifdef __cplusplus +} +#endif + +// NOLINTEND modernize-use-using diff --git a/libs/server-sdk/include/launchdarkly/server_side/bindings/c/config/config.h b/libs/server-sdk/include/launchdarkly/server_side/bindings/c/config/config.h new file mode 100644 index 000000000..b84f3cb26 --- /dev/null +++ b/libs/server-sdk/include/launchdarkly/server_side/bindings/c/config/config.h @@ -0,0 +1,26 @@ +/** @file */ +// NOLINTBEGIN modernize-use-using + +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { // only need to export C interface if +// used by C++ source code +#endif + +typedef struct _LDCServerConfig* LDServerConfig; + +/** + * Free the configuration. Configurations passed into an LDServerSDK_New call do + * not need to be freed. + * @param config Config to free. + */ +LD_EXPORT(void) LDServerConfig_Free(LDServerConfig config); + +#ifdef __cplusplus +} +#endif + +// NOLINTEND modernize-use-using diff --git a/libs/server-sdk/include/launchdarkly/server_side/bindings/c/sdk.h b/libs/server-sdk/include/launchdarkly/server_side/bindings/c/sdk.h new file mode 100644 index 000000000..acccc586d --- /dev/null +++ b/libs/server-sdk/include/launchdarkly/server_side/bindings/c/sdk.h @@ -0,0 +1,581 @@ +/** @file sdk.h + * @brief LaunchDarkly Server-side C Bindings. + */ +// NOLINTBEGIN modernize-use-using +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { // only need to export C interface if +// used by C++ source code +#endif + +typedef struct _LDServerSDK* LDServerSDK; + +/** + * Constructs a new server-side LaunchDarkly SDK from a configuration. + * + * @param config The configuration. Ownership is transferred. Do not free or + * access the LDServerConfig in any way after this call, otherwise behavior is + * undefined. Must not be NULL. + * @return New SDK instance. Must be freed with LDServerSDK_Free when no longer + * needed. + */ +LD_EXPORT(LDServerSDK) +LDServerSDK_New(LDServerConfig config); + +/** + * Returns the version of the SDK. + * @return String representation of the SDK version. + */ +LD_EXPORT(char const*) +LDServerSDK_Version(void); + +/** +* Starts the SDK, initiating a connection to LaunchDarkly if not offline. +* +* Only one Start call can be in progress at once; calling it +* concurrently invokes undefined behavior. +* +* The method may be blocking or asynchronous depending on the arguments. +* +* To block, pass a positive milliseconds value and an optional pointer to a +boolean. The return +* value will be true if the SDK started within the specified timeframe, or +false if the +* operation couldn't complete in time. The value of out_succeeded will be true +* if the SDK successfully initialized. +* +* Example: +* @code +* bool initialized_successfully; +* if (LDServerSDK_Start(client, 5000, &initialized_successfully)) { +* // The client was able to initialize in less than 5 seconds. +* if (initialized_successfully) { +* // Initialization succeeded. +* else { +* // Initialization failed. +* } +* } else { +* // The client is still initializing. +* } +* @endcode +* +* To start asynchronously, pass `LD_NONBLOCKING`. In this case, the return +value +* will be false and you may pass NULL to out_succeeded. +* +* @code +* // Returns immediately. +* LDServerSDK_Start(client, LD_NONBLOCKING, NULL); +* @endcode +* +* @param sdk SDK. Must not be NULL. +* @param milliseconds Milliseconds to wait for initialization or +`LD_NONBLOCKING` to return immediately. +* @param out_succeeded Pointer to bool representing successful initialization. +Only +* modified if a positive milliseconds value is passed; may be NULL. +* @return True if the client started within the specified timeframe. +*/ +LD_EXPORT(bool) +LDServerSDK_Start(LDServerSDK sdk, + unsigned int milliseconds, + bool* out_succeeded); + +/** + * Returns a boolean value indicating LaunchDarkly connection and flag state + * within the client. + * + * When you first start the client, once Start has completed, Initialized + * should return true if and only if either 1. it connected to LaunchDarkly and + * successfully retrieved flags, or 2. it started in offline mode so there's no + * need to connect to LaunchDarkly. + * + * If the client timed out trying to connect to + * LD, then Initialized returns false (even if we do have cached flags). If the + * client connected and got a 401 error, Initialized is will return false. This + * serves the purpose of letting the app know that there was a problem of some + * kind. + * + * @param sdk SDK. Must not be NULL. + * @return True if initialized. + */ +LD_EXPORT(bool) LDServerSDK_Initialized(LDServerSDK sdk); + +/** + * Tracks that the given context performed an event with the given event name. + * @param sdk SDK. Must not be NULL. + * @param context The context. Ownership is NOT transferred. Must not be NULL. + * @param event_name Name of the event. Must not be NULL. + */ +LD_EXPORT(void) +LDServerSDK_TrackEvent(LDServerSDK sdk, + LDContext context, + char const* event_name); + +/** + * Tracks that the given context performed an event with the given event + * name, and associates it with a numeric metric and value. + * + * @param sdk SDK. Must not be NULL. + * @param context The context. Ownership is NOT transferred. Must not be NULL. + * @param event_name The name of the event. Must not be NULL. + * @param metric_value This value is used by the LaunchDarkly experimentation + * feature in numeric custom metrics, and will also be returned as part of the + * custom event for Data Export. + * @param data A JSON value containing additional data associated with the + * event. Ownership is transferred into the SDK. Must not be NULL. + */ +LD_EXPORT(void) +LDServerSDK_TrackMetric(LDServerSDK sdk, + LDContext context, + char const* event_name, + double metric_value, + LDValue data); + +/** + * Tracks that the given context performed an event with the given event + * name, with additional JSON data. + * @param sdk SDK. Must not be NULL. + * @param context The context. Ownership is NOT transferred. Must not be NULL. + * @param event_name The name of the event. Must not be NULL. + * @param data A JSON value containing additional data associated with the + * event. Ownership is transferred. Must not be NULL. + */ +LD_EXPORT(void) +LDServerSDK_TrackData(LDServerSDK sdk, + LDContext context, + char const* event_name, + LDValue data); + +/** + * Requests delivery of all pending analytic events (if any). + * + * You MUST pass `LD_NONBLOCKING` as the second parameter. + * + * @param sdk SDK. Must not be NULL. + * @param milliseconds Must pass `LD_NONBLOCKING`. + */ +LD_EXPORT(void) +LDServerSDK_Flush(LDServerSDK sdk, unsigned int reserved); + +/** + * Generates an identify event for the given context. + * + * @param sdk SDK. Must not be NULL. + * @param context The context. Ownership is NOT transferred. Must not be NULL. + */ +LD_EXPORT(void) +LDServerSDK_Identify(LDServerSDK sdk, LDContext context); + +/** + * Returns the boolean value of a feature flag for a given flag key and context. + * @param sdk SDK. Must not be NULL. + * @param context The context. Ownership is NOT transferred. Must not be NULL. + * @param flag_key The unique key for the feature flag. Must not be NULL. + * @param default_value The default value of the flag. + * @return The variation for the given context, or default_value if the + * flag is disabled in the LaunchDarkly control panel. + */ +LD_EXPORT(bool) +LDServerSDK_BoolVariation(LDServerSDK sdk, + LDContext context, + char const* flag_key, + bool default_value); + +/** + * Returns the boolean value of a feature flag for a given flag key and context, + * and details that also describes the way the value was determined. + * @param sdk SDK. Must not be NULL. + * @param context The context. Ownership is NOT transferred. Must not be NULL. + * @param flag_key The unique key for the feature flag. Must not be NULL. + * @param default_value The default value of the flag. + * @param detail Out parameter to store the details. May pass LD_DISCARD_DETAILS + * or NULL to discard the details. The details object must be freed with + * LDEvalDetail_Free. + * @return The variation for the given context, or default_value if the + * flag is disabled in the LaunchDarkly control panel. + */ +LD_EXPORT(bool) +LDServerSDK_BoolVariationDetail(LDServerSDK sdk, + LDContext context, + char const* flag_key, + bool default_value, + LDEvalDetail* out_detail); + +/** + * Returns the string value of a feature flag for a given flag key and context. + * Ensure the string is freed with LDMemory_FreeString. + * @param sdk SDK. Must not be NULL. + * @param context The context. Ownership is NOT transferred. Must not be NULL. + * @param flag_key The unique key for the feature flag. Must not be NULL. + * @param default_value The default value of the flag. + * @return The variation for the given context, or a copy of default_value if + * the flag is disabled in the LaunchDarkly control panel. Must be freed with + * LDMemory_FreeString. + */ +LD_EXPORT(char*) +LDServerSDK_StringVariation(LDServerSDK sdk, + LDContext context, + char const* flag_key, + char const* default_value); + +/** + * Returns the string value of a feature flag for a given flag key and context, + * and details that also describes the way the value was determined. Ensure the + * string is freed with LDMemory_FreeString. + * @param sdk SDK. Must not be NULL. + * @param context The context. Ownership is NOT transferred. Must not be NULL. + * @param flag_key The unique key for the feature flag. Must not be NULL. + * @param default_value The default value of the flag. + * @param detail Out parameter to store the details. May pass LD_DISCARD_DETAILS + * or NULL to discard the details. The details object must be freed with + * LDEvalDetail_Free. + * @return The variation for the given context, or a copy of default_value if + * the flag is disabled in the LaunchDarkly control panel. Must be freed with + * LDMemory_FreeString. + */ +LD_EXPORT(char*) +LDServerSDK_StringVariationDetail(LDServerSDK sdk, + LDContext context, + char const* flag_key, + char const* default_value, + LDEvalDetail* out_detail); + +/** + * Returns the int value of a feature flag for a given flag key and context. + * @param sdk SDK. Must not be NULL. + * @param context The context. Ownership is NOT transferred. Must not be NULL. + * @param flag_key The unique key for the feature flag. Must not be NULL. + * @param default_value The default value of the flag. + * @return The variation for the given context, or default_value if the + * flag is disabled in the LaunchDarkly control panel. + */ +LD_EXPORT(int) +LDServerSDK_IntVariation(LDServerSDK sdk, + LDContext context, + char const* flag_key, + int default_value); + +/** + * Returns the int value of a feature flag for a given flag key and context, and + * details that also describes the way the value was determined. + * @param sdk SDK. Must not be NULL. + * @param context The context. Ownership is NOT transferred. Must not be NULL. + * @param flag_key The unique key for the feature flag. Must not be NULL. + * @param default_value The default value of the flag. + * @param detail Out parameter to store the details. May pass LD_DISCARD_DETAILS + * or NULL to discard the details. The details object must be freed with + * LDEvalDetail_Free. + * @return The variation for the given context, or default_value if the + * flag is disabled in the LaunchDarkly control panel. + */ +LD_EXPORT(int) +LDServerSDK_IntVariationDetail(LDServerSDK sdk, + LDContext context, + char const* flag_key, + int default_value, + LDEvalDetail* out_detail); + +/** + * Returns the double value of a feature flag for a given flag key and context. + * @param sdk SDK. Must not be NULL. + * @param context The context. Ownership is NOT transferred. Must not be NULL. + * @param flag_key The unique key for the feature flag. Must not be NULL. + * @param default_value The default value of the flag. + * @return The variation for the given context, or default_value if the + * flag is disabled in the LaunchDarkly control panel. + */ +LD_EXPORT(int) +LDServerSDK_DoubleVariation(LDServerSDK sdk, + LDContext context, + char const* flag_key, + double default_value); + +/** + * Returns the double value of a feature flag for a given flag key and context, + * and details that also describes the way the value was determined. + * @param sdk SDK. Must not be NULL. + * @param context The context. Ownership is NOT transferred. Must not be NULL. + * @param flag_key The unique key for the feature flag. Must not be NULL. + * @param default_value The default value of the flag. + * @param detail Out parameter to store the details. May pass LD_DISCARD_DETAILS + * or NULL to discard the details. The details object must be freed with + * LDEvalDetail_Free. + * @return The variation for the given context, or default_value if the + * flag is disabled in the LaunchDarkly control panel. + */ +LD_EXPORT(int) +LDServerSDK_DoubleVariationDetail(LDServerSDK sdk, + LDContext context, + char const* flag_key, + double default_value, + LDEvalDetail* out_detail); + +/** + * Returns the JSON value of a feature flag for a given flag key and context. + * @param sdk SDK. Must not be NULL. + * @param context The context. Ownership is NOT transferred. Must not be NULL. + * @param flag_key The unique key for the feature flag. Must not be NULL. + * @param default_value The default value of the flag. Ownership is NOT + * transferred. + * @return The variation for the given context, or a copy of default_value if + * the flag is disabled in the LaunchDarkly control panel. The returned value + * must be freed using LDValue_Free. + */ +LD_EXPORT(LDValue) +LDServerSDK_JsonVariation(LDServerSDK sdk, + LDContext context, + char const* flag_key, + LDValue default_value); + +/** + * Returns the JSON value of a feature flag for a given flag key and context, + * and details that also describes the way the value was determined. + * @param sdk SDK. Must not be NULL. + * @param context The context. Ownership is NOT transferred. Must not be NULL. + * @param flag_key The unique key for the feature flag. Must not be NULL. + * @param default_value The default value of the flag. Ownership is NOT + * transferred. + * @param detail Out parameter to store the details. May pass LD_DISCARD_DETAILS + * or NULL to discard the details. The details object must be freed with + * LDEvalDetail_Free. + * @return The variation for the given context, or a copy of default_value if + * the flag is disabled in the LaunchDarkly control panel. The returned value + * must be freed using LDValue_Free. + */ +LD_EXPORT(LDValue) +LDServerSDK_JsonVariationDetail(LDServerSDK sdk, + LDContext context, + char const* flag_key, + LDValue default_value, + LDEvalDetail* out_detail); + +/** + * Evaluates all flags for a context, returning a data structure containing + * the results and additional flag metadata. + * + * The method's behavior can be controlled by passing a combination of + * one or more options. + * + * A common use-case for AllFlagsState is to generate data suitable for + * bootstrapping the client-side JavaScript SDK. + * + * This method will not send analytics events back to LaunchDarkly. + * + * @param sdk SDK. Must not be NULL. + * @param context The context against which all flags will be evaluated. + * Ownership is NOT transferred. Must not be NULL. + * @param options A combination of one or more options. Pass + * LD_ALLFLAGSSTATE_DEFAULT for default behavior. + * @return An AllFlagsState data structure. Must be freed with + * LDAllFlagsState_Free. + */ +LD_EXPORT(LDAllFlagsState) +LDServerSDK_AllFlagsState(LDServerSDK sdk, + LDContext context, + enum LDAllFlagsState_Options options); + +/** + * Frees the SDK's resources, shutting down any connections. May block. + * @param sdk SDK. + */ +LD_EXPORT(void) LDServerSDK_Free(LDServerSDK sdk); + +typedef struct _LDServerDataSourceStatus* LDServerDataSourceStatus; + +/** + * Enumeration of possible data source states. + */ +enum LDServerDataSourceStatus_State { + /** + * The initial state of the data source when the SDK is being + * initialized. + * + * If it encounters an error that requires it to retry initialization, + * the state will remain at kInitializing until it either succeeds and + * becomes LD_SERVERDATASOURCESTATUS_STATE_VALID, or permanently fails and + * becomes LD_SERVERDATASOURCESTATUS_STATE_SHUTDOWN. + */ + LD_SERVERDATASOURCESTATUS_STATE_INITIALIZING = 0, + + /** + * Indicates that the data source is currently operational and has not + * had any problems since the last time it received data. + * + * In streaming mode, this means that there is currently an open stream + * connection and that at least one initial message has been received on + * the stream. In polling mode, it means that the last poll request + * succeeded. + */ + LD_SERVERDATASOURCESTATUS_STATE_VALID = 1, + + /** + * Indicates that the data source encountered an error that it will + * attempt to recover from. + * + * In streaming mode, this means that the stream connection failed, or + * had to be dropped due to some other error, and will be retried after + * a backoff delay. In polling mode, it means that the last poll request + * failed, and a new poll request will be made after the configured + * polling interval. + */ + LD_SERVERDATASOURCESTATUS_STATE_INTERRUPTED = 2, + + /** + * Indicates that the data source has been permanently shut down. + * + * This could be because it encountered an unrecoverable error (for + * instance, the LaunchDarkly service rejected the SDK key; an invalid + * SDK key will never become valid), or because the SDK client was + * explicitly shut down. + */ + LD_SERVERDATASOURCESTATUS_STATE_OFF = 3 +}; + +/** + * Get an enumerated value representing the overall current state of the data + * source. + */ +LD_EXPORT(enum LDServerDataSourceStatus_State) +LDServerDataSourceStatus_GetState(LDServerDataSourceStatus status); + +/** + * Information about the last error that the data source encountered, if + * any. If there has not been an error, then NULL will be returned. + * + * If a non-NULL value is returned, then it should be freed using + * LDDataSourceStatus_ErrorInfo_Free. + * + * This property should be updated whenever the data source encounters a + * problem, even if it does not cause the state to change. For instance, if + * a stream connection fails and the state changes to + * LD_SERVERDATASOURCESTATUS_STATE_INTERRUPTED, and then subsequent attempts to + * restart the connection also fail, the state will remain + * LD_SERVERDATASOURCESTATUS_STATE_INTERRUPTED but the error information will be + * updated each time-- and the last error will still be reported in this + * property even if the state later becomes + * LD_SERVERDATASOURCESTATUS_STATE_VALID. + */ +LD_EXPORT(LDDataSourceStatus_ErrorInfo) +LDServerDataSourceStatus_GetLastError(LDServerDataSourceStatus status); + +/** + * The date/time that the value of State most recently changed, in seconds + * since epoch. + * + * The meaning of this depends on the current state: + * - For LD_SERVERDATASOURCESTATUS_STATE_INITIALIZING, it is the time that the + * SDK started initializing. + * - For LD_SERVERDATASOURCESTATUS_STATE_VALID, it is the time that the data + * source most recently entered a valid state, after previously having been + * LD_SERVERDATASOURCESTATUS_STATE_INITIALIZING or an invalid state such as + * LD_SERVERDATASOURCESTATUS_STATE_INTERRUPTED. + * - For LD_SERVERDATASOURCESTATUS_STATE_INTERRUPTED, it is the time that the + * data source most recently entered an error state, after previously having + * been DataSourceState::kValid. + * - For LD_SERVERDATASOURCESTATUS_STATE_SHUTDOWN, it is the time that the data + * source encountered an unrecoverable error or that the SDK was explicitly shut + * down. + */ +LD_EXPORT(time_t) +LDServerDataSourceStatus_StateSince(LDServerDataSourceStatus status); + +typedef void (*ServerDataSourceStatusCallbackFn)( + LDServerDataSourceStatus status, + void* user_data); + +/** + * Defines a data source status listener which may be used to listen for + * changes to the data source status. + * The struct should be initialized using LDServerDataSourceStatusListener_Init + * before use. + */ +struct LDServerDataSourceStatusListener { + /** + * Callback function which is invoked for data source status changes. + * + * The provided pointers are only valid for the duration of the function + * call (excluding UserData, whose lifetime is controlled by the caller). + * + * @param status The updated data source status. + */ + ServerDataSourceStatusCallbackFn StatusChanged; + + /** + * UserData is forwarded into callback functions. + */ + void* UserData; +}; + +/** + * Initializes a data source status change listener. Must be called before + * passing the listener to LDServerSDK_DataSourceStatus_OnStatusChange. + * + * If the StatusChanged member of the listener struct is not set (NULL), then + * the function will not register a listener. In that case the return value + * will be NULL. + * + * Create the struct, initialize the struct, set the StatusChanged handler + * and optionally UserData, and then pass the struct to + * LDServerSDK_DataSourceStatus_OnStatusChange. NULL will be returned if the + * StatusChanged member of the listener struct is NULL. + * + * @param listener Listener to initialize. + */ +LD_EXPORT(void) +LDServerDataSourceStatusListener_Init( + struct LDServerDataSourceStatusListener* listener); + +/** + * Listen for changes to the data source status. + * + * @param sdk SDK. Must not be NULL. + * @param listener The listener, whose StatusChanged callback will be invoked, + * when the data source status changes. Must not be NULL. + * + * @return A LDListenerConnection. The connection can be freed using + * LDListenerConnection_Free and the listener can be disconnected using + * LDListenerConnection_Disconnect. + */ +LD_EXPORT(LDListenerConnection) +LDServerSDK_DataSourceStatus_OnStatusChange( + LDServerSDK sdk, + struct LDServerDataSourceStatusListener listener); + +/** + * The current status of the data source. + * + * The caller must free the returned value using LDServerDataSourceStatus_Free. + */ +LD_EXPORT(LDServerDataSourceStatus) +LDServerSDK_DataSourceStatus_Status(LDServerSDK sdk); + +/** + * Frees the data source status. + * @param status The data source status to free. + */ +LD_EXPORT(void) LDServerDataSourceStatus_Free(LDServerDataSourceStatus status); + +#ifdef __cplusplus +} +#endif + +// NOLINTEND modernize-use-using diff --git a/libs/server-sdk/include/launchdarkly/server_side/change_notifier.hpp b/libs/server-sdk/include/launchdarkly/server_side/change_notifier.hpp new file mode 100644 index 000000000..70f2fe17e --- /dev/null +++ b/libs/server-sdk/include/launchdarkly/server_side/change_notifier.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include + +#include +#include +#include +#include + +namespace launchdarkly::server_side { + +/** + * Interface to allow listening for flag changes. Notification events should + * be distributed after the store has been updated. + */ +class IChangeNotifier { + public: + using ChangeSet = std::set; + using ChangeHandler = std::function)>; + + /** + * Listen for changes to flag configuration. The change handler will be + * called with a set of affected flag keys. Changes include flags whose + * dependencies (either other flags, or segments) changed. + * + * @param signal The handler for the changes. + * @return A connection which can be used to stop listening. + */ + virtual std::unique_ptr OnFlagChange( + ChangeHandler handler) = 0; + + virtual ~IChangeNotifier() = default; + IChangeNotifier(IChangeNotifier const& item) = delete; + IChangeNotifier(IChangeNotifier&& item) = delete; + IChangeNotifier& operator=(IChangeNotifier const&) = delete; + IChangeNotifier& operator=(IChangeNotifier&&) = delete; + + protected: + IChangeNotifier() = default; +}; + +} // namespace launchdarkly::server_side diff --git a/libs/server-sdk/include/launchdarkly/server_side/client.hpp b/libs/server-sdk/include/launchdarkly/server_side/client.hpp new file mode 100644 index 000000000..e7f1b8056 --- /dev/null +++ b/libs/server-sdk/include/launchdarkly/server_side/client.hpp @@ -0,0 +1,355 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +namespace launchdarkly::server_side { +/** + * Interface for the standard SDK client methods and properties. + */ +class IClient { + public: + /** + * Represents the key of a feature flag. + */ + using FlagKey = std::string; + + /** Connects the client to LaunchDarkly's flag delivery endpoints. + * + * If StartAsync isn't called, the client is able to post events but is + * unable to obtain flag data. + * + * The returned future will resolve to true or false based on the logic + * outlined on @ref Initialized. + */ + virtual std::future StartAsync() = 0; + + /** + * Returns a boolean value indicating LaunchDarkly connection and flag state + * within the client. + * + * When you first start the client, once StartAsync has completed, + * Initialized should return true if and only if either 1. it connected to + * LaunchDarkly and successfully retrieved flags, or 2. it started in + * offline mode so there's no need to connect to LaunchDarkly. If the client + * timed out trying to connect to LD, then Initialized returns false (even + * if we do have cached flags). If the client connected and got a 401 error, + * Initialized is will return false. This serves the purpose of letting the + * app know that there was a problem of some kind. + * + * @return True if the client is initialized. + */ + [[nodiscard]] virtual bool Initialized() const = 0; + + /** + * Evaluates all flags for a context, returning a data structure containing + * the results and additional flag metadata. + * + * The method's behavior can be controlled by passing a combination of + * one or more options. + * + * A common use-case for AllFlagsState is to generate data suitable for + * bootstrapping the client-side JavaScript SDK. + * + * This method will not send analytics events back to LaunchDarkly. + * + * @param context The context against which all flags will be + * evaluated. + * @param options A combination of one or more options. Omitting this + * argument is equivalent to passing AllFlagsState::Options::Default. + * @return An AllFlagsState data structure. + */ + [[nodiscard]] virtual class AllFlagsState AllFlagsState( + Context const& context, + AllFlagsState::Options options = AllFlagsState::Options::Default) = 0; + + /** + * Tracks that the current context performed an event for the given event + * name, and associates it with a numeric metric value. + * + * @param event_name The name of the event. + * @param data A JSON value containing additional data associated with the + * event. + * @param metric_value this value is used by the LaunchDarkly + * experimentation feature in numeric custom metrics, and will also be + * returned as part of the custom event for Data Export + */ + virtual void Track(Context const& ctx, + std::string event_name, + Value data, + double metric_value) = 0; + + /** + * Tracks that the current context performed an event for the given event + * name, with additional JSON data. + * + * @param event_name The name of the event. + * @param data A JSON value containing additional data associated with the + * event. + */ + virtual void Track(Context const& ctx, + std::string event_name, + Value data) = 0; + + /** + * Tracks that the current context performed an event for the given event + * name. + * + * @param event_name The name of the event. + */ + virtual void Track(Context const& ctx, std::string event_name) = 0; + + /** + * Tells the client that all pending analytics events (if any) should be + * delivered as soon as possible. + */ + virtual void FlushAsync() = 0; + + /** + * Generates an identify event for a context. + * + * @param context The new evaluation context. + */ + + virtual void Identify(Context context) = 0; + + /** + * Returns the boolean value of a feature flag for a given flag key. + * + * @param key The unique feature key for the feature flag. + * @param default_value The default value of the flag. + * @return The variation for the selected context, or default_value if the + * flag is disabled in the LaunchDarkly control panel + */ + virtual bool BoolVariation(Context const& ctx, + FlagKey const& key, + bool default_value) = 0; + + /** + * Returns the boolean value of a feature flag for a given flag key, in an + * object that also describes the way the value was determined. + * + * @param key The unique feature key for the feature flag. + * @param default_value The default value of the flag. + * @return An evaluation detail object. + */ + virtual EvaluationDetail BoolVariationDetail(Context const& ctx, + FlagKey const& key, + bool default_value) = 0; + + /** + * Returns the string value of a feature flag for a given flag key. + * + * @param key The unique feature key for the feature flag. + * @param default_value The default value of the flag. + * @return The variation for the selected context, or default_value if the + * flag is disabled in the LaunchDarkly control panel + */ + virtual std::string StringVariation(Context const& ctx, + FlagKey const& key, + std::string default_value) = 0; + + /** + * Returns the string value of a feature flag for a given flag key, in an + * object that also describes the way the value was determined. + * + * @param key The unique feature key for the feature flag. + * @param default_value The default value of the flag. + * @return An evaluation detail object. + */ + virtual EvaluationDetail StringVariationDetail( + Context const& ctx, + FlagKey const& key, + std::string default_value) = 0; + + /** + * Returns the double value of a feature flag for a given flag key. + * + * @param key The unique feature key for the feature flag. + * @param default_value The default value of the flag. + * @return The variation for the selected context, or default_value if the + * flag is disabled in the LaunchDarkly control panel + */ + virtual double DoubleVariation(Context const& ctx, + FlagKey const& key, + double default_value) = 0; + + /** + * Returns the double value of a feature flag for a given flag key, in an + * object that also describes the way the value was determined. + * + * @param key The unique feature key for the feature flag. + * @param default_value The default value of the flag. + * @return An evaluation detail object. + */ + virtual EvaluationDetail DoubleVariationDetail( + Context const& ctx, + FlagKey const& key, + double default_value) = 0; + + /** + * Returns the int value of a feature flag for a given flag key. + * + * @param key The unique feature key for the feature flag. + * @param default_value The default value of the flag. + * @return The variation for the selected context, or default_value if the + * flag is disabled in the LaunchDarkly control panel + */ + virtual int IntVariation(Context const& ctx, + FlagKey const& key, + int default_value) = 0; + + /** + * Returns the int value of a feature flag for a given flag key, in an + * object that also describes the way the value was determined. + * + * @param key The unique feature key for the feature flag. + * @param default_value The default value of the flag. + * @return An evaluation detail object. + */ + virtual EvaluationDetail IntVariationDetail(Context const& ctx, + FlagKey const& key, + int default_value) = 0; + + /** + * Returns the JSON value of a feature flag for a given flag key. + * + * @param key The unique feature key for the feature flag. + * @param default_value The default value of the flag. + * @return The variation for the selected context, or default_value if the + * flag is disabled in the LaunchDarkly control panel + */ + virtual Value JsonVariation(Context const& ctx, + FlagKey const& key, + Value default_value) = 0; + + /** + * Returns the JSON value of a feature flag for a given flag key, in an + * object that also describes the way the value was determined. + * + * @param key The unique feature key for the feature flag. + * @param default_value The default value of the flag. + * @return An evaluation detail object. + */ + virtual EvaluationDetail JsonVariationDetail( + Context const& ctx, + FlagKey const& key, + Value default_value) = 0; + + /** + * Returns an interface which provides methods for subscribing to data + * source status. + * @return A data source status provider. + */ + virtual data_sources::IDataSourceStatusProvider& DataSourceStatus() = 0; + + virtual ~IClient() = default; + IClient(IClient const& item) = delete; + IClient(IClient&& item) = delete; + IClient& operator=(IClient const&) = delete; + IClient& operator=(IClient&&) = delete; + + protected: + IClient() = default; +}; + +class Client : public IClient { + public: + Client(Config config); + + Client(Client&&) = delete; + Client(Client const&) = delete; + Client& operator=(Client) = delete; + Client& operator=(Client&& other) = delete; + + std::future StartAsync() override; + + [[nodiscard]] bool Initialized() const override; + + using FlagKey = std::string; + [[nodiscard]] class AllFlagsState AllFlagsState( + Context const& context, + enum AllFlagsState::Options options = + AllFlagsState::Options::Default) override; + + void Track(Context const& ctx, + std::string event_name, + Value data, + double metric_value) override; + + void Track(Context const& ctx, std::string event_name, Value data) override; + + void Track(Context const& ctx, std::string event_name) override; + + void FlushAsync() override; + + void Identify(Context context) override; + + bool BoolVariation(Context const& ctx, + FlagKey const& key, + bool default_value) override; + + EvaluationDetail BoolVariationDetail(Context const& ctx, + FlagKey const& key, + bool default_value) override; + + std::string StringVariation(Context const& ctx, + FlagKey const& key, + std::string default_value) override; + + EvaluationDetail StringVariationDetail( + Context const& ctx, + FlagKey const& key, + std::string default_value) override; + + double DoubleVariation(Context const& ctx, + FlagKey const& key, + double default_value) override; + + EvaluationDetail DoubleVariationDetail( + Context const& ctx, + FlagKey const& key, + double default_value) override; + + int IntVariation(Context const& ctx, + FlagKey const& key, + int default_value) override; + + EvaluationDetail IntVariationDetail(Context const& ctx, + FlagKey const& key, + int default_value) override; + + Value JsonVariation(Context const& ctx, + FlagKey const& key, + Value default_value) override; + + EvaluationDetail JsonVariationDetail(Context const& ctx, + FlagKey const& key, + Value default_value) override; + + data_sources::IDataSourceStatusProvider& DataSourceStatus() override; + + /** + * Returns the version of the SDK. + * @return String representing version of the SDK. + */ + [[nodiscard]] static char const* Version(); + + private: + inline static char const* const kVersion = + "0.1.0"; // {x-release-please-version} + std::unique_ptr client; +}; + +} // namespace launchdarkly::server_side diff --git a/libs/server-sdk/include/launchdarkly/server_side/data_source_status.hpp b/libs/server-sdk/include/launchdarkly/server_side/data_source_status.hpp new file mode 100644 index 000000000..c6f16f7ea --- /dev/null +++ b/libs/server-sdk/include/launchdarkly/server_side/data_source_status.hpp @@ -0,0 +1,117 @@ +#pragma once + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace launchdarkly::server_side::data_sources { + +/** + * Enumeration of possible data source states. + */ +enum class DataSourceState { + /** + * The initial state of the data source when the SDK is being + * initialized. + * + * If it encounters an error that requires it to retry initialization, + * the state will remain at kInitializing until it either succeeds and + * becomes kValid, or permanently fails and becomes kShutdown. + */ + kInitializing = 0, + + /** + * Indicates that the data source is currently operational and has not + * had any problems since the last time it received data. Alternatively if + * the client was configured for offline mode, the state will be kValid. + * + * In streaming mode, this means that there is currently an open stream + * connection and that at least one initial message has been received on + * the stream. In polling mode, it means that the last poll request + * succeeded. + */ + kValid = 1, + + /** + * Indicates that the data source encountered an error that it will + * attempt to recover from. + * + * In streaming mode, this means that the stream connection failed, or + * had to be dropped due to some other error, and will be retried after + * a backoff delay. In polling mode, it means that the last poll request + * failed, and a new poll request will be made after the configured + * polling interval. + */ + kInterrupted = 2, + + /** + * Indicates that the data source has been permanently shut down. + * + * This could be because it encountered an unrecoverable error (for + * instance, the LaunchDarkly service rejected the SDK key; an invalid + * SDK key will never become valid), or because the SDK client was + * explicitly closed. + */ + kOff = 3, +}; + +using DataSourceStatus = + common::data_sources::DataSourceStatusBase; + +/** + * Interface for accessing and listening to the data source status. + */ +class IDataSourceStatusProvider { + public: + /** + * The current status of the data source. Suitable for broadcast to + * data source status listeners. + */ + [[nodiscard]] virtual DataSourceStatus Status() const = 0; + + /** + * Listen to changes to the data source status. + * + * @param handler Function which will be called with the new status. + * @return A IConnection which can be used to stop listening to the status. + */ + virtual std::unique_ptr OnDataSourceStatusChange( + std::function handler) = 0; + + /** + * Listen to changes to the data source status, with ability for listener + * to unregister itself. + * + * @param handler Function which will be called with the new status. Return + * true to unregister. + * @return A IConnection which can be used to stop listening to the status. + */ + virtual std::unique_ptr OnDataSourceStatusChangeEx( + std::function handler) = 0; + + virtual ~IDataSourceStatusProvider() = default; + IDataSourceStatusProvider(IDataSourceStatusProvider const& item) = delete; + IDataSourceStatusProvider(IDataSourceStatusProvider&& item) = delete; + IDataSourceStatusProvider& operator=(IDataSourceStatusProvider const&) = + delete; + IDataSourceStatusProvider& operator=(IDataSourceStatusProvider&&) = delete; + + protected: + IDataSourceStatusProvider() = default; +}; + +std::ostream& operator<<(std::ostream& out, + DataSourceStatus::DataSourceState const& state); + +std::ostream& operator<<(std::ostream& out, DataSourceStatus const& status); + +} // namespace launchdarkly::server_side::data_sources diff --git a/libs/server-sdk/include/launchdarkly/server_side/integrations/persistent_store_core.hpp b/libs/server-sdk/include/launchdarkly/server_side/integrations/persistent_store_core.hpp new file mode 100644 index 000000000..a49f389be --- /dev/null +++ b/libs/server-sdk/include/launchdarkly/server_side/integrations/persistent_store_core.hpp @@ -0,0 +1,214 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include + +namespace launchdarkly::server_side::integrations { + +/** + * A versioned item which can be stored in a persistent store. + */ +struct SerializedItemDescriptor { + uint64_t version; + + /** + * During an Init/Upsert, when this is true, the serializedItem will + * contain a tombstone representation. If the persistence implementation + * can efficiently store the deletion state, and version, then it may + * choose to discard the item. + */ + bool deleted; + + /** + * When reading from a persistent store the serializedItem may be + * std::nullopt for deleted items. + */ + std::optional serializedItem; +}; + +/** + * Represents a namespace of persistent data. + */ +class IPersistentKind { + public: + /** + * The namespace for the data. + */ + [[nodiscard]] virtual std::string const& Namespace(); + + /** + * Deserialize data and return the version of the data. + * + * This is for cases where the persistent store cannot avoid deserializing + * data to determine its version. For instance a Redis store where + * the only columns are the prefixed key and the serialized data. + * + * @param data The data to deserialize. + * @return The version of the data. + */ + [[nodiscard]] virtual uint64_t Version(std::string const& data); + + IPersistentKind(IPersistentKind const& item) = delete; + IPersistentKind(IPersistentKind&& item) = delete; + IPersistentKind& operator=(IPersistentKind const&) = delete; + IPersistentKind& operator=(IPersistentKind&&) = delete; + virtual ~IPersistentKind() = default; + + protected: + IPersistentKind() = default; +}; + +/** + * Interface for a data store that holds feature flags and related data in a + * serialized form. + * + * This interface should be used for database integrations, or any other data + * store implementation that stores data in some external service. + * The SDK will take care of converting between its own internal data model and + * a serialized string form; the data store interacts only with the serialized + * form. + * + * The SDK will also provide its own caching layer on top of the persistent data + * store; the data store implementation should not provide caching, but simply + * do every query or update that the SDK tells it to do. + * + * Implementations must be thread-safe. + */ +class IPersistentStoreCore { + public: + enum class InitResult { + /** + * The init operation completed successfully. + */ + kSuccess, + + /** + * There was an error with the init operation. + */ + kError, + }; + + enum class UpsertResult { + /** + * The upsert completed successfully. + */ + kSuccess, + + /** + * There was an error with the upsert operation. + */ + kError, + + /** + * The upsert did not encounter errors, but the version of the + * existing item was greater than that the version of the upsert item. + */ + kNotUpdated + }; + + struct Error { + std::string message; + }; + + using GetResult = + tl::expected, Error>; + + using AllResult = + tl::expected, + Error>; + + using ItemKey = std::string; + using KeyItemPair = std::pair; + using OrderedNamepace = std::vector; + using KindCollectionPair = + std::pair; + using OrderedData = std::vector; + + /** + * Overwrites the store's contents with a set of items for each collection. + * + * All previous data should be discarded, regardless of versioning. + * + * The update should be done atomically. If it cannot be done atomically, + * then the store must first add or update each item in the same order that + * they are given in the input data, and then delete any previously stored + * items that were not in the input data. + * + * @param allData The ordered set of data to replace all current data with. + * @return The status of the init operation. + */ + virtual InitResult Init(OrderedData const& allData) = 0; + + /** + * Updates or inserts an item in the specified collection. For updates, the + * object will only be updated if the existing version is less than the new + * version. + * + * @param kind The collection kind to use. + * @param itemKey The unique key for the item within the collection. + * @param item The item to insert or update. + * + * @return The status of the operation. + */ + virtual UpsertResult Upsert(IPersistentKind const& kind, + std::string const& itemKey, + SerializedItemDescriptor const& item) = 0; + + /** + * Retrieves an item from the specified collection, if available. + * + * @param kind The kind of the item. + * @param itemKey The key for the item. + * @return A serialized item descriptor if the item existed, a std::nullopt + * if the item did not exist, or an error. For a deleted item the serialized + * item descriptor may contain a std::nullopt for the serializedItem. + */ + virtual GetResult Get(IPersistentKind const& kind, + std::string const& itemKey) const = 0; + + /** + * Retrieves all items from the specified collection. + * + * If the store contains placeholders for deleted items, it should include + * them in the results, not filter them out. + * @param kind The kind of data to get. + * @return Either all of the items of the type, or an error. If there are + * no items of the specified type, then return an empty collection. + */ + virtual AllResult All(IPersistentKind const& kind) const = 0; + + /** + * Returns true if this store has been initialized. + * + * In a shared data store, the implementation should be able to detect this + * state even if Init was called in a different process, i.e. it must query + * the underlying data store in some way. The method does not need to worry + * about caching this value; the SDK will call it rarely. + * + * @return True if the store has been initialized. + */ + virtual bool Initialized() const = 0; + + /** + * A short description of the store, for instance "Redis". May be used + * in diagnostic information and logging. + * + * @return A short description of the sore. + */ + virtual std::string const& Description() const = 0; + + IPersistentStoreCore(IPersistentStoreCore const& item) = delete; + IPersistentStoreCore(IPersistentStoreCore&& item) = delete; + IPersistentStoreCore& operator=(IPersistentStoreCore const&) = delete; + IPersistentStoreCore& operator=(IPersistentStoreCore&&) = delete; + virtual ~IPersistentStoreCore() = default; + + protected: + IPersistentStoreCore() = default; +}; +} // namespace launchdarkly::server_side::integrations diff --git a/libs/server-sdk/include/launchdarkly/server_side/serialization/json_all_flags_state.hpp b/libs/server-sdk/include/launchdarkly/server_side/serialization/json_all_flags_state.hpp new file mode 100644 index 000000000..b644119a1 --- /dev/null +++ b/libs/server-sdk/include/launchdarkly/server_side/serialization/json_all_flags_state.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include + +#include + +namespace launchdarkly::server_side { + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + server_side::AllFlagsState::State const& state); + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + server_side::AllFlagsState const& state); +} // namespace launchdarkly::server_side diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt new file mode 100644 index 000000000..220dad451 --- /dev/null +++ b/libs/server-sdk/src/CMakeLists.txt @@ -0,0 +1,98 @@ + +file(GLOB HEADER_LIST CONFIGURE_DEPENDS + "${LaunchDarklyCPPServer_SOURCE_DIR}/include/launchdarkly/server_side/*.hpp" + "${LaunchDarklyCPPServer_SOURCE_DIR}/include/launchdarkly/server_side/integrations/*.hpp" +) + +if (LD_BUILD_SHARED_LIBS) + message(STATUS "LaunchDarkly: building server-sdk as shared library") + add_library(${LIBNAME} SHARED) +else () + message(STATUS "LaunchDarkly: building server-sdk as static library") + add_library(${LIBNAME} STATIC) +endif () + + +target_sources(${LIBNAME} + PRIVATE + ${HEADER_LIST} + boost.cpp + client.cpp + client_impl.cpp + all_flags_state/all_flags_state.cpp + all_flags_state/json_all_flags_state.cpp + all_flags_state/all_flags_state_builder.cpp + data_sources/data_source_update_sink.hpp + data_store/data_store.hpp + data_store/data_store_updater.hpp + data_store/data_store_updater.cpp + data_store/memory_store.cpp + data_store/dependency_tracker.hpp + data_store/dependency_tracker.cpp + data_store/descriptors.hpp + data_sources/data_source_event_handler.cpp + data_sources/data_source_event_handler.hpp + data_sources/data_source_status.cpp + data_sources/polling_data_source.hpp + data_sources/polling_data_source.cpp + data_sources/data_source_status_manager.hpp + data_sources/streaming_data_source.hpp + data_sources/streaming_data_source.cpp + data_sources/null_data_source.cpp + evaluation/evaluator.cpp + evaluation/rules.cpp + evaluation/bucketing.cpp + evaluation/operators.cpp + evaluation/evaluation_error.cpp + evaluation/detail/evaluation_stack.cpp + evaluation/detail/semver_operations.cpp + evaluation/detail/timestamp_operations.cpp + data_store/persistent/persistent_data_store.hpp + data_store/persistent/expiration_tracker.hpp + data_store/persistent/persistent_data_store.cpp + data_store/persistent/expiration_tracker.cpp + events/event_factory.cpp + bindings/c/sdk.cpp + bindings/c/builder.cpp + bindings/c/config.cpp + bindings/c/all_flags_state/all_flags_state.cpp +) + +if (MSVC OR (NOT LD_BUILD_SHARED_LIBS)) + target_link_libraries(${LIBNAME} + PUBLIC launchdarkly::common + PRIVATE Boost::headers Boost::json Boost::url launchdarkly::sse launchdarkly::internal foxy timestamp) +else () + # The default static lib builds, for linux, are position independent. + # So they do not link into a shared object without issues. So, when + # building shared objects do not link the static libraries and instead + # use the "src.hpp" files for required libraries. + # macOS shares the same path for simplicity. + target_link_libraries(${LIBNAME} + PUBLIC launchdarkly::common + PRIVATE Boost::headers launchdarkly::sse launchdarkly::internal foxy timestamp) + + target_sources(${LIBNAME} PRIVATE boost.cpp) +endif () + +add_library(launchdarkly::server ALIAS ${LIBNAME}) + +set_property(TARGET ${LIBNAME} PROPERTY + MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL") + +install(TARGETS ${LIBNAME}) +if (LD_BUILD_SHARED_LIBS AND MSVC) + install(FILES $ DESTINATION bin OPTIONAL) +endif () +# Using PUBLIC_HEADERS would flatten the include. +# This will preserve it, but dependencies must do the same. + +install(DIRECTORY "${LaunchDarklyCPPServer_SOURCE_DIR}/include/launchdarkly" + DESTINATION "include" +) + +# Need the public headers to build. +target_include_directories(${LIBNAME} PUBLIC ../include) + +# Minimum C++ standard needed for consuming the public API is C++17. +target_compile_features(${LIBNAME} PUBLIC cxx_std_17) diff --git a/libs/server-sdk/src/all_flags_state/all_flags_state.cpp b/libs/server-sdk/src/all_flags_state/all_flags_state.cpp new file mode 100644 index 000000000..d2b1254ab --- /dev/null +++ b/libs/server-sdk/src/all_flags_state/all_flags_state.cpp @@ -0,0 +1,86 @@ +#include "launchdarkly/server_side/all_flags_state.hpp" + +namespace launchdarkly::server_side { + +AllFlagsState::State::State( + std::uint64_t version, + std::optional variation, + std::optional reason, + bool track_events, + bool track_reason, + std::optional debug_events_until_date) + : version_(version), + variation_(variation), + reason_(reason), + track_events_(track_events), + track_reason_(track_reason), + debug_events_until_date_(debug_events_until_date), + omit_details_(false) {} + +std::uint64_t AllFlagsState::State::Version() const { + return version_; +} + +std::optional AllFlagsState::State::Variation() const { + return variation_; +} + +std::optional const& AllFlagsState::State::Reason() const { + return reason_; +} + +bool AllFlagsState::State::TrackEvents() const { + return track_events_; +} + +bool AllFlagsState::State::TrackReason() const { + return track_reason_; +} + +std::optional const& AllFlagsState::State::DebugEventsUntilDate() + const { + return debug_events_until_date_; +} + +bool AllFlagsState::State::OmitDetails() const { + return omit_details_; +} + +AllFlagsState::AllFlagsState() + : valid_(false), evaluations_(), flags_state_() {} + +AllFlagsState::AllFlagsState(std::unordered_map evaluations, + std::unordered_map flags_state) + : valid_(true), + evaluations_(std::move(evaluations)), + flags_state_(std::move(flags_state)) {} + +bool AllFlagsState::Valid() const { + return valid_; +} + +std::unordered_map const& +AllFlagsState::States() const { + return flags_state_; +} + +std::unordered_map const& AllFlagsState::Values() const { + return evaluations_; +} + +bool operator==(AllFlagsState const& lhs, AllFlagsState const& rhs) { + return lhs.Valid() == rhs.Valid() && lhs.Values() == rhs.Values() && + lhs.States() == rhs.States(); +} + +bool operator==(AllFlagsState::State const& lhs, + AllFlagsState::State const& rhs) { + return lhs.Version() == rhs.Version() && + lhs.Variation() == rhs.Variation() && lhs.Reason() == rhs.Reason() && + lhs.TrackEvents() == rhs.TrackEvents() && + lhs.TrackReason() == rhs.TrackReason() && + lhs.DebugEventsUntilDate() == rhs.DebugEventsUntilDate() && + lhs.OmitDetails() == rhs.OmitDetails(); +} + +} // namespace launchdarkly::server_side diff --git a/libs/server-sdk/src/all_flags_state/all_flags_state_builder.cpp b/libs/server-sdk/src/all_flags_state/all_flags_state_builder.cpp new file mode 100644 index 000000000..efa4314b6 --- /dev/null +++ b/libs/server-sdk/src/all_flags_state/all_flags_state_builder.cpp @@ -0,0 +1,71 @@ +#include "all_flags_state_builder.hpp" + +namespace launchdarkly::server_side { + +bool IsDebuggingEnabled(std::optional debug_events_until); + +AllFlagsStateBuilder::AllFlagsStateBuilder(AllFlagsState::Options options) + : options_(options), flags_state_(), evaluations_() {} + +void AllFlagsStateBuilder::AddFlag(std::string const& key, + Value value, + AllFlagsState::State flag) { + if (IsSet(options_, AllFlagsState::Options::DetailsOnlyForTrackedFlags)) { + if (!flag.TrackEvents() && !flag.TrackReason() && + !IsDebuggingEnabled(flag.DebugEventsUntilDate())) { + flag.omit_details_ = true; + } + } + if (NotSet(options_, AllFlagsState::Options::IncludeReasons) && + !flag.TrackReason()) { + flag.reason_ = std::nullopt; + } + flags_state_.emplace(key, std::move(flag)); + evaluations_.emplace(std::move(key), std::move(value)); +} + +AllFlagsState AllFlagsStateBuilder::Build() { + return AllFlagsState{std::move(evaluations_), std::move(flags_state_)}; +} + +bool IsExperimentationEnabled(data_model::Flag const& flag, + std::optional const& reason) { + if (!reason) { + return false; + } + if (reason->InExperiment()) { + return true; + } + switch (reason->Kind()) { + case EvaluationReason::Kind::kFallthrough: + return flag.trackEventsFallthrough; + case EvaluationReason::Kind::kRuleMatch: + if (!reason->RuleIndex() || + reason->RuleIndex() >= flag.rules.size()) { + return false; + } + return flag.rules.at(*reason->RuleIndex()).trackEvents; + default: + return false; + } +} + +bool IsSet(AllFlagsState::Options options, AllFlagsState::Options flag) { + return (options & flag) == flag; +} + +bool NotSet(AllFlagsState::Options options, AllFlagsState::Options flag) { + return !IsSet(options, flag); +} + +std::uint64_t NowUnixMillis() { + return std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count(); +} + +bool IsDebuggingEnabled(std::optional debug_events_until) { + return debug_events_until && *debug_events_until > NowUnixMillis(); +} + +} // namespace launchdarkly::server_side diff --git a/libs/server-sdk/src/all_flags_state/all_flags_state_builder.hpp b/libs/server-sdk/src/all_flags_state/all_flags_state_builder.hpp new file mode 100644 index 000000000..34e1a839b --- /dev/null +++ b/libs/server-sdk/src/all_flags_state/all_flags_state_builder.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include + +#include "../data_store/data_store.hpp" +#include "../evaluation/evaluator.hpp" + +namespace launchdarkly::server_side { + +bool IsSet(AllFlagsState::Options options, AllFlagsState::Options flag); +bool NotSet(AllFlagsState::Options options, AllFlagsState::Options flag); + +class AllFlagsStateBuilder { + public: + /** + * Constructs a builder capable of generating a AllFlagsState structure. + * @param options Options affecting the behavior of the builder. + */ + AllFlagsStateBuilder(AllFlagsState::Options options); + + /** + * Adds a flag, including its evaluation result and additional state. + * @param key Key of the flag. + * @param value Value of the flag. + * @param state State of the flag. + */ + void AddFlag(std::string const& key, + Value value, + AllFlagsState::State state); + + /** + * Builds a AllFlagsState structure from the flags added to the builder. + * This operation consumes the builder, and must only be called once. + * @return + */ + [[nodiscard]] AllFlagsState Build(); + + private: + enum AllFlagsState::Options options_; + std::unordered_map flags_state_; + std::unordered_map evaluations_; +}; +} // namespace launchdarkly::server_side diff --git a/libs/server-sdk/src/all_flags_state/json_all_flags_state.cpp b/libs/server-sdk/src/all_flags_state/json_all_flags_state.cpp new file mode 100644 index 000000000..25a095fdd --- /dev/null +++ b/libs/server-sdk/src/all_flags_state/json_all_flags_state.cpp @@ -0,0 +1,49 @@ +#include +#include +#include + +#include +#include +#include "launchdarkly/serialization/value_mapping.hpp" + +namespace launchdarkly::server_side { + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + server_side::AllFlagsState::State const& state) { + boost::ignore_unused(unused); + auto& obj = json_value.emplace_object(); + + if (!state.OmitDetails()) { + obj.emplace("version", state.Version()); + WriteMinimal(obj, "reason", state.Reason()); + } + + if (auto const& variation = state.Variation()) { + obj.emplace("variation", *variation); + } + + WriteMinimal(obj, "trackEvents", state.TrackEvents()); + WriteMinimal(obj, "trackReason", state.TrackReason()); + + if (auto const& date = state.DebugEventsUntilDate()) { + if (*date > 0) { + obj.emplace("debugEventsUntilDate", boost::json::value_from(*date)); + } + } +} + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + server_side::AllFlagsState const& state) { + boost::ignore_unused(unused); + auto& obj = json_value.emplace_object(); + obj.emplace("$valid", state.Valid()); + + obj.emplace("$flagsState", boost::json::value_from(state.States())); + + for (auto const& [k, v] : state.Values()) { + obj.emplace(k, boost::json::value_from(v)); + } +} +} // namespace launchdarkly::server_side diff --git a/libs/server-sdk/src/bindings/c/all_flags_state/all_flags_state.cpp b/libs/server-sdk/src/bindings/c/all_flags_state/all_flags_state.cpp new file mode 100644 index 000000000..112b18666 --- /dev/null +++ b/libs/server-sdk/src/bindings/c/all_flags_state/all_flags_state.cpp @@ -0,0 +1,56 @@ +#include +#include + +#include + +#include +#include + +#include + +#define TO_ALLFLAGS(ptr) (reinterpret_cast(ptr)) +#define FROM_ALLFLAGS(ptr) (reinterpret_cast(ptr)) + +#define TO_VALUE(ptr) (reinterpret_cast(ptr)) +#define FROM_VALUE(ptr) (reinterpret_cast(ptr)) + +using namespace launchdarkly; +using namespace launchdarkly::server_side; + +LD_EXPORT(void) LDAllFlagsState_Free(LDAllFlagsState state) { + delete TO_ALLFLAGS(state); +} + +LD_EXPORT(bool) LDAllFlagsState_Valid(LDAllFlagsState state) { + LD_ASSERT_NOT_NULL(state); + + return TO_ALLFLAGS(state)->Valid(); +} + +LD_EXPORT(char*) +LDAllFlagsState_SerializeJSON(LDAllFlagsState state) { + LD_ASSERT_NOT_NULL(state); + + auto json_value = boost::json::value_from(*TO_ALLFLAGS(state)); + std::string json_str = boost::json::serialize(json_value); + + return strdup(json_str.c_str()); +} + +LD_EXPORT(LDValue) +LDAllFlagsState_Value(LDAllFlagsState state, char const* flag_key) { + LD_ASSERT_NOT_NULL(state); + LD_ASSERT_NOT_NULL(flag_key); + + auto const& values = TO_ALLFLAGS(state)->Values(); + + std::unordered_map::const_iterator iter = + values.find(flag_key); + if (iter == values.end()) { + return FROM_VALUE(const_cast(&Value::Null())); + } + + Value const& val_ref = iter->second; + + return FROM_VALUE(const_cast(&val_ref)); +} diff --git a/libs/server-sdk/src/bindings/c/builder.cpp b/libs/server-sdk/src/bindings/c/builder.cpp new file mode 100644 index 000000000..461bcf204 --- /dev/null +++ b/libs/server-sdk/src/bindings/c/builder.cpp @@ -0,0 +1,289 @@ +// NOLINTBEGIN cppcoreguidelines-pro-type-reinterpret-cast +// NOLINTBEGIN OCInconsistentNamingInspection + +#include + +#include +#include + +using namespace launchdarkly::server_side; + +#define TO_BUILDER(ptr) (reinterpret_cast(ptr)) +#define FROM_BUILDER(ptr) (reinterpret_cast(ptr)) + +#define TO_STREAM_BUILDER(ptr) \ + (reinterpret_cast(ptr)) + +#define FROM_STREAM_BUILDER(ptr) \ + (reinterpret_cast(ptr)) + +#define TO_POLL_BUILDER(ptr) \ + (reinterpret_cast(ptr)) + +#define FROM_POLL_BUILDER(ptr) \ + (reinterpret_cast(ptr)) + +#define TO_BASIC_LOGGING_BUILDER(ptr) \ + (reinterpret_cast(ptr)) + +#define FROM_BASIC_LOGGING_BUILDER(ptr) \ + (reinterpret_cast(ptr)) + +#define TO_CUSTOM_LOGGING_BUILDER(ptr) \ + (reinterpret_cast(ptr)) + +#define FROM_CUSTOM_LOGGING_BUILDER(ptr) \ + (reinterpret_cast(ptr)) + +#define TO_CUSTOM_PERSISTENCE_BUILDER(ptr) \ + (reinterpret_cast(ptr)) + +#define FROM_CUSTOM_PERSISTENCE_BUILDER(ptr) \ + (reinterpret_cast(ptr)) + +LD_EXPORT(LDServerConfigBuilder) +LDServerConfigBuilder_New(char const* sdk_key) { + LD_ASSERT_NOT_NULL(sdk_key); + + return FROM_BUILDER(new ConfigBuilder(sdk_key)); +} + +LD_EXPORT(LDStatus) +LDServerConfigBuilder_Build(LDServerConfigBuilder b, + LDServerConfig* out_config) { + LD_ASSERT_NOT_NULL(b); + LD_ASSERT_NOT_NULL(out_config); + + return launchdarkly::detail::ConsumeBuilder(b, out_config); +} + +LD_EXPORT(void) +LDServerConfigBuilder_Free(LDServerConfigBuilder builder) { + delete TO_BUILDER(builder); +} + +LD_EXPORT(void) +LDServerConfigBuilder_ServiceEndpoints_PollingBaseURL(LDServerConfigBuilder b, + char const* url) { + LD_ASSERT_NOT_NULL(b); + LD_ASSERT_NOT_NULL(url); + + TO_BUILDER(b)->ServiceEndpoints().PollingBaseUrl(url); +} + +LD_EXPORT(void) +LDServerConfigBuilder_ServiceEndpoints_StreamingBaseURL(LDServerConfigBuilder b, + char const* url) { + LD_ASSERT_NOT_NULL(b); + LD_ASSERT_NOT_NULL(url); + + TO_BUILDER(b)->ServiceEndpoints().StreamingBaseUrl(url); +} + +LD_EXPORT(void) +LDServerConfigBuilder_ServiceEndpoints_EventsBaseURL(LDServerConfigBuilder b, + char const* url) { + LD_ASSERT_NOT_NULL(b); + LD_ASSERT_NOT_NULL(url); + + TO_BUILDER(b)->ServiceEndpoints().EventsBaseUrl(url); +} + +LD_EXPORT(void) +LDServerConfigBuilder_ServiceEndpoints_RelayProxyBaseURL( + LDServerConfigBuilder b, + char const* url) { + LD_ASSERT_NOT_NULL(b); + LD_ASSERT_NOT_NULL(url); + + TO_BUILDER(b)->ServiceEndpoints().RelayProxyBaseURL(url); +} + +LD_EXPORT(void) +LDServerConfigBuilder_AppInfo_Identifier(LDServerConfigBuilder b, + char const* app_id) { + LD_ASSERT_NOT_NULL(b); + LD_ASSERT_NOT_NULL(app_id); + + TO_BUILDER(b)->AppInfo().Identifier(app_id); +} + +LD_EXPORT(void) +LDServerConfigBuilder_AppInfo_Version(LDServerConfigBuilder b, + char const* app_version) { + LD_ASSERT_NOT_NULL(b); + LD_ASSERT_NOT_NULL(app_version); + + TO_BUILDER(b)->AppInfo().Version(app_version); +} + +LD_EXPORT(void) +LDServerConfigBuilder_Offline(LDServerConfigBuilder b, bool offline) { + LD_ASSERT_NOT_NULL(b); + + TO_BUILDER(b)->Offline(offline); +} + +LD_EXPORT(void) +LDServerConfigBuilder_Events_Enabled(LDServerConfigBuilder b, bool enabled) { + LD_ASSERT_NOT_NULL(b); + + TO_BUILDER(b)->Events().Enabled(enabled); +} + +LD_EXPORT(void) +LDServerConfigBuilder_Events_Capacity(LDServerConfigBuilder b, + size_t capacity) { + LD_ASSERT_NOT_NULL(b); + + TO_BUILDER(b)->Events().Capacity(capacity); +} + +LD_EXPORT(void) +LDServerConfigBuilder_Events_FlushIntervalMs(LDServerConfigBuilder b, + unsigned int milliseconds) { + LD_ASSERT_NOT_NULL(b); + + TO_BUILDER(b)->Events().FlushInterval( + std::chrono::milliseconds{milliseconds}); +} + +LD_EXPORT(void) +LDServerConfigBuilder_Events_AllAttributesPrivate(LDServerConfigBuilder b, + bool all_attributes_private) { + LD_ASSERT_NOT_NULL(b); + + TO_BUILDER(b)->Events().AllAttributesPrivate(all_attributes_private); +} + +LD_EXPORT(void) +LDServerConfigBuilder_Events_PrivateAttribute(LDServerConfigBuilder b, + char const* attribute_reference) { + LD_ASSERT_NOT_NULL(b); + + TO_BUILDER(b)->Events().PrivateAttribute(attribute_reference); +} + +LD_EXPORT(void) +LDServerConfigBuilder_DataSource_MethodStream( + LDServerConfigBuilder b, + LDServerDataSourceStreamBuilder stream_builder) { + LD_ASSERT_NOT_NULL(b); + LD_ASSERT_NOT_NULL(stream_builder); + + DataSourceBuilder::Streaming* sb = TO_STREAM_BUILDER(stream_builder); + TO_BUILDER(b)->DataSource().Method(*sb); + LDServerDataSourceStreamBuilder_Free(stream_builder); +} + +LD_EXPORT(void) +LDServerConfigBuilder_DataSource_MethodPoll( + LDServerConfigBuilder b, + LDServerDataSourcePollBuilder poll_builder) { + LD_ASSERT_NOT_NULL(b); + LD_ASSERT_NOT_NULL(poll_builder); + + DataSourceBuilder::Polling* pb = TO_POLL_BUILDER(poll_builder); + TO_BUILDER(b)->DataSource().Method(*pb); + LDServerDataSourcePollBuilder_Free(poll_builder); +} + +LD_EXPORT(LDServerDataSourceStreamBuilder) +LDServerDataSourceStreamBuilder_New() { + return FROM_STREAM_BUILDER(new DataSourceBuilder::Streaming()); +} + +LD_EXPORT(void) +LDServerDataSourceStreamBuilder_InitialReconnectDelayMs( + LDServerDataSourceStreamBuilder b, + unsigned int milliseconds) { + LD_ASSERT_NOT_NULL(b); + + TO_STREAM_BUILDER(b)->InitialReconnectDelay( + std::chrono::milliseconds{milliseconds}); +} + +LD_EXPORT(void) +LDServerDataSourceStreamBuilder_Free(LDServerDataSourceStreamBuilder b) { + delete TO_STREAM_BUILDER(b); +} + +LD_EXPORT(LDServerDataSourcePollBuilder) LDServerDataSourcePollBuilder_New() { + return FROM_POLL_BUILDER(new DataSourceBuilder::Polling()); +} + +LD_EXPORT(void) +LDServerDataSourcePollBuilder_IntervalS(LDServerDataSourcePollBuilder b, + unsigned int seconds) { + LD_ASSERT_NOT_NULL(b); + + TO_POLL_BUILDER(b)->PollInterval(std::chrono::seconds{seconds}); +} + +LD_EXPORT(void) +LDServerDataSourcePollBuilder_Free(LDServerDataSourcePollBuilder b) { + delete TO_POLL_BUILDER(b); +} + +LD_EXPORT(void) +LDServerConfigBuilder_HttpProperties_WrapperName(LDServerConfigBuilder b, + char const* wrapper_name) { + LD_ASSERT_NOT_NULL(b); + LD_ASSERT_NOT_NULL(wrapper_name); + + TO_BUILDER(b)->HttpProperties().WrapperName(wrapper_name); +} + +LD_EXPORT(void) +LDServerConfigBuilder_HttpProperties_WrapperVersion( + LDServerConfigBuilder b, + char const* wrapper_version) { + LD_ASSERT_NOT_NULL(b); + LD_ASSERT_NOT_NULL(wrapper_version); + + TO_BUILDER(b)->HttpProperties().WrapperVersion(wrapper_version); +} + +LD_EXPORT(void) +LDServerConfigBuilder_HttpProperties_Header(LDServerConfigBuilder b, + char const* key, + char const* value) { + LD_ASSERT_NOT_NULL(b); + LD_ASSERT_NOT_NULL(key); + LD_ASSERT_NOT_NULL(value); + + TO_BUILDER(b)->HttpProperties().Header(key, value); +} + +LD_EXPORT(void) +LDServerConfigBuilder_Logging_Disable(LDServerConfigBuilder b) { + LD_ASSERT_NOT_NULL(b); + + TO_BUILDER(b)->Logging().Logging(LoggingBuilder::NoLogging()); +} + +LD_EXPORT(void) +LDServerConfigBuilder_Logging_Basic(LDServerConfigBuilder b, + LDLoggingBasicBuilder basic_builder) { + LD_ASSERT_NOT_NULL(b); + LD_ASSERT_NOT_NULL(basic_builder); + + LoggingBuilder::BasicLogging* bb = TO_BASIC_LOGGING_BUILDER(basic_builder); + TO_BUILDER(b)->Logging().Logging(*bb); + LDLoggingBasicBuilder_Free(basic_builder); +} + +LD_EXPORT(void) +LDServerConfigBuilder_Logging_Custom(LDServerConfigBuilder b, + LDLoggingCustomBuilder custom_builder) { + LD_ASSERT_NOT_NULL(b); + LD_ASSERT_NOT_NULL(custom_builder); + + LoggingBuilder::CustomLogging* cb = + TO_CUSTOM_LOGGING_BUILDER(custom_builder); + TO_BUILDER(b)->Logging().Logging(*cb); + LDLoggingCustomBuilder_Free(custom_builder); +} + +// NOLINTEND cppcoreguidelines-pro-type-reinterpret-cast +// NOLINTEND OCInconsistentNamingInspection diff --git a/libs/server-sdk/src/bindings/c/config.cpp b/libs/server-sdk/src/bindings/c/config.cpp new file mode 100644 index 000000000..72b23bace --- /dev/null +++ b/libs/server-sdk/src/bindings/c/config.cpp @@ -0,0 +1,16 @@ +// NOLINTBEGIN cppcoreguidelines-pro-type-reinterpret-cast +// NOLINTBEGIN OCInconsistentNamingInspection + +#include +#include + +#define TO_CONFIG(ptr) (reinterpret_cast(ptr)) + +using namespace launchdarkly::server_side; + +LD_EXPORT(void) LDServerConfig_Free(LDServerConfig config) { + delete TO_CONFIG(config); +} + +// NOLINTEND cppcoreguidelines-pro-type-reinterpret-cast +// NOLINTEND OCInconsistentNamingInspection diff --git a/libs/server-sdk/src/bindings/c/sdk.cpp b/libs/server-sdk/src/bindings/c/sdk.cpp new file mode 100644 index 000000000..78d13f0f8 --- /dev/null +++ b/libs/server-sdk/src/bindings/c/sdk.cpp @@ -0,0 +1,414 @@ +// NOLINTBEGIN cppcoreguidelines-pro-type-reinterpret-cast +// NOLINTBEGIN OCInconsistentNamingInspection + +#include +#include +#include +#include +#include + +#include +#include + +using namespace launchdarkly::server_side; +using namespace launchdarkly; + +struct Detail; + +#define TO_SDK(ptr) (reinterpret_cast(ptr)) +#define FROM_SDK(ptr) (reinterpret_cast(ptr)) + +#define TO_CONTEXT(ptr) (reinterpret_cast(ptr)) +#define FROM_CONTEXT(ptr) (reinterpret_cast(ptr)) + +#define TO_VALUE(ptr) (reinterpret_cast(ptr)) +#define FROM_VALUE(ptr) (reinterpret_cast(ptr)) + +#define FROM_DETAIL(ptr) (reinterpret_cast(ptr)) + +#define TO_DATASOURCESTATUS(ptr) \ + (reinterpret_cast< \ + launchdarkly::server_side::data_sources::DataSourceStatus*>(ptr)) +#define FROM_DATASOURCESTATUS(ptr) \ + (reinterpret_cast(ptr)) + +#define FROM_DATASOURCESTATUS_ERRORINFO(ptr) \ + (reinterpret_cast(ptr)) + +#define TO_ALLFLAGS(ptr) (reinterpret_cast(ptr)) +#define FROM_ALLFLAGS(ptr) (reinterpret_cast(ptr)) + +/* + * Helper to perform the common functionality of checking if the user + * requested a detail out parameter. If so, we allocate a copy of it + * on the heap and return it along with the result. Otherwise, + * we let it destruct and only return the result. + */ +template +inline static auto MaybeDetail(LDServerSDK sdk, + LDEvalDetail* out_detail, + Callable&& fn) { + auto internal_detail = fn(TO_SDK(sdk)); + + auto result = internal_detail.Value(); + + if (!out_detail) { + return result; + } + + *out_detail = FROM_DETAIL(new CEvaluationDetail(internal_detail)); + + return result; +} + +LD_EXPORT(LDServerSDK) +LDServerSDK_New(LDServerConfig config) { + LD_ASSERT_NOT_NULL(config); + + auto as_cfg = reinterpret_cast(config); + auto sdk = new Client(std::move(*as_cfg)); + + LDServerConfig_Free(config); + + return FROM_SDK(sdk); +} + +LD_EXPORT(char const*) +LDServerSDK_Version(void) { + return Client::Version(); +} + +LD_EXPORT(bool) +LDServerSDK_Start(LDServerSDK sdk, + unsigned int milliseconds, + bool* out_succeeded) { + LD_ASSERT_NOT_NULL(sdk); + + auto future = TO_SDK(sdk)->StartAsync(); + + if (milliseconds == LD_NONBLOCKING) { + return false; + } + + if (future.wait_for(std::chrono::milliseconds{milliseconds}) == + std::future_status::ready) { + if (out_succeeded) { + *out_succeeded = future.get(); + } + return true; + } + + return false; +} + +LD_EXPORT(bool) LDServerSDK_Initialized(LDServerSDK sdk) { + LD_ASSERT_NOT_NULL(sdk); + + return TO_SDK(sdk)->Initialized(); +} + +LD_EXPORT(void) +LDServerSDK_TrackEvent(LDServerSDK sdk, + LDContext context, + char const* event_name) { + LD_ASSERT_NOT_NULL(sdk); + LD_ASSERT_NOT_NULL(context); + LD_ASSERT_NOT_NULL(event_name); + + TO_SDK(sdk)->Track(*TO_CONTEXT(context), event_name); +} + +LD_EXPORT(void) +LDServerSDK_TrackMetric(LDServerSDK sdk, + LDContext context, + char const* event_name, + double metric_value, + LDValue value) { + LD_ASSERT_NOT_NULL(sdk); + LD_ASSERT_NOT_NULL(context); + LD_ASSERT_NOT_NULL(event_name); + LD_ASSERT_NOT_NULL(value); + + Value* as_value = TO_VALUE(value); + + TO_SDK(sdk)->Track(*TO_CONTEXT(context), event_name, metric_value, + std::move(*as_value)); + + LDValue_Free(value); +} + +LD_EXPORT(void) +LDServerSDK_TrackData(LDServerSDK sdk, + LDContext context, + char const* event_name, + LDValue value) { + LD_ASSERT_NOT_NULL(sdk); + LD_ASSERT_NOT_NULL(context); + LD_ASSERT_NOT_NULL(event_name); + LD_ASSERT_NOT_NULL(value); + + Value* as_value = TO_VALUE(value); + + TO_SDK(sdk)->Track(*TO_CONTEXT(context), event_name, std::move(*as_value)); + + LDValue_Free(value); +} + +LD_EXPORT(void) +LDServerSDK_Flush(LDServerSDK sdk, unsigned int reserved) { + LD_ASSERT_NOT_NULL(sdk); + LD_ASSERT(reserved == LD_NONBLOCKING); + TO_SDK(sdk)->FlushAsync(); +} + +LD_EXPORT(void) +LDServerSDK_Identify(LDServerSDK sdk, LDContext context) { + LD_ASSERT_NOT_NULL(sdk); + LD_ASSERT_NOT_NULL(context); + + TO_SDK(sdk)->Identify(*TO_CONTEXT(context)); +} + +LD_EXPORT(bool) +LDServerSDK_BoolVariation(LDServerSDK sdk, + LDContext context, + char const* flag_key, + bool default_value) { + LD_ASSERT_NOT_NULL(sdk); + LD_ASSERT_NOT_NULL(context); + LD_ASSERT_NOT_NULL(flag_key); + + return TO_SDK(sdk)->BoolVariation(*TO_CONTEXT(context), flag_key, + default_value); +} + +LD_EXPORT(bool) +LDServerSDK_BoolVariationDetail(LDServerSDK sdk, + LDContext context, + char const* flag_key, + bool default_value, + LDEvalDetail* out_detail) { + LD_ASSERT_NOT_NULL(sdk); + LD_ASSERT_NOT_NULL(context); + LD_ASSERT_NOT_NULL(flag_key); + + return MaybeDetail(sdk, out_detail, [&](Client* client) { + return client->BoolVariationDetail(*TO_CONTEXT(context), flag_key, + default_value); + }); +} + +LD_EXPORT(char*) +LDServerSDK_StringVariation(LDServerSDK sdk, + LDContext context, + char const* flag_key, + char const* default_value) { + LD_ASSERT_NOT_NULL(sdk); + LD_ASSERT_NOT_NULL(context); + LD_ASSERT_NOT_NULL(flag_key); + LD_ASSERT_NOT_NULL(default_value); + + return strdup( + TO_SDK(sdk) + ->StringVariation(*TO_CONTEXT(context), flag_key, default_value) + .c_str()); +} + +LD_EXPORT(char*) +LDServerSDK_StringVariationDetail(LDServerSDK sdk, + LDContext context, + char const* flag_key, + char const* default_value, + LDEvalDetail* out_detail) { + LD_ASSERT_NOT_NULL(sdk); + LD_ASSERT_NOT_NULL(context); + LD_ASSERT_NOT_NULL(flag_key); + LD_ASSERT_NOT_NULL(default_value); + + return strdup(MaybeDetail(sdk, out_detail, [&](Client* client) { + return client->StringVariationDetail( + *TO_CONTEXT(context), flag_key, default_value); + }).c_str()); +} + +LD_EXPORT(int) +LDServerSDK_IntVariation(LDServerSDK sdk, + LDContext context, + char const* flag_key, + int default_value) { + LD_ASSERT_NOT_NULL(sdk); + LD_ASSERT_NOT_NULL(context); + LD_ASSERT_NOT_NULL(flag_key); + + return TO_SDK(sdk)->IntVariation(*TO_CONTEXT(context), flag_key, + default_value); +} + +LD_EXPORT(int) +LDServerSDK_IntVariationDetail(LDServerSDK sdk, + LDContext context, + char const* flag_key, + int default_value, + LDEvalDetail* out_detail) { + LD_ASSERT_NOT_NULL(sdk); + LD_ASSERT_NOT_NULL(context); + LD_ASSERT_NOT_NULL(flag_key); + + return MaybeDetail(sdk, out_detail, [&](Client* client) { + return client->IntVariationDetail(*TO_CONTEXT(context), flag_key, + default_value); + }); +} + +LD_EXPORT(int) +LDServerSDK_DoubleVariation(LDServerSDK sdk, + LDContext context, + char const* flag_key, + double default_value) { + LD_ASSERT_NOT_NULL(sdk); + LD_ASSERT_NOT_NULL(context); + LD_ASSERT_NOT_NULL(flag_key); + + return TO_SDK(sdk)->DoubleVariation(*TO_CONTEXT(context), flag_key, + default_value); +} + +LD_EXPORT(int) +LDServerSDK_DoubleVariationDetail(LDServerSDK sdk, + LDContext context, + char const* flag_key, + double default_value, + LDEvalDetail* out_detail) { + LD_ASSERT_NOT_NULL(sdk); + LD_ASSERT_NOT_NULL(context); + LD_ASSERT_NOT_NULL(flag_key); + + return MaybeDetail(sdk, out_detail, [&](Client* client) { + return client->DoubleVariationDetail(*TO_CONTEXT(context), flag_key, + default_value); + }); +} + +LD_EXPORT(LDValue) +LDServerSDK_JsonVariation(LDServerSDK sdk, + LDContext context, + char const* flag_key, + LDValue default_value) { + LD_ASSERT_NOT_NULL(sdk); + LD_ASSERT_NOT_NULL(context); + LD_ASSERT_NOT_NULL(flag_key); + LD_ASSERT(default_value); + + Value* as_value = TO_VALUE(default_value); + + return FROM_VALUE(new Value( + TO_SDK(sdk)->JsonVariation(*TO_CONTEXT(context), flag_key, *as_value))); +} + +LD_EXPORT(LDValue) +LDServerSDK_JsonVariationDetail(LDServerSDK sdk, + LDContext context, + char const* flag_key, + LDValue default_value, + LDEvalDetail* out_detail) { + LD_ASSERT_NOT_NULL(sdk); + LD_ASSERT_NOT_NULL(context); + LD_ASSERT_NOT_NULL(flag_key); + LD_ASSERT(default_value); + + Value* as_value = TO_VALUE(default_value); + + return FROM_VALUE( + new Value(MaybeDetail(sdk, out_detail, [&](Client* client) { + return client->JsonVariationDetail(*TO_CONTEXT(context), flag_key, + *as_value); + }))); +} + +LD_EXPORT(LDAllFlagsState) +LDServerSDK_AllFlagsState(LDServerSDK sdk, + LDContext context, + enum LDAllFlagsState_Options options) { + LD_ASSERT_NOT_NULL(sdk); + LD_ASSERT_NOT_NULL(context); + + AllFlagsState state = TO_SDK(sdk)->AllFlagsState( + *TO_CONTEXT(context), static_cast(options)); + + return FROM_ALLFLAGS(new AllFlagsState(std::move(state))); +} + +LD_EXPORT(void) LDServerSDK_Free(LDServerSDK sdk) { + delete TO_SDK(sdk); +} + +LD_EXPORT(LDServerDataSourceStatus_State) +LDServerDataSourceStatus_GetState(LDServerDataSourceStatus status) { + LD_ASSERT_NOT_NULL(status); + return static_cast( + TO_DATASOURCESTATUS(status)->State()); +} + +LD_EXPORT(LDDataSourceStatus_ErrorInfo) +LDServerDataSourceStatus_GetLastError(LDServerDataSourceStatus status) { + LD_ASSERT_NOT_NULL(status); + auto error = TO_DATASOURCESTATUS(status)->LastError(); + if (!error) { + return nullptr; + } + return FROM_DATASOURCESTATUS_ERRORINFO( + new data_sources::DataSourceStatus::ErrorInfo( + error->Kind(), error->StatusCode(), error->Message(), + error->Time())); +} + +LD_EXPORT(time_t) +LDServerDataSourceStatus_StateSince(LDServerDataSourceStatus status) { + LD_ASSERT_NOT_NULL(status); + + return std::chrono::duration_cast( + TO_DATASOURCESTATUS(status)->StateSince().time_since_epoch()) + .count(); +} + +LD_EXPORT(void) +LDServerDataSourceStatusListener_Init( + struct LDServerDataSourceStatusListener* listener) { + listener->StatusChanged = nullptr; + listener->UserData = nullptr; +} + +LD_EXPORT(LDListenerConnection) +LDServerSDK_DataSourceStatus_OnStatusChange( + LDServerSDK sdk, + struct LDServerDataSourceStatusListener listener) { + LD_ASSERT_NOT_NULL(sdk); + + if (listener.StatusChanged) { + auto connection = + TO_SDK(sdk)->DataSourceStatus().OnDataSourceStatusChange( + [listener](data_sources::DataSourceStatus status) { + listener.StatusChanged(FROM_DATASOURCESTATUS(&status), + listener.UserData); + }); + + return reinterpret_cast(connection.release()); + } + + return nullptr; +} + +LD_EXPORT(LDServerDataSourceStatus) +LDServerSDK_DataSourceStatus_Status(LDServerSDK sdk) { + LD_ASSERT_NOT_NULL(sdk); + + return FROM_DATASOURCESTATUS(new data_sources::DataSourceStatus( + TO_SDK(sdk)->DataSourceStatus().Status())); +} + +LD_EXPORT(void) LDServerDataSourceStatus_Free(LDServerDataSourceStatus status) { + delete TO_DATASOURCESTATUS(status); +} + +// NOLINTEND cppcoreguidelines-pro-type-reinterpret-cast +// NOLINTEND OCInconsistentNamingInspection diff --git a/libs/server-sdk/src/boost.cpp b/libs/server-sdk/src/boost.cpp new file mode 100644 index 000000000..608953363 --- /dev/null +++ b/libs/server-sdk/src/boost.cpp @@ -0,0 +1,6 @@ +// This file is used to include boost url/json when building a shared library on +// linux/mac. Windows links static libs in this case and does not include these +// src files, as there are issues compiling the value.ipp file from JSON with +// MSVC. +#include +#include diff --git a/libs/server-sdk/src/client.cpp b/libs/server-sdk/src/client.cpp new file mode 100644 index 000000000..ce992de85 --- /dev/null +++ b/libs/server-sdk/src/client.cpp @@ -0,0 +1,135 @@ +#include + +#include "client_impl.hpp" + +namespace launchdarkly::server_side { + +void operator|=(AllFlagsState::Options& lhs, AllFlagsState::Options rhs) { + lhs = lhs | rhs; +} + +AllFlagsState::Options operator|(AllFlagsState::Options lhs, + AllFlagsState::Options rhs) { + return static_cast( + static_cast>(lhs) | + static_cast>(rhs)); +} + +AllFlagsState::Options operator&(AllFlagsState::Options lhs, + AllFlagsState::Options rhs) { + return static_cast( + static_cast>(lhs) & + static_cast>(rhs)); +} + +Client::Client(Config config) + : client(std::make_unique(std::move(config), kVersion)) {} + +bool Client::Initialized() const { + return client->Initialized(); +} + +std::future Client::StartAsync() { + return client->StartAsync(); +} + +using FlagKey = std::string; +[[nodiscard]] AllFlagsState Client::AllFlagsState( + Context const& context, + enum AllFlagsState::Options options) { + return client->AllFlagsState(context, options); +} + +void Client::Track(Context const& ctx, + std::string event_name, + Value data, + double metric_value) { + client->Track(ctx, std::move(event_name), std::move(data), metric_value); +} + +void Client::Track(Context const& ctx, std::string event_name, Value data) { + client->Track(ctx, std::move(event_name), std::move(data)); +} + +void Client::Track(Context const& ctx, std::string event_name) { + client->Track(ctx, std::move(event_name)); +} + +void Client::FlushAsync() { + client->FlushAsync(); +} + +void Client::Identify(Context context) { + return client->Identify(std::move(context)); +} + +bool Client::BoolVariation(Context const& ctx, + FlagKey const& key, + bool default_value) { + return client->BoolVariation(ctx, key, default_value); +} + +EvaluationDetail Client::BoolVariationDetail(Context const& ctx, + FlagKey const& key, + bool default_value) { + return client->BoolVariationDetail(ctx, key, default_value); +} + +std::string Client::StringVariation(Context const& ctx, + FlagKey const& key, + std::string default_value) { + return client->StringVariation(ctx, key, std::move(default_value)); +} + +EvaluationDetail Client::StringVariationDetail( + Context const& ctx, + FlagKey const& key, + std::string default_value) { + return client->StringVariationDetail(ctx, key, std::move(default_value)); +} + +double Client::DoubleVariation(Context const& ctx, + FlagKey const& key, + double default_value) { + return client->DoubleVariation(ctx, key, default_value); +} + +EvaluationDetail Client::DoubleVariationDetail(Context const& ctx, + FlagKey const& key, + double default_value) { + return client->DoubleVariationDetail(ctx, key, default_value); +} + +int Client::IntVariation(Context const& ctx, + FlagKey const& key, + int default_value) { + return client->IntVariation(ctx, key, default_value); +} + +EvaluationDetail Client::IntVariationDetail(Context const& ctx, + FlagKey const& key, + int default_value) { + return client->IntVariationDetail(ctx, key, default_value); +} + +Value Client::JsonVariation(Context const& ctx, + FlagKey const& key, + Value default_value) { + return client->JsonVariation(ctx, key, std::move(default_value)); +} + +EvaluationDetail Client::JsonVariationDetail(Context const& ctx, + FlagKey const& key, + Value default_value) { + return client->JsonVariationDetail(ctx, key, std::move(default_value)); +} + +data_sources::IDataSourceStatusProvider& Client::DataSourceStatus() { + return client->DataSourceStatus(); +} + +char const* Client::Version() { + return kVersion; +} + +} // namespace launchdarkly::server_side diff --git a/libs/server-sdk/src/client_impl.cpp b/libs/server-sdk/src/client_impl.cpp new file mode 100644 index 000000000..5dfc5b56b --- /dev/null +++ b/libs/server-sdk/src/client_impl.cpp @@ -0,0 +1,451 @@ + +#include + +#include +#include + +#include "client_impl.hpp" + +#include "all_flags_state/all_flags_state_builder.hpp" +#include "data_sources/null_data_source.hpp" +#include "data_sources/polling_data_source.hpp" +#include "data_sources/streaming_data_source.hpp" +#include "data_store/memory_store.hpp" + +#include +#include +#include +#include +#include + +namespace launchdarkly::server_side { + +// The ASIO implementation assumes that the io_context will be run from a +// single thread, and applies several optimisations based on this +// assumption. +auto const kAsioConcurrencyHint = 1; + +// Client's destructor attempts to gracefully shut down the datasource +// connection in this amount of time. +auto const kDataSourceShutdownWait = std::chrono::milliseconds(100); + +using config::shared::ServerSDK; +using launchdarkly::config::shared::built::DataSourceConfig; +using launchdarkly::config::shared::built::HttpProperties; +using launchdarkly::server_side::data_sources::DataSourceStatus; + +static std::shared_ptr<::launchdarkly::data_sources::IDataSource> +MakeDataSource(HttpProperties const& http_properties, + Config const& config, + boost::asio::any_io_executor const& executor, + data_sources::IDataSourceUpdateSink& flag_updater, + data_sources::DataSourceStatusManager& status_manager, + Logger& logger) { + if (config.Offline()) { + return std::make_shared(executor, + status_manager); + } + + auto builder = HttpPropertiesBuilder(http_properties); + + auto data_source_properties = builder.Build(); + + if (config.DataSourceConfig().method.index() == 0) { + return std::make_shared< + launchdarkly::server_side::data_sources::StreamingDataSource>( + config.ServiceEndpoints(), config.DataSourceConfig(), + data_source_properties, executor, flag_updater, status_manager, + logger); + } + return std::make_shared< + launchdarkly::server_side::data_sources::PollingDataSource>( + config.ServiceEndpoints(), config.DataSourceConfig(), + data_source_properties, executor, flag_updater, status_manager, logger); +} + +static Logger MakeLogger(config::shared::built::Logging const& config) { + if (config.disable_logging) { + return {std::make_shared()}; + } + if (config.backend) { + return {config.backend}; + } + return { + std::make_shared(config.level, config.tag)}; +} + +bool EventsEnabled(Config const& config) { + return config.Events().Enabled() && !config.Offline(); +} + +std::unique_ptr> MakeEventProcessor( + Config const& config, + boost::asio::any_io_executor const& exec, + HttpProperties const& http_properties, + Logger& logger) { + if (EventsEnabled(config)) { + return std::make_unique>( + exec, config.ServiceEndpoints(), config.Events(), http_properties, + logger); + } + return nullptr; +} + +/** + * Returns true if the flag pointer is valid and the underlying item is present. + */ +bool IsFlagPresent( + std::shared_ptr const& flag_desc); + +ClientImpl::ClientImpl(Config config, std::string const& version) + : config_(config), + http_properties_( + HttpPropertiesBuilder(config.HttpProperties()) + .Header("user-agent", "CPPClient/" + version) + .Header("authorization", config.SdkKey()) + .Header("x-launchdarkly-tags", config.ApplicationTag()) + .Build()), + logger_(MakeLogger(config.Logging())), + ioc_(kAsioConcurrencyHint), + work_(boost::asio::make_work_guard(ioc_)), + memory_store_(), + status_manager_(), + data_store_updater_(memory_store_, memory_store_), + data_source_(MakeDataSource(http_properties_, + config_, + ioc_.get_executor(), + data_store_updater_, + status_manager_, + logger_)), + event_processor_(MakeEventProcessor(config, + ioc_.get_executor(), + http_properties_, + logger_)), + evaluator_(logger_, memory_store_), + events_default_(event_processor_.get(), EventFactory::WithoutReasons()), + events_with_reasons_(event_processor_.get(), + EventFactory::WithReasons()) { + run_thread_ = std::move(std::thread([&]() { ioc_.run(); })); +} + +static bool IsInitializedSuccessfully(DataSourceStatus::DataSourceState state) { + return state == DataSourceStatus::DataSourceState::kValid; +} + +static bool IsInitialized(DataSourceStatus::DataSourceState state) { + return IsInitializedSuccessfully(state) || + (state != DataSourceStatus::DataSourceState::kInitializing); +} + +void ClientImpl::Identify(Context context) { + events_default_.Send([&](EventFactory const& factory) { + return factory.Identify(std::move(context)); + }); +} + +std::future ClientImpl::StartAsyncInternal( + std::function result_predicate) { + auto pr = std::make_shared>(); + auto fut = pr->get_future(); + + status_manager_.OnDataSourceStatusChangeEx( + [result_predicate, pr](data_sources::DataSourceStatus status) { + auto state = status.State(); + if (IsInitialized(state)) { + pr->set_value(result_predicate(status.State())); + return true; /* delete this change listener since the + desired state was reached */ + } + return false; /* keep the change listener */ + }); + + data_source_->Start(); + + return fut; +} + +std::future ClientImpl::StartAsync() { + return StartAsyncInternal(IsInitializedSuccessfully); +} + +bool ClientImpl::Initialized() const { + return IsInitializedSuccessfully(status_manager_.Status().State()); +} + +AllFlagsState ClientImpl::AllFlagsState(Context const& context, + AllFlagsState::Options options) { + std::unordered_map result; + + if (!Initialized()) { + if (memory_store_.Initialized()) { + LD_LOG(logger_, LogLevel::kWarn) + << "AllFlagsState() called before client has finished " + "initializing; using last known values from data store"; + } else { + LD_LOG(logger_, LogLevel::kWarn) + << "AllFlagsState() called before client has finished " + "initializing. Data store not available. Returning empty " + "state"; + return {}; + } + } + + AllFlagsStateBuilder builder{options}; + + EventScope no_events; + + for (auto const& [k, v] : memory_store_.AllFlags()) { + if (!v || !v->item) { + continue; + } + + auto const& flag = *(v->item); + + if (IsSet(options, AllFlagsState::Options::ClientSideOnly) && + !flag.clientSideAvailability.usingEnvironmentId) { + continue; + } + + EvaluationDetail detail = + evaluator_.Evaluate(flag, context, no_events); + + bool in_experiment = flag.IsExperimentationEnabled(detail.Reason()); + builder.AddFlag(k, detail.Value(), + AllFlagsState::State{ + flag.Version(), detail.VariationIndex(), + detail.Reason(), flag.trackEvents || in_experiment, + in_experiment, flag.debugEventsUntilDate}); + } + + return builder.Build(); +} + +void ClientImpl::TrackInternal(Context const& ctx, + std::string event_name, + std::optional data, + std::optional metric_value) { + events_default_.Send([&](EventFactory const& factory) { + return factory.Custom(ctx, std::move(event_name), std::move(data), + metric_value); + }); +} + +void ClientImpl::Track(Context const& ctx, + std::string event_name, + Value data, + double metric_value) { + this->TrackInternal(ctx, std::move(event_name), std::move(data), + metric_value); +} + +void ClientImpl::Track(Context const& ctx, std::string event_name, Value data) { + this->TrackInternal(ctx, std::move(event_name), std::move(data), + std::nullopt); +} + +void ClientImpl::Track(Context const& ctx, std::string event_name) { + this->TrackInternal(ctx, std::move(event_name), std::nullopt, std::nullopt); +} + +void ClientImpl::FlushAsync() { + if (event_processor_) { + event_processor_->FlushAsync(); + } +} + +void ClientImpl::LogVariationCall(std::string const& key, + bool flag_present) const { + if (Initialized()) { + if (!flag_present) { + LD_LOG(logger_, LogLevel::kInfo) << "Unknown feature flag " << key + << "; returning default value"; + } + } else { + if (flag_present) { + LD_LOG(logger_, LogLevel::kInfo) + << "LaunchDarkly client has not yet been initialized; using " + "last " + "known flag rules from data store"; + } else { + LD_LOG(logger_, LogLevel::kInfo) + << "LaunchDarkly client has not yet been initialized; " + "returning default value"; + } + } +} + +Value ClientImpl::Variation(Context const& ctx, + enum Value::Type value_type, + IClient::FlagKey const& key, + Value const& default_value) { + auto result = *VariationInternal(ctx, key, default_value, events_default_); + if (result.Type() != value_type) { + return default_value; + } + return result; +} + +EvaluationDetail ClientImpl::VariationInternal( + Context const& context, + IClient::FlagKey const& key, + Value const& default_value, + EventScope const& event_scope) { + if (auto error = PreEvaluationChecks(context)) { + return PostEvaluation(key, context, default_value, *error, event_scope, + std::nullopt); + } + + auto flag_rule = memory_store_.GetFlag(key); + + bool flag_present = IsFlagPresent(flag_rule); + + LogVariationCall(key, flag_present); + + if (!flag_present) { + return PostEvaluation(key, context, default_value, + EvaluationReason::ErrorKind::kFlagNotFound, + event_scope, std::nullopt); + } + + EvaluationDetail result = + evaluator_.Evaluate(*flag_rule->item, context, event_scope); + return PostEvaluation(key, context, default_value, result, event_scope, + flag_rule.get()->item); +} + +std::optional ClientImpl::PreEvaluationChecks( + Context const& context) { + if (!memory_store_.Initialized()) { + return EvaluationReason::ErrorKind::kClientNotReady; + } + if (!context.Valid()) { + return EvaluationReason::ErrorKind::kUserNotSpecified; + } + return std::nullopt; +} + +EvaluationDetail ClientImpl::PostEvaluation( + std::string const& key, + Context const& context, + Value const& default_value, + std::variant> + error_or_detail, + EventScope const& event_scope, + std::optional const& flag) { + return std::visit( + [&](auto&& arg) { + using T = std::decay_t; + // VARIANT: ErrorKind + if constexpr (std::is_same_v) { + auto detail = EvaluationDetail{arg, default_value}; + + event_scope.Send([&](EventFactory const& factory) { + return factory.UnknownFlag(key, context, detail, + default_value); + }); + + return detail; + } + // VARIANT: EvaluationDetail + else if constexpr (std::is_same_v>) { + auto detail = EvaluationDetail{ + (!arg.VariationIndex() ? default_value : arg.Value()), + arg.VariationIndex(), arg.Reason()}; + + event_scope.Send([&](EventFactory const& factory) { + return factory.Eval(key, context, flag, detail, + default_value, std::nullopt); + }); + + return detail; + } + }, + std::move(error_or_detail)); +} + +bool IsFlagPresent( + std::shared_ptr const& flag_desc) { + return flag_desc && flag_desc->item; +} + +EvaluationDetail ClientImpl::BoolVariationDetail( + Context const& ctx, + IClient::FlagKey const& key, + bool default_value) { + return VariationDetail(ctx, Value::Type::kBool, key, default_value); +} + +bool ClientImpl::BoolVariation(Context const& ctx, + IClient::FlagKey const& key, + bool default_value) { + return Variation(ctx, Value::Type::kBool, key, default_value); +} + +EvaluationDetail ClientImpl::StringVariationDetail( + Context const& ctx, + ClientImpl::FlagKey const& key, + std::string default_value) { + return VariationDetail(ctx, Value::Type::kString, key, + default_value); +} + +std::string ClientImpl::StringVariation(Context const& ctx, + IClient::FlagKey const& key, + std::string default_value) { + return Variation(ctx, Value::Type::kString, key, default_value); +} + +EvaluationDetail ClientImpl::DoubleVariationDetail( + Context const& ctx, + ClientImpl::FlagKey const& key, + double default_value) { + return VariationDetail(ctx, Value::Type::kNumber, key, + default_value); +} + +double ClientImpl::DoubleVariation(Context const& ctx, + IClient::FlagKey const& key, + double default_value) { + return Variation(ctx, Value::Type::kNumber, key, default_value); +} + +EvaluationDetail ClientImpl::IntVariationDetail( + Context const& ctx, + IClient::FlagKey const& key, + int default_value) { + return VariationDetail(ctx, Value::Type::kNumber, key, default_value); +} + +int ClientImpl::IntVariation(Context const& ctx, + IClient::FlagKey const& key, + int default_value) { + return Variation(ctx, Value::Type::kNumber, key, default_value); +} + +EvaluationDetail ClientImpl::JsonVariationDetail( + Context const& ctx, + IClient::FlagKey const& key, + Value default_value) { + return VariationInternal(ctx, key, default_value, events_with_reasons_); +} + +Value ClientImpl::JsonVariation(Context const& ctx, + IClient::FlagKey const& key, + Value default_value) { + return *VariationInternal(ctx, key, default_value, events_default_); +} + +data_sources::IDataSourceStatusProvider& ClientImpl::DataSourceStatus() { + return status_manager_; +} + +// flag_manager::IFlagNotifier& ClientImpl::FlagNotifier() { +// return flag_manager_.Notifier(); +// } + +ClientImpl::~ClientImpl() { + ioc_.stop(); + // TODO(SC-219101) + run_thread_.join(); +} +} // namespace launchdarkly::server_side diff --git a/libs/server-sdk/src/client_impl.hpp b/libs/server-sdk/src/client_impl.hpp new file mode 100644 index 000000000..d6c77ecee --- /dev/null +++ b/libs/server-sdk/src/client_impl.hpp @@ -0,0 +1,195 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "data_sources/data_source_status_manager.hpp" +#include "data_sources/data_source_update_sink.hpp" + +#include "data_store/data_store_updater.hpp" +#include "data_store/memory_store.hpp" + +#include "evaluation/evaluator.hpp" + +#include "events/event_scope.hpp" + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace launchdarkly::server_side { + +class ClientImpl : public IClient { + public: + ClientImpl(Config config, std::string const& version); + + ClientImpl(ClientImpl&&) = delete; + ClientImpl(ClientImpl const&) = delete; + ClientImpl& operator=(ClientImpl) = delete; + ClientImpl& operator=(ClientImpl&& other) = delete; + + bool Initialized() const override; + + using FlagKey = std::string; + [[nodiscard]] class AllFlagsState AllFlagsState( + Context const& context, + AllFlagsState::Options options = + AllFlagsState::Options::Default) override; + + void Track(Context const& ctx, + std::string event_name, + Value data, + double metric_value) override; + + void Track(Context const& ctx, std::string event_name, Value data) override; + + void Track(Context const& ctx, std::string event_name) override; + + void FlushAsync() override; + + void Identify(Context context) override; + + bool BoolVariation(Context const& ctx, + FlagKey const& key, + bool default_value) override; + + EvaluationDetail BoolVariationDetail(Context const& ctx, + FlagKey const& key, + bool default_value) override; + + std::string StringVariation(Context const& ctx, + FlagKey const& key, + std::string default_value) override; + + EvaluationDetail StringVariationDetail( + Context const& ctx, + FlagKey const& key, + std::string default_value) override; + + double DoubleVariation(Context const& ctx, + FlagKey const& key, + double default_value) override; + + EvaluationDetail DoubleVariationDetail( + Context const& ctx, + FlagKey const& key, + double default_value) override; + + int IntVariation(Context const& ctx, + FlagKey const& key, + int default_value) override; + + EvaluationDetail IntVariationDetail(Context const& ctx, + FlagKey const& key, + int default_value) override; + + Value JsonVariation(Context const& ctx, + FlagKey const& key, + Value default_value) override; + + EvaluationDetail JsonVariationDetail(Context const& ctx, + FlagKey const& key, + Value default_value) override; + + data_sources::IDataSourceStatusProvider& DataSourceStatus() override; + + ~ClientImpl(); + + std::future StartAsync() override; + + private: + [[nodiscard]] EvaluationDetail VariationInternal( + Context const& ctx, + FlagKey const& key, + Value const& default_value, + EventScope const& scope); + + template + [[nodiscard]] EvaluationDetail VariationDetail( + Context const& ctx, + enum Value::Type value_type, + IClient::FlagKey const& key, + Value const& default_value) { + auto result = + VariationInternal(ctx, key, default_value, events_with_reasons_); + if (result.Value().Type() == value_type) { + return EvaluationDetail{result.Value(), result.VariationIndex(), + result.Reason()}; + } + return EvaluationDetail{EvaluationReason::ErrorKind::kWrongType, + default_value}; + } + + [[nodiscard]] Value Variation(Context const& ctx, + enum Value::Type value_type, + std::string const& key, + Value const& default_value); + + [[nodiscard]] EvaluationDetail PostEvaluation( + std::string const& key, + Context const& context, + Value const& default_value, + std::variant> + result, + EventScope const& event_scope, + std::optional const& flag); + + [[nodiscard]] std::optional + PreEvaluationChecks(Context const& context); + + void TrackInternal(Context const& ctx, + std::string event_name, + std::optional data, + std::optional metric_value); + + std::future StartAsyncInternal( + std::function + predicate); + + void LogVariationCall(std::string const& key, bool flag_present) const; + + Config config_; + Logger logger_; + + launchdarkly::config::shared::built::HttpProperties http_properties_; + + boost::asio::io_context ioc_; + boost::asio::executor_work_guard + work_; + + data_store::MemoryStore memory_store_; + + data_sources::DataSourceStatusManager status_manager_; + data_store::DataStoreUpdater data_store_updater_; + + std::shared_ptr<::launchdarkly::data_sources::IDataSource> data_source_; + + std::unique_ptr event_processor_; + + mutable std::mutex init_mutex_; + std::condition_variable init_waiter_; + + evaluation::Evaluator evaluator_; + + EventScope const events_default_; + EventScope const events_with_reasons_; + + std::thread run_thread_; +}; +} // namespace launchdarkly::server_side diff --git a/libs/server-sdk/src/data_sources/data_source_event_handler.cpp b/libs/server-sdk/src/data_sources/data_source_event_handler.cpp new file mode 100644 index 000000000..2d8c436b3 --- /dev/null +++ b/libs/server-sdk/src/data_sources/data_source_event_handler.cpp @@ -0,0 +1,241 @@ +#include "data_source_event_handler.hpp" + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +#include "tl/expected.hpp" + +namespace launchdarkly::server_side::data_sources { + +static char const* const kErrorParsingPut = "Could not parse PUT message"; +static char const* const kErrorPutInvalid = + "PUT message contained invalid data"; +static char const* const kErrorParsingPatch = "Could not parse PATCH message"; +static char const* const kErrorPatchInvalid = + "PATCH message contained invalid data"; +static char const* const kErrorParsingDelete = "Could not parse DELETE message"; +static char const* const kErrorDeleteInvalid = + "DELETE message contained invalid data\""; + +template +tl::expected Patch( + std::string const& path, + boost::json::object const& obj) { + auto const* data_iter = obj.find("data"); + if (data_iter == obj.end()) { + return tl::unexpected(JsonError::kSchemaFailure); + } + auto data = + boost::json::value_to, JsonError>>( + data_iter->value()); + if (!data.has_value()) { + return tl::unexpected(JsonError::kSchemaFailure); + } + return DataSourceEventHandler::Patch{ + TStreamingDataKind::Key(path), + data_model::ItemDescriptor(data->value())}; +} + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, + JsonError>> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + + if (!json_value.is_object()) { + return tl::unexpected(JsonError::kSchemaFailure); + } + + DataSourceEventHandler::Put put; + std::string path; + auto const& obj = json_value.as_object(); + PARSE_FIELD(path, obj, "path"); + // We don't know what to do with a path other than "/". + if (!(path == "/" || path.empty())) { + return std::nullopt; + } + PARSE_FIELD(put.data, obj, "data"); + + return put; +} + +tl::expected, JsonError> +tag_invoke(boost::json::value_to_tag< + tl::expected, + JsonError>> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + if (!json_value.is_object()) { + return tl::unexpected(JsonError::kSchemaFailure); + } + + auto const& obj = json_value.as_object(); + + std::string path; + PARSE_FIELD(path, obj, "path"); + + if (StreamingDataKinds::Flag::IsKind(path)) { + return Patch(path, obj); + } + + if (StreamingDataKinds::Segment::IsKind(path)) { + return Patch(path, + obj); + } + + return std::nullopt; +} + +static tl::expected tag_invoke( + boost::json::value_to_tag< + tl::expected> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + + if (!json_value.is_object()) { + return tl::unexpected(JsonError::kSchemaFailure); + } + + auto const& obj = json_value.as_object(); + + DataSourceEventHandler::Delete del; + PARSE_REQUIRED_FIELD(del.version, obj, "version"); + std::string path; + PARSE_REQUIRED_FIELD(path, obj, "path"); + + auto kind = StreamingDataKinds::Kind(path); + auto key = StreamingDataKinds::Key(path); + + if (kind.has_value() && key.has_value()) { + del.kind = *kind; + del.key = *key; + return del; + } + return tl::unexpected(JsonError::kSchemaFailure); +} + +DataSourceEventHandler::DataSourceEventHandler( + IDataSourceUpdateSink& handler, + Logger const& logger, + DataSourceStatusManager& status_manager) + : handler_(handler), logger_(logger), status_manager_(status_manager) {} + +DataSourceEventHandler::MessageStatus DataSourceEventHandler::HandleMessage( + std::string const& type, + std::string const& data) { + if (type == "put") { + boost::json::error_code error_code; + auto parsed = boost::json::parse(data, error_code); + if (error_code) { + LD_LOG(logger_, LogLevel::kError) << kErrorParsingPut; + status_manager_.SetError( + DataSourceStatus::ErrorInfo::ErrorKind::kInvalidData, + kErrorParsingPut); + return DataSourceEventHandler::MessageStatus::kInvalidMessage; + } + auto res = + boost::json::value_to, JsonError>>( + parsed); + + if (!res) { + LD_LOG(logger_, LogLevel::kError) << kErrorPutInvalid; + status_manager_.SetError( + DataSourceStatus::ErrorInfo::ErrorKind::kInvalidData, + kErrorPutInvalid); + return DataSourceEventHandler::MessageStatus::kInvalidMessage; + } + + // Check the inner optional. + if (res->has_value()) { + handler_.Init(std::move((*res)->data)); + status_manager_.SetState(DataSourceStatus::DataSourceState::kValid); + return DataSourceEventHandler::MessageStatus::kMessageHandled; + } + return DataSourceEventHandler::MessageStatus::kMessageHandled; + } + if (type == "patch") { + boost::json::error_code error_code; + auto parsed = boost::json::parse(data, error_code); + if (error_code) { + LD_LOG(logger_, LogLevel::kError) << kErrorParsingPut; + status_manager_.SetError( + DataSourceStatus::ErrorInfo::ErrorKind::kInvalidData, + kErrorParsingPatch); + return DataSourceEventHandler::MessageStatus::kInvalidMessage; + } + + auto res = boost::json::value_to< + tl::expected, JsonError>>(parsed); + + if (!res.has_value()) { + status_manager_.SetError( + DataSourceStatus::ErrorInfo::ErrorKind::kInvalidData, + kErrorPatchInvalid); + return DataSourceEventHandler::MessageStatus::kInvalidMessage; + } + + // This references the optional inside the expected. + if (res->has_value()) { + auto const& patch = (**res); + auto const& key = patch.key; + std::visit([this, &key](auto&& arg) { handler_.Upsert(key, arg); }, + patch.data); + return DataSourceEventHandler::MessageStatus::kMessageHandled; + } + // We didn't recognize the type of the patch. So we ignore it. + return DataSourceEventHandler::MessageStatus::kMessageHandled; + } + if (type == "delete") { + boost::json::error_code error_code; + auto parsed = boost::json::parse(data, error_code); + if (error_code) { + LD_LOG(logger_, LogLevel::kError) << kErrorParsingDelete; + status_manager_.SetError( + DataSourceStatus::ErrorInfo::ErrorKind::kInvalidData, + kErrorParsingDelete); + return DataSourceEventHandler::MessageStatus::kInvalidMessage; + } + + auto res = + boost::json::value_to>(parsed); + + if (res.has_value()) { + switch (res->kind) { + case data_store::DataKind::kFlag: { + handler_.Upsert(res->key, + data_store::FlagDescriptor(res->version)); + return DataSourceEventHandler::MessageStatus:: + kMessageHandled; + } + case data_store::DataKind::kSegment: { + handler_.Upsert( + res->key, data_store::SegmentDescriptor(res->version)); + return DataSourceEventHandler::MessageStatus:: + kMessageHandled; + } + default: { + } break; + } + } + + status_manager_.SetError( + DataSourceStatus::ErrorInfo::ErrorKind::kInvalidData, + kErrorDeleteInvalid); + return DataSourceEventHandler::MessageStatus::kInvalidMessage; + } + + return DataSourceEventHandler::MessageStatus::kUnhandledVerb; +} + +} // namespace launchdarkly::server_side::data_sources diff --git a/libs/server-sdk/src/data_sources/data_source_event_handler.hpp b/libs/server-sdk/src/data_sources/data_source_event_handler.hpp new file mode 100644 index 000000000..798796506 --- /dev/null +++ b/libs/server-sdk/src/data_sources/data_source_event_handler.hpp @@ -0,0 +1,124 @@ +#pragma once + +#include + +#include + +#include "../data_store/data_kind.hpp" +#include "data_source_status_manager.hpp" +#include "data_source_update_sink.hpp" + +#include +#include +#include +#include +#include + +namespace launchdarkly::server_side::data_sources { + +// The FlagsPath and SegmentsPath are made to turn a string literal into a type +// for use in a template. +// You can use a char array as a const char* template +// parameter, but this causes a number of issues with the clang linter. + +struct FlagsPath { + static constexpr std::string_view path = "/flags/"; +}; + +struct SegmentsPath { + static constexpr std::string_view path = "/segments/"; +}; + +template +class StreamingDataKind { + public: + static data_store::DataKind Kind() { return kind; } + static bool IsKind(std::string const& patch_path) { + return patch_path.rfind(TPath::path) == 0; + } + static std::string Key(std::string const& patch_path) { + return patch_path.substr(TPath::path.size()); + } +}; + +struct StreamingDataKinds { + using Flag = StreamingDataKind; + using Segment = + StreamingDataKind; + + static std::optional Kind(std::string const& path) { + if (Flag::IsKind(path)) { + return data_store::DataKind::kFlag; + } + if (Segment::IsKind(path)) { + return data_store::DataKind::kSegment; + } + return std::nullopt; + } + + static std::optional Key(std::string const& path) { + if (Flag::IsKind(path)) { + return Flag::Key(path); + } + if (Segment::IsKind(path)) { + return Segment::Key(path); + } + return std::nullopt; + } +}; + +/** + * This class handles LaunchDarkly events, parses them, and then uses + * a IDataSourceUpdateSink to process the parsed events. + * + * This is only used for streaming. For server polling the shape of the poll + * response is different than the put, so there is limited utility in + * sharing this handler. + */ +class DataSourceEventHandler { + public: + /** + * Status indicating if the message was processed, or if there + * was an issue encountered. + */ + enum class MessageStatus { + kMessageHandled, + kInvalidMessage, + kUnhandledVerb + }; + + struct Put { + data_model::SDKDataSet data; + }; + + struct Patch { + std::string key; + std::variant + data; + }; + + struct Delete { + std::string key; + data_store::DataKind kind; + uint64_t version; + }; + + DataSourceEventHandler(IDataSourceUpdateSink& handler, + Logger const& logger, + DataSourceStatusManager& status_manager); + + /** + * Handles an event from the LaunchDarkly service. + * @param type The type of the event. "put"/"patch"/"delete". + * @param data The content of the event. + * @return A status indicating if the message could be handled. + */ + MessageStatus HandleMessage(std::string const& type, + std::string const& data); + + private: + IDataSourceUpdateSink& handler_; + Logger const& logger_; + DataSourceStatusManager& status_manager_; +}; +} // namespace launchdarkly::server_side::data_sources diff --git a/libs/server-sdk/src/data_sources/data_source_status.cpp b/libs/server-sdk/src/data_sources/data_source_status.cpp new file mode 100644 index 000000000..6881ed4ba --- /dev/null +++ b/libs/server-sdk/src/data_sources/data_source_status.cpp @@ -0,0 +1,40 @@ +#include + +#include + +namespace launchdarkly::server_side::data_sources { + +std::ostream& operator<<(std::ostream& out, + DataSourceStatus::DataSourceState const& state) { + switch (state) { + case DataSourceStatus::DataSourceState::kInitializing: + out << "INITIALIZING"; + break; + case DataSourceStatus::DataSourceState::kValid: + out << "VALID"; + break; + case DataSourceStatus::DataSourceState::kInterrupted: + out << "INTERRUPTED"; + break; + case DataSourceStatus::DataSourceState::kOff: + out << "OFF"; + break; + } + + return out; +} + +std::ostream& operator<<(std::ostream& out, DataSourceStatus const& status) { + std::time_t as_time_t = + std::chrono::system_clock::to_time_t(status.StateSince()); + out << "Status(" << status.State() << ", Since(" + << std::put_time(std::gmtime(&as_time_t), "%Y-%m-%d %H:%M:%S") << ")"; + auto const& last_error = status.LastError(); + if (last_error.has_value()) { + out << ", " << last_error.value(); + } + out << ")"; + return out; +} + +} // namespace launchdarkly::server_side::data_sources diff --git a/libs/server-sdk/src/data_sources/data_source_status_manager.hpp b/libs/server-sdk/src/data_sources/data_source_status_manager.hpp new file mode 100644 index 000000000..d19040ed8 --- /dev/null +++ b/libs/server-sdk/src/data_sources/data_source_status_manager.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include +#include + +#include + +#include +#include +#include + +namespace launchdarkly::server_side::data_sources { + +class DataSourceStatusManager + : public internal::data_sources::DataSourceStatusManagerBase< + DataSourceStatus, + IDataSourceStatusProvider> { + public: + DataSourceStatusManager() = default; + + ~DataSourceStatusManager() override = default; + DataSourceStatusManager(DataSourceStatusManager const& item) = delete; + DataSourceStatusManager(DataSourceStatusManager&& item) = delete; + DataSourceStatusManager& operator=(DataSourceStatusManager const&) = delete; + DataSourceStatusManager& operator=(DataSourceStatusManager&&) = delete; +}; + +} // namespace launchdarkly::server_side::data_sources diff --git a/libs/server-sdk/src/data_sources/data_source_update_sink.hpp b/libs/server-sdk/src/data_sources/data_source_update_sink.hpp new file mode 100644 index 000000000..294f82ee7 --- /dev/null +++ b/libs/server-sdk/src/data_sources/data_source_update_sink.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include +#include +#include +#include + +#include "../data_store/descriptors.hpp" + +namespace launchdarkly::server_side::data_sources { +/** + * Interface for handling updates from LaunchDarkly. + */ +class IDataSourceUpdateSink { + public: + virtual void Init(launchdarkly::data_model::SDKDataSet data_set) = 0; + virtual void Upsert(std::string const& key, + data_store::FlagDescriptor flag) = 0; + virtual void Upsert(std::string const& key, + data_store::SegmentDescriptor segment) = 0; + + IDataSourceUpdateSink(IDataSourceUpdateSink const& item) = delete; + IDataSourceUpdateSink(IDataSourceUpdateSink&& item) = delete; + IDataSourceUpdateSink& operator=(IDataSourceUpdateSink const&) = delete; + IDataSourceUpdateSink& operator=(IDataSourceUpdateSink&&) = delete; + virtual ~IDataSourceUpdateSink() = default; + + protected: + IDataSourceUpdateSink() = default; +}; +} // namespace launchdarkly::server_side::data_sources diff --git a/libs/server-sdk/src/data_sources/null_data_source.cpp b/libs/server-sdk/src/data_sources/null_data_source.cpp new file mode 100644 index 000000000..223dee39c --- /dev/null +++ b/libs/server-sdk/src/data_sources/null_data_source.cpp @@ -0,0 +1,19 @@ +#include "null_data_source.hpp" + +#include + +namespace launchdarkly::server_side::data_sources { + +void NullDataSource::Start() { + status_manager_.SetState(DataSourceStatus::DataSourceState::kValid); +} + +void NullDataSource::ShutdownAsync(std::function complete) { + boost::asio::post(exec_, complete); +} + +NullDataSource::NullDataSource(boost::asio::any_io_executor exec, + DataSourceStatusManager& status_manager) + : status_manager_(status_manager), exec_(exec) {} + +} // namespace launchdarkly::server_side::data_sources diff --git a/libs/server-sdk/src/data_sources/null_data_source.hpp b/libs/server-sdk/src/data_sources/null_data_source.hpp new file mode 100644 index 000000000..7a1102f49 --- /dev/null +++ b/libs/server-sdk/src/data_sources/null_data_source.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include "data_source_status_manager.hpp" + +#include + +#include + +namespace launchdarkly::server_side::data_sources { + +class NullDataSource : public ::launchdarkly::data_sources::IDataSource { + public: + explicit NullDataSource(boost::asio::any_io_executor exec, + DataSourceStatusManager& status_manager); + void Start() override; + void ShutdownAsync(std::function) override; + + private: + DataSourceStatusManager& status_manager_; + boost::asio::any_io_executor exec_; +}; + +} // namespace launchdarkly::server_side::data_sources diff --git a/libs/server-sdk/src/data_sources/polling_data_source.cpp b/libs/server-sdk/src/data_sources/polling_data_source.cpp new file mode 100644 index 000000000..2f250de01 --- /dev/null +++ b/libs/server-sdk/src/data_sources/polling_data_source.cpp @@ -0,0 +1,245 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "data_source_update_sink.hpp" +#include "polling_data_source.hpp" + +namespace launchdarkly::server_side::data_sources { + +static char const* const kErrorParsingPut = "Could not parse polling payload"; +static char const* const kErrorPutInvalid = + "Polling payload contained invalid data"; + +static char const* const kCouldNotParseEndpoint = + "Could not parse polling endpoint URL"; + +static network::HttpRequest MakeRequest( + config::shared::built::DataSourceConfig const& + data_source_config, + config::shared::built::ServiceEndpoints const& endpoints, + config::shared::built::HttpProperties const& http_properties) { + auto url = std::make_optional(endpoints.PollingBaseUrl()); + + auto const& polling_config = std::get< + config::shared::built::PollingConfig>( + data_source_config.method); + + url = network::AppendUrl(url, polling_config.polling_get_path); + + network::HttpRequest::BodyType body; + network::HttpMethod method = network::HttpMethod::kGet; + + config::shared::builders::HttpPropertiesBuilder + builder(http_properties); + + // If no URL is set, then we will fail the request. + return {url.value_or(""), method, builder.Build(), body}; +} + +PollingDataSource::PollingDataSource( + config::shared::built::ServiceEndpoints const& endpoints, + config::shared::built::DataSourceConfig const& + data_source_config, + config::shared::built::HttpProperties const& http_properties, + boost::asio::any_io_executor const& ioc, + IDataSourceUpdateSink& handler, + DataSourceStatusManager& status_manager, + Logger const& logger) + : ioc_(ioc), + logger_(logger), + status_manager_(status_manager), + update_sink_(handler), + requester_(ioc), + timer_(ioc), + polling_interval_( + std::get< + config::shared::built::PollingConfig>( + data_source_config.method) + .poll_interval), + request_(MakeRequest(data_source_config, endpoints, http_properties)) { + auto const& polling_config = std::get< + config::shared::built::PollingConfig>( + data_source_config.method); + if (polling_interval_ < polling_config.min_polling_interval) { + LD_LOG(logger_, LogLevel::kWarn) + << "Polling interval too frequent, defaulting to " + << std::chrono::duration_cast( + polling_config.min_polling_interval) + .count() + << " seconds"; + + polling_interval_ = polling_config.min_polling_interval; + } +} + +void PollingDataSource::DoPoll() { + last_poll_start_ = std::chrono::system_clock::now(); + + auto weak_self = weak_from_this(); + requester_.Request(request_, [weak_self](network::HttpResult const& res) { + if (auto self = weak_self.lock()) { + self->HandlePollResult(res); + } + }); +} + +void PollingDataSource::HandlePollResult(network::HttpResult const& res) { + auto header_etag = res.Headers().find("etag"); + bool has_etag = header_etag != res.Headers().end(); + + if (etag_ && has_etag) { + if (etag_.value() == header_etag->second) { + // Got the same etag; we know the content has not changed. + // So we can just start the next timer. + + // We don't need to update the "request_" because it would have + // the same Etag. + StartPollingTimer(); + return; + } + } + + if (has_etag) { + config::shared::builders::HttpPropertiesBuilder< + config::shared::ServerSDK> + builder(request_.Properties()); + builder.Header("If-None-Match", header_etag->second); + request_ = network::HttpRequest(request_, builder.Build()); + + etag_ = header_etag->second; + } + + if (res.IsError()) { + auto const& error_message = res.ErrorMessage(); + status_manager_.SetState( + DataSourceStatus::DataSourceState::kInterrupted, + DataSourceStatus::ErrorInfo::ErrorKind::kNetworkError, + error_message.has_value() ? *error_message : "unknown error"); + LD_LOG(logger_, LogLevel::kWarn) + << "Polling for feature flag updates failed: " + << (error_message.has_value() ? *error_message : "unknown error"); + } else if (res.Status() == 200) { + auto const& body = res.Body(); + if (body.has_value()) { + boost::json::error_code error_code; + auto parsed = boost::json::parse(body.value(), error_code); + if (error_code) { + LD_LOG(logger_, LogLevel::kError) << kErrorParsingPut; + status_manager_.SetError( + DataSourceStatus::ErrorInfo::ErrorKind::kInvalidData, + kErrorParsingPut); + return; + } + auto poll_result = boost::json::value_to< + tl::expected>(parsed); + + if (poll_result.has_value()) { + update_sink_.Init(std::move(*poll_result)); + status_manager_.SetState( + DataSourceStatus::DataSourceState::kValid); + return; + } + LD_LOG(logger_, LogLevel::kError) << kErrorPutInvalid; + status_manager_.SetError( + DataSourceStatus::ErrorInfo::ErrorKind::kInvalidData, + kErrorPutInvalid); + return; + } + status_manager_.SetState( + DataSourceStatus::DataSourceState::kInterrupted, + DataSourceStatus::ErrorInfo::ErrorKind::kUnknown, + "polling response contained no body."); + + } else if (res.Status() == 304) { + // This should be handled ahead of here, but if we get a 304, + // and it didn't have an etag, we still don't want to try to + // parse the body. + } else { + if (network::IsRecoverableStatus(res.Status())) { + status_manager_.SetState( + DataSourceStatus::DataSourceState::kInterrupted, res.Status(), + launchdarkly::network::ErrorForStatusCode( + res.Status(), "polling request", "will retry")); + } else { + status_manager_.SetState( + DataSourceStatus::DataSourceState::kOff, res.Status(), + launchdarkly::network::ErrorForStatusCode( + res.Status(), "polling request", std::nullopt)); + // We are giving up. Do not start a new polling request. + return; + } + } + + StartPollingTimer(); +} + +void PollingDataSource::StartPollingTimer() { + auto time_since_poll_seconds = + std::chrono::duration_cast( + std::chrono::system_clock::now() - last_poll_start_); + + // Calculate a delay based on the polling interval and the duration elapsed + // since the last poll. + + // Example: If the poll took 5 seconds, and the interval is 30 seconds, then + // we want to poll after 25 seconds. We do not want the interval to be + // negative, so we clamp it to 0. + auto delay = std::chrono::seconds(std::max( + polling_interval_ - time_since_poll_seconds, std::chrono::seconds(0))); + + timer_.cancel(); + timer_.expires_after(delay); + + auto weak_self = weak_from_this(); + + timer_.async_wait([weak_self](boost::system::error_code const& ec) { + if (ec == boost::asio::error::operation_aborted) { + // The timer was canceled. Stop polling. + return; + } + if (auto self = weak_self.lock()) { + if (ec) { + // Something unexpected happened. Log it and continue to try + // polling. + LD_LOG(self->logger_, LogLevel::kError) + << "Unexpected error in polling timer: " << ec.message(); + } + self->DoPoll(); + } + }); +} + +void PollingDataSource::Start() { + status_manager_.SetState(DataSourceStatus::DataSourceState::kInitializing); + if (!request_.Valid()) { + LD_LOG(logger_, LogLevel::kError) << kCouldNotParseEndpoint; + status_manager_.SetState( + DataSourceStatus::DataSourceState::kOff, + DataSourceStatus::ErrorInfo::ErrorKind::kNetworkError, + kCouldNotParseEndpoint); + + // No need to attempt to poll if the URL is not valid. + return; + } + + DoPoll(); +} + +void PollingDataSource::ShutdownAsync(std::function completion) { + status_manager_.SetState(DataSourceStatus::DataSourceState::kInitializing); + timer_.cancel(); + if (completion) { + boost::asio::post(timer_.get_executor(), completion); + } +} + +} // namespace launchdarkly::server_side::data_sources diff --git a/libs/server-sdk/src/data_sources/polling_data_source.hpp b/libs/server-sdk/src/data_sources/polling_data_source.hpp new file mode 100644 index 000000000..1d99417b3 --- /dev/null +++ b/libs/server-sdk/src/data_sources/polling_data_source.hpp @@ -0,0 +1,58 @@ +#pragma once + +#include + +#include + +#include "data_source_event_handler.hpp" +#include "data_source_status_manager.hpp" +#include "data_source_update_sink.hpp" + +#include +#include +#include +#include +#include +#include + +namespace launchdarkly::server_side::data_sources { + +class PollingDataSource + : public ::launchdarkly::data_sources::IDataSource, + public std::enable_shared_from_this { + public: + PollingDataSource( + config::shared::built::ServiceEndpoints const& endpoints, + config::shared::built::DataSourceConfig< + config::shared::ServerSDK> const& data_source_config, + config::shared::built::HttpProperties const& http_properties, + boost::asio::any_io_executor const& ioc, + IDataSourceUpdateSink& handler, + DataSourceStatusManager& status_manager, + Logger const& logger); + + void Start() override; + void ShutdownAsync(std::function completion) override; + + private: + void DoPoll(); + void HandlePollResult(network::HttpResult const& res); + + DataSourceStatusManager& status_manager_; + std::string polling_endpoint_; + + network::AsioRequester requester_; + Logger const& logger_; + boost::asio::any_io_executor ioc_; + std::chrono::seconds polling_interval_; + network::HttpRequest request_; + std::optional etag_; + + boost::asio::steady_timer timer_; + std::chrono::time_point last_poll_start_; + IDataSourceUpdateSink& update_sink_; + + void StartPollingTimer(); +}; + +} // namespace launchdarkly::server_side::data_sources diff --git a/libs/server-sdk/src/data_sources/streaming_data_source.cpp b/libs/server-sdk/src/data_sources/streaming_data_source.cpp new file mode 100644 index 000000000..0e8b36884 --- /dev/null +++ b/libs/server-sdk/src/data_sources/streaming_data_source.cpp @@ -0,0 +1,152 @@ +#include +#include +#include +#include + +#include + +#include "streaming_data_source.hpp" + +#include + +namespace launchdarkly::server_side::data_sources { + +static char const* const kCouldNotParseEndpoint = + "Could not parse streaming endpoint URL"; + +static char const* DataSourceErrorToString(launchdarkly::sse::Error error) { + switch (error) { + case sse::Error::NoContent: + return "server responded 204 (No Content), will not attempt to " + "reconnect"; + case sse::Error::InvalidRedirectLocation: + return "server responded with an invalid redirection"; + case sse::Error::UnrecoverableClientError: + return "unrecoverable client-side error"; + default: + return "unrecognized error"; + } +} + +StreamingDataSource::StreamingDataSource( + config::shared::built::ServiceEndpoints const& endpoints, + config::shared::built::DataSourceConfig const& + data_source_config, + config::shared::built::HttpProperties http_properties, + boost::asio::any_io_executor ioc, + IDataSourceUpdateSink& handler, + DataSourceStatusManager& status_manager, + Logger const& logger) + : exec_(std::move(ioc)), + logger_(logger), + status_manager_(status_manager), + data_source_handler_( + DataSourceEventHandler(handler, logger, status_manager_)), + http_config_(std::move(http_properties)), + streaming_config_( + std::get>(data_source_config.method)), + streaming_endpoint_(endpoints.StreamingBaseUrl()) {} + +void StreamingDataSource::Start() { + status_manager_.SetState(DataSourceStatus::DataSourceState::kInitializing); + + auto updated_url = network::AppendUrl(streaming_endpoint_, + streaming_config_.streaming_path); + + // Bad URL, don't set the client. Start will then report the bad status. + if (!updated_url) { + LD_LOG(logger_, LogLevel::kError) << kCouldNotParseEndpoint; + status_manager_.SetState( + DataSourceStatus::DataSourceState::kOff, + DataSourceStatus::ErrorInfo::ErrorKind::kNetworkError, + kCouldNotParseEndpoint); + return; + } + + auto uri_components = boost::urls::parse_uri(*updated_url); + + // Unlikely that it could be parsed earlier, and it cannot be parsed now. + if (!uri_components) { + LD_LOG(logger_, LogLevel::kError) << kCouldNotParseEndpoint; + status_manager_.SetState( + DataSourceStatus::DataSourceState::kOff, + DataSourceStatus::ErrorInfo::ErrorKind::kNetworkError, + kCouldNotParseEndpoint); + return; + } + + boost::urls::url url = uri_components.value(); + + auto client_builder = launchdarkly::sse::Builder(exec_, url.buffer()); + + client_builder.method(boost::beast::http::verb::get); + + // TODO: can the read timeout be shared with *all* http requests? Or should + // it have a default in defaults.hpp? This must be greater than the + // heartbeat interval of the streaming service. + client_builder.read_timeout(std::chrono::minutes(5)); + + client_builder.write_timeout(http_config_.WriteTimeout()); + + client_builder.connect_timeout(http_config_.ConnectTimeout()); + + client_builder.initial_reconnect_delay( + streaming_config_.initial_reconnect_delay); + + for (auto const& header : http_config_.BaseHeaders()) { + client_builder.header(header.first, header.second); + } + + auto weak_self = weak_from_this(); + + client_builder.receiver([weak_self](launchdarkly::sse::Event const& event) { + if (auto self = weak_self.lock()) { + self->data_source_handler_.HandleMessage(event.type(), + event.data()); + // TODO: Use the result of handle message to restart the + // event source if we got bad data. sc-204387 + } + }); + + client_builder.logger([weak_self](auto msg) { + if (auto self = weak_self.lock()) { + LD_LOG(self->logger_, LogLevel::kDebug) << msg; + } + }); + + client_builder.errors([weak_self](auto error) { + if (auto self = weak_self.lock()) { + auto error_string = DataSourceErrorToString(error); + LD_LOG(self->logger_, LogLevel::kError) << error_string; + self->status_manager_.SetState( + DataSourceStatus::DataSourceState::kOff, + DataSourceStatus::ErrorInfo::ErrorKind::kErrorResponse, + error_string); + } + }); + + client_ = client_builder.build(); + + if (!client_) { + LD_LOG(logger_, LogLevel::kError) << kCouldNotParseEndpoint; + status_manager_.SetState( + DataSourceStatus::DataSourceState::kOff, + DataSourceStatus::ErrorInfo::ErrorKind::kNetworkError, + kCouldNotParseEndpoint); + return; + } + client_->async_connect(); +} + +void StreamingDataSource::ShutdownAsync(std::function completion) { + if (client_) { + status_manager_.SetState( + DataSourceStatus::DataSourceState::kInitializing); + return client_->async_shutdown(std::move(completion)); + } + if (completion) { + boost::asio::post(exec_, completion); + } +} +} // namespace launchdarkly::server_side::data_sources diff --git a/libs/server-sdk/src/data_sources/streaming_data_source.hpp b/libs/server-sdk/src/data_sources/streaming_data_source.hpp new file mode 100644 index 000000000..9663501f0 --- /dev/null +++ b/libs/server-sdk/src/data_sources/streaming_data_source.hpp @@ -0,0 +1,55 @@ +#pragma once + +#include +using namespace std::chrono_literals; + +#include + +#include "data_source_event_handler.hpp" +#include "data_source_status_manager.hpp" +#include "data_source_update_sink.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace launchdarkly::server_side::data_sources { + +class StreamingDataSource final + : public ::launchdarkly::data_sources::IDataSource, + public std::enable_shared_from_this { + public: + StreamingDataSource( + config::shared::built::ServiceEndpoints const& endpoints, + config::shared::built::DataSourceConfig< + config::shared::ServerSDK> const& data_source_config, + config::shared::built::HttpProperties http_properties, + boost::asio::any_io_executor ioc, + IDataSourceUpdateSink& handler, + DataSourceStatusManager& status_manager, + Logger const& logger); + + void Start() override; + void ShutdownAsync(std::function completion) override; + + private: + boost::asio::any_io_executor exec_; + DataSourceStatusManager& status_manager_; + DataSourceEventHandler data_source_handler_; + std::string streaming_endpoint_; + + config::shared::built::StreamingConfig + streaming_config_; + + config::shared::built::HttpProperties http_config_; + + Logger const& logger_; + std::shared_ptr client_; +}; +} // namespace launchdarkly::server_side::data_sources diff --git a/libs/server-sdk/src/data_store/data_kind.hpp b/libs/server-sdk/src/data_store/data_kind.hpp new file mode 100644 index 000000000..17ead105b --- /dev/null +++ b/libs/server-sdk/src/data_store/data_kind.hpp @@ -0,0 +1,7 @@ +#pragma once + +#include + +namespace launchdarkly::server_side::data_store { +enum class DataKind : std::size_t { kFlag = 0, kSegment = 1, kKindCount = 2 }; +} // namespace launchdarkly::server_side::data_store diff --git a/libs/server-sdk/src/data_store/data_store.hpp b/libs/server-sdk/src/data_store/data_store.hpp new file mode 100644 index 000000000..3ae0184e0 --- /dev/null +++ b/libs/server-sdk/src/data_store/data_store.hpp @@ -0,0 +1,81 @@ +#pragma once + +#include "descriptors.hpp" + +#include +#include +#include + +#include +#include +#include + +namespace launchdarkly::server_side::data_store { + +/** + * Interface for readonly access to SDK data. + */ +class IDataStore { + public: + /** + * Get a flag from the store. + * + * @param key The key for the flag. + * @return Returns a shared_ptr to the FlagDescriptor, or a nullptr if there + * is no such flag or the flag was deleted. + */ + [[nodiscard]] virtual std::shared_ptr GetFlag( + std::string const& key) const = 0; + + /** + * Get a segment from the store. + * + * @param key The key for the segment. + * @return Returns a shared_ptr to the SegmentDescriptor, or a nullptr if + * there is no such segment, or the segment was deleted. + */ + [[nodiscard]] virtual std::shared_ptr GetSegment( + std::string const& key) const = 0; + + /** + * Get all of the flags. + * + * @return Returns an unordered map of FlagDescriptors. + */ + [[nodiscard]] virtual std::unordered_map> + AllFlags() const = 0; + + /** + * Get all of the segments. + * + * @return Returns an unordered map of SegmentDescriptors. + */ + [[nodiscard]] virtual std::unordered_map> + AllSegments() const = 0; + + /** + * Check if the store is initialized. + * + * @return Returns true if the store is initialized. + */ + [[nodiscard]] virtual bool Initialized() const = 0; + + /** + * Get a description of the store. + * @return Returns a string containing a description of the store. + */ + [[nodiscard]] virtual std::string const& Description() const = 0; + + IDataStore(IDataStore const& item) = delete; + IDataStore(IDataStore&& item) = delete; + IDataStore& operator=(IDataStore const&) = delete; + IDataStore& operator=(IDataStore&&) = delete; + virtual ~IDataStore() = default; + + protected: + IDataStore() = default; +}; + +} // namespace launchdarkly::server_side::data_store diff --git a/libs/server-sdk/src/data_store/data_store_updater.cpp b/libs/server-sdk/src/data_store/data_store_updater.cpp new file mode 100644 index 000000000..973a7d585 --- /dev/null +++ b/libs/server-sdk/src/data_store/data_store_updater.cpp @@ -0,0 +1,76 @@ +#include "data_store_updater.hpp" + +#include +#include + +namespace launchdarkly::server_side::data_store { + +std::unique_ptr DataStoreUpdater::OnFlagChange( + launchdarkly::server_side::IChangeNotifier::ChangeHandler handler) { + std::lock_guard lock{signal_mutex_}; + + return std::make_unique( + signals_.connect(handler)); +} + +void DataStoreUpdater::Init(launchdarkly::data_model::SDKDataSet data_set) { + // Optional outside the HasListeners() scope, this allows for the changes + // to be calculated before the update and then the notification to be + // sent after the update completes. + std::optional change_notifications; + if (HasListeners()) { + DependencySet updated_items; + + CalculateChanges(DataKind::kFlag, store_.AllFlags(), data_set.flags, + updated_items); + CalculateChanges(DataKind::kSegment, store_.AllSegments(), + data_set.segments, updated_items); + change_notifications = updated_items; + } + + dependency_tracker_.Clear(); + for (auto const& flag : data_set.flags) { + dependency_tracker_.UpdateDependencies(flag.first, flag.second); + } + for (auto const& segment : data_set.segments) { + dependency_tracker_.UpdateDependencies(segment.first, segment.second); + } + // Data will move into the store, so we want to update dependencies before + // it is moved. + sink_.Init(std::move(data_set)); + // After updating the sink, let listeners know of changes. + if (change_notifications) { + NotifyChanges(std::move(*change_notifications)); + } +} + +void DataStoreUpdater::Upsert(std::string const& key, + data_store::FlagDescriptor flag) { + UpsertCommon(DataKind::kFlag, key, store_.GetFlag(key), std::move(flag)); +} + +void DataStoreUpdater::Upsert(std::string const& key, + data_store::SegmentDescriptor segment) { + UpsertCommon(DataKind::kSegment, key, store_.GetSegment(key), + std::move(segment)); +} + +bool DataStoreUpdater::HasListeners() const { + std::lock_guard lock{signal_mutex_}; + return !signals_.empty(); +} + +void DataStoreUpdater::NotifyChanges(DependencySet changes) { + std::lock_guard lock{signal_mutex_}; + auto flag_changes = changes.SetForKind(DataKind::kFlag); + // Only emit an event if there are changes. + if (!flag_changes.empty()) { + signals_(std::make_shared(std::move(flag_changes))); + } +} + +DataStoreUpdater::DataStoreUpdater(IDataSourceUpdateSink& sink, + IDataStore const& store) + : sink_(sink), store_(store) {} + +} // namespace launchdarkly::server_side::data_store diff --git a/libs/server-sdk/src/data_store/data_store_updater.hpp b/libs/server-sdk/src/data_store/data_store_updater.hpp new file mode 100644 index 000000000..0b5a2e153 --- /dev/null +++ b/libs/server-sdk/src/data_store/data_store_updater.hpp @@ -0,0 +1,121 @@ +#pragma once + +#include "../data_sources/data_source_update_sink.hpp" +#include "data_store.hpp" +#include "dependency_tracker.hpp" + +#include + +#include + +#include + +namespace launchdarkly::server_side::data_store { + +class DataStoreUpdater + : public launchdarkly::server_side::data_sources::IDataSourceUpdateSink, + public launchdarkly::server_side::IChangeNotifier { + public: + template + using Collection = data_model::SDKDataSet::Collection; + + template + using SharedItem = std::shared_ptr>; + + template + using SharedCollection = + std::unordered_map>; + + DataStoreUpdater(IDataSourceUpdateSink& sink, IDataStore const& store); + + std::unique_ptr OnFlagChange(ChangeHandler handler) override; + + void Init(launchdarkly::data_model::SDKDataSet data_set) override; + void Upsert(std::string const& key, FlagDescriptor flag) override; + void Upsert(std::string const& key, SegmentDescriptor segment) override; + ~DataStoreUpdater() override = default; + + DataStoreUpdater(DataStoreUpdater const& item) = delete; + DataStoreUpdater(DataStoreUpdater&& item) = delete; + DataStoreUpdater& operator=(DataStoreUpdater const&) = delete; + DataStoreUpdater& operator=(DataStoreUpdater&&) = delete; + + private: + bool HasListeners() const; + + template + void UpsertCommon( + DataKind kind, + std::string key, + SharedItem existing, + launchdarkly::data_model::ItemDescriptor updated) { + if (existing && (updated.version <= existing->version)) { + // Out of order update, ignore it. + return; + } + + dependency_tracker_.UpdateDependencies(key, updated); + + if (HasListeners()) { + auto updated_deps = DependencySet(); + dependency_tracker_.CalculateChanges(kind, key, updated_deps); + NotifyChanges(updated_deps); + } + + sink_.Upsert(key, updated); + } + + template + void CalculateChanges( + DataKind kind, + SharedCollection const& existing_flags_or_segments, + Collection const& new_flags_or_segments, + DependencySet& updated_items) { + for (auto const& old_flag_or_segment : existing_flags_or_segments) { + auto new_flag_or_segment = + new_flags_or_segments.find(old_flag_or_segment.first); + if (new_flag_or_segment != new_flags_or_segments.end() && + new_flag_or_segment->second.version <= + old_flag_or_segment.second->version) { + continue; + } + + // Deleted. + dependency_tracker_.CalculateChanges( + kind, old_flag_or_segment.first, updated_items); + } + + for (auto const& flag_or_segment : new_flags_or_segments) { + auto oldItem = + existing_flags_or_segments.find(flag_or_segment.first); + if (oldItem != existing_flags_or_segments.end() && + flag_or_segment.second.version <= oldItem->second->version) { + continue; + } + + // Updated or new. + dependency_tracker_.CalculateChanges(kind, flag_or_segment.first, + updated_items); + } + } + + void NotifyChanges(DependencySet changes); + + IDataSourceUpdateSink& sink_; + IDataStore const& store_; + + boost::signals2::signal)> signals_; + + // Recursive mutex so that has_listeners can non-conditionally lock + // the mutex. Otherwise, a pre-condition for the call would be holding + // the mutex, which is more difficult to keep consistent over the code + // lifetime. + // + // Signals themselves are thread-safe, and this mutex only allows us to + // prevent the addition of listeners between the listener check, calculation + // and dispatch of events. + mutable std::recursive_mutex signal_mutex_; + + DependencyTracker dependency_tracker_; +}; +} // namespace launchdarkly::server_side::data_store diff --git a/libs/server-sdk/src/data_store/dependency_tracker.cpp b/libs/server-sdk/src/data_store/dependency_tracker.cpp new file mode 100644 index 000000000..d401fe327 --- /dev/null +++ b/libs/server-sdk/src/data_store/dependency_tracker.cpp @@ -0,0 +1,197 @@ +#include "dependency_tracker.hpp" +#include "tagged_data.hpp" + +#include + +namespace launchdarkly::server_side::data_store { + +DependencySet::DependencySet() + : data_{ + TaggedData>(DataKind::kFlag), + TaggedData>(DataKind::kSegment), + } {} + +void DependencySet::Set(DataKind kind, std::string key) { + Data(kind).emplace(std::move(key)); +} + +void DependencySet::Remove(DataKind kind, std::string const& key) { + Data(kind).erase(key); +} + +bool DependencySet::Contains(DataKind kind, std::string const& key) const { + return Data(kind).count(key) != 0; +} + +std::size_t DependencySet::Size() const { + std::size_t size = 0; + for (auto data_kind : data_) { + size += data_kind.Data().size(); + } + return size; +} + +std::array>, 2>::const_iterator +DependencySet::begin() const { + return data_.begin(); +} + +std::array>, 2>::const_iterator +DependencySet::end() const { + return data_.end(); +} + +std::set const& DependencySet::SetForKind(DataKind kind) { + return Data(kind); +} + +std::set const& DependencySet::Data(DataKind kind) const { + return data_[static_cast>(kind)].Data(); +} + +std::set& DependencySet::Data(DataKind kind) { + return data_[static_cast>(kind)].Data(); +} + +DependencyMap::DependencyMap() + : data_{ + TaggedData>( + DataKind::kFlag), + TaggedData>( + DataKind::kSegment), + } {} + +void DependencyMap::Set(DataKind kind, std::string key, DependencySet val) { + data_[static_cast>(kind)].Data().emplace( + std::move(key), std::move(val)); +} + +std::optional DependencyMap::Get(DataKind kind, + std::string const& key) const { + auto const& scope = + data_[static_cast>(kind)].Data(); + auto found = scope.find(key); + if (found != scope.end()) { + return found->second; + } + return std::nullopt; +} + +void DependencyMap::Clear() { + for (auto& data_kind : data_) { + data_kind.Data().clear(); + } +} + +std::array>, + 2>::const_iterator +DependencyMap::begin() const { + return data_.begin(); +} + +std::array>, + 2>::const_iterator +DependencyMap::end() const { + return data_.end(); +} + +void DependencyTracker::UpdateDependencies( + std::string const& key, + DependencyTracker::FlagDescriptor const& flag) { + DependencySet dependencies; + if (flag.item) { + for (auto const& prereq : flag.item->prerequisites) { + dependencies.Set(DataKind::kFlag, prereq.key); + } + + for (auto const& rule : flag.item->rules) { + CalculateClauseDeps(dependencies, rule.clauses); + } + } + UpdateDependencies(DataKind::kFlag, key, dependencies); +} + +void DependencyTracker::UpdateDependencies( + std::string const& key, + DependencyTracker::SegmentDescriptor const& segment) { + DependencySet dependencies; + if (segment.item) { + for (auto const& rule : segment.item->rules) { + CalculateClauseDeps(dependencies, rule.clauses); + } + } + UpdateDependencies(DataKind::kSegment, key, dependencies); +} + +// Function intentionally uses recursion. +// NOLINTBEGIN misc-no-recursion + +void DependencyTracker::CalculateChanges(DataKind kind, + std::string const& key, + DependencySet& dependency_set) { + if (!dependency_set.Contains(kind, key)) { + dependency_set.Set(kind, key); + auto affected_items = dependencies_to_.Get(kind, key); + if (affected_items) { + for (auto& deps_by_kind : *affected_items) { + for (auto& dep : deps_by_kind.Data()) { + CalculateChanges(deps_by_kind.Kind(), dep, dependency_set); + } + } + } + } +} + +// NOLINTEND misc-no-recursion + +void DependencyTracker::UpdateDependencies(DataKind kind, + std::string const& key, + DependencySet const& deps) { + auto current_deps = dependencies_from_.Get(kind, key); + if (current_deps) { + for (auto const& deps_by_kind : *current_deps) { + auto kind_of_dep = deps_by_kind.Kind(); + for (auto const& dep : deps_by_kind.Data()) { + auto deps_to_this_dep = dependencies_to_.Get(kind_of_dep, dep); + if (deps_to_this_dep) { + deps_to_this_dep->Remove(kind_of_dep, key); + } + } + } + } + + dependencies_from_.Set(kind, key, deps); + for (auto const& deps_by_kind : deps) { + for (auto const& dep : deps_by_kind.Data()) { + auto deps_to_this_dep = + dependencies_to_.Get(deps_by_kind.Kind(), dep); + if (!deps_to_this_dep) { + auto new_deps_to_this_dep = DependencySet(); + new_deps_to_this_dep.Set(kind, key); + dependencies_to_.Set(deps_by_kind.Kind(), dep, + new_deps_to_this_dep); + } else { + deps_to_this_dep->Set(kind, key); + } + } + } +} + +void DependencyTracker::CalculateClauseDeps( + DependencySet& dependencies, + std::vector const& clauses) { + for (auto const& clause : clauses) { + if (clause.op == data_model::Clause::Op::kSegmentMatch) { + for (auto const& value : clause.values) { + dependencies.Set(DataKind::kSegment, value.AsString()); + } + } + } +} + +void DependencyTracker::Clear() { + dependencies_to_.Clear(); + dependencies_from_.Clear(); +} + +} // namespace launchdarkly::server_side::data_store diff --git a/libs/server-sdk/src/data_store/dependency_tracker.hpp b/libs/server-sdk/src/data_store/dependency_tracker.hpp new file mode 100644 index 000000000..d62ba2c7c --- /dev/null +++ b/libs/server-sdk/src/data_store/dependency_tracker.hpp @@ -0,0 +1,156 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "data_kind.hpp" +#include "tagged_data.hpp" + +namespace launchdarkly::server_side::data_store { + +/** + * Class used to maintain a set of dependencies. Each dependency may be either + * a flag or segment. + * For instance, if we have a flagA, which has a prerequisite of flagB, and + * a segmentMatch targeting segmentA, then its dependency set would be + * ``` + * [{DataKind::kFlag, "flagB"}, {DataKind::kSegment, "segmentA"}] + * ``` + */ +class DependencySet { + public: + DependencySet(); + using DataType = std::array>, + static_cast(DataKind::kKindCount)>; + void Set(DataKind kind, std::string key); + + void Remove(DataKind kind, std::string const& key); + + [[nodiscard]] bool Contains(DataKind kind, std::string const& key) const; + + [[nodiscard]] std::set const& SetForKind(DataKind kind); + + /** + * Return the size of all the data kind sets. + * @return The combined size of all the data kind sets. + */ + [[nodiscard]] std::size_t Size() const; + + [[nodiscard]] typename DataType::const_iterator begin() const; + + [[nodiscard]] typename DataType::const_iterator end() const; + + private: + [[nodiscard]] std::set const& Data(DataKind kind) const; + + [[nodiscard]] std::set& Data(DataKind kind); + + DataType data_; +}; + +/** + * Class used to map flag/segments to their set of dependencies. + * For instance, if we have a flagA, which has a prerequisite of flagB, and + * a segmentMatch targeting segmentA, then a dependency map, containing + * this set, would be: + * ``` + * {{DataKind::kFlag, "flagA"}, [{DataKind::kFlag, "flagB"}, + * {DataKind::kSegment, "segmentA"}]} + * ``` + */ +class DependencyMap { + public: + DependencyMap(); + using DataType = + std::array>, + static_cast(DataKind::kKindCount)>; + void Set(DataKind kind, std::string key, DependencySet val); + + [[nodiscard]] std::optional Get( + DataKind kind, + std::string const& key) const; + + void Clear(); + + [[nodiscard]] typename DataType::const_iterator begin() const; + + [[nodiscard]] typename DataType::const_iterator end() const; + + private: + DataType data_; +}; + +/** + * This class implements a mechanism of tracking dependencies of flags and + * segments. Both the forward dependencies (flag A depends on flag B) but also + * the reverse (flag B is depended on by flagA). + */ +class DependencyTracker { + public: + using FlagDescriptor = data_model::ItemDescriptor; + using SegmentDescriptor = data_model::ItemDescriptor; + + /** + * Update the dependency tracker with a new or updated flag. + * + * @param key The key for the flag. + * @param flag A descriptor for the flag. + */ + void UpdateDependencies(std::string const& key, FlagDescriptor const& flag); + + /** + * Update the dependency tracker with a new or updated segment. + * + * @param key The key for the segment. + * @param flag A descriptor for the segment. + */ + void UpdateDependencies(std::string const& key, + SegmentDescriptor const& segment); + + /** + * Given the current dependencies, determine what flags or segments may be + * impacted by a change to the given flag/segment. + * + * @param kind The kind of data. + * @param key The key for the data. + * @param dependency_set A dependency set, which dependencies are + * accumulated in. + */ + void CalculateChanges(DataKind kind, + std::string const& key, + DependencySet& dependency_set); + + /** + * Clear all existing dependencies. + */ + void Clear(); + + private: + /** + * Common logic for dependency updates used for both flags and segments. + */ + void UpdateDependencies(DataKind kind, + std::string const& key, + DependencySet const& deps); + + DependencyMap dependencies_from_; + DependencyMap dependencies_to_; + + /** + * Determine dependencies for a set of clauses. + * @param dependencies A set of dependencies to extend. + * @param clauses The clauses to determine dependencies for. + */ + static void CalculateClauseDeps( + DependencySet& dependencies, + std::vector const& clauses); +}; + +} // namespace launchdarkly::server_side::data_store diff --git a/libs/server-sdk/src/data_store/descriptors.hpp b/libs/server-sdk/src/data_store/descriptors.hpp new file mode 100644 index 000000000..075d3a31d --- /dev/null +++ b/libs/server-sdk/src/data_store/descriptors.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include +#include +#include + +namespace launchdarkly::server_side::data_store { +using FlagDescriptor = + launchdarkly::data_model::ItemDescriptor; +using SegmentDescriptor = + launchdarkly::data_model::ItemDescriptor; +} // namespace launchdarkly::server_side::data_store diff --git a/libs/server-sdk/src/data_store/memory_store.cpp b/libs/server-sdk/src/data_store/memory_store.cpp new file mode 100644 index 000000000..321c6249d --- /dev/null +++ b/libs/server-sdk/src/data_store/memory_store.cpp @@ -0,0 +1,75 @@ + + +#include "memory_store.hpp" + +namespace launchdarkly::server_side::data_store { + +std::shared_ptr MemoryStore::GetFlag( + std::string const& key) const { + std::lock_guard lock{data_mutex_}; + auto found = flags_.find(key); + if (found != flags_.end()) { + return found->second; + } + return nullptr; +} + +std::shared_ptr MemoryStore::GetSegment( + std::string const& key) const { + std::lock_guard lock{data_mutex_}; + auto found = segments_.find(key); + if (found != segments_.end()) { + return found->second; + } + return nullptr; +} + +std::unordered_map> +MemoryStore::AllFlags() const { + std::lock_guard lock{data_mutex_}; + return {flags_}; +} + +std::unordered_map> +MemoryStore::AllSegments() const { + std::lock_guard lock{data_mutex_}; + return {segments_}; +} + +bool MemoryStore::Initialized() const { + std::lock_guard lock{data_mutex_}; + return initialized_; +} + +std::string const& MemoryStore::Description() const { + return description_; +} + +void MemoryStore::Init(launchdarkly::data_model::SDKDataSet dataSet) { + std::lock_guard lock{data_mutex_}; + initialized_ = true; + flags_.clear(); + segments_.clear(); + for (auto flag : dataSet.flags) { + flags_.emplace(flag.first, std::make_shared( + std::move(flag.second))); + } + for (auto segment : dataSet.segments) { + segments_.emplace(segment.first, std::make_shared( + std::move(segment.second))); + } +} + +void MemoryStore::Upsert(std::string const& key, + data_store::FlagDescriptor flag) { + std::lock_guard lock{data_mutex_}; + flags_[key] = std::make_shared(std::move(flag)); +} + +void MemoryStore::Upsert(std::string const& key, + data_store::SegmentDescriptor segment) { + std::lock_guard lock{data_mutex_}; + segments_[key] = std::make_shared(std::move(segment)); +} + +} // namespace launchdarkly::server_side::data_store diff --git a/libs/server-sdk/src/data_store/memory_store.hpp b/libs/server-sdk/src/data_store/memory_store.hpp new file mode 100644 index 000000000..f8758e4a8 --- /dev/null +++ b/libs/server-sdk/src/data_store/memory_store.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include "../data_sources/data_source_update_sink.hpp" +#include "data_store.hpp" + +#include +#include +#include +#include + +namespace launchdarkly::server_side::data_store { + +class MemoryStore : public IDataStore, + public data_sources::IDataSourceUpdateSink { + public: + std::shared_ptr GetFlag( + std::string const& key) const override; + std::shared_ptr GetSegment( + std::string const& key) const override; + + std::unordered_map> AllFlags() + const override; + std::unordered_map> + AllSegments() const override; + + bool Initialized() const override; + std::string const& Description() const override; + + void Init(launchdarkly::data_model::SDKDataSet dataSet) override; + void Upsert(std::string const& key, FlagDescriptor flag) override; + void Upsert(std::string const& key, SegmentDescriptor segment) override; + + MemoryStore() = default; + ~MemoryStore() override = default; + + MemoryStore(MemoryStore const& item) = delete; + MemoryStore(MemoryStore&& item) = delete; + MemoryStore& operator=(MemoryStore const&) = delete; + MemoryStore& operator=(MemoryStore&&) = delete; + + private: + static inline const std::string description_ = "memory"; + std::unordered_map> flags_; + std::unordered_map> + segments_; + bool initialized_ = false; + mutable std::mutex data_mutex_; +}; + +} // namespace launchdarkly::server_side::data_store diff --git a/libs/server-sdk/src/data_store/persistent/expiration_tracker.cpp b/libs/server-sdk/src/data_store/persistent/expiration_tracker.cpp new file mode 100644 index 000000000..09e6142ef --- /dev/null +++ b/libs/server-sdk/src/data_store/persistent/expiration_tracker.cpp @@ -0,0 +1,152 @@ +#include "expiration_tracker.hpp" + +namespace launchdarkly::server_side::data_store::persistent { + +void ExpirationTracker::Add(std::string const& key, + ExpirationTracker::TimePoint expiration) { + unscoped_.insert({key, expiration}); +} + +void ExpirationTracker::Remove(std::string const& key) { + unscoped_.erase(key); +} + +ExpirationTracker::TrackState ExpirationTracker::State( + std::string const& key, + ExpirationTracker::TimePoint current_time) const { + auto item = unscoped_.find(key); + if (item != unscoped_.end()) { + return State(item->second, current_time); + } + + return ExpirationTracker::TrackState::kNotTracked; +} + +void ExpirationTracker::Add(data_store::DataKind kind, + std::string const& key, + ExpirationTracker::TimePoint expiration) { + scoped_.Set(kind, key, expiration); +} + +void ExpirationTracker::Remove(data_store::DataKind kind, + std::string const& key) { + scoped_.Remove(kind, key); +} + +ExpirationTracker::TrackState ExpirationTracker::State( + data_store::DataKind kind, + std::string const& key, + ExpirationTracker::TimePoint current_time) const { + auto expiration = scoped_.Get(kind, key); + if (expiration.has_value()) { + return State(expiration.value(), current_time); + } + return ExpirationTracker::TrackState::kNotTracked; +} + +void ExpirationTracker::Clear() { + scoped_.Clear(); + unscoped_.clear(); +} +std::vector, std::string>> +ExpirationTracker::Prune(ExpirationTracker::TimePoint current_time) { + std::vector, std::string>> pruned; + + // Determine everything to be pruned. + for (auto const& item : unscoped_) { + if (State(item.second, current_time) == + ExpirationTracker::TrackState::kStale) { + pruned.emplace_back(std::nullopt, item.first); + } + } + for (auto const& scope : scoped_) { + for (auto const& item : scope.Data()) { + if (State(item.second, current_time) == + ExpirationTracker::TrackState::kStale) { + pruned.emplace_back(scope.Kind(), item.first); + } + } + } + + // Do the actual prune. + for (auto const& item : pruned) { + if (item.first.has_value()) { + scoped_.Remove(item.first.value(), item.second); + } else { + unscoped_.erase(item.second); + } + } + return pruned; +} +ExpirationTracker::TrackState ExpirationTracker::State( + ExpirationTracker::TimePoint expiration, + ExpirationTracker::TimePoint current_time) { + if (expiration > current_time) { + return ExpirationTracker::TrackState::kFresh; + } + return ExpirationTracker::TrackState::kStale; +} + +void ExpirationTracker::ScopedTtls::Set( + DataKind kind, + std::string const& key, + ExpirationTracker::TimePoint expiration) { + data_[static_cast>(kind)].Data().insert( + {key, expiration}); +} + +void ExpirationTracker::ScopedTtls::Remove(DataKind kind, + std::string const& key) { + data_[static_cast>(kind)].Data().erase( + key); +} + +void ExpirationTracker::ScopedTtls::Clear() { + for (auto& scope : data_) { + scope.Data().clear(); + } +} + +std::optional ExpirationTracker::ScopedTtls::Get( + DataKind kind, + std::string const& key) const { + auto const& scope = + data_[static_cast>(kind)]; + auto found = scope.Data().find(key); + if (found != scope.Data().end()) { + return found->second; + } + return std::nullopt; +} +ExpirationTracker::ScopedTtls::ScopedTtls() + : data_{ + TaggedData(DataKind::kFlag), + TaggedData(DataKind::kSegment), + } {} + +std::array, 2>::iterator +ExpirationTracker::ScopedTtls::begin() { + return data_.begin(); +} + +std::array, 2>::iterator +ExpirationTracker::ScopedTtls::end() { + return data_.end(); +} + +std::ostream& operator<<(std::ostream& out, + ExpirationTracker::TrackState const& state) { + switch (state) { + case ExpirationTracker::TrackState::kFresh: + out << "FRESH"; + break; + case ExpirationTracker::TrackState::kStale: + out << "STALE"; + break; + case ExpirationTracker::TrackState::kNotTracked: + out << "NOT_TRACKED"; + break; + } + return out; +} +} // namespace launchdarkly::server_side::data_store::persistent diff --git a/libs/server-sdk/src/data_store/persistent/expiration_tracker.hpp b/libs/server-sdk/src/data_store/persistent/expiration_tracker.hpp new file mode 100644 index 000000000..bc57398c0 --- /dev/null +++ b/libs/server-sdk/src/data_store/persistent/expiration_tracker.hpp @@ -0,0 +1,145 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +#include "../../data_store/data_kind.hpp" +#include "../tagged_data.hpp" + +namespace launchdarkly::server_side::data_store::persistent { + +class ExpirationTracker { + public: + using TimePoint = std::chrono::time_point; + + /** + * The state of the key in the tracker. + */ + enum class TrackState { + /** + * The key is tracked and the key expiration is in the future. + */ + kFresh, + /** + * The key is tracked and the expiration is either now or in the past. + */ + kStale, + /** + * The key is not being tracked. + */ + kNotTracked + }; + + /** + * Add an unscoped key to the tracker. + * + * @param key The key to track. + * @param expiration The time that the key expires. + * used. + */ + void Add(std::string const& key, TimePoint expiration); + + /** + * Remove an unscoped key from the tracker. + * + * @param key The key to stop tracking. + */ + void Remove(std::string const& key); + + /** + * Check the state of an unscoped key. + * + * @param key The key to check. + * @param current_time The current time. + * @return The state of the key. + */ + TrackState State(std::string const& key, TimePoint current_time) const; + + /** + * Add a scoped key to the tracker. Will use the specified TTL for the kind. + * + * @param kind The scope (kind) of the key. + * @param key The key to track. + * @param expiration The time that the key expires. + */ + void Add(data_store::DataKind kind, + std::string const& key, + TimePoint expiration); + + /** + * Remove a scoped key from the tracker. + * + * @param kind The scope (kind) of the key. + * @param key The key to stop tracking. + */ + void Remove(data_store::DataKind kind, std::string const& key); + + /** + * Check the state of a scoped key. + * + * @param kind The scope (kind) of the key. + * @param key The key to check. + * @return The state of the key. + */ + TrackState State(data_store::DataKind kind, + std::string const& key, + TimePoint current_time) const; + + /** + * Stop tracking all keys. + */ + void Clear(); + + /** + * Prune expired keys from the tracker. + * @param current_time The current time. + * @return A list of all the kinds and associated keys that expired. + * Unscoped keys will have std::nullopt as the kind. + */ + std::vector, std::string>> Prune( + TimePoint current_time); + + private: + using TtlMap = std::unordered_map; + + TtlMap unscoped_; + + static ExpirationTracker::TrackState State( + ExpirationTracker::TimePoint expiration, + ExpirationTracker::TimePoint current_time); + + class ScopedTtls { + public: + ScopedTtls(); + + using DataType = + std::array, + static_cast>( + DataKind::kKindCount)>; + void Set(DataKind kind, std::string const& key, TimePoint expiration); + void Remove(DataKind kind, std::string const& key); + std::optional Get(DataKind kind, + std::string const& key) const; + void Clear(); + + [[nodiscard]] typename DataType::iterator begin(); + + [[nodiscard]] typename DataType::iterator end(); + + private: + DataType data_; + }; + + ScopedTtls scoped_; +}; + +std::ostream& operator<<(std::ostream& out, + ExpirationTracker::TrackState const& state); + +} // namespace launchdarkly::server_side::data_store::persistent diff --git a/libs/server-sdk/src/data_store/persistent/persistent_data_store.cpp b/libs/server-sdk/src/data_store/persistent/persistent_data_store.cpp new file mode 100644 index 000000000..706f78796 --- /dev/null +++ b/libs/server-sdk/src/data_store/persistent/persistent_data_store.cpp @@ -0,0 +1,3 @@ +#include "persistent_data_store.hpp" + +namespace launchdarkly::server_side::data_store::persistent {} diff --git a/libs/server-sdk/src/data_store/persistent/persistent_data_store.hpp b/libs/server-sdk/src/data_store/persistent/persistent_data_store.hpp new file mode 100644 index 000000000..523e09889 --- /dev/null +++ b/libs/server-sdk/src/data_store/persistent/persistent_data_store.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include "../../data_sources/data_source_update_sink.hpp" +#include "../data_store.hpp" +#include "../memory_store.hpp" +#include "expiration_tracker.hpp" + +#include + +#include +#include +#include +#include + +namespace launchdarkly::server_side::data_store::persistent { + +class PersistentStore : public IDataStore, + public data_sources::IDataSourceUpdateSink { + public: + std::shared_ptr GetFlag( + std::string const& key) const override; + std::shared_ptr GetSegment( + std::string const& key) const override; + + std::unordered_map> AllFlags() + const override; + std::unordered_map> + AllSegments() const override; + + bool Initialized() const override; + std::string const& Description() const override; + + void Init(launchdarkly::data_model::SDKDataSet dataSet) override; + void Upsert(std::string const& key, FlagDescriptor flag) override; + void Upsert(std::string const& key, SegmentDescriptor segment) override; + + PersistentStore() = default; + ~PersistentStore() override = default; + + PersistentStore(PersistentStore const& item) = delete; + PersistentStore(PersistentStore&& item) = delete; + PersistentStore& operator=(PersistentStore const&) = delete; + PersistentStore& operator=(PersistentStore&&) = delete; + + private: + MemoryStore memory_store_; + std::shared_ptr persistent_store_core_; + ExpirationTracker ttl_tracker_; +}; + +} // namespace launchdarkly::server_side::data_store::persistent diff --git a/libs/server-sdk/src/data_store/tagged_data.hpp b/libs/server-sdk/src/data_store/tagged_data.hpp new file mode 100644 index 000000000..320146721 --- /dev/null +++ b/libs/server-sdk/src/data_store/tagged_data.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include + +#include "data_kind.hpp" + +namespace launchdarkly::server_side::data_store { +/** + * Class which can be used to tag a collection with the DataKind that collection + * is for. This is primarily to decrease the complexity of iterating collections + * allowing for a kvp style iteration, but with an array storage container. + * @tparam Storage + */ +template +class TaggedData { + public: + explicit TaggedData(launchdarkly::server_side::data_store::DataKind kind) + : kind_(kind) {} + [[nodiscard]] launchdarkly::server_side::data_store::DataKind Kind() const { + return kind_; + } + [[nodiscard]] Storage const& Data() const { return storage_; } + + [[nodiscard]] Storage& Data() { return storage_; } + + private: + launchdarkly::server_side::data_store::DataKind kind_; + Storage storage_; +}; + +} // namespace launchdarkly::server_side::data_store diff --git a/libs/server-sdk/src/evaluation/bucketing.cpp b/libs/server-sdk/src/evaluation/bucketing.cpp new file mode 100644 index 000000000..1333ee1c0 --- /dev/null +++ b/libs/server-sdk/src/evaluation/bucketing.cpp @@ -0,0 +1,209 @@ +#include "bucketing.hpp" + +#include +#include +#include + +#include +#include + +namespace launchdarkly::server_side::evaluation { + +using namespace launchdarkly::data_model; + +double const kBucketHashScale = static_cast(0x0FFFFFFFFFFFFFFF); + +AttributeReference const& Key(); + +std::optional ContextHash(Value const& value, + BucketPrefix prefix); + +std::optional BucketValue(Value const& value); + +bool IsIntegral(double f); + +BucketPrefix::BucketPrefix(Seed seed) : prefix_(seed) {} + +BucketPrefix::BucketPrefix(std::string key, std::string salt) + : prefix_(KeyAndSalt{key, salt}) {} + +std::ostream& operator<<(std::ostream& os, BucketPrefix const& prefix) { + std::visit( + [&](auto&& arg) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + os << arg.key << "." << arg.salt; + } else if constexpr (std::is_same_v) { + os << arg; + } + }, + prefix.prefix_); + return os; +} + +BucketResult::BucketResult(Flag::Rollout::WeightedVariation weighted_variation, + bool is_experiment) + : variation_index_(weighted_variation.variation), + in_experiment_(is_experiment && !weighted_variation.untracked) {} + +BucketResult::BucketResult(Flag::Variation variation, bool in_experiment) + : variation_index_(variation), in_experiment_(in_experiment) {} + +BucketResult::BucketResult(Flag::Variation variation) + : variation_index_(variation), in_experiment_(false) {} + +std::size_t BucketResult::VariationIndex() const { + return variation_index_; +} + +bool BucketResult::InExperiment() const { + return in_experiment_; +} + +tl::expected, Error> Bucket( + Context const& context, + AttributeReference const& by_attr, + BucketPrefix const& prefix, + bool is_experiment, + std::string const& context_kind) { + AttributeReference const& ref = is_experiment ? Key() : by_attr; + + if (!ref.Valid()) { + return tl::make_unexpected( + Error::InvalidAttributeReference(ref.RedactionName())); + } + + Value const& value = context.Get(context_kind, ref); + + bool is_bucketable = value.Type() == Value::Type::kNumber || + value.Type() == Value::Type::kString; + + if (is_bucketable) { + return std::make_pair(ContextHash(value, prefix).value_or(0.0), + RolloutKindLookup::kPresent); + } + + auto rollout_context_found = + std::count(context.Kinds().begin(), context.Kinds().end(), + context_kind) > 0; + + return std::make_pair(0.0, rollout_context_found + ? RolloutKindLookup::kPresent + : RolloutKindLookup::kAbsent); +} + +AttributeReference const& Key() { + static AttributeReference const key{"key"}; + LD_ASSERT(key.Valid()); + return key; +} + +std::optional ContextHash(Value const& value, BucketPrefix prefix) { + using namespace launchdarkly::encoding; + + std::optional id = BucketValue(value); + if (!id) { + return std::nullopt; + } + + std::stringstream input; + input << prefix << "." << *id; + + std::array const sha1hash = + Sha1String(input.str()); + + std::string const sha1hash_hexed = Base16Encode(sha1hash); + + std::string const sha1hash_hexed_first_15 = sha1hash_hexed.substr(0, 15); + + try { + unsigned long long as_number = + std::stoull(sha1hash_hexed_first_15.data(), nullptr, /* base */ 16); + + double as_double = static_cast(as_number); + return as_double / kBucketHashScale; + + } catch (std::invalid_argument) { + return std::nullopt; + } catch (std::out_of_range) { + return std::nullopt; + } +} + +std::optional BucketValue(Value const& value) { + switch (value.Type()) { + case Value::Type::kString: + return value.AsString(); + case Value::Type::kNumber: { + if (IsIntegral(value.AsDouble())) { + return std::to_string(value.AsInt()); + } + return std::nullopt; + } + default: + return std::nullopt; + } +} + +bool IsIntegral(double f) { + return std::trunc(f) == f; +} + +tl::expected Variation( + Flag::VariationOrRollout const& vr, + std::string const& flag_key, + Context const& context, + std::optional const& salt) { + if (!salt) { + return tl::make_unexpected(Error::MissingSalt(flag_key)); + } + return std::visit( + [&](auto&& arg) -> tl::expected { + using T = std::decay_t; + if constexpr (std::is_same_v>) { + if (!arg) { + return tl::make_unexpected( + Error::RolloutMissingVariations()); + } + return BucketResult(*arg); + } else if constexpr (std::is_same_v) { + if (arg.variations.empty()) { + return tl::make_unexpected( + Error::RolloutMissingVariations()); + } + + bool is_experiment = + arg.kind == Flag::Rollout::Kind::kExperiment; + + std::optional prefix = + arg.seed ? BucketPrefix(*arg.seed) + : BucketPrefix(flag_key, *salt); + + auto bucketing_result = Bucket(context, arg.bucketBy, *prefix, + is_experiment, arg.contextKind); + if (!bucketing_result) { + return tl::make_unexpected(bucketing_result.error()); + } + + auto [bucket, lookup] = *bucketing_result; + + double sum = 0.0; + + for (const auto& variation : arg.variations) { + sum += variation.weight / kBucketScale; + if (bucket < sum) { + return BucketResult( + variation, + is_experiment && + lookup == RolloutKindLookup::kPresent); + } + } + + return BucketResult( + arg.variations.back(), + is_experiment && lookup == RolloutKindLookup::kPresent); + } + }, + vr); +} +} // namespace launchdarkly::server_side::evaluation diff --git a/libs/server-sdk/src/evaluation/bucketing.hpp b/libs/server-sdk/src/evaluation/bucketing.hpp new file mode 100644 index 000000000..346374590 --- /dev/null +++ b/libs/server-sdk/src/evaluation/bucketing.hpp @@ -0,0 +1,123 @@ +#pragma once + +#include "evaluation_error.hpp" + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +namespace launchdarkly::server_side::evaluation { + +double const kBucketScale = 100'000.0; + +enum RolloutKindLookup { + /* The rollout's context kind was found in the supplied evaluation context. + */ + kPresent, + /* The rollout's context kind was not found in the supplied evaluation + * context. */ + kAbsent +}; + +/** + * Bucketing is performed by hashing an input string. This string + * may be comprised of a seed (if the flag rule has a seed) or a combined + * key/salt pair. + */ +class BucketPrefix { + public: + struct KeyAndSalt { + std::string key; + std::string salt; + }; + + using Seed = std::int64_t; + + /** + * Constructs a BucketPrefix from a seed value. + * @param seed Value of the seed. + */ + explicit BucketPrefix(Seed seed); + + /** + * Constructs a BucketPrefix from a key and salt. + * @param key Key to use. + * @param salt Salt to use. + */ + BucketPrefix(std::string key, std::string salt); + + friend std::ostream& operator<<(std::ostream& os, + BucketPrefix const& prefix); + + private: + std::variant prefix_; +}; + +using ContextHashValue = double; + +/** + * Computes the context hash value for an attribute in the given context + * identified by the given attribute reference. The hash value is + * augmented with the supplied bucket prefix. + * + * @param context Context to query. + * @param by_attr Identifier of the attribute to hash. If is_experiment is true, + * then "key" will be used regardless of by_attr's value. + * @param prefix Prefix to use when hashing. + * @param is_experiment Whether this rollout is an experiment. + * @param context_kind Which kind to inspect in the context. + * @return A context hash value and indication of whether or not context_kind + * was found in the context. + */ +tl::expected, Error> Bucket( + Context const& context, + AttributeReference const& by_attr, + BucketPrefix const& prefix, + bool is_experiment, + std::string const& context_kind); + +class BucketResult { + public: + BucketResult( + data_model::Flag::Rollout::WeightedVariation weighted_variation, + bool is_experiment); + + BucketResult(data_model::Flag::Variation variation, bool in_experiment); + + BucketResult(data_model::Flag::Variation variation); + + [[nodiscard]] std::size_t VariationIndex() const; + + [[nodiscard]] bool InExperiment() const; + + private: + std::size_t variation_index_; + bool in_experiment_; +}; + +/** + * Given a variation or rollout and associated flag key, computes the proper + * variation index for the context. For a plain variation, this is simply the + * variation index. For a rollout, this utilizes the Bucket function. + * + * @param vr Variation or rollout. + * @param flag_key Key of flag. + * @param context Context to bucket. + * @param salt Salt to use when bucketing. + * @return A BucketResult on success, or an error if bucketing failed. + */ +tl::expected Variation( + data_model::Flag::VariationOrRollout const& vr, + std::string const& flag_key, + launchdarkly::Context const& context, + std::optional const& salt); + +} // namespace launchdarkly::server_side::evaluation diff --git a/libs/server-sdk/src/evaluation/detail/evaluation_stack.cpp b/libs/server-sdk/src/evaluation/detail/evaluation_stack.cpp new file mode 100644 index 000000000..7c971f510 --- /dev/null +++ b/libs/server-sdk/src/evaluation/detail/evaluation_stack.cpp @@ -0,0 +1,30 @@ +#include "evaluation_stack.hpp" + +namespace launchdarkly::server_side::evaluation::detail { + +Guard::Guard(std::unordered_set& set, std::string key) + : set_(set), key_(std::move(key)) { + set_.insert(key_); +} + +Guard::~Guard() { + set_.erase(key_); +} + +std::optional EvaluationStack::NoticePrerequisite( + std::string prerequisite_key) { + if (prerequisites_seen_.count(prerequisite_key) != 0) { + return std::nullopt; + } + return std::make_optional(prerequisites_seen_, + std::move(prerequisite_key)); +} + +std::optional EvaluationStack::NoticeSegment(std::string segment_key) { + if (segments_seen_.count(segment_key) != 0) { + return std::nullopt; + } + return std::make_optional(segments_seen_, std::move(segment_key)); +} + +} // namespace launchdarkly::server_side::evaluation::detail diff --git a/libs/server-sdk/src/evaluation/detail/evaluation_stack.hpp b/libs/server-sdk/src/evaluation/detail/evaluation_stack.hpp new file mode 100644 index 000000000..7022dc375 --- /dev/null +++ b/libs/server-sdk/src/evaluation/detail/evaluation_stack.hpp @@ -0,0 +1,60 @@ +#pragma once + +#include +#include +#include + +namespace launchdarkly::server_side::evaluation::detail { + +/** + * Guard is an object used to track that a segment or flag key has been noticed. + * Upon destruction, the key is forgotten. + */ +struct Guard { + Guard(std::unordered_set& set, std::string key); + ~Guard(); + + Guard(Guard const&) = delete; + Guard& operator=(Guard const&) = delete; + + Guard(Guard&&) = delete; + Guard& operator=(Guard&&) = delete; + + private: + std::unordered_set& set_; + std::string const key_; +}; + +/** + * EvaluationStack is used to track which segments and flags have been noticed + * during evaluation in order to detect circular references. + */ +class EvaluationStack { + public: + EvaluationStack() = default; + + /** + * If the given prerequisite key has not been seen, marks it as seen + * and returns a Guard object. Otherwise, returns std::nullopt. + * + * @param prerequisite_key Key of the prerequisite. + * @return Guard object if not seen before, otherwise std::nullopt. + */ + [[nodiscard]] std::optional NoticePrerequisite( + std::string prerequisite_key); + + /** + * If the given segment key has not been seen, marks it as seen + * and returns a Guard object. Otherwise, returns std::nullopt. + * + * @param prerequisite_key Key of the segment. + * @return Guard object if not seen before, otherwise std::nullopt. + */ + [[nodiscard]] std::optional NoticeSegment(std::string segment_key); + + private: + std::unordered_set prerequisites_seen_; + std::unordered_set segments_seen_; +}; + +} // namespace launchdarkly::server_side::evaluation::detail diff --git a/libs/server-sdk/src/evaluation/detail/semver_operations.cpp b/libs/server-sdk/src/evaluation/detail/semver_operations.cpp new file mode 100644 index 000000000..1ecc471d7 --- /dev/null +++ b/libs/server-sdk/src/evaluation/detail/semver_operations.cpp @@ -0,0 +1,203 @@ +#include "semver_operations.hpp" + +#include + +#include +#include +#include + +#include + +namespace launchdarkly::server_side::evaluation::detail { + +/* + * Official SemVer 2.0 Regex + * https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string + * + * Modified for LaunchDarkly usage to allow missing minor and patch versions, + * i.e. "1" means "1.0.0" or "1.2" means "1.2.0". + */ +char const* const kSemVerRegex = + R"(^(?0|[1-9]\d*)(\.(?0|[1-9]\d*))?(\.(?0|[1-9]\d*))?(?:-(?(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$)"; + +/** + * Cache the parsed regex so it doesn't need to be rebuilt constantly. + * + * From Boost docs: + * Class basic_regex and its typedefs regex and wregex are thread safe, in that + * compiled regular expressions can safely be shared between threads. + */ +static boost::regex const& SemVerRegex() { + static boost::regex regex{kSemVerRegex, boost::regex_constants::no_except}; + LD_ASSERT(regex.status() == 0); + return regex; +} + +SemVer::SemVer(VersionType major, + VersionType minor, + VersionType patch, + std::vector prerelease) + : major_(major), minor_(minor), patch_(patch), prerelease_(prerelease) {} + +SemVer::SemVer(VersionType major, VersionType minor, VersionType patch) + : major_(major), minor_(minor), patch_(patch), prerelease_(std::nullopt) {} + +SemVer::SemVer() : SemVer(0, 0, 0) {} + +SemVer::VersionType SemVer::Major() const { + return major_; +} + +SemVer::VersionType SemVer::Minor() const { + return minor_; +} + +SemVer::VersionType SemVer::Patch() const { + return patch_; +} + +std::optional> const& SemVer::Prerelease() const { + return prerelease_; +} + +bool operator<(SemVer::Token const& lhs, SemVer::Token const& rhs) { + if (lhs.index() != rhs.index()) { + /* Numeric identifiers (index 0 of variant) always have lower precedence +than non-numeric identifiers. */ + return lhs.index() < rhs.index(); + } + if (lhs.index() == 0) { + return std::get<0>(lhs) < std::get<0>(rhs); + } + return std::get<1>(lhs) < std::get<1>(rhs); +} + +bool operator==(SemVer const& lhs, SemVer const& rhs) { + return lhs.Major() == rhs.Major() && lhs.Minor() == rhs.Minor() && + lhs.Patch() == rhs.Patch() && lhs.Prerelease() == rhs.Prerelease(); +} + +bool operator<(SemVer const& lhs, SemVer const& rhs) { + if (lhs.Major() < rhs.Major()) { + return true; + } + if (lhs.Major() > rhs.Major()) { + return false; + } + if (lhs.Minor() < rhs.Minor()) { + return true; + } + if (lhs.Minor() > rhs.Minor()) { + return false; + } + if (lhs.Patch() < rhs.Patch()) { + return true; + } + if (lhs.Patch() > rhs.Patch()) { + return false; + } + // At this point, lhs and rhs have equal major/minor/patch versions. + if (!lhs.Prerelease() && !rhs.Prerelease()) { + return false; + } + if (lhs.Prerelease() && !rhs.Prerelease()) { + return true; + } + if (!lhs.Prerelease() && rhs.Prerelease()) { + return false; + } + return *lhs.Prerelease() < *rhs.Prerelease(); +} + +bool operator>(SemVer const& lhs, SemVer const& rhs) { + return rhs < lhs; +} + +std::optional SemVer::Parse(std::string const& value) { + if (value.empty()) { + return std::nullopt; + } + + boost::regex const& semver_regex = SemVerRegex(); + boost::smatch match; + + try { + if (!boost::regex_match(value, match, semver_regex)) { + // Not a semantic version. + return std::nullopt; + } + } catch (std::runtime_error) { + /* std::runtime_error if the complexity of matching the expression + * against an N character string begins to exceed O(N2), or if the + * program runs out of stack space while matching the expression + * (if Boost.Regex is configured in recursive mode), or if the matcher + * exhausts its permitted memory allocation (if Boost.Regex + * is configured in non-recursive mode).*/ + return std::nullopt; + } + + SemVer::VersionType major = 0; + SemVer::VersionType minor = 0; + SemVer::VersionType patch = 0; + + try { + if (match["major"].matched) { + major = boost::lexical_cast(match["major"]); + } + if (match["minor"].matched) { + minor = boost::lexical_cast(match["minor"]); + } + if (match["patch"].matched) { + patch = boost::lexical_cast(match["patch"]); + } + } catch (boost::bad_lexical_cast) { + return std::nullopt; + } + + if (!match["prerelease"].matched) { + return SemVer{major, minor, patch}; + } + + std::vector tokens; + boost::split(tokens, match["prerelease"], boost::is_any_of(".")); + + std::vector prerelease; + + std::transform( + tokens.begin(), tokens.end(), std::back_inserter(prerelease), + [](std::string const& token) -> SemVer::Token { + try { + return boost::lexical_cast(token); + } catch (boost::bad_lexical_cast) { + return token; + } + }); + + return SemVer{major, minor, patch, prerelease}; +} + +std::ostream& operator<<(std::ostream& out, SemVer::Token const& token) { + if (token.index() == 0) { + out << std::get<0>(token); + } else { + out << std::get<1>(token); + } + return out; +} + +std::ostream& operator<<(std::ostream& out, SemVer const& ver) { + out << ver.Major() << "." << ver.Minor() << "." << ver.Patch(); + if (ver.Prerelease()) { + out << "-"; + + for (auto it = ver.Prerelease()->begin(); it != ver.Prerelease()->end(); + ++it) { + out << *it; + if (std::next(it) != ver.Prerelease()->end()) { + out << "."; + } + } + } + return out; +} +} // namespace launchdarkly::server_side::evaluation::detail diff --git a/libs/server-sdk/src/evaluation/detail/semver_operations.hpp b/libs/server-sdk/src/evaluation/detail/semver_operations.hpp new file mode 100644 index 000000000..85b371116 --- /dev/null +++ b/libs/server-sdk/src/evaluation/detail/semver_operations.hpp @@ -0,0 +1,88 @@ +#pragma once + +#include +#include +#include +#include + +namespace launchdarkly::server_side::evaluation::detail { + +/** + * Represents a LaunchDarkly-flavored Semantic Version v2. + * + * Semantic versions can be compared using ==, <, and >. + * + * The main difference from the official spec is that missing minor and patch + * versions are allowed, i.e. "1" means "1.0.0" and "1.2" means "1.2.0". + */ +class SemVer { + public: + using VersionType = std::uint64_t; + + using Token = std::variant; + + /** + * Constructs a SemVer representing "0.0.0". + */ + SemVer(); + + /** + * Constructs a SemVer from a major, minor, patch, and prerelease. The + * prerelease consists of list of string/nonzero-number tokens, e.g. + * ["alpha", 1"]. + * @param major Major version. + * @param minor Minor version. + * @param patch Patch version. + * @param prerelease Prerelease tokens. + */ + SemVer(VersionType major, + VersionType minor, + VersionType patch, + std::vector prerelease); + + /** + * Constructs a SemVer from a major, minor, and patch. + * @param major Major version. + * @param minor Minor version. + * @param patch Patch version. + */ + SemVer(VersionType major, VersionType minor, VersionType patch); + + [[nodiscard]] SemVer::VersionType Major() const; + [[nodiscard]] SemVer::VersionType Minor() const; + [[nodiscard]] SemVer::VersionType Patch() const; + + [[nodiscard]] std::optional> const& Prerelease() const; + + /** + * Attempts to parse a semantic version string, returning std::nullopt on + * failure. Build information is discarded. + * @param value Version string, e.g. "1.2.3-alpha.1". + * @return SemVer on success, or std::nullopt on failure. + */ + [[nodiscard]] static std::optional Parse(std::string const& value); + + private: + VersionType major_; + VersionType minor_; + VersionType patch_; + std::optional> prerelease_; +}; + +bool operator<(SemVer::Token const& lhs, SemVer::Token const& rhs); + +bool operator==(SemVer const& lhs, SemVer const& rhs); + +bool operator<(SemVer const& lhs, SemVer const& rhs); + +bool operator>(SemVer const& lhs, SemVer const& rhs); + +/** Prints a SemVer to an ostream. If the SemVer was parsed from a string + * containing a build string, it will not be present as this information + * is discarded when parsing. + */ +std::ostream& operator<<(std::ostream& out, SemVer const& sv); + +std::ostream& operator<<(std::ostream& out, SemVer::Token const& sv); + +} // namespace launchdarkly::server_side::evaluation::detail diff --git a/libs/server-sdk/src/evaluation/detail/timestamp_operations.cpp b/libs/server-sdk/src/evaluation/detail/timestamp_operations.cpp new file mode 100644 index 000000000..57389a6a6 --- /dev/null +++ b/libs/server-sdk/src/evaluation/detail/timestamp_operations.cpp @@ -0,0 +1,55 @@ +#include "timestamp_operations.hpp" + +#include "timestamp.h" + +#include +#include + +namespace launchdarkly::server_side::evaluation::detail { + +std::optional MillisecondsToTimepoint(double ms); + +std::optional RFC3339ToTimepoint(std::string const& timestamp); + +std::optional ToTimepoint(Value const& value) { + if (value.Type() == Value::Type::kNumber) { + double const epoch_ms = value.AsDouble(); + return MillisecondsToTimepoint(epoch_ms); + } + if (value.Type() == Value::Type::kString) { + std::string const& rfc3339_timestamp = value.AsString(); + return RFC3339ToTimepoint(rfc3339_timestamp); + } + return std::nullopt; +} + +std::optional MillisecondsToTimepoint(double ms) { + if (ms < 0.0) { + return std::nullopt; + } + if (std::trunc(ms) == ms) { + return std::chrono::system_clock::time_point{ + std::chrono::milliseconds{static_cast(ms)}}; + } + return std::nullopt; +} + +std::optional RFC3339ToTimepoint(std::string const& timestamp) { + if (timestamp.empty()) { + return std::nullopt; + } + + timestamp_t ts{}; + if (timestamp_parse(timestamp.c_str(), timestamp.size(), &ts)) { + return std::nullopt; + } + + Timepoint epoch{}; + epoch += std::chrono::seconds{ts.sec}; + epoch += + std::chrono::floor(std::chrono::nanoseconds{ts.nsec}); + + return epoch; +} + +} // namespace launchdarkly::server_side::evaluation::detail diff --git a/libs/server-sdk/src/evaluation/detail/timestamp_operations.hpp b/libs/server-sdk/src/evaluation/detail/timestamp_operations.hpp new file mode 100644 index 000000000..5c25b84dd --- /dev/null +++ b/libs/server-sdk/src/evaluation/detail/timestamp_operations.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include + +#include +#include +#include + +namespace launchdarkly::server_side::evaluation::detail { + +using Clock = std::chrono::system_clock; +using Timepoint = Clock::time_point; + +[[nodiscard]] std::optional ToTimepoint(Value const& value); + +} // namespace launchdarkly::server_side::evaluation::detail diff --git a/libs/server-sdk/src/evaluation/evaluation_error.cpp b/libs/server-sdk/src/evaluation/evaluation_error.cpp new file mode 100644 index 000000000..7f81c02f2 --- /dev/null +++ b/libs/server-sdk/src/evaluation/evaluation_error.cpp @@ -0,0 +1,62 @@ +#include "evaluation_error.hpp" +#include + +namespace launchdarkly::server_side::evaluation { + +Error::Error(char const* format, std::string arg) + : format_{format}, arg_{std::move(arg)} {} + +Error::Error(char const* format, std::int64_t arg) + : Error(format, std::to_string(arg)) {} + +Error::Error(char const* msg) : format_{msg}, arg_{std::nullopt} {} + +Error Error::CyclicSegmentReference(std::string segment_key) { + return { + "segment rule referencing segment \"%1%\" caused a circular " + "reference; this is probably a temporary condition due to an " + "incomplete update", + std::move(segment_key)}; +} + +Error Error::CyclicPrerequisiteReference(std::string prereq_key) { + return { + "prerequisite relationship to \"%1%\" caused a circular " + "reference; this is probably a temporary condition due to an " + "incomplete update", + std::move(prereq_key)}; +} + +Error Error::MissingSalt(std::string key) { + return {"\"%1%\" is missing a salt", std::move(key)}; +} + +Error Error::RolloutMissingVariations() { + return {"rollout or experiment with no variations"}; +} + +Error Error::InvalidAttributeReference(std::string ref) { + return {"invalid attribute reference: \"%1%\"", std::move(ref)}; +} + +Error Error::NonexistentVariationIndex(std::int64_t index) { + return { + "rule, fallthrough, or target referenced a nonexistent variation index " + "(%1%)", + index}; +} + +std::ostream& operator<<(std::ostream& out, Error const& err) { + if (err.arg_ == std::nullopt) { + out << err.format_; + } else { + out << boost::format(err.format_) % *err.arg_; + } + return out; +} + +bool operator==(Error const& lhs, Error const& rhs) { + return lhs.format_ == rhs.format_ && lhs.arg_ == rhs.arg_; +} + +} // namespace launchdarkly::server_side::evaluation diff --git a/libs/server-sdk/src/evaluation/evaluation_error.hpp b/libs/server-sdk/src/evaluation/evaluation_error.hpp new file mode 100644 index 000000000..7fb52d556 --- /dev/null +++ b/libs/server-sdk/src/evaluation/evaluation_error.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include +#include + +namespace launchdarkly::server_side::evaluation { + +class Error { + public: + static Error CyclicSegmentReference(std::string segment_key); + static Error CyclicPrerequisiteReference(std::string prereq_key); + static Error InvalidAttributeReference(std::string ref); + static Error RolloutMissingVariations(); + static Error NonexistentVariationIndex(std::int64_t index); + static Error MissingSalt(std::string item_key); + + friend std::ostream& operator<<(std::ostream& out, Error const& arr); + friend bool operator==(Error const& lhs, Error const& rhs); + + private: + Error(char const* format, std::string arg); + Error(char const* format, std::int64_t arg); + Error(char const* msg); + + char const* format_; + std::optional arg_; +}; + +} // namespace launchdarkly::server_side::evaluation diff --git a/libs/server-sdk/src/evaluation/evaluator.cpp b/libs/server-sdk/src/evaluation/evaluator.cpp new file mode 100644 index 000000000..97057fbd2 --- /dev/null +++ b/libs/server-sdk/src/evaluation/evaluator.cpp @@ -0,0 +1,225 @@ +#include "evaluator.hpp" +#include "rules.hpp" + +#include +#include + +#include +#include + +namespace launchdarkly::server_side::evaluation { + +using namespace data_model; + +std::optional AnyTargetMatchVariation( + launchdarkly::Context const& context, + Flag const& flag); + +std::optional TargetMatchVariation( + launchdarkly::Context const& context, + Flag::Target const& target); + +Evaluator::Evaluator(Logger& logger, data_store::IDataStore const& store) + : logger_(logger), store_(store), stack_() {} + +EvaluationDetail Evaluator::Evaluate( + data_model::Flag const& flag, + launchdarkly::Context const& context) { + return Evaluate(flag, context, EventScope{}); +} + +EvaluationDetail Evaluator::Evaluate( + Flag const& flag, + launchdarkly::Context const& context, + EventScope const& event_scope) { + return Evaluate(std::nullopt, flag, context, event_scope); +} + +EvaluationDetail Evaluator::Evaluate( + std::optional parent_key, + Flag const& flag, + launchdarkly::Context const& context, + EventScope const& event_scope) { + if (auto guard = stack_.NoticePrerequisite(flag.key)) { + if (!flag.on) { + return OffValue(flag, EvaluationReason::Off()); + } + + for (Flag::Prerequisite const& p : flag.prerequisites) { + std::shared_ptr maybe_flag = + store_.GetFlag(p.key); + + if (!maybe_flag) { + return OffValue(flag, + EvaluationReason::PrerequisiteFailed(p.key)); + } + + data_store::FlagDescriptor const& descriptor = *maybe_flag; + + if (!descriptor.item) { + // This flag existed at some point, but has since been deleted. + return OffValue(flag, + EvaluationReason::PrerequisiteFailed(p.key)); + } + + // Recursive call; cycles are detected by the guard. + EvaluationDetail detailed_evaluation = + Evaluate(flag.key, *descriptor.item, context, event_scope); + + if (detailed_evaluation.IsError()) { + return detailed_evaluation; + } + + std::optional variation_index = + detailed_evaluation.VariationIndex(); + + event_scope.Send([&](EventFactory const& factory) { + return factory.Eval(p.key, context, *descriptor.item, + detailed_evaluation, Value::Null(), + flag.key); + }); + + if (!descriptor.item->on || variation_index != p.variation) { + return OffValue(flag, + EvaluationReason::PrerequisiteFailed(p.key)); + } + } + } else { + LogError(parent_key.value_or("(no parent)"), + Error::CyclicPrerequisiteReference(flag.key)); + return EvaluationReason::MalformedFlag(); + } + + // If the flag is on, all prerequisites are on and valid, then + // determine if the context matches any targets. + // + // This happens before rule evaluation to ensure targets always have + // priority. + + if (auto variation_index = AnyTargetMatchVariation(context, flag)) { + return FlagVariation(flag, *variation_index, + EvaluationReason::TargetMatch()); + } + + for (std::size_t rule_index = 0; rule_index < flag.rules.size(); + rule_index++) { + auto const& rule = flag.rules[rule_index]; + + tl::expected rule_match = + Match(rule, context, store_, stack_); + + if (!rule_match) { + LogError(flag.key, rule_match.error()); + return EvaluationReason::MalformedFlag(); + } + + if (!(rule_match.value())) { + continue; + } + + tl::expected result = + Variation(rule.variationOrRollout, flag.key, context, flag.salt); + + if (!result) { + LogError(flag.key, result.error()); + return EvaluationReason::MalformedFlag(); + } + + EvaluationReason reason = EvaluationReason::RuleMatch( + rule_index, rule.id, result->InExperiment()); + + return FlagVariation(flag, result->VariationIndex(), std::move(reason)); + } + + // If there were no rule matches, then return the fallthrough variation. + + tl::expected result = + Variation(flag.fallthrough, flag.key, context, flag.salt); + + if (!result) { + LogError(flag.key, result.error()); + return EvaluationReason::MalformedFlag(); + } + + EvaluationReason reason = + EvaluationReason::Fallthrough(result->InExperiment()); + + return FlagVariation(flag, result->VariationIndex(), std::move(reason)); +} + +EvaluationDetail Evaluator::FlagVariation( + Flag const& flag, + Flag::Variation variation_index, + EvaluationReason reason) const { + if (variation_index < 0 || variation_index >= flag.variations.size()) { + LogError(flag.key, Error::NonexistentVariationIndex(variation_index)); + return EvaluationReason::MalformedFlag(); + } + + return {flag.variations.at(variation_index), variation_index, + std::move(reason)}; +} + +EvaluationDetail Evaluator::OffValue(Flag const& flag, + EvaluationReason reason) const { + if (flag.offVariation) { + return FlagVariation(flag, *flag.offVariation, std::move(reason)); + } + + return reason; +} + +std::optional AnyTargetMatchVariation( + launchdarkly::Context const& context, + Flag const& flag) { + if (flag.contextTargets.empty()) { + for (auto const& target : flag.targets) { + if (auto index = TargetMatchVariation(context, target)) { + return index; + } + } + return std::nullopt; + } + + for (auto const& context_target : flag.contextTargets) { + if (IsUser(context_target.contextKind) && + context_target.values.empty()) { + for (auto const& target : flag.targets) { + if (target.variation == context_target.variation) { + if (auto index = TargetMatchVariation(context, target)) { + return index; + } + } + } + } else if (auto index = TargetMatchVariation(context, context_target)) { + return index; + } + } + + return std::nullopt; +} + +std::optional TargetMatchVariation( + launchdarkly::Context const& context, + Flag::Target const& target) { + Value const& key = context.Get(target.contextKind, "key"); + if (key.IsNull()) { + return std::nullopt; + } + + for (auto const& value : target.values) { + if (value == key) { + return target.variation; + } + } + + return std::nullopt; +} + +void Evaluator::LogError(std::string const& key, Error const& error) const { + LD_LOG(logger_, LogLevel::kError) + << "Invalid flag configuration detected in flag \"" << key + << "\": " << error; +} + +} // namespace launchdarkly::server_side::evaluation diff --git a/libs/server-sdk/src/evaluation/evaluator.hpp b/libs/server-sdk/src/evaluation/evaluator.hpp new file mode 100644 index 000000000..cf7a15468 --- /dev/null +++ b/libs/server-sdk/src/evaluation/evaluator.hpp @@ -0,0 +1,70 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "../data_store/data_store.hpp" +#include "../events/event_scope.hpp" +#include "bucketing.hpp" +#include "detail/evaluation_stack.hpp" +#include "evaluation_error.hpp" + +#include + +namespace launchdarkly::server_side::evaluation { + +class Evaluator { + public: + Evaluator(Logger& logger, data_store::IDataStore const& store); + + /** + * Evaluates a flag for a given context. + * Warning: not thread safe. + * + * @param flag The flag to evaluate. + * @param context The context to evaluate the flag against. + * @param event_scope The event scope used for recording prerequisite + * events. + */ + [[nodiscard]] EvaluationDetail Evaluate( + data_model::Flag const& flag, + launchdarkly::Context const& context, + EventScope const& event_scope); + + /** + * Evaluates a flag for a given context. Does not record prerequisite + * events. Warning: not thread safe. + * + * @param flag The flag to evaluate. + * @param context The context to evaluate the flag against. + */ + [[nodiscard]] EvaluationDetail Evaluate( + data_model::Flag const& flag, + launchdarkly::Context const& context); + + private: + [[nodiscard]] EvaluationDetail Evaluate( + std::optional parent_key, + data_model::Flag const& flag, + launchdarkly::Context const& context, + EventScope const& event_scope); + + [[nodiscard]] EvaluationDetail FlagVariation( + data_model::Flag const& flag, + data_model::Flag::Variation variation_index, + EvaluationReason reason) const; + + [[nodiscard]] EvaluationDetail OffValue( + data_model::Flag const& flag, + EvaluationReason reason) const; + + void LogError(std::string const& key, Error const& error) const; + + Logger& logger_; + data_store::IDataStore const& store_; + detail::EvaluationStack stack_; +}; +} // namespace launchdarkly::server_side::evaluation diff --git a/libs/server-sdk/src/evaluation/operators.cpp b/libs/server-sdk/src/evaluation/operators.cpp new file mode 100644 index 000000000..31ceb960f --- /dev/null +++ b/libs/server-sdk/src/evaluation/operators.cpp @@ -0,0 +1,146 @@ +#include "operators.hpp" +#include "detail/semver_operations.hpp" +#include "detail/timestamp_operations.hpp" + +#include + +namespace launchdarkly::server_side::evaluation::operators { + +template +bool StringOp(Value const& context_value, + Value const& clause_value, + Callable&& op) { + if (!(context_value.Type() == Value::Type::kString && + clause_value.Type() == Value::Type::kString)) { + return false; + } + return op(context_value.AsString(), clause_value.AsString()); +} + +template +bool SemverOp(Value const& context_value, + Value const& clause_value, + Callable&& op) { + return StringOp(context_value, clause_value, + [op = std::move(op)](std::string const& context, + std::string const& clause) { + auto context_semver = detail::SemVer::Parse(context); + if (!context_semver) { + return false; + } + + auto clause_semver = detail::SemVer::Parse(clause); + if (!clause_semver) { + return false; + } + + return op(*context_semver, *clause_semver); + }); +} + +template +bool TimeOp(Value const& context_value, + Value const& clause_value, + Callable&& op) { + auto context_tp = detail::ToTimepoint(context_value); + if (!context_tp) { + return false; + } + auto clause_tp = detail::ToTimepoint(clause_value); + if (!clause_tp) { + return false; + } + return op(*context_tp, *clause_tp); +} + +bool StartsWith(std::string const& context_value, + std::string const& clause_value) { + return clause_value.size() <= context_value.size() && + std::equal(clause_value.begin(), clause_value.end(), + context_value.begin()); +} + +bool EndsWith(std::string const& context_value, + std::string const& clause_value) { + return clause_value.size() <= context_value.size() && + std::equal(clause_value.rbegin(), clause_value.rend(), + context_value.rbegin()); +} + +bool Contains(std::string const& context_value, + std::string const& clause_value) { + return context_value.find(clause_value) != std::string::npos; +} + +/* RegexMatch uses boost::regex instead of std::regex, because the former + * appears to be significantly more performant according to boost benchmarks. + * For more information, see here: + * https://www.boost.org/doc/libs/1_82_0/libs/regex/doc/html/boost_regex/background/performance.html + */ +bool RegexMatch(std::string const& context_value, + std::string const& clause_value) { + /* See here for FAQ on boost::regex exceptions: + * https:// + * www.boost.org/doc/libs/1_82_0/libs/regex/doc/html/boost_regex/background/faq.html + * */ + try { + return boost::regex_search(context_value, boost::regex(clause_value)); + } catch (boost::bad_expression) { + // boost::bad_expression can be thrown by basic_regex when compiling a + // regular expression. + return false; + } catch (std::runtime_error) { + // std::runtime_error can be thrown when a call + // to regex_search results in an "everlasting" search + return false; + } +} + +bool Match(data_model::Clause::Op op, + Value const& context_value, + Value const& clause_value) { + switch (op) { + case data_model::Clause::Op::kIn: + return context_value == clause_value; + case data_model::Clause::Op::kStartsWith: + return StringOp(context_value, clause_value, StartsWith); + case data_model::Clause::Op::kEndsWith: + return StringOp(context_value, clause_value, EndsWith); + case data_model::Clause::Op::kMatches: + return StringOp(context_value, clause_value, RegexMatch); + case data_model::Clause::Op::kContains: + return StringOp(context_value, clause_value, Contains); + case data_model::Clause::Op::kLessThan: + return context_value < clause_value; + case data_model::Clause::Op::kLessThanOrEqual: + return context_value <= clause_value; + case data_model::Clause::Op::kGreaterThan: + return context_value > clause_value; + case data_model::Clause::Op::kGreaterThanOrEqual: + return context_value >= clause_value; + case data_model::Clause::Op::kBefore: + return TimeOp( + context_value, clause_value, + [](auto const& lhs, auto const& rhs) { return lhs < rhs; }); + case data_model::Clause::Op::kAfter: + return TimeOp( + context_value, clause_value, + [](auto const& lhs, auto const& rhs) { return lhs > rhs; }); + case data_model::Clause::Op::kSemVerEqual: + return SemverOp( + context_value, clause_value, + [](auto const& lhs, auto const& rhs) { return lhs == rhs; }); + case data_model::Clause::Op::kSemVerLessThan: + return SemverOp( + context_value, clause_value, + [](auto const& lhs, auto const& rhs) { return lhs < rhs; }); + case data_model::Clause::Op::kSemVerGreaterThan: + return SemverOp( + context_value, clause_value, + [](auto const& lhs, auto const& rhs) { return lhs > rhs; }); + default: + return false; + } +} + +} // namespace launchdarkly::server_side::evaluation::operators diff --git a/libs/server-sdk/src/evaluation/operators.hpp b/libs/server-sdk/src/evaluation/operators.hpp new file mode 100644 index 000000000..159c1d614 --- /dev/null +++ b/libs/server-sdk/src/evaluation/operators.hpp @@ -0,0 +1,10 @@ +#pragma once +#include + +namespace launchdarkly::server_side::evaluation::operators { + +bool Match(data_model::Clause::Op op, + Value const& context_value, + Value const& clause_value); + +} // namespace launchdarkly::server_side::evaluation::operators diff --git a/libs/server-sdk/src/evaluation/rules.cpp b/libs/server-sdk/src/evaluation/rules.cpp new file mode 100644 index 000000000..313cc44b1 --- /dev/null +++ b/libs/server-sdk/src/evaluation/rules.cpp @@ -0,0 +1,214 @@ +#include "rules.hpp" +#include "bucketing.hpp" +#include "operators.hpp" + +namespace launchdarkly::server_side::evaluation { + +using namespace data_model; + +bool MaybeNegate(Clause const& clause, bool value) { + if (clause.negate) { + return !value; + } + return value; +} + +tl::expected Match(Flag::Rule const& rule, + launchdarkly::Context const& context, + data_store::IDataStore const& store, + detail::EvaluationStack& stack) { + for (Clause const& clause : rule.clauses) { + tl::expected result = Match(clause, context, store, stack); + if (!result) { + return result; + } + if (!(result.value())) { + return false; + } + } + return true; +} + +tl::expected Match(Segment::Rule const& rule, + Context const& context, + data_store::IDataStore const& store, + detail::EvaluationStack& stack, + std::string const& key, + std::string const& salt) { + for (Clause const& clause : rule.clauses) { + auto maybe_match = Match(clause, context, store, stack); + if (!maybe_match) { + return tl::make_unexpected(maybe_match.error()); + } + if (!(maybe_match.value())) { + return false; + } + } + + if (rule.weight && rule.weight >= 0.0) { + BucketPrefix prefix(key, salt); + auto maybe_bucket = Bucket(context, rule.bucketBy, prefix, false, + rule.rolloutContextKind); + if (!maybe_bucket) { + return tl::make_unexpected(maybe_bucket.error()); + } + auto [bucket, ignored] = *maybe_bucket; + return bucket < (*rule.weight / kBucketScale); + } + + return true; +} + +tl::expected Match(Clause const& clause, + launchdarkly::Context const& context, + data_store::IDataStore const& store, + detail::EvaluationStack& stack) { + if (clause.op == Clause::Op::kSegmentMatch) { + return MatchSegment(clause, context, store, stack); + } + return MatchNonSegment(clause, context); +} + +tl::expected MatchSegment(Clause const& clause, + launchdarkly::Context const& context, + data_store::IDataStore const& store, + detail::EvaluationStack& stack) { + for (Value const& value : clause.values) { + // A segment key represented as a Value is a string; non-strings are + // ignored. + if (value.Type() != Value::Type::kString) { + continue; + } + + std::string const& segment_key = value.AsString(); + + std::shared_ptr segment_ptr = + store.GetSegment(segment_key); + + if (!segment_ptr || !segment_ptr->item) { + // Segments that don't exist are ignored. + continue; + } + + auto maybe_contains = + Contains(*segment_ptr->item, context, store, stack); + + if (!maybe_contains) { + return tl::make_unexpected(maybe_contains.error()); + } + + if (maybe_contains.value()) { + return MaybeNegate(clause, true); + } + } + + return MaybeNegate(clause, false); +} + +tl::expected MatchNonSegment( + Clause const& clause, + launchdarkly::Context const& context) { + if (!clause.attribute.Valid()) { + return tl::make_unexpected( + Error::InvalidAttributeReference(clause.attribute.RedactionName())); + } + + if (clause.attribute.IsKind()) { + for (auto const& clause_value : clause.values) { + for (auto const& kind : context.Kinds()) { + if (operators::Match(clause.op, kind, clause_value)) { + return MaybeNegate(clause, true); + } + } + } + return MaybeNegate(clause, false); + } + + Value const& attribute = context.Get(clause.contextKind, clause.attribute); + if (attribute.IsNull()) { + return false; + } + + if (attribute.IsArray()) { + for (Value const& clause_value : clause.values) { + for (Value const& context_value : attribute.AsArray()) { + if (operators::Match(clause.op, context_value, clause_value)) { + return MaybeNegate(clause, true); + } + } + } + return MaybeNegate(clause, false); + } + + if (std::any_of(clause.values.begin(), clause.values.end(), + [&](Value const& clause_value) { + return operators::Match(clause.op, attribute, + clause_value); + })) { + return MaybeNegate(clause, true); + } + + return MaybeNegate(clause, false); +} + +tl::expected Contains(Segment const& segment, + Context const& context, + data_store::IDataStore const& store, + detail::EvaluationStack& stack) { + auto guard = stack.NoticeSegment(segment.key); + if (!guard) { + return tl::make_unexpected(Error::CyclicSegmentReference(segment.key)); + } + + if (segment.unbounded) { + // TODO(sc209881): set big segment status to NOT_CONFIGURED. + return false; + } + + if (IsTargeted(context, segment.included, segment.includedContexts)) { + return true; + } + + if (IsTargeted(context, segment.excluded, segment.excludedContexts)) { + return false; + } + + for (auto const& rule : segment.rules) { + if (!segment.salt) { + return tl::make_unexpected(Error::MissingSalt(segment.key)); + } + tl::expected maybe_match = + Match(rule, context, store, stack, segment.key, *segment.salt); + if (!maybe_match) { + return tl::make_unexpected(maybe_match.error()); + } + if (maybe_match.value()) { + return true; + } + } + + return false; +} + +bool IsTargeted(Context const& context, + std::vector const& user_keys, + std::vector const& context_targets) { + for (auto const& target : context_targets) { + Value const& key = context.Get(target.contextKind, "key"); + if (!key.IsString()) { + continue; + } + if (std::find(target.values.begin(), target.values.end(), key) != + target.values.end()) { + return true; + } + } + + if (auto key = context.Get("user", "key"); !key.IsNull()) { + return std::find(user_keys.begin(), user_keys.end(), key.AsString()) != + user_keys.end(); + } + + return false; +} +} // namespace launchdarkly::server_side::evaluation diff --git a/libs/server-sdk/src/evaluation/rules.hpp b/libs/server-sdk/src/evaluation/rules.hpp new file mode 100644 index 000000000..5ca9a974e --- /dev/null +++ b/libs/server-sdk/src/evaluation/rules.hpp @@ -0,0 +1,60 @@ +#pragma once + +#include +#include +#include + +#include "../data_store/data_store.hpp" +#include "detail/evaluation_stack.hpp" +#include "evaluation_error.hpp" + +#include + +#include + +namespace launchdarkly::server_side::evaluation { + +[[nodiscard]] tl::expected Match( + data_model::Flag::Rule const&, + Context const&, + data_store::IDataStore const& store, + detail::EvaluationStack& stack); + +[[nodiscard]] tl::expected Match(data_model::Clause const&, + Context const&, + data_store::IDataStore const&, + detail::EvaluationStack&); + +[[nodiscard]] tl::expected Match( + data_model::Segment::Rule const& rule, + Context const& context, + data_store::IDataStore const& store, + detail::EvaluationStack& stack, + std::string const& key, + std::string const& salt); + +[[nodiscard]] tl::expected MatchSegment( + data_model::Clause const&, + Context const&, + data_store::IDataStore const&, + detail::EvaluationStack& stack); + +[[nodiscard]] tl::expected MatchNonSegment( + data_model::Clause const&, + Context const&); + +[[nodiscard]] tl::expected Contains( + data_model::Segment const&, + Context const&, + data_store::IDataStore const& store, + detail::EvaluationStack& stack); + +[[nodiscard]] bool MaybeNegate(data_model::Clause const& clause, bool value); + +[[nodiscard]] bool IsTargeted(Context const&, + std::vector const&, + std::vector const&); + +[[nodiscard]] bool IsUser(Context const& context); + +} // namespace launchdarkly::server_side::evaluation diff --git a/libs/server-sdk/src/events/event_factory.cpp b/libs/server-sdk/src/events/event_factory.cpp new file mode 100644 index 000000000..2be43f6fe --- /dev/null +++ b/libs/server-sdk/src/events/event_factory.cpp @@ -0,0 +1,94 @@ +#include "event_factory.hpp" + +#include +namespace launchdarkly::server_side { + +EventFactory::EventFactory( + launchdarkly::server_side::EventFactory::ReasonPolicy reason_policy) + : reason_policy_(reason_policy), + now_([]() { return events::Date{std::chrono::system_clock::now()}; }) {} + +EventFactory EventFactory::WithReasons() { + return {ReasonPolicy::Require}; +} + +EventFactory EventFactory::WithoutReasons() { + return {ReasonPolicy::Default}; +} + +events::InputEvent EventFactory::UnknownFlag( + std::string const& key, + launchdarkly::Context const& ctx, + EvaluationDetail detail, + launchdarkly::Value default_val) const { + return FeatureRequest(key, ctx, std::nullopt, detail, default_val, + std::nullopt); +} + +events::InputEvent EventFactory::Eval( + std::string const& key, + Context const& ctx, + std::optional const& flag, + EvaluationDetail detail, + Value default_value, + std::optional prereq_of) const { + return FeatureRequest(key, ctx, flag, detail, default_value, prereq_of); +} + +events::InputEvent EventFactory::Identify(launchdarkly::Context ctx) const { + return events::IdentifyEventParams{now_(), std::move(ctx)}; +} + +events::InputEvent EventFactory::Custom( + Context const& ctx, + std::string event_name, + std::optional data, + std::optional metric_value) const { + return events::ServerTrackEventParams{ + {now_(), std::move(event_name), ctx.KindsToKeys(), std::move(data), + metric_value}, + ctx}; +} + +events::InputEvent EventFactory::FeatureRequest( + std::string const& key, + launchdarkly::Context const& context, + std::optional const& flag, + EvaluationDetail detail, + launchdarkly::Value default_val, + std::optional prereq_of) const { + bool flag_track_events = false; + bool require_experiment_data = false; + std::optional debug_events_until_date; + + if (flag.has_value()) { + flag_track_events = flag->trackEvents; + require_experiment_data = + flag->IsExperimentationEnabled(detail.Reason()); + if (flag->debugEventsUntilDate) { + debug_events_until_date = + events::Date{std::chrono::system_clock::time_point{ + std::chrono::milliseconds(*flag->debugEventsUntilDate)}}; + } + } + + std::optional reason; + if (reason_policy_ == ReasonPolicy::Require || require_experiment_data) { + reason = detail.Reason(); + } + + return events::FeatureEventParams{ + now_(), + key, + context, + detail.Value(), + default_val, + flag.has_value() ? std::make_optional(flag->version) : std::nullopt, + detail.VariationIndex(), + reason, + flag_track_events || require_experiment_data, + debug_events_until_date, + prereq_of}; +} + +} // namespace launchdarkly::server_side diff --git a/libs/server-sdk/src/events/event_factory.hpp b/libs/server-sdk/src/events/event_factory.hpp new file mode 100644 index 000000000..e9a10eec3 --- /dev/null +++ b/libs/server-sdk/src/events/event_factory.hpp @@ -0,0 +1,57 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include + +namespace launchdarkly::server_side { + +class EventFactory { + enum class ReasonPolicy { + Default = 0, + Require = 1, + }; + + public: + [[nodiscard]] static EventFactory WithReasons(); + [[nodiscard]] static EventFactory WithoutReasons(); + + [[nodiscard]] events::InputEvent UnknownFlag(std::string const& key, + Context const& ctx, + EvaluationDetail detail, + Value default_val) const; + + [[nodiscard]] events::InputEvent Eval( + std::string const& key, + Context const& ctx, + std::optional const& flag, + EvaluationDetail detail, + Value default_value, + std::optional prereq_of) const; + + [[nodiscard]] events::InputEvent Identify(Context ctx) const; + + [[nodiscard]] events::InputEvent Custom( + Context const& ctx, + std::string event_name, + std::optional data, + std::optional metric_value) const; + + private: + EventFactory(ReasonPolicy reason_policy); + events::InputEvent FeatureRequest( + std::string const& key, + Context const& ctx, + std::optional const& flag, + EvaluationDetail detail, + Value default_val, + std::optional prereq_of) const; + + ReasonPolicy const reason_policy_; + std::function now_; +}; +} // namespace launchdarkly::server_side diff --git a/libs/server-sdk/src/events/event_scope.hpp b/libs/server-sdk/src/events/event_scope.hpp new file mode 100644 index 000000000..2eb32ade4 --- /dev/null +++ b/libs/server-sdk/src/events/event_scope.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include + +#include "event_factory.hpp" + +namespace launchdarkly::server_side { + +/** + * EventScope is responsible for forwarding events to an + * IEventProcessor. If the given interface is nullptr, then events will not + * be forwarded at all. + */ +class EventScope { + public: + /** + * Constructs an EventScope with a non-owned IEventProcessor and factory. + * When Send is called, the factory will be passed to the caller, which must + * return a constructed event. + * @param processor The event processor to forward events to. + * @param factory The factory used for generating events. + */ + EventScope(events::IEventProcessor* processor, EventFactory factory) + : processor_(processor), factory_(std::move(factory)) {} + + /** + * Default constructs an EventScope which will not forward events. + */ + EventScope() : EventScope(nullptr, EventFactory::WithoutReasons()) {} + + /** + * Sends an event created by the given callable. The callable will be + * passed an EventFactory. + * @param callable Returns an InputEvent. + */ + template + void Send(Callable&& callable) const { + if (processor_) { + processor_->SendAsync(callable(factory_)); + } + } + + private: + events::IEventProcessor* processor_; + EventFactory const factory_; +}; + +} // namespace launchdarkly::server_side diff --git a/libs/server-sdk/tests/CMakeLists.txt b/libs/server-sdk/tests/CMakeLists.txt new file mode 100644 index 000000000..707abb839 --- /dev/null +++ b/libs/server-sdk/tests/CMakeLists.txt @@ -0,0 +1,20 @@ +cmake_minimum_required(VERSION 3.10) +include(GoogleTest) + +include_directories("${PROJECT_SOURCE_DIR}/include") +include_directories("${PROJECT_SOURCE_DIR}/src") + +file(GLOB tests "${PROJECT_SOURCE_DIR}/tests/*.cpp") + +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) + +# Get things in the same directory on windows. +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_DEBUG "${CMAKE_BINARY_DIR}../") +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}../") + +add_executable(gtest_${LIBNAME} + ${tests} + ) +target_link_libraries(gtest_${LIBNAME} launchdarkly::server launchdarkly::internal launchdarkly::sse timestamp GTest::gtest_main) + +gtest_discover_tests(gtest_${LIBNAME}) diff --git a/libs/server-sdk/tests/all_flags_state_test.cpp b/libs/server-sdk/tests/all_flags_state_test.cpp new file mode 100644 index 000000000..c08a4fe54 --- /dev/null +++ b/libs/server-sdk/tests/all_flags_state_test.cpp @@ -0,0 +1,150 @@ +#include + +#include "all_flags_state/all_flags_state_builder.hpp" + +#include + +using namespace launchdarkly; +using namespace launchdarkly::server_side; + +TEST(AllFlagsTest, Empty) { + AllFlagsStateBuilder builder{AllFlagsState::Options::Default}; + auto state = builder.Build(); + ASSERT_TRUE(state.Valid()); + ASSERT_TRUE(state.States().empty()); + ASSERT_TRUE(state.Values().empty()); +} + +TEST(AllFlagsTest, DefaultOptions) { + AllFlagsStateBuilder builder{AllFlagsState::Options::Default}; + + builder.AddFlag( + "myFlag", true, + AllFlagsState::State{42, 1, std::nullopt, false, false, std::nullopt}); + + auto state = builder.Build(); + ASSERT_TRUE(state.Valid()); + + auto expected = boost::json::parse(R"({ + "myFlag": true, + "$flagsState": { + "myFlag": { + "version": 42, + "variation": 1 + } + }, + "$valid": true + })"); + + auto got = boost::json::value_from(state); + ASSERT_EQ(got, expected); +} + +TEST(AllFlagsTest, DetailsOnlyForTrackedFlags) { + AllFlagsStateBuilder builder{ + AllFlagsState::Options::DetailsOnlyForTrackedFlags}; + builder.AddFlag( + "myFlagTracked", true, + AllFlagsState::State{42, 1, EvaluationReason::Fallthrough(false), true, + true, std::nullopt}); + builder.AddFlag( + "myFlagUntracked", true, + AllFlagsState::State{42, 1, EvaluationReason::Fallthrough(false), false, + false, std::nullopt}); + + auto state = builder.Build(); + ASSERT_TRUE(state.Valid()); + + auto expected = boost::json::parse(R"({ + "myFlagTracked" : true, + "myFlagUntracked" : true, + "$flagsState": { + "myFlagTracked": { + "version": 42, + "variation": 1, + "reason":{ + "kind" : "FALLTHROUGH" + }, + "trackReason" : true, + "trackEvents" : true + }, + "myFlagUntracked" : { + "variation" : 1 + } + }, + "$valid": true + })"); + + auto got = boost::json::value_from(state); + ASSERT_EQ(got, expected); +} + +TEST(AllFlagsTest, IncludeReasons) { + AllFlagsStateBuilder builder{AllFlagsState::Options::IncludeReasons}; + builder.AddFlag( + "myFlag", true, + AllFlagsState::State{42, 1, EvaluationReason::Fallthrough(false), false, + false, std::nullopt}); + auto state = builder.Build(); + ASSERT_TRUE(state.Valid()); + + auto expected = boost::json::parse(R"({ + "myFlag": true, + "$flagsState": { + "myFlag": { + "version": 42, + "variation": 1, + "reason" : { + "kind": "FALLTHROUGH" + } + } + }, + "$valid": true + })"); + + auto got = boost::json::value_from(state); + ASSERT_EQ(got, expected); +} + +TEST(AllFlagsTest, FlagValues) { + AllFlagsStateBuilder builder{AllFlagsState::Options::Default}; + + const std::size_t kNumFlags = 10; + + for (std::size_t i = 0; i < kNumFlags; i++) { + builder.AddFlag("myFlag" + std::to_string(i), "value", + AllFlagsState::State{42, 1, std::nullopt, false, false, + std::nullopt}); + } + + auto state = builder.Build(); + + auto const& vals = state.Values(); + + ASSERT_EQ(vals.size(), kNumFlags); + + ASSERT_TRUE(std::all_of(vals.begin(), vals.end(), [](auto const& kvp) { + return kvp.second.AsString() == "value"; + })); +} + +TEST(AllFlagsTest, FlagState) { + AllFlagsStateBuilder builder{AllFlagsState::Options::Default}; + + const std::size_t kNumFlags = 10; + + AllFlagsState::State state{42, 1, std::nullopt, false, false, std::nullopt}; + for (std::size_t i = 0; i < kNumFlags; i++) { + builder.AddFlag("myFlag" + std::to_string(i), "value", state); + } + + auto all_flags_state = builder.Build(); + + auto const& states = all_flags_state.States(); + + ASSERT_EQ(states.size(), kNumFlags); + + ASSERT_TRUE(std::all_of(states.begin(), states.end(), [&](auto const& kvp) { + return kvp.second == state; + })); +} diff --git a/libs/server-sdk/tests/bucketing_tests.cpp b/libs/server-sdk/tests/bucketing_tests.cpp new file mode 100644 index 000000000..23d63b4ae --- /dev/null +++ b/libs/server-sdk/tests/bucketing_tests.cpp @@ -0,0 +1,367 @@ +#include "evaluation/bucketing.hpp" +#include "evaluation/evaluator.hpp" + +#include +#include + +using namespace launchdarkly; +using namespace launchdarkly::data_model; +using namespace launchdarkly::server_side::evaluation; +using WeightedVariation = Flag::Rollout::WeightedVariation; + +/** + * Note: These tests are meant to be exact duplicates of tests + * in other SDKs. Do not change any of the values unless they + * are also changed in other SDKs. These are not traditional behavioral + * tests so much as consistency tests to guarantee that the implementation + * is identical across SDKs. + * + * Tests in this file may derive from BucketingConsistencyTests to gain access + * to shared constants. + */ +class BucketingConsistencyTests : public ::testing::Test { + public: + // Bucket results must be no more than this distance from the expected + // value. + double const kBucketTolerance = 0.0000001; + const std::string kHashKey = "hashKey"; + const std::string kSalt = "saltyA"; +}; + +TEST_F(BucketingConsistencyTests, BucketContextByKey) { + const BucketPrefix kPrefix{kHashKey, kSalt}; + + auto tests = + std::vector>{{"userKeyA", 0.42157587}, + {"userKeyB", 0.6708485}, + {"userKeyC", 0.10343106}}; + + for (auto [key, bucket] : tests) { + auto context = ContextBuilder().Kind("user", key).Build(); + auto result = Bucket(context, "key", kPrefix, false, "user"); + ASSERT_TRUE(result) + << key << " should be bucketed but got " << result.error(); + + ASSERT_NEAR(result->first, bucket, kBucketTolerance); + } +} + +TEST_F(BucketingConsistencyTests, BucketContextByKeyWithSeed) { + const BucketPrefix kPrefix{61}; + + auto tests = + std::vector>{{"userKeyA", 0.09801207}, + {"userKeyB", 0.14483777}, + {"userKeyC", 0.9242641}}; + + for (auto [key, bucket] : tests) { + auto context = ContextBuilder().Kind("user", key).Build(); + auto result = Bucket(context, "key", kPrefix, false, "user"); + ASSERT_TRUE(result) + << key << " should be bucketed but got " << result.error(); + + ASSERT_NEAR(result->first, bucket, kBucketTolerance); + + auto result_different_seed = + Bucket(context, "key", BucketPrefix{60}, false, "user"); + ASSERT_TRUE(result_different_seed) + << key << " should be bucketed but got " + << result_different_seed.error(); + + ASSERT_NE(result_different_seed->first, result->first); + } +} + +TEST_F(BucketingConsistencyTests, BucketContextByInvalidReference) { + const BucketPrefix kPrefix{kHashKey, kSalt}; + const AttributeReference kInvalidRef; + + ASSERT_FALSE(kInvalidRef.Valid()); + + auto context = ContextBuilder().Kind("user", "userKeyA").Build(); + auto result = Bucket(context, kInvalidRef, kPrefix, false, "user"); + + ASSERT_FALSE(result); + ASSERT_EQ(result.error(), + Error::InvalidAttributeReference(kInvalidRef.RedactionName())); +} + +TEST_F(BucketingConsistencyTests, BucketContextByIntAttribute) { + const std::string kUserKey = "userKeyD"; + const BucketPrefix kPrefix{kHashKey, kSalt}; + + auto context = + ContextBuilder().Kind("user", kUserKey).Set("intAttr", 33'333).Build(); + auto result = Bucket(context, "intAttr", kPrefix, false, "user"); + + ASSERT_TRUE(result) << kUserKey << " should be bucketed but got " + << result.error(); + ASSERT_NEAR(result->first, 0.54771423, kBucketTolerance); +} + +TEST_F(BucketingConsistencyTests, BucketContextByStringifiedIntAttribute) { + const std::string kUserKey = "userKeyD"; + const BucketPrefix kPrefix{kHashKey, kSalt}; + + auto context = ContextBuilder() + .Kind("user", kUserKey) + .Set("stringAttr", "33333") + .Build(); + auto result = Bucket(context, "stringAttr", kPrefix, false, "user"); + ASSERT_TRUE(result) << kUserKey << " should be bucketed but got " + << result.error(); + ASSERT_NEAR(result->first, 0.54771423, kBucketTolerance); +} + +TEST_F(BucketingConsistencyTests, BucketContextByFloatAttributeNotAllowed) { + const std::string kUserKey = "userKeyE"; + const BucketPrefix kPrefix{kHashKey, kSalt}; + + auto context = ContextBuilder() + .Kind("user", kUserKey) + .Set("floatAttr", 999.999) + .Build(); + auto result = Bucket(context, "floatAttr", kPrefix, false, "user"); + + ASSERT_TRUE(result) << kUserKey << " should be bucketed but got " + << result.error(); + ASSERT_NEAR(result->first, 0.0, kBucketTolerance); +} + +TEST_F(BucketingConsistencyTests, BucketContextByFloatAttributeThatIsInteger) { + const std::string kUserKey = "userKeyE"; + const BucketPrefix kPrefix{kHashKey, kSalt}; + + auto context = ContextBuilder() + .Kind("user", kUserKey) + .Set("floatAttr", 33333.0) + .Build(); + auto result = Bucket(context, "floatAttr", kPrefix, false, "user"); + + ASSERT_TRUE(result) << kUserKey << " should be bucketed but got " + << result.error(); + ASSERT_NEAR(result->first, 0.54771423, kBucketTolerance); +} + +// Parameterized tests may be instantiated with one or more BucketTests for +// convenience. +struct BucketTest { + // Context key. + std::string key; + // Expected bucket value as a string; this is only used for printing on + // error. + std::string expectedBucket; + // Expected computed variation index. + Flag::Variation expectedVariation; + // Whether the context was determined to be in an experiment. + bool expectedInExperiment; + // The rollout used for the test, which may be a percent rollout or an + // experiment. + Flag::Rollout rollout; +}; + +#define IN_EXPERIMENT true +#define NOT_IN_EXPERIMENT false + +class BucketVariationTest : public BucketingConsistencyTests, + public ::testing::WithParamInterface {}; + +static Flag::Rollout PercentRollout() { + WeightedVariation wv0(0, 60'000); + WeightedVariation wv1(1, 40'000); + return Flag::Rollout({wv0, wv1}); +} + +static Flag::Rollout ExperimentRollout() { + auto wv0 = WeightedVariation(0, 10'000); + auto wv1 = WeightedVariation(1, 20'000); + auto wv0_untracked = WeightedVariation::Untracked(0, 70'000); + Flag::Rollout rollout({wv0, wv1, wv0_untracked}); + rollout.kind = Flag::Rollout::Kind::kExperiment; + rollout.seed = 61; + return rollout; +} + +static Flag::Rollout IncompleteWeighting() { + WeightedVariation wv0(0, 1); + WeightedVariation wv1(1, 2); + WeightedVariation wv2(2, 3); + + return Flag::Rollout({wv0, wv1, wv2}); +} + +TEST_P(BucketVariationTest, VariationIndexForContext) { + auto const& param = GetParam(); + + auto result = + Variation(param.rollout, kHashKey, + ContextBuilder().Kind("user", param.key).Build(), kSalt); + + ASSERT_TRUE(result) << param.key + << " should be assigned a bucket result, but got: " + << result.error(); + + ASSERT_EQ(result->VariationIndex(), param.expectedVariation) + << param.key << " (bucket " << param.expectedBucket + << ") should get variation " << param.expectedVariation << ", but got " + << result->VariationIndex(); + + ASSERT_EQ(result->InExperiment(), param.expectedInExperiment) + << param.key << " " + << (param.expectedInExperiment ? "should" : "should not") + << " be in experiment"; +} + +INSTANTIATE_TEST_SUITE_P( + PercentRollout, + BucketVariationTest, + ::testing::ValuesIn({BucketTest{"userKeyA", "0.42157587", 0, + NOT_IN_EXPERIMENT, PercentRollout()}, + BucketTest{"userKeyB", "0.6708485", 1, + NOT_IN_EXPERIMENT, PercentRollout()}, + BucketTest{"userKeyC", "0.10343106", 0, + NOT_IN_EXPERIMENT, PercentRollout()}})); + +INSTANTIATE_TEST_SUITE_P(ExperimentRollout, + BucketVariationTest, + ::testing::ValuesIn({ + BucketTest{"userKeyA", "0.09801207", 0, + IN_EXPERIMENT, ExperimentRollout()}, + BucketTest{"userKeyB", "0.14483777", 1, + IN_EXPERIMENT, ExperimentRollout()}, + BucketTest{"userKeyC", "0.9242641", 0, + NOT_IN_EXPERIMENT, ExperimentRollout()}, + })); + +INSTANTIATE_TEST_SUITE_P(IncompleteWeightingDefaultsToLastVariation, + BucketVariationTest, + ::testing::ValuesIn({ + BucketTest{"userKeyD", "0.7816281", 2, + NOT_IN_EXPERIMENT, + IncompleteWeighting()}, + })); + +#undef IN_EXPERIMENT +#undef NOT_IN_EXPERIMENT + +TEST_F(BucketingConsistencyTests, VariationIndexForContextWithCustomAttribute) { + WeightedVariation wv0(0, 60'000); + WeightedVariation wv1(1, 40'000); + + Flag::Rollout rollout({wv0, wv1}); + rollout.bucketBy = "intAttr"; + + auto tests = std::vector>{ + {33'333, 0, "0.54771423"}, {99'999, 1, "0.7309658"}}; + + for (auto [bucketAttr, expectedVariation, expectedBucket] : tests) { + auto result = Variation(rollout, kHashKey, + ContextBuilder() + .Kind("user", "userKeyA") + .Set("intAttr", bucketAttr) + .Build(), + kSalt); + + ASSERT_TRUE(result) + << "userKeyA should be assigned a bucket result, but got: " + << result.error(); + + ASSERT_EQ(result->VariationIndex(), expectedVariation) + << "userKeyA (bucket " << expectedBucket + << ") should get variation " << expectedVariation << ", but got " + << result->VariationIndex(); + + ASSERT_EQ(result->InExperiment(), false) + << "userKeyA should not be in experiment"; + } +} + +struct ExperimentBucketTest { + std::optional seed; + std::string key; + Flag::Variation expectedVariationIndex; +}; + +class ExperimentBucketingTests + : public BucketingConsistencyTests, + public ::testing::WithParamInterface {}; + +TEST_P(ExperimentBucketingTests, VariationIndexForExperiment) { + auto const& params = GetParam(); + + WeightedVariation wv0(0, 10'000); + WeightedVariation wv1(1, 20'000); + WeightedVariation wv2(2, 70'000); + + Flag::Rollout rollout({wv0, wv1, wv2}); + rollout.bucketBy = "numberAttr"; + rollout.kind = Flag::Rollout::Kind::kExperiment; + rollout.seed = params.seed; + + auto result = Variation(rollout, kHashKey, + ContextBuilder() + .Kind("user", params.key) + .Set("numberAttr", 0.6708485) + .Build(), + kSalt); + + ASSERT_TRUE(result); + + ASSERT_EQ(result->VariationIndex(), params.expectedVariationIndex) + << params.key << " with seed " + << (params.seed ? std::to_string(*params.seed) : "(none)") + << " should get variation " << params.expectedVariationIndex; +} + +INSTANTIATE_TEST_SUITE_P( + VariationsForSeedsAndKeys, + ExperimentBucketingTests, + ::testing::ValuesIn({ + ExperimentBucketTest{std::nullopt, "userKeyA", 2}, // 0.42157587, + ExperimentBucketTest{std::nullopt, "userKeyB", 2}, // 0.6708485, + ExperimentBucketTest{std::nullopt, "userKeyC", 1}, // 0.10343106, + ExperimentBucketTest{61, "userKeyA", 0}, // 0.09801207, + ExperimentBucketTest{61, "userKeyB", 1}, // 0.14483777, + ExperimentBucketTest{61, "userKeyC", 2} // 0.9242641, + })); + +TEST_F(BucketingConsistencyTests, + BucketValueBeyondLastBucketIsPinnedToLastBucket) { + WeightedVariation wv0(0, 5'000); + WeightedVariation wv1(1, 5'000); + + Flag::Rollout rollout({wv0, wv1}); + rollout.seed = 61; + + auto context = ContextBuilder() + .Kind("user", "userKeyD") + .Set("intAttr", 99'999) + .Build(); + + auto result = Variation(rollout, kHashKey, context, kSalt); + + ASSERT_TRUE(result); + ASSERT_EQ(result->VariationIndex(), 1); + ASSERT_FALSE(result->InExperiment()); +} + +TEST_F(BucketingConsistencyTests, + BucketValueBeyongLastBucketIsPinnedToLastBucketForExperiment) { + WeightedVariation wv0(0, 5'000); + WeightedVariation wv1(1, 5'000); + + Flag::Rollout rollout({wv0, wv1}); + rollout.seed = 61; + rollout.kind = Flag::Rollout::Kind::kExperiment; + + auto context = ContextBuilder() + .Kind("user", "userKeyD") + .Set("intAttr", 99'999) + .Build(); + + auto result = Variation(rollout, kHashKey, context, kSalt); + + ASSERT_TRUE(result); + ASSERT_EQ(result->VariationIndex(), 1); + ASSERT_TRUE(result->InExperiment()); +} diff --git a/libs/server-sdk/tests/client_test.cpp b/libs/server-sdk/tests/client_test.cpp new file mode 100644 index 000000000..73d0c0dd7 --- /dev/null +++ b/libs/server-sdk/tests/client_test.cpp @@ -0,0 +1,79 @@ +#include +#include +#include +#include + +using namespace launchdarkly; +using namespace launchdarkly::server_side; + +class ClientTest : public ::testing::Test { + protected: + ClientTest() + : client_(ConfigBuilder("sdk-123").Build().value()), + context_(ContextBuilder().Kind("cat", "shadow").Build()) {} + + Client client_; + Context const context_; +}; + +TEST_F(ClientTest, ClientConstructedWithMinimalConfigAndContextT) { + char const* version = client_.Version(); + ASSERT_TRUE(version); + ASSERT_STREQ(version, "0.1.0"); // {x-release-please-version} +} + +TEST_F(ClientTest, BoolVariationDefaultPassesThrough) { + const std::string flag = "extra-cat-food"; + std::vector values = {true, false}; + for (auto const& v : values) { + ASSERT_EQ(client_.BoolVariation(context_, flag, v), v); + ASSERT_EQ(*client_.BoolVariationDetail(context_, flag, v), v); + } +} + +TEST_F(ClientTest, StringVariationDefaultPassesThrough) { + const std::string flag = "treat"; + std::vector values = {"chicken", "fish", "cat-grass"}; + for (auto const& v : values) { + ASSERT_EQ(client_.StringVariation(context_, flag, v), v); + ASSERT_EQ(*client_.StringVariationDetail(context_, flag, v), v); + } +} + +TEST_F(ClientTest, IntVariationDefaultPassesThrough) { + const std::string flag = "weight"; + std::vector values = {0, 12, 13, 24, 1000}; + for (auto const& v : values) { + ASSERT_EQ(client_.IntVariation(context_, flag, v), v); + ASSERT_EQ(*client_.IntVariationDetail(context_, flag, v), v); + } +} + +TEST_F(ClientTest, DoubleVariationDefaultPassesThrough) { + const std::string flag = "weight"; + std::vector values = {0.0, 12.0, 13.0, 24.0, 1000.0}; + for (auto const& v : values) { + ASSERT_EQ(client_.DoubleVariation(context_, flag, v), v); + ASSERT_EQ(*client_.DoubleVariationDetail(context_, flag, v), v); + } +} + +TEST_F(ClientTest, JsonVariationDefaultPassesThrough) { + const std::string flag = "assorted-values"; + std::vector values = { + Value({"running", "jumping"}), Value(3), Value(1.0), Value(true), + Value(std::map{{"weight", 20}})}; + for (auto const& v : values) { + ASSERT_EQ(client_.JsonVariation(context_, flag, v), v); + ASSERT_EQ(*client_.JsonVariationDetail(context_, flag, v), v); + } +} + +TEST_F(ClientTest, AllFlagsStateNotValid) { + // Since we don't have any ability to insert into the data store, assert + // only that the state is not valid. + auto flags = client_.AllFlagsState( + context_, AllFlagsState::Options::IncludeReasons | + AllFlagsState::Options::ClientSideOnly); + ASSERT_FALSE(flags.Valid()); +} diff --git a/libs/server-sdk/tests/data_source_event_handler_test.cpp b/libs/server-sdk/tests/data_source_event_handler_test.cpp new file mode 100644 index 000000000..4092c78a2 --- /dev/null +++ b/libs/server-sdk/tests/data_source_event_handler_test.cpp @@ -0,0 +1,202 @@ +#include + +#include +#include "data_sources/data_source_event_handler.hpp" +#include "data_store/memory_store.hpp" + +using namespace launchdarkly; +using namespace launchdarkly::server_side; +using namespace launchdarkly::server_side::data_sources; +using namespace launchdarkly::server_side::data_store; + +TEST(DataSourceEventHandlerTests, HandlesEmptyPutMessage) { + auto logger = launchdarkly::logging::NullLogger(); + auto store = std::make_shared(); + DataSourceStatusManager manager; + DataSourceEventHandler event_handler(*store, logger, manager); + + auto res = event_handler.HandleMessage("put", R"({"path":"/", "data":{}})"); + + ASSERT_EQ(DataSourceEventHandler::MessageStatus::kMessageHandled, res); + ASSERT_TRUE(store->Initialized()); + EXPECT_EQ(0, store->AllFlags().size()); + EXPECT_EQ(0, store->AllSegments().size()); + EXPECT_EQ(DataSourceStatus::DataSourceState::kValid, + manager.Status().State()); +} + +TEST(DataSourceEventHandlerTests, HandlesInvalidPut) { + auto logger = launchdarkly::logging::NullLogger(); + auto store = std::make_shared(); + DataSourceStatusManager manager; + DataSourceEventHandler event_handler(*store, logger, manager); + + auto res = event_handler.HandleMessage("put", "{sorry"); + + ASSERT_EQ(DataSourceEventHandler::MessageStatus::kInvalidMessage, res); + ASSERT_FALSE(store->Initialized()); + EXPECT_EQ(0, store->AllFlags().size()); + EXPECT_EQ(0, store->AllSegments().size()); + EXPECT_EQ(DataSourceStatus::DataSourceState::kInitializing, + manager.Status().State()); +} + +TEST(DataSourceEventHandlerTests, HandlesInvalidPatch) { + auto logger = launchdarkly::logging::NullLogger(); + auto store = std::make_shared(); + DataSourceStatusManager manager; + DataSourceEventHandler event_handler(*store, logger, manager); + + auto res = event_handler.HandleMessage("put", "{sorry"); + + ASSERT_EQ(DataSourceEventHandler::MessageStatus::kInvalidMessage, res); + ASSERT_FALSE(store->Initialized()); + EXPECT_EQ(0, store->AllFlags().size()); + EXPECT_EQ(0, store->AllSegments().size()); + EXPECT_EQ(DataSourceStatus::DataSourceState::kInitializing, + manager.Status().State()); +} + +TEST(DataSourceEventHandlerTests, HandlesPatchForUnknownPath) { + auto logger = launchdarkly::logging::NullLogger(); + auto store = std::make_shared(); + DataSourceStatusManager manager; + DataSourceEventHandler event_handler(*store, logger, manager); + + auto res = event_handler.HandleMessage( + "patch", R"({"path":"potato", "data": "SPUD"})"); + + ASSERT_EQ(DataSourceEventHandler::MessageStatus::kMessageHandled, res); + EXPECT_EQ(DataSourceStatus::DataSourceState::kInitializing, + manager.Status().State()); +} + +TEST(DataSourceEventHandlerTests, HandlesPutForUnknownPath) { + auto logger = launchdarkly::logging::NullLogger(); + auto store = std::make_shared(); + DataSourceStatusManager manager; + DataSourceEventHandler event_handler(*store, logger, manager); + + auto res = event_handler.HandleMessage( + "put", R"({"path":"potato", "data": "SPUD"})"); + + ASSERT_EQ(DataSourceEventHandler::MessageStatus::kMessageHandled, res); + EXPECT_EQ(DataSourceStatus::DataSourceState::kInitializing, + manager.Status().State()); +} + +TEST(DataSourceEventHandlerTests, HandlesInvalidDelete) { + auto logger = launchdarkly::logging::NullLogger(); + auto store = std::make_shared(); + DataSourceStatusManager manager; + DataSourceEventHandler event_handler(*store, logger, manager); + + auto res = event_handler.HandleMessage("put", "{sorry"); + + ASSERT_EQ(DataSourceEventHandler::MessageStatus::kInvalidMessage, res); + ASSERT_FALSE(store->Initialized()); + EXPECT_EQ(0, store->AllFlags().size()); + EXPECT_EQ(0, store->AllSegments().size()); + EXPECT_EQ(DataSourceStatus::DataSourceState::kInitializing, + manager.Status().State()); +} + +TEST(DataSourceEventHandlerTests, HandlesPayloadWithFlagAndSegment) { + auto logger = launchdarkly::logging::NullLogger(); + auto store = std::make_shared(); + DataSourceStatusManager manager; + DataSourceEventHandler event_handler(*store, logger, manager); + auto payload = + R"({"path":"/","data":{"segments":{"special":{"key":"special","included":["bob"], + "version":2}},"flags":{"HasBob":{"key":"HasBob","on":true,"fallthrough": + {"variation":1},"variations":[true,false],"version":4}}}})"; + auto res = event_handler.HandleMessage("put", payload); + + ASSERT_EQ(DataSourceEventHandler::MessageStatus::kMessageHandled, res); + ASSERT_TRUE(store->Initialized()); + EXPECT_EQ(1, store->AllFlags().size()); + EXPECT_EQ(1, store->AllSegments().size()); + EXPECT_TRUE(store->GetFlag("HasBob")); + EXPECT_TRUE(store->GetSegment("special")); + EXPECT_EQ(DataSourceStatus::DataSourceState::kValid, + manager.Status().State()); +} + +TEST(DataSourceEventHandlerTests, HandlesValidFlagPatch) { + auto logger = launchdarkly::logging::NullLogger(); + auto store = std::make_shared(); + DataSourceStatusManager manager; + DataSourceEventHandler event_handler(*store, logger, manager); + + event_handler.HandleMessage("put", "{}"); + + auto patch_res = event_handler.HandleMessage( + "patch", + R"({"path": "/flags/flagA", "data":{"key": "flagA", "version":2}})"); + + ASSERT_EQ(DataSourceEventHandler::MessageStatus::kMessageHandled, + patch_res); + + EXPECT_EQ(1, store->AllFlags().size()); +} + +TEST(DataSourceEventHandlerTests, HandlesValidSegmentPatch) { + auto logger = launchdarkly::logging::NullLogger(); + auto store = std::make_shared(); + DataSourceStatusManager manager; + DataSourceEventHandler event_handler(*store, logger, manager); + + event_handler.HandleMessage("put", "{}"); + + auto patch_res = event_handler.HandleMessage( + "patch", + R"({"path": "/segments/segmentA", "data":{"key": "segmentA", "version":2}})"); + + ASSERT_EQ(DataSourceEventHandler::MessageStatus::kMessageHandled, + patch_res); + + EXPECT_EQ(1, store->AllSegments().size()); +} + +TEST(DataSourceEventHandlerTests, HandlesDeleteFlag) { + auto logger = launchdarkly::logging::NullLogger(); + auto store = std::make_shared(); + DataSourceStatusManager manager; + DataSourceEventHandler event_handler(*store, logger, manager); + + event_handler.HandleMessage( + "put", R"({"path":"/","data":{"segments":{})" + R"(, "flags":{"flagA": {"key":"flagA", "version": 0}}}})"); + + ASSERT_TRUE(store->GetFlag("flagA")->item); + + auto patch_res = event_handler.HandleMessage( + "delete", R"({"path": "/flags/flagA", "version": 1})"); + + ASSERT_EQ(DataSourceEventHandler::MessageStatus::kMessageHandled, + patch_res); + + ASSERT_FALSE(store->GetFlag("flagA")->item); +} + +TEST(DataSourceEventHandlerTests, HandlesDeleteSegment) { + auto logger = launchdarkly::logging::NullLogger(); + auto store = std::make_shared(); + DataSourceStatusManager manager; + DataSourceEventHandler event_handler(*store, logger, manager); + + event_handler.HandleMessage( + "put", + R"({"path":"/","data":{"flags":{})" + R"(, "segments":{"segmentA": {"key":"segmentA", "version": 0}}}})"); + + ASSERT_TRUE(store->GetSegment("segmentA")->item); + + auto patch_res = event_handler.HandleMessage( + "delete", R"({"path": "/segments/segmentA", "version": 1})"); + + ASSERT_EQ(DataSourceEventHandler::MessageStatus::kMessageHandled, + patch_res); + + ASSERT_FALSE(store->GetSegment("segmentA")->item); +} diff --git a/libs/server-sdk/tests/data_store_updater_test.cpp b/libs/server-sdk/tests/data_store_updater_test.cpp new file mode 100644 index 000000000..87fc139b9 --- /dev/null +++ b/libs/server-sdk/tests/data_store_updater_test.cpp @@ -0,0 +1,430 @@ +#include + +#include "data_store/data_store_updater.hpp" +#include "data_store/descriptors.hpp" +#include "data_store/memory_store.hpp" + +using launchdarkly::data_model::SDKDataSet; +using launchdarkly::server_side::data_store::DataStoreUpdater; +using launchdarkly::server_side::data_store::FlagDescriptor; +using launchdarkly::server_side::data_store::IDataStore; +using launchdarkly::server_side::data_store::MemoryStore; +using launchdarkly::server_side::data_store::SegmentDescriptor; + +using launchdarkly::Value; +using launchdarkly::data_model::Flag; +using launchdarkly::data_model::Segment; + +TEST(DataStoreUpdaterTest, DoesNotInitializeStoreUntilInit) { + MemoryStore store; + DataStoreUpdater updater(store, store); + EXPECT_FALSE(store.Initialized()); +} + +TEST(DataStoreUpdaterTest, InitializesStore) { + MemoryStore store; + DataStoreUpdater updater(store, store); + updater.Init(SDKDataSet()); + EXPECT_TRUE(store.Initialized()); +} + +TEST(DataStoreUpdaterTest, InitPropagatesData) { + MemoryStore store; + DataStoreUpdater updater(store, store); + Flag flag; + flag.version = 1; + flag.key = "flagA"; + flag.on = true; + flag.variations = std::vector{true, false}; + Flag::Variation variation = 0; + flag.fallthrough = variation; + + auto segment = Segment(); + segment.version = 1; + segment.key = "segmentA"; + + updater.Init(SDKDataSet{ + std::unordered_map{ + {"flagA", FlagDescriptor(flag)}}, + std::unordered_map{ + {"segmentA", SegmentDescriptor(segment)}}, + }); + + auto fetched_flag = store.GetFlag("flagA"); + EXPECT_TRUE(fetched_flag); + EXPECT_TRUE(fetched_flag->item); + EXPECT_EQ("flagA", fetched_flag->item->key); + EXPECT_EQ(1, fetched_flag->item->version); + EXPECT_EQ(fetched_flag->version, fetched_flag->item->version); + + auto fetched_segment = store.GetSegment("segmentA"); + EXPECT_TRUE(fetched_segment); + EXPECT_TRUE(fetched_segment->item); + EXPECT_EQ("segmentA", fetched_segment->item->key); + EXPECT_EQ(1, fetched_segment->item->version); + EXPECT_EQ(fetched_segment->version, fetched_segment->item->version); +} + +TEST(DataStoreUpdaterTest, SecondInitProducesChanges) { + MemoryStore store; + DataStoreUpdater updater(store, store); + Flag flag_a_v1; + flag_a_v1.version = 1; + flag_a_v1.key = "flagA"; + flag_a_v1.on = true; + flag_a_v1.variations = std::vector{true, false}; + Flag::Variation variation = 0; + flag_a_v1.fallthrough = variation; + + Flag flag_b_v1; + flag_b_v1.version = 1; + flag_b_v1.key = "flagA"; + flag_b_v1.on = true; + flag_b_v1.variations = std::vector{true, false}; + flag_b_v1.fallthrough = variation; + + Flag flab_c_v1; + flab_c_v1.version = 1; + flab_c_v1.key = "flagA"; + flab_c_v1.on = true; + flab_c_v1.variations = std::vector{true, false}; + flab_c_v1.fallthrough = variation; + + updater.Init(SDKDataSet{ + std::unordered_map{ + {"flagA", FlagDescriptor(flag_a_v1)}, + {"flagB", FlagDescriptor(flag_b_v1)}}, + std::unordered_map(), + }); + + Flag flag_a_v2; + flag_a_v2.version = 2; + flag_a_v2.key = "flagA"; + flag_a_v2.on = true; + flag_a_v2.variations = std::vector{true, false}; + flag_a_v2.fallthrough = variation; + + // Not updated. + Flag flag_c_v1_second; + flag_c_v1_second.version = 1; + flag_c_v1_second.key = "flagC"; + flag_c_v1_second.on = true; + flag_c_v1_second.variations = std::vector{true, false}; + flag_c_v1_second.fallthrough = variation; + + // New flag + Flag flag_d; + flag_d.version = 2; + flag_d.key = "flagD"; + flag_d.on = true; + flag_d.variations = std::vector{true, false}; + flag_d.fallthrough = variation; + + std::atomic got_event(false); + updater.OnFlagChange( + [&got_event](std::shared_ptr> changeset) { + got_event = true; + std::vector diff; + auto expectedSet = std::set{"flagA", "flagB", "flagD"}; + std::set_difference(expectedSet.begin(), expectedSet.end(), + changeset->begin(), changeset->end(), + std::inserter(diff, diff.begin())); + EXPECT_EQ(0, diff.size()); + }); + + // Updated flag A, deleted flag B, added flag C. + updater.Init(SDKDataSet{ + std::unordered_map{ + {"flagA", FlagDescriptor(flag_a_v2)}, + {"flagD", FlagDescriptor(flag_d)}, + {"flagC", FlagDescriptor(flag_c_v1_second)}}, + std::unordered_map(), + }); + + EXPECT_TRUE(got_event); +} + +TEST(DataStoreUpdaterTest, CanUpsertNewFlag) { + MemoryStore store; + DataStoreUpdater updater(store, store); + + Flag flag_a; + flag_a.version = 1; + flag_a.key = "flagA"; + + updater.Init(SDKDataSet{ + std::unordered_map(), + std::unordered_map(), + }); + updater.Upsert("flagA", FlagDescriptor(flag_a)); + + auto fetched_flag = store.GetFlag("flagA"); + EXPECT_TRUE(fetched_flag); + EXPECT_TRUE(fetched_flag->item); + EXPECT_EQ("flagA", fetched_flag->item->key); + EXPECT_EQ(1, fetched_flag->item->version); + EXPECT_EQ(fetched_flag->version, fetched_flag->item->version); +} + +TEST(DataStoreUpdaterTest, CanUpsertExitingFlag) { + Flag flag_a; + flag_a.version = 1; + flag_a.key = "flagA"; + + MemoryStore store; + DataStoreUpdater updater(store, store); + + updater.Init(SDKDataSet{ + std::unordered_map{ + {"flagA", FlagDescriptor(flag_a)}}, + std::unordered_map(), + }); + + Flag flag_a_2; + flag_a_2.version = 2; + flag_a_2.key = "flagA"; + + updater.Upsert("flagA", FlagDescriptor(flag_a_2)); + + auto fetched_flag = store.GetFlag("flagA"); + EXPECT_TRUE(fetched_flag); + EXPECT_TRUE(fetched_flag->item); + EXPECT_EQ("flagA", fetched_flag->item->key); + EXPECT_EQ(2, fetched_flag->item->version); + EXPECT_EQ(fetched_flag->version, fetched_flag->item->version); +} + +TEST(DataStoreUpdaterTest, OldVersionIsDiscardedOnUpsertFlag) { + Flag flag_a; + flag_a.version = 2; + flag_a.key = "flagA"; + flag_a.variations = std::vector{"potato", "ham"}; + + MemoryStore store; + DataStoreUpdater updater(store, store); + + updater.Init(SDKDataSet{ + std::unordered_map{ + {"flagA", FlagDescriptor(flag_a)}}, + std::unordered_map(), + }); + + Flag flag_a_2; + flag_a_2.version = 1; + flag_a_2.key = "flagA"; + flag_a.variations = std::vector{"potato"}; + + updater.Upsert("flagA", FlagDescriptor(flag_a_2)); + + auto fetched_flag = store.GetFlag("flagA"); + EXPECT_TRUE(fetched_flag); + EXPECT_TRUE(fetched_flag->item); + EXPECT_EQ("flagA", fetched_flag->item->key); + EXPECT_EQ(2, fetched_flag->item->version); + EXPECT_EQ(fetched_flag->version, fetched_flag->item->version); + EXPECT_EQ(2, fetched_flag->item->variations.size()); + EXPECT_EQ(std::string("potato"), + fetched_flag->item->variations[0].AsString()); + EXPECT_EQ(std::string("ham"), fetched_flag->item->variations[1].AsString()); +} + +TEST(DataStoreUpdaterTest, CanUpsertNewSegment) { + Segment segment_a; + segment_a.version = 1; + segment_a.key = "segmentA"; + + MemoryStore store; + DataStoreUpdater updater(store, store); + + updater.Init(SDKDataSet{ + std::unordered_map(), + std::unordered_map(), + }); + updater.Upsert("segmentA", SegmentDescriptor(segment_a)); + + auto fetched_segment = store.GetSegment("segmentA"); + EXPECT_TRUE(fetched_segment); + EXPECT_TRUE(fetched_segment->item); + EXPECT_EQ("segmentA", fetched_segment->item->key); + EXPECT_EQ(1, fetched_segment->item->version); + EXPECT_EQ(fetched_segment->version, fetched_segment->item->version); +} + +TEST(DataStoreUpdaterTest, CanUpsertExitingSegment) { + Segment segment_a; + segment_a.version = 1; + segment_a.key = "segmentA"; + + MemoryStore store; + DataStoreUpdater updater(store, store); + + updater.Init(SDKDataSet{ + std::unordered_map(), + std::unordered_map{ + {"segmentA", SegmentDescriptor(segment_a)}}, + }); + + Segment segment_a_2; + segment_a_2.version = 2; + segment_a_2.key = "segmentA"; + + updater.Upsert("segmentA", SegmentDescriptor(segment_a_2)); + + auto fetched_segment = store.GetSegment("segmentA"); + EXPECT_TRUE(fetched_segment); + EXPECT_TRUE(fetched_segment->item); + EXPECT_EQ("segmentA", fetched_segment->item->key); + EXPECT_EQ(2, fetched_segment->item->version); + EXPECT_EQ(fetched_segment->version, fetched_segment->item->version); +} + +TEST(DataStoreUpdaterTest, OldVersionIsDiscardedOnUpsertSegment) { + Segment segment_a; + segment_a.version = 2; + segment_a.key = "segmentA"; + + MemoryStore store; + DataStoreUpdater updater(store, store); + + updater.Init(SDKDataSet{ + std::unordered_map(), + std::unordered_map{ + {"segmentA", SegmentDescriptor(segment_a)}}, + }); + + Segment segment_a_2; + segment_a_2.version = 1; + segment_a_2.key = "segmentA"; + + updater.Upsert("segmentA", SegmentDescriptor(segment_a_2)); + + auto fetched_segment = store.GetSegment("segmentA"); + EXPECT_TRUE(fetched_segment); + EXPECT_TRUE(fetched_segment->item); + EXPECT_EQ("segmentA", fetched_segment->item->key); + EXPECT_EQ(2, fetched_segment->item->version); + EXPECT_EQ(fetched_segment->version, fetched_segment->item->version); +} + +TEST(DataStoreUpdaterTest, ProducesChangeEventsOnUpsert) { + Flag flag_a; + Flag flag_b; + + flag_a.key = "flagA"; + flag_a.version = 1; + + flag_b.key = "flagB"; + flag_b.version = 1; + + flag_b.prerequisites.push_back(Flag::Prerequisite{"flagA", 0}); + + MemoryStore store; + DataStoreUpdater updater(store, store); + + updater.Init(SDKDataSet{ + std::unordered_map{ + {"flagA", FlagDescriptor(flag_a)}, + {"flagB", FlagDescriptor(flag_b)}}, + std::unordered_map(), + }); + + Flag flag_a_2; + flag_a_2.key = "flagA"; + flag_a_2.version = 2; + + std::atomic got_event(false); + updater.OnFlagChange( + [&got_event](std::shared_ptr> changeset) { + got_event = true; + std::vector diff; + auto expectedSet = std::set{"flagA", "flagB"}; + std::set_difference(expectedSet.begin(), expectedSet.end(), + changeset->begin(), changeset->end(), + std::inserter(diff, diff.begin())); + EXPECT_EQ(0, diff.size()); + }); + + updater.Upsert("flagA", FlagDescriptor(flag_a_2)); + + EXPECT_EQ(true, got_event); +} + +TEST(DataStoreUpdaterTest, ProducesNoEventIfNoFlagChanged) { + Flag flag_a; + Flag flag_b; + + flag_a.key = "flagA"; + flag_a.version = 1; + + flag_b.key = "flagB"; + flag_b.version = 1; + + flag_b.prerequisites.push_back(Flag::Prerequisite{"flagA", 0}); + + MemoryStore store; + DataStoreUpdater updater(store, store); + + Segment segment_a; + segment_a.version = 1; + segment_a.key = "segmentA"; + + updater.Init(SDKDataSet{ + std::unordered_map{ + {"flagA", FlagDescriptor(flag_a)}, + {"flagB", FlagDescriptor(flag_b)}}, + std::unordered_map{ + {"segmentA", SegmentDescriptor(segment_a)}, + }, + }); + + Segment segment_a_2; + segment_a_2.key = "flagA"; + segment_a_2.version = 2; + + std::atomic got_event(false); + updater.OnFlagChange( + [&got_event](std::shared_ptr> changeset) { + got_event = true; + }); + + updater.Upsert("segmentA", SegmentDescriptor(segment_a_2)); + + EXPECT_EQ(false, got_event); +} + +TEST(DataStoreUpdaterTest, NoEventOnDiscardedUpsert) { + Flag flag_a; + Flag flag_b; + + flag_a.key = "flagA"; + flag_a.version = 1; + + flag_b.key = "flagB"; + flag_b.version = 1; + + flag_b.prerequisites.push_back(Flag::Prerequisite{"flagA", 0}); + + MemoryStore store; + DataStoreUpdater updater(store, store); + + updater.Init(SDKDataSet{ + std::unordered_map{ + {"flagA", FlagDescriptor(flag_a)}, + {"flagB", FlagDescriptor(flag_b)}}, + std::unordered_map(), + }); + + Flag flag_a_2; + flag_a_2.key = "flagA"; + flag_a_2.version = 1; + + std::atomic got_event(false); + updater.OnFlagChange( + [&got_event](std::shared_ptr> changeset) { + got_event = true; + }); + + updater.Upsert("flagA", FlagDescriptor(flag_a_2)); + + EXPECT_EQ(false, got_event); +} diff --git a/libs/server-sdk/tests/dependency_tracker_test.cpp b/libs/server-sdk/tests/dependency_tracker_test.cpp new file mode 100644 index 000000000..bbffbc09b --- /dev/null +++ b/libs/server-sdk/tests/dependency_tracker_test.cpp @@ -0,0 +1,298 @@ +#include + +#include "data_store/dependency_tracker.hpp" +#include "data_store/descriptors.hpp" + +using launchdarkly::server_side::data_store::DataKind; +using launchdarkly::server_side::data_store::DependencyMap; +using launchdarkly::server_side::data_store::DependencySet; +using launchdarkly::server_side::data_store::DependencyTracker; +using launchdarkly::server_side::data_store::FlagDescriptor; +using launchdarkly::server_side::data_store::SegmentDescriptor; + +using launchdarkly::AttributeReference; +using launchdarkly::Value; +using launchdarkly::data_model::Clause; +using launchdarkly::data_model::ContextKind; +using launchdarkly::data_model::Flag; +using launchdarkly::data_model::ItemDescriptor; +using launchdarkly::data_model::Segment; + +TEST(ScopedSetTest, CanAddItem) { + DependencySet set; + set.Set(DataKind::kFlag, "flagA"); +} + +TEST(ScopedSetTest, CanCheckIfContains) { + DependencySet set; + set.Set(DataKind::kFlag, "flagA"); + + EXPECT_TRUE(set.Contains(DataKind::kFlag, "flagA")); + EXPECT_FALSE(set.Contains(DataKind::kFlag, "flagB")); + EXPECT_FALSE(set.Contains(DataKind::kSegment, "flagA")); +} + +TEST(ScopedSetTest, CanRemoveItem) { + DependencySet set; + set.Set(DataKind::kFlag, "flagA"); + set.Remove(DataKind::kFlag, "flagA"); + EXPECT_FALSE(set.Contains(DataKind::kFlag, "flagA")); +} + +TEST(ScopedSetTest, CanIterate) { + DependencySet set; + set.Set(DataKind::kFlag, "flagA"); + set.Set(DataKind::kFlag, "flagB"); + set.Set(DataKind::kSegment, "segmentA"); + set.Set(DataKind::kSegment, "segmentB"); + + auto count = 0; + auto expectations = + std::vector{"flagA", "flagB", "segmentA", "segmentB"}; + + for (auto& ns : set) { + if (count == 0) { + EXPECT_EQ(DataKind::kFlag, ns.Kind()); + } else { + EXPECT_EQ(DataKind::kSegment, ns.Kind()); + } + for (auto val : ns.Data()) { + EXPECT_EQ(expectations[count], val); + count++; + } + } + EXPECT_EQ(4, count); +} + +TEST(ScopedMapTest, CanAddItem) { + DependencyMap map; + DependencySet deps; + deps.Set(DataKind::kSegment, "segmentA"); + + map.Set(DataKind::kFlag, "flagA", deps); +} + +TEST(ScopedMapTest, CanGetItem) { + DependencyMap map; + DependencySet deps; + deps.Set(DataKind::kSegment, "segmentA"); + + map.Set(DataKind::kFlag, "flagA", deps); + + EXPECT_TRUE(map.Get(DataKind::kFlag, "flagA") + ->Contains(DataKind::kSegment, "segmentA")); +} + +TEST(ScopedMapTest, CanIterate) { + DependencyMap map; + + DependencySet dep_flags; + dep_flags.Set(DataKind::kSegment, "segmentA"); + dep_flags.Set(DataKind::kFlag, "flagB"); + + DependencySet depSegments; + depSegments.Set(DataKind::kSegment, "segmentB"); + + map.Set(DataKind::kFlag, "flagA", dep_flags); + map.Set(DataKind::kSegment, "segmentA", depSegments); + + auto expectationKeys = + std::set{"segmentA", "flagB", "segmentB"}; + auto expectationKinds = std::vector{ + DataKind::kFlag, DataKind::kSegment, DataKind::kSegment}; + + auto count = 0; + for (auto& ns : map) { + if (count == 0) { + EXPECT_EQ(DataKind::kFlag, ns.Kind()); + } else { + EXPECT_EQ(DataKind::kSegment, ns.Kind()); + } + for (auto const& depSet : ns.Data()) { + for (auto const& deps : depSet.second) { + for (auto& dep : deps.Data()) { + EXPECT_EQ(expectationKinds[count], deps.Kind()); + EXPECT_TRUE(expectationKeys.count(dep) != 0); + expectationKeys.erase(dep); + count++; + } + } + } + } + EXPECT_EQ(3, count); +} + +TEST(ScopedMapTest, CanClear) { + DependencyMap map; + + DependencySet dep_flags; + dep_flags.Set(DataKind::kSegment, "segmentA"); + dep_flags.Set(DataKind::kFlag, "flagB"); + + DependencySet dep_segments; + dep_segments.Set(DataKind::kSegment, "segmentB"); + + map.Set(DataKind::kFlag, "flagA", dep_flags); + map.Set(DataKind::kSegment, "segmentA", dep_segments); + map.Clear(); + + for (auto& ns : map) { + for ([[maybe_unused]] auto& set_set : ns.Data()) { + GTEST_FAIL(); + } + } +} + +TEST(DependencyTrackerTest, TreatsPrerequisitesAsDependencies) { + DependencyTracker tracker; + + Flag flag_a; + Flag flag_b; + Flag flag_c; + + flag_a.key = "flagA"; + flag_a.version = 1; + + flag_b.key = "flagB"; + flag_b.version = 1; + + // Unused, to make sure not everything is just included in the dependencies. + flag_c.key = "flagC"; + flag_c.version = 1; + + flag_b.prerequisites.push_back(Flag::Prerequisite{"flagA", 0}); + + tracker.UpdateDependencies("flagA", FlagDescriptor(flag_a)); + tracker.UpdateDependencies("flagB", FlagDescriptor(flag_b)); + tracker.UpdateDependencies("flagC", FlagDescriptor(flag_c)); + + DependencySet changes; + tracker.CalculateChanges(DataKind::kFlag, "flagA", changes); + + EXPECT_TRUE(changes.Contains(DataKind::kFlag, "flagB")); + EXPECT_TRUE(changes.Contains(DataKind::kFlag, "flagA")); + EXPECT_EQ(2, changes.Size()); +} + +TEST(DependencyTrackerTest, UsesSegmentRulesToCalculateDependencies) { + DependencyTracker tracker; + + Flag flag_a; + Segment segment_a; + + Flag flag_b; + Segment segment_b; + + flag_a.key = "flagA"; + flag_a.version = 1; + + segment_a.key = "segmentA"; + segment_a.version = 1; + + // flagB and segmentB are unused. + flag_b.key = "flagB"; + flag_b.version = 1; + + segment_b.key = "segmentB"; + segment_b.version = 1; + + flag_a.rules.push_back(Flag::Rule{std::vector{ + Clause{Clause::Op::kSegmentMatch, std::vector{"segmentA"}, false, + ContextKind("user"), AttributeReference()}}}); + + tracker.UpdateDependencies("flagA", FlagDescriptor(flag_a)); + tracker.UpdateDependencies("segmentA", SegmentDescriptor(segment_a)); + + tracker.UpdateDependencies("flagB", FlagDescriptor(flag_b)); + tracker.UpdateDependencies("segmentB", SegmentDescriptor(segment_b)); + + DependencySet changes; + tracker.CalculateChanges(DataKind::kSegment, "segmentA", changes); + + EXPECT_TRUE(changes.Contains(DataKind::kFlag, "flagA")); + EXPECT_TRUE(changes.Contains(DataKind::kSegment, "segmentA")); + EXPECT_EQ(2, changes.Size()); +} + +TEST(DependencyTrackerTest, TracksSegmentDependencyOfPrerequisite) { + DependencyTracker tracker; + + Flag flag_a; + Flag flag_b; + Segment segment_a; + + flag_a.key = "flagA"; + flag_a.version = 1; + + flag_b.key = "flagB"; + flag_b.version = 1; + + segment_a.key = "segmentA"; + segment_a.version = 1; + + flag_a.rules.push_back(Flag::Rule{std::vector{ + Clause{Clause::Op::kSegmentMatch, std::vector{"segmentA"}, false, + ContextKind(""), AttributeReference()}}}); + + flag_b.prerequisites.push_back(Flag::Prerequisite{"flagA", 0}); + + tracker.UpdateDependencies("flagA", FlagDescriptor(flag_a)); + tracker.UpdateDependencies("flagB", FlagDescriptor(flag_b)); + tracker.UpdateDependencies("segmentA", SegmentDescriptor(segment_a)); + + DependencySet changes; + tracker.CalculateChanges(DataKind::kSegment, "segmentA", changes); + + // The segment itself was changed. + EXPECT_TRUE(changes.Contains(DataKind::kSegment, "segmentA")); + // flagA has a rule which depends on segmentA. + EXPECT_TRUE(changes.Contains(DataKind::kFlag, "flagA")); + // flagB has a prerequisite of flagA. + EXPECT_TRUE(changes.Contains(DataKind::kFlag, "flagB")); + EXPECT_EQ(3, changes.Size()); +} + +TEST(DependencyTrackerTest, HandlesSegmentsDependentOnOtherSegments) { + DependencyTracker tracker; + + Segment segment_a; + Segment segment_b; + Segment segment_c; + + segment_a.key = "segmentA"; + segment_a.version = 1; + + segment_b.key = "segmentB"; + segment_b.version = 1; + + segment_c.key = "segmentC"; + segment_c.version = 1; + + segment_b.rules.push_back(Segment::Rule{ + std::vector{Clause{Clause::Op::kSegmentMatch, + std::vector{"segmentA"}, false, + ContextKind("user"), AttributeReference()}}, + std::nullopt, std::nullopt, ContextKind(""), AttributeReference()}); + + tracker.UpdateDependencies("segmentA", SegmentDescriptor(segment_a)); + tracker.UpdateDependencies("segmentB", SegmentDescriptor(segment_b)); + tracker.UpdateDependencies("segmentC", SegmentDescriptor(segment_c)); + + DependencySet changes; + tracker.CalculateChanges(DataKind::kSegment, "segmentA", changes); + + EXPECT_TRUE(changes.Contains(DataKind::kSegment, "segmentB")); + EXPECT_TRUE(changes.Contains(DataKind::kSegment, "segmentA")); + EXPECT_EQ(2, changes.Size()); +} + +TEST(DependencyTrackerTest, HandlesUpdateForSomethingThatDoesNotExist) { + // This shouldn't happen, but it should also not break. + DependencyTracker tracker; + + DependencySet changes; + tracker.CalculateChanges(DataKind::kFlag, "potato", changes); + + EXPECT_EQ(1, changes.Size()); + EXPECT_TRUE(changes.Contains(DataKind::kFlag, "potato")); +} diff --git a/libs/server-sdk/tests/evaluation_stack_test.cpp b/libs/server-sdk/tests/evaluation_stack_test.cpp new file mode 100644 index 000000000..23193c8df --- /dev/null +++ b/libs/server-sdk/tests/evaluation_stack_test.cpp @@ -0,0 +1,53 @@ +#include + +#include "evaluation/detail/evaluation_stack.hpp" + +using namespace launchdarkly::server_side::evaluation::detail; + +TEST(EvalStackTests, SegmentIsNoticed) { + EvaluationStack stack; + auto g1 = stack.NoticeSegment("foo"); + ASSERT_TRUE(g1); + ASSERT_FALSE(stack.NoticeSegment("foo")); +} + +TEST(EvalStackTests, PrereqIsNoticed) { + EvaluationStack stack; + auto g1 = stack.NoticePrerequisite("foo"); + ASSERT_TRUE(g1); + ASSERT_FALSE(stack.NoticePrerequisite("foo")); +} + +TEST(EvalStackTests, NestedScopes) { + EvaluationStack stack; + { + auto g1 = stack.NoticeSegment("foo"); + ASSERT_TRUE(g1); + ASSERT_FALSE(stack.NoticeSegment("foo")); + { + auto g2 = stack.NoticeSegment("bar"); + ASSERT_TRUE(g2); + ASSERT_FALSE(stack.NoticeSegment("bar")); + ASSERT_FALSE(stack.NoticeSegment("foo")); + } + ASSERT_TRUE(stack.NoticeSegment("bar")); + } + ASSERT_TRUE(stack.NoticeSegment("foo")); + ASSERT_TRUE(stack.NoticeSegment("bar")); +} + +TEST(EvalStackTests, SegmentAndPrereqHaveSeparateCaches) { + EvaluationStack stack; + auto g1 = stack.NoticeSegment("foo"); + ASSERT_TRUE(g1); + auto g2 = stack.NoticePrerequisite("foo"); + ASSERT_TRUE(g2); +} + +TEST(EvalStackTests, ImmediateDestructionOfGuard) { + EvaluationStack stack; + + ASSERT_TRUE(stack.NoticeSegment("foo")); + ASSERT_TRUE(stack.NoticeSegment("foo")); + ASSERT_TRUE(stack.NoticeSegment("foo")); +} diff --git a/libs/server-sdk/tests/evaluator_tests.cpp b/libs/server-sdk/tests/evaluator_tests.cpp new file mode 100644 index 000000000..85807a848 --- /dev/null +++ b/libs/server-sdk/tests/evaluator_tests.cpp @@ -0,0 +1,248 @@ +#include "evaluation/evaluator.hpp" +#include "test_store.hpp" + +#include +#include + +#include "spy_logger.hpp" + +#include + +using namespace launchdarkly; +using namespace launchdarkly::server_side; + +/** + * Use if the test does not require inspecting log messages. + */ +class EvaluatorTests : public ::testing::Test { + public: + EvaluatorTests() + : logger_(logging::NullLogger()), + store_(test_store::TestData()), + eval_(logger_, *store_) {} + + private: + Logger logger_; + + protected: + std::unique_ptr store_; + evaluation::Evaluator eval_; +}; + +/** + * Use if the test requires making assertions based on log messages generated + * during evaluation. + */ +class EvaluatorTestsWithLogs : public ::testing::Test { + public: + EvaluatorTestsWithLogs() + : messages_(std::make_shared()), + logger_(messages_), + store_(test_store::TestData()), + eval_(logger_, *store_) {} + + protected: + std::shared_ptr messages_; + + private: + Logger logger_; + + protected: + std::unique_ptr store_; + evaluation::Evaluator eval_; +}; + +TEST_F(EvaluatorTests, BasicChanges) { + auto alice = ContextBuilder().Kind("user", "alice").Build(); + auto bob = ContextBuilder().Kind("user", "bob").Build(); + + auto flag = store_->GetFlag("flagWithTarget")->item.value(); + + ASSERT_FALSE(flag.on); + + auto detail = eval_.Evaluate(flag, alice); + + ASSERT_FALSE(detail.IsError()); + ASSERT_EQ(*detail, Value(false)); + ASSERT_EQ(detail.VariationIndex(), 0); + ASSERT_EQ(detail.Reason(), EvaluationReason::Off()); + + // flip off variation + flag.offVariation = 1; + detail = eval_.Evaluate(flag, alice); + ASSERT_FALSE(detail.IsError()); + ASSERT_EQ(detail.VariationIndex(), 1); + ASSERT_EQ(*detail, Value(true)); + + // off variation unspecified + flag.offVariation = std::nullopt; + detail = eval_.Evaluate(flag, alice); + ASSERT_FALSE(detail.IsError()); + ASSERT_EQ(detail.VariationIndex(), std::nullopt); + ASSERT_EQ(*detail, Value::Null()); + + // flip targeting on + flag.on = true; + detail = eval_.Evaluate(flag, alice); + ASSERT_FALSE(detail.IsError()); + ASSERT_EQ(detail.VariationIndex(), 1); + ASSERT_EQ(*detail, Value(true)); + ASSERT_EQ(detail.Reason(), EvaluationReason::Fallthrough(false)); + + detail = eval_.Evaluate(flag, bob); + ASSERT_FALSE(detail.IsError()); + ASSERT_EQ(detail.VariationIndex(), 0); + ASSERT_EQ(*detail, Value(false)); + ASSERT_EQ(detail.Reason(), EvaluationReason::TargetMatch()); + + // flip default variation + flag.fallthrough = data_model::Flag::Variation{0}; + detail = eval_.Evaluate(flag, alice); + ASSERT_FALSE(detail.IsError()); + ASSERT_EQ(detail.VariationIndex(), 0); + ASSERT_EQ(*detail, Value(false)); + + // bob's reason should still be TargetMatch even though his value is now the + // default + detail = eval_.Evaluate(flag, bob); + ASSERT_FALSE(detail.IsError()); + ASSERT_EQ(detail.VariationIndex(), 0); + ASSERT_EQ(*detail, Value(false)); + ASSERT_EQ(detail.Reason(), EvaluationReason::TargetMatch()); +} + +TEST_F(EvaluatorTests, EvaluateWithMatchesOpGroups) { + auto alice = ContextBuilder().Kind("user", "alice").Build(); + auto bob = ContextBuilder() + .Kind("user", "bob") + .Set("groups", {"my-group"}) + .Build(); + + auto flag = store_->GetFlag("flagWithMatchesOpOnGroups")->item.value(); + + auto detail = eval_.Evaluate(flag, alice); + ASSERT_FALSE(detail.IsError()); + ASSERT_EQ(*detail, Value(true)); + ASSERT_EQ(detail.Reason(), EvaluationReason::Fallthrough(false)); + + detail = eval_.Evaluate(flag, bob); + ASSERT_FALSE(detail.IsError()); + ASSERT_EQ(*detail, Value(false)); + ASSERT_EQ(detail.VariationIndex(), 0); + ASSERT_EQ(detail.Reason(), + EvaluationReason::RuleMatch( + 0, "6a7755ac-e47a-40ea-9579-a09dd5f061bd", false)); +} + +TEST_F(EvaluatorTests, EvaluateWithMatchesOpKinds) { + auto alice = ContextBuilder().Kind("user", "alice").Build(); + auto bob = ContextBuilder().Kind("company", "bob").Build(); + + auto flag = store_->GetFlag("flagWithMatchesOpOnKinds")->item.value(); + + auto detail = eval_.Evaluate(flag, alice); + ASSERT_FALSE(detail.IsError()); + ASSERT_EQ(*detail, Value(false)); + ASSERT_EQ(detail.VariationIndex(), 0); + ASSERT_EQ(detail.Reason(), + EvaluationReason::RuleMatch( + 0, "6a7755ac-e47a-40ea-9579-a09dd5f061bd", false)); + + detail = eval_.Evaluate(flag, bob); + ASSERT_FALSE(detail.IsError()); + ASSERT_EQ(*detail, Value(true)); + ASSERT_EQ(detail.Reason(), EvaluationReason::Fallthrough(false)); + + auto new_bob = ContextBuilder().Kind("org", "bob").Build(); + detail = eval_.Evaluate(flag, new_bob); + ASSERT_FALSE(detail.IsError()); + ASSERT_EQ(*detail, Value(false)); + ASSERT_EQ(detail.VariationIndex(), 0); + ASSERT_EQ(detail.Reason(), + EvaluationReason::RuleMatch( + 0, "6a7755ac-e47a-40ea-9579-a09dd5f061bd", false)); +} + +TEST_F(EvaluatorTestsWithLogs, PrerequisiteCycle) { + auto alice = ContextBuilder().Kind("user", "alice").Build(); + + auto flag = store_->GetFlag("cycleFlagA")->item.value(); + + auto detail = eval_.Evaluate(flag, alice); + ASSERT_TRUE(detail.IsError()); + ASSERT_EQ(detail.Reason(), EvaluationReason::MalformedFlag()); + ASSERT_TRUE(messages_->Count(1)); + ASSERT_TRUE(messages_->Contains(0, LogLevel::kError, "circular reference")); +} + +TEST_F(EvaluatorTests, FlagWithSegment) { + auto alice = ContextBuilder().Kind("user", "alice").Build(); + auto bob = ContextBuilder().Kind("user", "bob").Build(); + + auto flag = store_->GetFlag("flagWithSegmentMatchRule")->item.value(); + + auto detail = eval_.Evaluate(flag, alice); + ASSERT_FALSE(detail.IsError()); + ASSERT_EQ(*detail, Value(false)); + ASSERT_EQ(detail.Reason(), + EvaluationReason::RuleMatch(0, "match-rule", false)); + + detail = eval_.Evaluate(flag, bob); + ASSERT_FALSE(detail.IsError()); + ASSERT_EQ(*detail, Value(true)); + ASSERT_EQ(detail.Reason(), EvaluationReason::Fallthrough(false)); +} + +TEST_F(EvaluatorTests, FlagWithSegmentContainingRules) { + auto alice = ContextBuilder().Kind("user", "alice").Build(); + auto bob = ContextBuilder().Kind("user", "bob").Build(); + + auto flag = + store_->GetFlag("flagWithSegmentMatchesUserAlice")->item.value(); + + auto detail = eval_.Evaluate(flag, alice); + ASSERT_FALSE(detail.IsError()); + ASSERT_EQ(detail.Reason(), + EvaluationReason::RuleMatch(0, "match-rule", false)); + ASSERT_EQ(*detail, Value(false)); + + detail = eval_.Evaluate(flag, bob); + ASSERT_FALSE(detail.IsError()); + ASSERT_EQ(detail.Reason(), EvaluationReason::Fallthrough(false)); + ASSERT_EQ(*detail, Value(true)); +} + +TEST_F(EvaluatorTests, FlagWithExperiment) { + auto user_a = ContextBuilder().Kind("user", "userKeyA").Build(); + auto user_b = ContextBuilder().Kind("user", "userKeyB").Build(); + auto user_c = ContextBuilder().Kind("user", "userKeyC").Build(); + + auto flag = store_->GetFlag("flagWithExperiment")->item.value(); + + auto detail = eval_.Evaluate(flag, user_a); + ASSERT_FALSE(detail.IsError()); + ASSERT_EQ(*detail, Value(false)); + ASSERT_TRUE(detail.Reason()->InExperiment()); + + detail = eval_.Evaluate(flag, user_b); + ASSERT_FALSE(detail.IsError()); + ASSERT_EQ(*detail, Value(true)); + ASSERT_TRUE(detail.Reason()->InExperiment()); + + detail = eval_.Evaluate(flag, user_c); + ASSERT_FALSE(detail.IsError()); + ASSERT_EQ(*detail, Value(false)); + ASSERT_FALSE(detail.Reason()->InExperiment()); +} + +TEST_F(EvaluatorTests, FlagWithExperimentTargetingMissingContext) { + auto flag = + store_->GetFlag("flagWithExperimentTargetingContext")->item.value(); + + auto user_a = ContextBuilder().Kind("user", "userKeyA").Build(); + + auto detail = eval_.Evaluate(flag, user_a); + ASSERT_FALSE(detail.IsError()); + ASSERT_EQ(*detail, Value(false)); + ASSERT_EQ(detail.Reason(), EvaluationReason::Fallthrough(false)); +} diff --git a/libs/server-sdk/tests/event_factory_tests.cpp b/libs/server-sdk/tests/event_factory_tests.cpp new file mode 100644 index 000000000..11eccd1e8 --- /dev/null +++ b/libs/server-sdk/tests/event_factory_tests.cpp @@ -0,0 +1,42 @@ +#include + +#include +#include +#include + +#include "events/event_factory.hpp" + +using namespace launchdarkly; +using namespace launchdarkly::server_side; + +class EventFactoryTests : public testing::Test { + public: + EventFactoryTests() + : context_(ContextBuilder().Kind("cat", "shadow").Build()) {} + Context context_; +}; + +TEST_F(EventFactoryTests, IncludesReasonIfInExperiment) { + auto factory = EventFactory::WithoutReasons(); + auto event = + factory.Eval("flag", context_, data_model::Flag{}, + EvaluationReason::Fallthrough(true), false, std::nullopt); + ASSERT_TRUE(std::get(event).reason.has_value()); +} + +TEST_F(EventFactoryTests, DoesNotIncludeReasonIfNotInExperiment) { + auto factory = EventFactory::WithoutReasons(); + auto event = + factory.Eval("flag", context_, data_model::Flag{}, + EvaluationReason::Fallthrough(false), false, std::nullopt); + ASSERT_FALSE( + std::get(event).reason.has_value()); +} + +TEST_F(EventFactoryTests, IncludesReasonIfForcedByFactory) { + auto factory = EventFactory::WithReasons(); + auto event = + factory.Eval("flag", context_, data_model::Flag{}, + EvaluationReason::Fallthrough(false), false, std::nullopt); + ASSERT_TRUE(std::get(event).reason.has_value()); +} diff --git a/libs/server-sdk/tests/event_scope_test.cpp b/libs/server-sdk/tests/event_scope_test.cpp new file mode 100644 index 000000000..4497d6bb3 --- /dev/null +++ b/libs/server-sdk/tests/event_scope_test.cpp @@ -0,0 +1,73 @@ +#include + +#include + +#include "spy_event_processor.hpp" + +#include "events/event_scope.hpp" + +using namespace launchdarkly; +using namespace launchdarkly::server_side; + +TEST(EventScope, DefaultConstructedScopeHasNoObservableEffects) { + EventScope default_scope; + default_scope.Send([](EventFactory const& factory) { + return factory.Identify(ContextBuilder().Kind("cat", "shadow").Build()); + }); +} + +TEST(EventScope, SendWithNullProcessorHasNoObservableEffects) { + EventScope scope(nullptr, EventFactory::WithoutReasons()); + scope.Send([](EventFactory const& factory) { + return factory.Identify(ContextBuilder().Kind("cat", "shadow").Build()); + }); +} + +TEST(EventScope, ForwardsEvents) { + SpyEventProcessor processor; + EventScope scope(&processor, EventFactory::WithoutReasons()); + + const std::size_t kEventCount = 10; + + for (std::size_t i = 0; i < kEventCount; ++i) { + scope.Send([](EventFactory const& factory) { + return factory.Identify( + ContextBuilder().Kind("cat", "shadow").Build()); + }); + } + + ASSERT_TRUE(processor.Count(kEventCount)); +} + +TEST(EventScope, ForwardsCorrectEventTypes) { + SpyEventProcessor processor; + EventScope scope(&processor, EventFactory::WithoutReasons()); + + scope.Send([](EventFactory const& factory) { + return factory.Identify(ContextBuilder().Kind("cat", "shadow").Build()); + }); + + scope.Send([](EventFactory const& factory) { + return factory.UnknownFlag( + "flag", ContextBuilder().Kind("cat", "shadow").Build(), + EvaluationReason::Fallthrough(false), true); + }); + + scope.Send([](EventFactory const& factory) { + return factory.Eval("flag", + ContextBuilder().Kind("cat", "shadow").Build(), + std::nullopt, EvaluationReason::Fallthrough(false), + false, std::nullopt); + }); + + scope.Send([](EventFactory const& factory) { + return factory.Custom(ContextBuilder().Kind("cat", "shadow").Build(), + "event", std::nullopt, std::nullopt); + }); + + ASSERT_TRUE(processor.Count(4)); + ASSERT_TRUE(processor.Kind(0)); + ASSERT_TRUE(processor.Kind(1)); + ASSERT_TRUE(processor.Kind(2)); + ASSERT_TRUE(processor.Kind(3)); +} diff --git a/libs/server-sdk/tests/expiration_tracker_test.cpp b/libs/server-sdk/tests/expiration_tracker_test.cpp new file mode 100644 index 000000000..a8d17f99c --- /dev/null +++ b/libs/server-sdk/tests/expiration_tracker_test.cpp @@ -0,0 +1,122 @@ +#include + +#include "data_store/persistent/expiration_tracker.hpp" + +using launchdarkly::server_side::data_store::DataKind; +using launchdarkly::server_side::data_store::persistent::ExpirationTracker; + +ExpirationTracker::TimePoint Second(uint64_t second) { + return std::chrono::steady_clock::time_point{std::chrono::seconds{second}}; +} + +TEST(ExpirationTrackerTest, CanTrackUnscopedItem) { + ExpirationTracker tracker; + tracker.Add("Potato", Second(10)); + EXPECT_EQ(ExpirationTracker::TrackState::kFresh, + tracker.State("Potato", Second(0))); + + EXPECT_EQ(ExpirationTracker::TrackState::kStale, + tracker.State("Potato", Second(11))); +} + +TEST(ExpirationTrackerTest, CanGetStateOfUntrackedUnscopedItem) { + ExpirationTracker tracker; + EXPECT_EQ(ExpirationTracker::TrackState::kNotTracked, + tracker.State("Potato", Second(0))); +} + +TEST(ExpirationTrackerTest, CanTrackScopedItem) { + ExpirationTracker tracker; + tracker.Add(DataKind::kFlag, "Potato", Second(10)); + + EXPECT_EQ(ExpirationTracker::TrackState::kFresh, + tracker.State(DataKind::kFlag, "Potato", Second(0))); + + EXPECT_EQ(ExpirationTracker::TrackState::kStale, + tracker.State(DataKind::kFlag, "Potato", Second(11))); + + // Is not considered unscoped. + EXPECT_EQ(ExpirationTracker::TrackState::kNotTracked, + tracker.State("Potato", Second(11))); + + // The wrong scope is not tracked. + EXPECT_EQ(ExpirationTracker::TrackState::kNotTracked, + tracker.State(DataKind::kSegment, "Potato", Second(11))); +} + +TEST(ExpirationTrackerTest, CanTrackSameKeyInMultipleScopes) { + ExpirationTracker tracker; + tracker.Add("Potato", Second(0)); + tracker.Add(DataKind::kFlag, "Potato", Second(10)); + tracker.Add(DataKind::kSegment, "Potato", Second(20)); + + EXPECT_EQ(ExpirationTracker::TrackState::kStale, + tracker.State("Potato", Second(9))); + + EXPECT_EQ(ExpirationTracker::TrackState::kFresh, + tracker.State(DataKind::kFlag, "Potato", Second(9))); + + EXPECT_EQ(ExpirationTracker::TrackState::kFresh, + tracker.State(DataKind::kSegment, "Potato", Second(10))); + + EXPECT_EQ(ExpirationTracker::TrackState::kStale, + tracker.State(DataKind::kFlag, "Potato", Second(11))); + + EXPECT_EQ(ExpirationTracker::TrackState::kFresh, + tracker.State(DataKind::kSegment, "Potato", Second(11))); +} + +TEST(ExpirationTrackerTest, CanClear) { + ExpirationTracker tracker; + tracker.Add("Potato", Second(0)); + tracker.Add(DataKind::kFlag, "Potato", Second(10)); + tracker.Add(DataKind::kSegment, "Potato", Second(20)); + + tracker.Clear(); + + EXPECT_EQ(ExpirationTracker::TrackState::kNotTracked, + tracker.State("Potato", Second(0))); + + EXPECT_EQ(ExpirationTracker::TrackState::kNotTracked, + tracker.State(DataKind::kFlag, "Potato", Second(0))); + + EXPECT_EQ(ExpirationTracker::TrackState::kNotTracked, + tracker.State(DataKind::kSegment, "Potato", Second(0))); +} + +TEST(ExpirationTrackerTest, CanPrune) { + ExpirationTracker tracker; + tracker.Add("freshUnscoped", Second(100)); + tracker.Add(DataKind::kFlag, "freshFlag", Second(100)); + tracker.Add(DataKind::kSegment, "freshSegment", Second(100)); + + tracker.Add("staleUnscoped", Second(50)); + tracker.Add(DataKind::kFlag, "staleFlag", Second(50)); + tracker.Add(DataKind::kSegment, "staleSegment", Second(50)); + + auto pruned = tracker.Prune(Second(80)); + EXPECT_EQ(3, pruned.size()); + std::vector, std::string>> + expected_pruned{{std::nullopt, "staleUnscoped"}, + {DataKind::kFlag, "staleFlag"}, + {DataKind::kSegment, "staleSegment"}}; + EXPECT_EQ(expected_pruned, pruned); + + EXPECT_EQ(ExpirationTracker::TrackState::kNotTracked, + tracker.State("staleUnscoped", Second(80))); + + EXPECT_EQ(ExpirationTracker::TrackState::kNotTracked, + tracker.State(DataKind::kFlag, "staleFlag", Second(80))); + + EXPECT_EQ(ExpirationTracker::TrackState::kNotTracked, + tracker.State(DataKind::kSegment, "staleSegment", Second(80))); + + EXPECT_EQ(ExpirationTracker::TrackState::kFresh, + tracker.State("freshUnscoped", Second(80))); + + EXPECT_EQ(ExpirationTracker::TrackState::kFresh, + tracker.State(DataKind::kFlag, "freshFlag", Second(80))); + + EXPECT_EQ(ExpirationTracker::TrackState::kFresh, + tracker.State(DataKind::kSegment, "freshSegment", Second(80))); +} diff --git a/libs/server-sdk/tests/memory_store_test.cpp b/libs/server-sdk/tests/memory_store_test.cpp new file mode 100644 index 000000000..852d112dc --- /dev/null +++ b/libs/server-sdk/tests/memory_store_test.cpp @@ -0,0 +1,286 @@ +#include + +#include "data_store/descriptors.hpp" +#include "data_store/memory_store.hpp" + +using launchdarkly::data_model::SDKDataSet; +using launchdarkly::server_side::data_store::FlagDescriptor; +using launchdarkly::server_side::data_store::IDataStore; +using launchdarkly::server_side::data_store::MemoryStore; +using launchdarkly::server_side::data_store::SegmentDescriptor; + +using launchdarkly::Value; +using launchdarkly::data_model::Flag; +using launchdarkly::data_model::Segment; + +TEST(MemoryStoreTest, StartsUninitialized) { + MemoryStore store; + EXPECT_FALSE(store.Initialized()); +} + +TEST(MemoryStoreTest, IsInitializedAfterInit) { + MemoryStore store; + store.Init(SDKDataSet()); + EXPECT_TRUE(store.Initialized()); +} + +TEST(MemoryStoreTest, HasDescription) { + MemoryStore store; + EXPECT_EQ(std::string("memory"), store.Description()); +} + +TEST(MemoryStoreTest, CanGetFlag) { + MemoryStore store; + Flag flag; + flag.version = 1; + flag.key = "flagA"; + flag.on = true; + flag.variations = std::vector{true, false}; + Flag::Variation variation = 0; + flag.fallthrough = variation; + store.Init(SDKDataSet{ + std::unordered_map{ + {"flagA", FlagDescriptor(flag)}}, + std::unordered_map(), + }); + + auto fetched_flag = store.GetFlag("flagA"); + EXPECT_TRUE(fetched_flag); + EXPECT_TRUE(fetched_flag->item); + EXPECT_EQ("flagA", fetched_flag->item->key); + EXPECT_EQ(1, fetched_flag->item->version); + EXPECT_EQ(fetched_flag->version, fetched_flag->item->version); +} + +TEST(MemoryStoreTest, CanGetAllFlags) { + Flag flag_a; + flag_a.version = 1; + flag_a.key = "flagA"; + + Flag flag_b; + flag_b.version = 2; + flag_b.key = "flagB"; + + MemoryStore store; + store.Init(SDKDataSet{ + std::unordered_map{ + {"flagA", FlagDescriptor(flag_a)}, + {"flagB", FlagDescriptor(flag_b)}}, + std::unordered_map(), + }); + + auto fetched = store.AllFlags(); + EXPECT_EQ(2, fetched.size()); + + EXPECT_EQ(std::string("flagA"), fetched["flagA"]->item->key); + EXPECT_EQ(std::string("flagB"), fetched["flagB"]->item->key); +} + +TEST(MemoryStoreTest, CanGetAllFlagsWhenThereAreNoFlags) { + MemoryStore store; + store.Init(SDKDataSet{ + std::unordered_map(), + std::unordered_map(), + }); + + auto fetched = store.AllFlags(); + EXPECT_EQ(0, fetched.size()); +} + +TEST(MemoryStoreTest, CanGetSegment) { + MemoryStore store; + auto segment = Segment(); + segment.version = 1; + segment.key = "segmentA"; + store.Init(SDKDataSet{ + std::unordered_map(), + std::unordered_map{ + {"segmentA", SegmentDescriptor(segment)}}, + }); + + auto fetched_segment = store.GetSegment("segmentA"); + EXPECT_TRUE(fetched_segment); + EXPECT_TRUE(fetched_segment->item); + EXPECT_EQ("segmentA", fetched_segment->item->key); + EXPECT_EQ(1, fetched_segment->item->version); + EXPECT_EQ(fetched_segment->version, fetched_segment->item->version); +} + +TEST(MemoryStoreTest, CanGetAllSegments) { + auto segment_a = Segment(); + segment_a.version = 1; + segment_a.key = "segmentA"; + + auto segment_b = Segment(); + segment_b.version = 2; + segment_b.key = "segmentB"; + + MemoryStore store; + store.Init(SDKDataSet{ + std::unordered_map(), + std::unordered_map{ + {"segmentA", SegmentDescriptor(segment_a)}, + {"segmentB", SegmentDescriptor(segment_b)}}, + }); + + auto fetched = store.AllSegments(); + EXPECT_EQ(2, fetched.size()); + + EXPECT_EQ(std::string("segmentA"), fetched["segmentA"]->item->key); + EXPECT_EQ(std::string("segmentB"), fetched["segmentB"]->item->key); +} + +TEST(MemoryStoreTest, CanGetAllSegmentsWhenThereAreNoSegments) { + MemoryStore store; + store.Init(SDKDataSet{ + std::unordered_map(), + std::unordered_map(), + }); + + auto fetched = store.AllSegments(); + EXPECT_EQ(0, fetched.size()); +} + +TEST(MemoryStoreTest, GetMissingFlagOrSegment) { + MemoryStore store; + auto fetched_flag = store.GetFlag("flagA"); + EXPECT_FALSE(fetched_flag); + auto fetched_segment = store.GetSegment("segmentA"); + EXPECT_FALSE(fetched_segment); +} + +TEST(MemoryStoreTest, CanUpsertNewFlag) { + Flag flag_a; + flag_a.version = 1; + flag_a.key = "flagA"; + + MemoryStore store; + store.Init(SDKDataSet{ + std::unordered_map(), + std::unordered_map(), + }); + store.Upsert("flagA", FlagDescriptor(flag_a)); + + auto fetched_flag = store.GetFlag("flagA"); + EXPECT_TRUE(fetched_flag); + EXPECT_TRUE(fetched_flag->item); + EXPECT_EQ("flagA", fetched_flag->item->key); + EXPECT_EQ(1, fetched_flag->item->version); + EXPECT_EQ(fetched_flag->version, fetched_flag->item->version); +} + +TEST(MemoryStoreTest, CanUpsertExitingFlag) { + Flag flag_a; + flag_a.version = 1; + flag_a.key = "flagA"; + + MemoryStore store; + store.Init(SDKDataSet{ + std::unordered_map{ + {"flagA", FlagDescriptor(flag_a)}}, + std::unordered_map(), + }); + + Flag flag_a_2; + flag_a_2.version = 2; + flag_a_2.key = "flagA"; + + store.Upsert("flagA", FlagDescriptor(flag_a_2)); + + auto fetched_flag = store.GetFlag("flagA"); + EXPECT_TRUE(fetched_flag); + EXPECT_TRUE(fetched_flag->item); + EXPECT_EQ("flagA", fetched_flag->item->key); + EXPECT_EQ(2, fetched_flag->item->version); + EXPECT_EQ(fetched_flag->version, fetched_flag->item->version); +} + +TEST(MemoryStoreTest, CanUpsertNewSegment) { + Segment segment_a; + segment_a.version = 1; + segment_a.key = "segmentA"; + + MemoryStore store; + store.Init(SDKDataSet{ + std::unordered_map(), + std::unordered_map(), + }); + store.Upsert("segmentA", SegmentDescriptor(segment_a)); + + auto fetched_segment = store.GetSegment("segmentA"); + EXPECT_TRUE(fetched_segment); + EXPECT_TRUE(fetched_segment->item); + EXPECT_EQ("segmentA", fetched_segment->item->key); + EXPECT_EQ(1, fetched_segment->item->version); + EXPECT_EQ(fetched_segment->version, fetched_segment->item->version); +} + +TEST(MemoryStoreTest, CanUpsertExitingSegment) { + Segment segment_a; + segment_a.version = 1; + segment_a.key = "segmentA"; + + MemoryStore store; + store.Init(SDKDataSet{ + std::unordered_map(), + std::unordered_map{ + {"segmentA", SegmentDescriptor(segment_a)}}, + }); + + Segment segment_a_2; + segment_a_2.version = 2; + segment_a_2.key = "segmentA"; + + store.Upsert("segmentA", SegmentDescriptor(segment_a_2)); + + auto fetched_segment = store.GetSegment("segmentA"); + EXPECT_TRUE(fetched_segment); + EXPECT_TRUE(fetched_segment->item); + EXPECT_EQ("segmentA", fetched_segment->item->key); + EXPECT_EQ(2, fetched_segment->item->version); + EXPECT_EQ(fetched_segment->version, fetched_segment->item->version); +} + +TEST(MemoryStoreTest, OriginalFlagValidAfterUpsertOfFlag) { + Flag flag_a; + flag_a.version = 1; + flag_a.key = "flagA"; + flag_a.variations = std::vector{"potato", "ham"}; + + MemoryStore store; + store.Init(SDKDataSet{ + std::unordered_map{ + {"flagA", FlagDescriptor(flag_a)}}, + std::unordered_map(), + }); + auto fetched_flag_before = store.GetFlag("flagA"); + + Flag flag_a_2; + flag_a_2.version = 2; + flag_a_2.key = "flagA"; + flag_a_2.variations = std::vector{"potato"}; + + store.Upsert("flagA", FlagDescriptor(flag_a_2)); + + auto fetched_flag_after = store.GetFlag("flagA"); + + EXPECT_TRUE(fetched_flag_before); + EXPECT_TRUE(fetched_flag_before->item); + EXPECT_EQ("flagA", fetched_flag_before->item->key); + EXPECT_EQ(1, fetched_flag_before->item->version); + EXPECT_EQ(fetched_flag_before->version, fetched_flag_before->item->version); + EXPECT_EQ(2, fetched_flag_before->item->variations.size()); + EXPECT_EQ(std::string("potato"), + fetched_flag_before->item->variations[0].AsString()); + EXPECT_EQ(std::string("ham"), + fetched_flag_before->item->variations[1].AsString()); + + EXPECT_TRUE(fetched_flag_after); + EXPECT_TRUE(fetched_flag_after->item); + EXPECT_EQ("flagA", fetched_flag_after->item->key); + EXPECT_EQ(2, fetched_flag_after->item->version); + EXPECT_EQ(fetched_flag_after->version, fetched_flag_after->item->version); + EXPECT_EQ(1, fetched_flag_after->item->variations.size()); + EXPECT_EQ(std::string("potato"), + fetched_flag_after->item->variations[0].AsString()); +} diff --git a/libs/server-sdk/tests/operator_tests.cpp b/libs/server-sdk/tests/operator_tests.cpp new file mode 100644 index 000000000..e804c22e3 --- /dev/null +++ b/libs/server-sdk/tests/operator_tests.cpp @@ -0,0 +1,260 @@ +#include + +#include + +#include "evaluation/evaluator.hpp" +#include "evaluation/operators.hpp" +#include "evaluation/rules.hpp" + +using namespace launchdarkly::server_side::evaluation::operators; +using namespace launchdarkly::data_model; +using namespace launchdarkly; + +TEST(OpTests, StartsWith) { + EXPECT_TRUE(Match(Clause::Op::kStartsWith, "", "")); + EXPECT_TRUE(Match(Clause::Op::kStartsWith, "a", "")); + EXPECT_TRUE(Match(Clause::Op::kStartsWith, "a", "a")); + + EXPECT_TRUE(Match(Clause::Op::kStartsWith, "food", "foo")); + EXPECT_FALSE(Match(Clause::Op::kStartsWith, "foo", "food")); + + EXPECT_FALSE(Match(Clause::Op::kStartsWith, "Food", "foo")); +} + +TEST(OpTests, EndsWith) { + EXPECT_TRUE(Match(Clause::Op::kEndsWith, "", "")); + EXPECT_TRUE(Match(Clause::Op::kEndsWith, "a", "")); + EXPECT_TRUE(Match(Clause::Op::kEndsWith, "a", "a")); + + EXPECT_TRUE(Match(Clause::Op::kEndsWith, "food", "ood")); + EXPECT_FALSE(Match(Clause::Op::kEndsWith, "ood", "food")); + + EXPECT_FALSE(Match(Clause::Op::kEndsWith, "FOOD", "ood")); +} + +TEST(OpTests, NumericComparisons) { + EXPECT_TRUE(Match(Clause::Op::kLessThan, 0, 1)); + EXPECT_FALSE(Match(Clause::Op::kLessThan, 1, 0)); + EXPECT_FALSE(Match(Clause::Op::kLessThan, 0, 0)); + + EXPECT_TRUE(Match(Clause::Op::kGreaterThan, 1, 0)); + EXPECT_FALSE(Match(Clause::Op::kGreaterThan, 0, 1)); + EXPECT_FALSE(Match(Clause::Op::kGreaterThan, 0, 0)); + + EXPECT_TRUE(Match(Clause::Op::kLessThanOrEqual, 0, 1)); + EXPECT_TRUE(Match(Clause::Op::kLessThanOrEqual, 0, 0)); + EXPECT_FALSE(Match(Clause::Op::kLessThanOrEqual, 1, 0)); + + EXPECT_TRUE(Match(Clause::Op::kGreaterThanOrEqual, 1, 0)); + EXPECT_TRUE(Match(Clause::Op::kGreaterThanOrEqual, 0, 0)); + EXPECT_FALSE(Match(Clause::Op::kGreaterThanOrEqual, 0, 1)); +} + +// We can only support microsecond precision due to resolution of the +// system_clock::time_point. +// +// The spec says we should support no more than 9 digits +// (nanoseconds.) This test attempts to verify that microsecond precision +// differences are handled. +TEST(OpTests, DateComparisonMicrosecondPrecision) { + auto dates = std::vector>{ + // Using Zulu suffix. + {"2023-10-08T02:00:00.000001Z", "2023-10-08T02:00:00.000002Z"}, + // Using offset suffix. + {"2023-10-08T02:00:00.000001+00:00", + "2023-10-08T02:00:00.000002+00:00"}}; + + for (auto const& [date1, date2] : dates) { + EXPECT_TRUE(Match(Clause::Op::kBefore, date1, date2)) + << date1 << " < " << date2; + + EXPECT_FALSE(Match(Clause::Op::kAfter, date1, date2)) + << date1 << " not > " << date2; + + EXPECT_FALSE(Match(Clause::Op::kBefore, date2, date1)) + << date2 << " not < " << date1; + + EXPECT_TRUE(Match(Clause::Op::kAfter, date2, date1)) + << date2 << " > " << date1; + } +} + +// This test is meant to verify that platforms with > microsecond precision +// still compare dates correctly. If the platform doesn't support > microsecond +// precision, then we try to verify that the reverse comparison is also false +// (if we had an equal operator we'd use that instead.) +TEST(OpTests, DateComparisonWithMoreThanMicrosecondPrecision) { + auto dates = std::vector>{ + // Using Zulu suffix. + {"2023-10-08T02:00:00.000001Z", "2023-10-08T02:00:00.0000011Z"}, + // Using offset suffix. + {"2023-10-08T02:00:00.0000000001+00:00", + "2023-10-08T02:00:00.00000000011+00:00"}}; + + for (auto const& [date1, date2] : dates) { + bool date1_before_date2 = Match(Clause::Op::kBefore, date1, date2); + if (date1_before_date2) { + // Platform seems to support > microsecond precision. + EXPECT_TRUE(Match(Clause::Op::kAfter, date2, date1)) + << date1 << " > " << date2; + } else { + EXPECT_FALSE(Match(Clause::Op::kBefore, date2, date1)) + << date2 << " not < " << date1; + } + } +} + +// Because RFC3339 timestamps may use 'Z' to indicate a 00:00 offset, +// we should ensure these timestamps can be compared to timestamps using normal +// offsets. +TEST(OpTests, AcceptsZuluAndNormalTimezoneOffsets) { + const std::string kDate1 = "1985-04-12T23:20:50Z"; + const std::string kDate2 = "1986-04-12T23:20:50-01:00"; + + EXPECT_TRUE(Match(Clause::Op::kBefore, kDate1, kDate2)); + EXPECT_FALSE(Match(Clause::Op::kAfter, kDate1, kDate2)); + + EXPECT_FALSE(Match(Clause::Op::kBefore, kDate2, kDate1)); + EXPECT_TRUE(Match(Clause::Op::kAfter, kDate2, kDate1)); +} + +TEST(OpTests, InvalidDates) { + EXPECT_FALSE(Match(Clause::Op::kBefore, "2021-01-08T02:00:00-00:00", + "2021-12345-08T02:00:00-00:00")); + + EXPECT_FALSE(Match(Clause::Op::kAfter, "2021-12345-08T02:00:00-00:00", + "2021-01-08T02:00:00-00:00")); + + EXPECT_FALSE(Match(Clause::Op::kBefore, "foo", "bar")); + + EXPECT_FALSE(Match(Clause::Op::kAfter, "foo", "bar")); + + EXPECT_FALSE(Match(Clause::Op::kBefore, "", "bar")); + EXPECT_FALSE(Match(Clause::Op::kAfter, "", "bar")); + + EXPECT_FALSE(Match(Clause::Op::kBefore, "foo", "")); + EXPECT_FALSE(Match(Clause::Op::kAfter, "foo", "")); +} + +struct RegexTest { + std::string input; + std::string regex; + bool shouldMatch; +}; + +class RegexTests : public ::testing::TestWithParam {}; + +TEST_P(RegexTests, Matches) { + auto const& param = GetParam(); + auto const result = Match(Clause::Op::kMatches, param.input, param.regex); + + EXPECT_EQ(result, param.shouldMatch) + << "input: (" << (param.input.empty() ? "empty string" : param.input) + << ")\nregex: (" << (param.regex.empty() ? "empty string" : param.regex) + << ")"; +} + +#define MATCH true +#define NO_MATCH false + +INSTANTIATE_TEST_SUITE_P(RegexComparisons, + RegexTests, + ::testing::ValuesIn({ + RegexTest{"", "", MATCH}, + RegexTest{"a", "", MATCH}, + RegexTest{"a", "a", MATCH}, + RegexTest{"a", ".", MATCH}, + RegexTest{"hello world", "hello.*rld", MATCH}, + RegexTest{"hello world", "hello.*orl", MATCH}, + RegexTest{"hello world", "l+", MATCH}, + RegexTest{"hello world", "(world|planet)", MATCH}, + RegexTest{"", ".", NO_MATCH}, + RegexTest{"", R"(\)", NO_MATCH}, + RegexTest{"hello world", "aloha", NO_MATCH}, + RegexTest{"hello world", "***bad regex", NO_MATCH}, + })); + +#define SEMVER_NOT_EQUAL "!=" +#define SEMVER_EQUAL "==" +#define SEMVER_GREATER ">" +#define SEMVER_LESS "<" + +struct SemVerTest { + std::string lhs; + std::string rhs; + std::string op; + bool shouldMatch; +}; + +class SemVerTests : public ::testing::TestWithParam {}; + +TEST_P(SemVerTests, Matches) { + auto const& param = GetParam(); + + bool result = false; + bool swapped = false; + + if (param.op == SEMVER_EQUAL) { + result = Match(Clause::Op::kSemVerEqual, param.lhs, param.rhs); + swapped = Match(Clause::Op::kSemVerEqual, param.rhs, param.lhs); + } else if (param.op == SEMVER_NOT_EQUAL) { + result = !Match(Clause::Op::kSemVerEqual, param.lhs, param.rhs); + swapped = !Match(Clause::Op::kSemVerEqual, param.rhs, param.lhs); + } else if (param.op == SEMVER_GREATER) { + result = Match(Clause::Op::kSemVerGreaterThan, param.lhs, param.rhs); + swapped = Match(Clause::Op::kSemVerLessThan, param.rhs, param.lhs); + } else if (param.op == SEMVER_LESS) { + result = Match(Clause::Op::kSemVerLessThan, param.lhs, param.rhs); + swapped = Match(Clause::Op::kSemVerGreaterThan, param.rhs, param.lhs); + } else { + FAIL() << "Invalid operator: " << param.op; + } + + EXPECT_EQ(result, param.shouldMatch) + << param.lhs << " " << param.op << " " << param.rhs << " should be " + << (param.shouldMatch ? "true" : "false"); + + EXPECT_EQ(result, swapped) + << "commutative property invalid for " << param.lhs << " " << param.op + << " " << param.rhs; +} + +INSTANTIATE_TEST_SUITE_P( + SemVerComparisons, + SemVerTests, + ::testing::ValuesIn( + {SemVerTest{"2.0.0", "2.0.0", SEMVER_EQUAL, MATCH}, + SemVerTest{"2.0", "2.0.0", SEMVER_EQUAL, MATCH}, + SemVerTest{"2", "2.0.0", SEMVER_EQUAL, MATCH}, + SemVerTest{"2", "2.0.0+123", SEMVER_EQUAL, MATCH}, + SemVerTest{"2+456", "2.0.0+123", SEMVER_EQUAL, MATCH}, + SemVerTest{"2.0.0", "3.0.0", SEMVER_NOT_EQUAL, MATCH}, + SemVerTest{"2.0.0", "2.1.0", SEMVER_NOT_EQUAL, MATCH}, + SemVerTest{"2.0.0", "2.0.1", SEMVER_NOT_EQUAL, MATCH}, + SemVerTest{"3.0.0", "2.0.0", SEMVER_GREATER, MATCH}, + SemVerTest{"2.1.0", "2.0.0", SEMVER_GREATER, MATCH}, + SemVerTest{"2.0.1", "2.0.0", SEMVER_GREATER, MATCH}, + SemVerTest{"2.0.0", "2.0.0", SEMVER_GREATER, NO_MATCH}, + SemVerTest{"1.9.0", "2.0.0", SEMVER_GREATER, NO_MATCH}, + SemVerTest{"2.0.0-rc", "2.0.0", SEMVER_GREATER, NO_MATCH}, + SemVerTest{"2.0.0+build", "2.0.0", SEMVER_GREATER, NO_MATCH}, + SemVerTest{"2.0.0+build", "2.0.0", SEMVER_EQUAL, MATCH}, + SemVerTest{"2.0.0", "200", SEMVER_EQUAL, NO_MATCH}, + SemVerTest{"2.0.0-rc.10.green", "2.0.0-rc.2.green", SEMVER_GREATER, + MATCH}, + SemVerTest{"2.0.0-rc.2.red", "2.0.0-rc.2.green", SEMVER_GREATER, + MATCH}, + SemVerTest{"2.0.0-rc.2.green.1", "2.0.0-rc.2.green", SEMVER_GREATER, + MATCH}, + SemVerTest{"2.0.0-rc.1.very.long.prerelease.version.1234567.keeps." + "going+123124", + "2.0.0", SEMVER_LESS, MATCH}, + SemVerTest{"1", "2", SEMVER_LESS, MATCH}, + SemVerTest{"0", "1", SEMVER_LESS, MATCH}})); + +#undef SEMVER_NOT_EQUAL +#undef SEMVER_EQUAL +#undef SEMVER_GREATER +#undef SEMVER_LESS +#undef MATCH +#undef NO_MATCH diff --git a/libs/server-sdk/tests/rule_tests.cpp b/libs/server-sdk/tests/rule_tests.cpp new file mode 100644 index 000000000..56d9d0f1b --- /dev/null +++ b/libs/server-sdk/tests/rule_tests.cpp @@ -0,0 +1,251 @@ +#include + +#include + +#include "evaluation/evaluator.hpp" +#include "evaluation/rules.hpp" + +#include "test_store.hpp" + +using namespace launchdarkly::data_model; +using namespace launchdarkly::server_side; +using namespace launchdarkly; + +struct ClauseTest { + Clause::Op op; + launchdarkly::Value contextValue; + launchdarkly::Value clauseValue; + bool expected; +}; + +class AllOperatorsTest : public ::testing::TestWithParam { + public: + const static std::string DATE_STR1; + const static std::string DATE_STR2; + const static int DATE_MS1; + const static int DATE_MS2; + const static int DATE_MS_NEGATIVE; + const static std::string INVALID_DATE; +}; + +const std::string AllOperatorsTest::DATE_STR1 = "2017-12-06T00:00:00.000-07:00"; +const std::string AllOperatorsTest::DATE_STR2 = "2017-12-06T00:01:01.000-07:00"; +int const AllOperatorsTest::DATE_MS1 = 10000000; +int const AllOperatorsTest::DATE_MS2 = 10000001; +int const AllOperatorsTest::DATE_MS_NEGATIVE = -10000; +const std::string AllOperatorsTest::INVALID_DATE = "hey what's this?"; + +TEST_P(AllOperatorsTest, Matches) { + using namespace launchdarkly::server_side::evaluation::detail; + using namespace launchdarkly; + + auto const& param = GetParam(); + + std::vector clauseValues; + + if (param.clauseValue.IsArray()) { + auto const& as_array = param.clauseValue.AsArray(); + clauseValues = std::vector{as_array.begin(), as_array.end()}; + } else { + clauseValues.push_back(param.clauseValue); + } + + Clause clause{param.op, std::move(clauseValues), false, ContextKind("user"), + "attr"}; + + auto context = launchdarkly::ContextBuilder() + .Kind("user", "key") + .Set("attr", param.contextValue) + .Build(); + ASSERT_TRUE(context.Valid()); + + EvaluationStack stack; + + auto store = test_store::Empty(); + + auto result = launchdarkly::server_side::evaluation::Match(clause, context, + *store, stack); + ASSERT_EQ(result, param.expected) + << context.Get("user", "attr") << " " << clause.op << " " + << Value(clause.values) << " should be " << param.expected; +} + +#define MATCH true +#define NO_MATCH false + +INSTANTIATE_TEST_SUITE_P( + NumericClauses, + AllOperatorsTest, + ::testing::ValuesIn({ + ClauseTest{Clause::Op::kIn, 99, 99, MATCH}, + ClauseTest{Clause::Op::kIn, 99.0, 99, MATCH}, + ClauseTest{Clause::Op::kIn, 99, 99.0, MATCH}, + ClauseTest{Clause::Op::kIn, 99, + std::vector{99, 98, 97, 96}, MATCH}, + ClauseTest{Clause::Op::kIn, 99.0001, 99.0001, MATCH}, + ClauseTest{Clause::Op::kIn, 99.0001, + std::vector{99.0001, 98.0, 97.0, 96.0}, + MATCH}, + ClauseTest{Clause::Op::kLessThan, 1, 1.99999, MATCH}, + ClauseTest{Clause::Op::kLessThan, 1.99999, 1, NO_MATCH}, + ClauseTest{Clause::Op::kLessThan, 1, 2, MATCH}, + ClauseTest{Clause::Op::kLessThanOrEqual, 1, 1.0, MATCH}, + ClauseTest{Clause::Op::kGreaterThan, 2, 1.99999, MATCH}, + ClauseTest{Clause::Op::kGreaterThan, 1.99999, 2, NO_MATCH}, + ClauseTest{Clause::Op::kGreaterThan, 2, 1, MATCH}, + ClauseTest{Clause::Op::kGreaterThanOrEqual, 1, 1.0, MATCH}, + + })); + +INSTANTIATE_TEST_SUITE_P( + StringClauses, + AllOperatorsTest, + ::testing::ValuesIn({ + ClauseTest{Clause::Op::kIn, "x", "x", MATCH}, + ClauseTest{Clause::Op::kIn, "x", + std::vector{"x", "a", "b", "c"}, MATCH}, + ClauseTest{Clause::Op::kIn, "x", "xyz", NO_MATCH}, + ClauseTest{Clause::Op::kStartsWith, "xyz", "x", MATCH}, + ClauseTest{Clause::Op::kStartsWith, "x", "xyz", NO_MATCH}, + ClauseTest{Clause::Op::kEndsWith, "xyz", "z", MATCH}, + ClauseTest{Clause::Op::kEndsWith, "z", "xyz", NO_MATCH}, + ClauseTest{Clause::Op::kContains, "xyz", "y", MATCH}, + ClauseTest{Clause::Op::kContains, "y", "xyz", NO_MATCH}, + })); + +INSTANTIATE_TEST_SUITE_P( + MixedStringAndNumbers, + AllOperatorsTest, + ::testing::ValuesIn({ + ClauseTest{Clause::Op::kIn, "99", 99, NO_MATCH}, + ClauseTest{Clause::Op::kIn, 99, "99", NO_MATCH}, + ClauseTest{Clause::Op::kContains, "99", 99, NO_MATCH}, + ClauseTest{Clause::Op::kStartsWith, "99", 99, NO_MATCH}, + ClauseTest{Clause::Op::kEndsWith, "99", 99, NO_MATCH}, + ClauseTest{Clause::Op::kLessThanOrEqual, "99", 99, NO_MATCH}, + ClauseTest{Clause::Op::kLessThanOrEqual, 99, "99", NO_MATCH}, + ClauseTest{Clause::Op::kGreaterThanOrEqual, "99", 99, NO_MATCH}, + ClauseTest{Clause::Op::kGreaterThanOrEqual, 99, "99", NO_MATCH}, + })); + +INSTANTIATE_TEST_SUITE_P( + BooleanEquality, + AllOperatorsTest, + ::testing::ValuesIn({ + ClauseTest{Clause::Op::kIn, true, true, MATCH}, + ClauseTest{Clause::Op::kIn, false, false, MATCH}, + ClauseTest{Clause::Op::kIn, true, false, NO_MATCH}, + ClauseTest{Clause::Op::kIn, false, true, NO_MATCH}, + ClauseTest{Clause::Op::kIn, true, + std::vector{false, true}, MATCH}, + })); + +INSTANTIATE_TEST_SUITE_P( + ArrayEquality, + AllOperatorsTest, + ::testing::ValuesIn({ + ClauseTest{Clause::Op::kIn, {{"x"}}, {{"x"}}, MATCH}, + ClauseTest{Clause::Op::kIn, {{"x"}}, {"x"}, NO_MATCH}, + ClauseTest{Clause::Op::kIn, {{"x"}}, {{"x"}, {"a"}, {"b"}}, MATCH}, + })); + +INSTANTIATE_TEST_SUITE_P( + ObjectEquality, + AllOperatorsTest, + ::testing::ValuesIn({ + ClauseTest{Clause::Op::kIn, Value::Object({{"x", "1"}}), + Value::Object({{"x", "1"}}), MATCH}, + ClauseTest{Clause::Op::kIn, Value::Object({{"x", "1"}}), + std::vector{ + Value::Object({{"x", "1"}}), + Value::Object({{"a", "2"}}), + Value::Object({{"b", "3"}}), + }, + MATCH}, + })); + +INSTANTIATE_TEST_SUITE_P( + RegexMatch, + AllOperatorsTest, + ::testing::ValuesIn({ + ClauseTest{Clause::Op::kMatches, "hello world", "hello.*rld", MATCH}, + ClauseTest{Clause::Op::kMatches, "hello world", "hello.*orl", MATCH}, + ClauseTest{Clause::Op::kMatches, "hello world", "l+", MATCH}, + ClauseTest{Clause::Op::kMatches, "hello world", "(world|planet)", + MATCH}, + ClauseTest{Clause::Op::kMatches, "hello world", "aloha", NO_MATCH}, + ClauseTest{Clause::Op::kMatches, "hello world", "***bad regex", + NO_MATCH}, + })); + +INSTANTIATE_TEST_SUITE_P( + DateClauses, + AllOperatorsTest, + ::testing::ValuesIn({ + ClauseTest{Clause::Op::kBefore, AllOperatorsTest::DATE_STR1, + AllOperatorsTest::DATE_STR2, MATCH}, + ClauseTest{Clause::Op::kBefore, AllOperatorsTest::DATE_MS1, + AllOperatorsTest::DATE_MS2, MATCH}, + ClauseTest{Clause::Op::kBefore, AllOperatorsTest::DATE_STR2, + AllOperatorsTest::DATE_STR1, NO_MATCH}, + ClauseTest{Clause::Op::kBefore, AllOperatorsTest::DATE_MS2, + AllOperatorsTest::DATE_MS1, NO_MATCH}, + ClauseTest{Clause::Op::kBefore, AllOperatorsTest::DATE_STR1, + AllOperatorsTest::DATE_STR1, NO_MATCH}, + ClauseTest{Clause::Op::kBefore, AllOperatorsTest::DATE_MS1, + AllOperatorsTest::DATE_MS1, NO_MATCH}, + ClauseTest{Clause::Op::kBefore, Value::Null(), + AllOperatorsTest::DATE_STR1, NO_MATCH}, + ClauseTest{Clause::Op::kBefore, AllOperatorsTest::DATE_STR1, + AllOperatorsTest::INVALID_DATE, NO_MATCH}, + ClauseTest{Clause::Op::kAfter, AllOperatorsTest::DATE_STR2, + AllOperatorsTest::DATE_STR1, MATCH}, + ClauseTest{Clause::Op::kAfter, AllOperatorsTest::DATE_MS2, + AllOperatorsTest::DATE_MS1, MATCH}, + ClauseTest{Clause::Op::kAfter, AllOperatorsTest::DATE_STR1, + AllOperatorsTest::DATE_STR2, NO_MATCH}, + ClauseTest{Clause::Op::kAfter, AllOperatorsTest::DATE_MS1, + AllOperatorsTest::DATE_MS2, NO_MATCH}, + ClauseTest{Clause::Op::kAfter, AllOperatorsTest::DATE_STR1, + AllOperatorsTest::DATE_STR1, NO_MATCH}, + ClauseTest{Clause::Op::kAfter, AllOperatorsTest::DATE_MS1, + AllOperatorsTest::DATE_MS1, NO_MATCH}, + ClauseTest{Clause::Op::kAfter, Value::Null(), + AllOperatorsTest::DATE_STR1, NO_MATCH}, + ClauseTest{Clause::Op::kAfter, AllOperatorsTest::DATE_STR1, + AllOperatorsTest::INVALID_DATE, NO_MATCH}, + ClauseTest{Clause::Op::kBefore, AllOperatorsTest::DATE_MS_NEGATIVE, + AllOperatorsTest::DATE_MS1, NO_MATCH}, + ClauseTest{Clause::Op::kAfter, AllOperatorsTest::DATE_MS1, + AllOperatorsTest::DATE_MS_NEGATIVE, NO_MATCH}, + + })); + +INSTANTIATE_TEST_SUITE_P( + SemVerTests, + AllOperatorsTest, + ::testing::ValuesIn( + {ClauseTest{Clause::Op::kSemVerEqual, "2.0.0", "2.0.0", MATCH}, + ClauseTest{Clause::Op::kSemVerEqual, "2.0", "2.0.0", MATCH}, + ClauseTest{Clause::Op::kSemVerEqual, "2-rc1", "2.0.0-rc1", MATCH}, + ClauseTest{Clause::Op::kSemVerEqual, "2+build2", "2.0.0+build2", + MATCH}, + ClauseTest{Clause::Op::kSemVerEqual, "2.0.0", "2.0.1", NO_MATCH}, + ClauseTest{Clause::Op::kSemVerLessThan, "2.0.0", "2.0.1", MATCH}, + ClauseTest{Clause::Op::kSemVerLessThan, "2.0", "2.0.1", MATCH}, + ClauseTest{Clause::Op::kSemVerLessThan, "2.0.1", "2.0.0", NO_MATCH}, + ClauseTest{Clause::Op::kSemVerLessThan, "2.0.1", "2.0", NO_MATCH}, + ClauseTest{Clause::Op::kSemVerLessThan, "2.0.1", "xbad%ver", NO_MATCH}, + ClauseTest{Clause::Op::kSemVerLessThan, "2.0.0-rc", "2.0.0-rc.beta", + MATCH}, + ClauseTest{Clause::Op::kSemVerGreaterThan, "2.0.1", "2.0", MATCH}, + ClauseTest{Clause::Op::kSemVerGreaterThan, "10.0.1", "2.0", MATCH}, + ClauseTest{Clause::Op::kSemVerGreaterThan, "2.0.0", "2.0.1", NO_MATCH}, + ClauseTest{Clause::Op::kSemVerGreaterThan, "2.0", "2.0.1", NO_MATCH}, + ClauseTest{Clause::Op::kSemVerGreaterThan, "2.0.1", "xbad%ver", + NO_MATCH}, + ClauseTest{Clause::Op::kSemVerGreaterThan, "2.0.0-rc.1", "2.0.0-rc.0", + MATCH}})); + +#undef MATCH +#undef NO_MATCH diff --git a/libs/server-sdk/tests/semver_tests.cpp b/libs/server-sdk/tests/semver_tests.cpp new file mode 100644 index 000000000..61cfe6121 --- /dev/null +++ b/libs/server-sdk/tests/semver_tests.cpp @@ -0,0 +1,66 @@ +#include +#include "evaluation/detail/semver_operations.hpp" + +using namespace launchdarkly::server_side::evaluation::detail; + +TEST(SemVer, DefaultConstruction) { + SemVer version; + EXPECT_EQ(version.Major(), 0); + EXPECT_EQ(version.Minor(), 0); + EXPECT_EQ(version.Patch(), 0); + EXPECT_FALSE(version.Prerelease()); +} + +TEST(SemVer, MinimalVersion) { + SemVer version{1, 2, 3}; + EXPECT_EQ(version.Major(), 1); + EXPECT_EQ(version.Minor(), 2); + EXPECT_EQ(version.Patch(), 3); + EXPECT_FALSE(version.Prerelease()); +} + +TEST(SemVer, ParseMinimalVersion) { + auto version = SemVer::Parse("1.2.3"); + ASSERT_TRUE(version); + EXPECT_EQ(version->Major(), 1); + EXPECT_EQ(version->Minor(), 2); + EXPECT_EQ(version->Patch(), 3); + EXPECT_FALSE(version->Prerelease()); +} + +TEST(SemVer, ParsePrereleaseVersion) { + auto version = SemVer::Parse("1.2.3-alpha.123.foo"); + ASSERT_TRUE(version); + ASSERT_TRUE(version->Prerelease()); + + auto const& pre = *version->Prerelease(); + ASSERT_EQ(pre.size(), 3); + EXPECT_EQ(pre[0], SemVer::Token("alpha")); + EXPECT_EQ(pre[1], SemVer::Token(123ull)); + EXPECT_EQ(pre[2], SemVer::Token("foo")); +} + +TEST(SemVer, ParseInvalid) { + ASSERT_FALSE(SemVer::Parse("")); + ASSERT_FALSE(SemVer::Parse("v1.2.3")); + ASSERT_FALSE(SemVer::Parse("foo")); + ASSERT_FALSE(SemVer::Parse("1.2.3 ")); + ASSERT_FALSE(SemVer::Parse("1.2.3.alpha.1")); + ASSERT_FALSE(SemVer::Parse("1.2.3.4")); + ASSERT_FALSE(SemVer::Parse("1.2.3-_")); +} + +TEST(SemVer, BasicComparison) { + EXPECT_LT(SemVer::Parse("1.0.0"), SemVer::Parse("2.0.0")); + ASSERT_LT(SemVer::Parse("1.0.0-alpha.1"), SemVer::Parse("1.0.0")); + + ASSERT_GT(SemVer::Parse("2.0.0"), SemVer::Parse("1.0.0")); + ASSERT_GT(SemVer::Parse("1.0.0"), SemVer::Parse("1.0.0-alpha.1")); + + ASSERT_EQ(SemVer::Parse("1.0.0"), SemVer::Parse("1.0.0")); + + // Build is irrelevant for comparisons. + ASSERT_EQ(SemVer::Parse("1.2.3+build12345"), SemVer::Parse("1.2.3")); + ASSERT_EQ(SemVer::Parse("1.2.3-alpha.1+1234"), + SemVer::Parse("1.2.3-alpha.1+4567")); +} diff --git a/libs/server-sdk/tests/server_c_bindings_test.cpp b/libs/server-sdk/tests/server_c_bindings_test.cpp new file mode 100644 index 000000000..893956cb8 --- /dev/null +++ b/libs/server-sdk/tests/server_c_bindings_test.cpp @@ -0,0 +1,161 @@ +#include + +#include +#include +#include + +#include + +#include + +#include + +using launchdarkly::server_side::data_sources::DataSourceStatus; + +TEST(ClientBindings, MinimalInstantiation) { + LDServerConfigBuilder cfg_builder = LDServerConfigBuilder_New("sdk-123"); + + LDServerConfig config; + LDStatus status = LDServerConfigBuilder_Build(cfg_builder, &config); + ASSERT_TRUE(LDStatus_Ok(status)); + + LDServerSDK sdk = LDServerSDK_New(config); + + char const* version = LDServerSDK_Version(); + ASSERT_TRUE(version); + ASSERT_STREQ(version, "0.1.0"); // {x-release-please-version} + + LDServerSDK_Free(sdk); +} + +void StatusListenerFunction(LDServerDataSourceStatus status, void* user_data) { + EXPECT_EQ(LD_SERVERDATASOURCESTATUS_STATE_VALID, + LDServerDataSourceStatus_GetState(status)); +} + +// This test registers a listener. It doesn't use the listener, but it +// will at least ensure 1.) Compilation, and 2.) Allow sanitizers to run. +TEST(ClientBindings, RegisterDataSourceStatusChangeListener) { + LDServerConfigBuilder cfg_builder = LDServerConfigBuilder_New("sdk-123"); + LDServerConfigBuilder_Offline(cfg_builder, true); + + LDServerConfig config; + LDStatus status = LDServerConfigBuilder_Build(cfg_builder, &config); + ASSERT_TRUE(LDStatus_Ok(status)); + + LDServerSDK sdk = LDServerSDK_New(config); + + struct LDServerDataSourceStatusListener listener {}; + LDServerDataSourceStatusListener_Init(&listener); + + listener.UserData = const_cast("Potato"); + listener.StatusChanged = StatusListenerFunction; + + LDListenerConnection connection = + LDServerSDK_DataSourceStatus_OnStatusChange(sdk, listener); + + bool success = false; + LDServerSDK_Start(sdk, 3000, &success); + EXPECT_TRUE(success); + + LDListenerConnection_Disconnect(connection); + + LDListenerConnection_Free(connection); + LDServerSDK_Free(sdk); +} + +TEST(ClientBindings, GetStatusOfOfflineClient) { + LDServerConfigBuilder cfg_builder = LDServerConfigBuilder_New("sdk-123"); + LDServerConfigBuilder_Offline(cfg_builder, true); + + LDServerConfig config; + LDStatus status = LDServerConfigBuilder_Build(cfg_builder, &config); + ASSERT_TRUE(LDStatus_Ok(status)); + + LDServerSDK sdk = LDServerSDK_New(config); + + LDServerDataSourceStatus status_1 = + LDServerSDK_DataSourceStatus_Status(sdk); + EXPECT_EQ(LD_SERVERDATASOURCESTATUS_STATE_INITIALIZING, + LDServerDataSourceStatus_GetState(status_1)); + + bool success = false; + LDServerSDK_Start(sdk, 3000, &success); + + LDServerDataSourceStatus status_2 = + LDServerSDK_DataSourceStatus_Status(sdk); + EXPECT_EQ(LD_SERVERDATASOURCESTATUS_STATE_VALID, + LDServerDataSourceStatus_GetState(status_2)); + + EXPECT_EQ(nullptr, LDServerDataSourceStatus_GetLastError(status_2)); + + EXPECT_NE(0, LDServerDataSourceStatus_StateSince(status_2)); + + LDServerDataSourceStatus_Free(status_1); + LDServerDataSourceStatus_Free(status_2); + LDServerSDK_Free(sdk); +} + +TEST(ClientBindings, ComplexDataSourceStatus) { + DataSourceStatus status( + DataSourceStatus::DataSourceState::kValid, + std::chrono::time_point{ + std::chrono::seconds{200}}, + DataSourceStatus::ErrorInfo( + DataSourceStatus::ErrorInfo::ErrorKind::kErrorResponse, 404, + "Not found", + std::chrono::time_point{ + std::chrono::seconds{100}})); + + EXPECT_EQ(LD_SERVERDATASOURCESTATUS_STATE_VALID, + LDServerDataSourceStatus_GetState( + reinterpret_cast(&status))); + + EXPECT_EQ(200, LDServerDataSourceStatus_StateSince( + reinterpret_cast(&status))); + + LDDataSourceStatus_ErrorInfo info = LDServerDataSourceStatus_GetLastError( + reinterpret_cast(&status)); + + EXPECT_EQ(LD_DATASOURCESTATUS_ERRORKIND_ERROR_RESPONSE, + LDDataSourceStatus_ErrorInfo_GetKind(info)); + + EXPECT_EQ(std::string("Not found"), + LDDataSourceStatus_ErrorInfo_Message(info)); + + EXPECT_EQ(100, LDDataSourceStatus_ErrorInfo_Time(info)); + + EXPECT_EQ(404, LDDataSourceStatus_ErrorInfo_StatusCode(info)); + + LDDataSourceStatus_ErrorInfo_Free(info); +} + +TEST(ClientBindings, AllFlagsState) { + LDServerConfigBuilder cfg_builder = LDServerConfigBuilder_New("sdk-123"); + + LDServerConfig config; + LDStatus status = LDServerConfigBuilder_Build(cfg_builder, &config); + ASSERT_TRUE(LDStatus_Ok(status)); + + LDServerSDK sdk = LDServerSDK_New(config); + + LDContextBuilder ctx_builder = LDContextBuilder_New(); + LDContextBuilder_AddKind(ctx_builder, "user", "shadow"); + LDContext context = LDContextBuilder_Build(ctx_builder); + + LDAllFlagsState state = + LDServerSDK_AllFlagsState(sdk, context, LD_ALLFLAGSSTATE_DEFAULT); + + ASSERT_FALSE(LDAllFlagsState_Valid(state)); + + char* json = LDAllFlagsState_SerializeJSON(state); + ASSERT_STREQ(json, "{\"$valid\":false,\"$flagsState\":{}}"); + LDMemory_FreeString(json); + + LDValue nonexistent_flag = LDAllFlagsState_Value(state, "nonexistent_flag"); + ASSERT_EQ(LDValue_Type(nonexistent_flag), LDValueType_Null); + + LDAllFlagsState_Free(state); + LDContext_Free(context); + LDServerSDK_Free(sdk); +} diff --git a/libs/server-sdk/tests/spy_event_processor.hpp b/libs/server-sdk/tests/spy_event_processor.hpp new file mode 100644 index 000000000..2981f39d0 --- /dev/null +++ b/libs/server-sdk/tests/spy_event_processor.hpp @@ -0,0 +1,65 @@ +#pragma once + +#include + +#include + +namespace launchdarkly { +class SpyEventProcessor : public events::IEventProcessor { + public: + struct Flush {}; + struct Shutdown {}; + + using Record = events::InputEvent; + + SpyEventProcessor() : events_() {} + + void SendAsync(events::InputEvent event) override { + events_.push_back(std::move(event)); + } + + void FlushAsync() override {} + + void ShutdownAsync() override {} + + /** + * Asserts that 'count' events were recorded. + * @param count Number of expected events. + */ + [[nodiscard]] testing::AssertionResult Count(std::size_t count) const { + if (events_.size() == count) { + return testing::AssertionSuccess(); + } + return testing::AssertionFailure() + << "Expected " << count << " events, got " << events_.size(); + } + + template + [[nodiscard]] testing::AssertionResult Kind(std::size_t index) const { + return GetIndex(index, [&](auto const& actual) { + if (std::holds_alternative(actual)) { + return testing::AssertionSuccess(); + } else { + return testing::AssertionFailure() + << "Expected message " << index << " to be of kind " + << typeid(T).name() << ", got variant index " + << actual.index(); + } + }); + } + + private: + [[nodiscard]] testing::AssertionResult GetIndex( + std::size_t index, + std::function const& f) const { + if (index >= events_.size()) { + return testing::AssertionFailure() + << "Event index " << index << " out of range"; + } + auto const& record = events_[index]; + return f(record); + } + using Records = std::vector; + Records events_; +}; +} // namespace launchdarkly diff --git a/libs/server-sdk/tests/spy_logger.hpp b/libs/server-sdk/tests/spy_logger.hpp new file mode 100644 index 000000000..a04edb197 --- /dev/null +++ b/libs/server-sdk/tests/spy_logger.hpp @@ -0,0 +1,94 @@ +#pragma once + +#include +#include + +#include + +#include + +namespace launchdarkly::logging { + +class SpyLoggerBackend : public launchdarkly::ILogBackend { + public: + using Record = std::pair; + + SpyLoggerBackend() : messages_() {} + + /** + * Always returns true. + */ + [[nodiscard]] bool Enabled(LogLevel level) noexcept override { + return true; + } + + /** + * Records the message internally. + */ + void Write(LogLevel level, std::string message) noexcept override { + messages_.push_back({level, std::move(message)}); + } + + /** + * Asserts that 'count' messages were recorded. + * @param count Number of expected messages. + */ + [[nodiscard]] testing::AssertionResult Count(std::size_t count) const { + if (messages_.size() == count) { + return testing::AssertionSuccess(); + } + return testing::AssertionFailure() + << "Expected " << count << " messages, got " << messages_.size(); + } + + [[nodiscard]] testing::AssertionResult Equals( + std::size_t index, + LogLevel level, + std::string const& expected) const { + return GetIndex(index, level, [&](auto const& actual) { + if (actual.second != expected) { + return testing::AssertionFailure() + << "Expected message " << index << " to be " << expected + << ", got " << actual.second; + } + return testing::AssertionSuccess(); + }); + } + + [[nodiscard]] testing::AssertionResult Contains( + std::size_t index, + LogLevel level, + std::string const& expected) const { + return GetIndex( + index, level, [&](auto const& actual) -> testing::AssertionResult { + if (actual.second.find(expected) != std::string::npos) { + return testing::AssertionSuccess(); + } + return testing::AssertionFailure() + << "Expected message " << index << " to contain " + << expected << ", got " << actual.second; + }); + } + + private: + [[nodiscard]] testing::AssertionResult GetIndex( + std::size_t index, + LogLevel level, + std::function const& f) const { + if (index >= messages_.size()) { + return testing::AssertionFailure() + << "Message index " << index << " out of range"; + } + auto const& record = messages_[index]; + if (level != record.first) { + return testing::AssertionFailure() + << "Expected message " << index << " to be " << level + << ", got " << record.first; + } + return f(record); + } + using Records = std::vector; + Records messages_; +}; + +} // namespace launchdarkly::logging diff --git a/libs/server-sdk/tests/test_store.cpp b/libs/server-sdk/tests/test_store.cpp new file mode 100644 index 000000000..128eb2b8d --- /dev/null +++ b/libs/server-sdk/tests/test_store.cpp @@ -0,0 +1,307 @@ +#include "test_store.hpp" + +#include "data_store/memory_store.hpp" + +#include +#include + +namespace launchdarkly::server_side::test_store { + +std::unique_ptr Empty() { + auto store = std::make_unique(); + store->Init({}); + return store; +} + +data_store::FlagDescriptor Flag(char const* json) { + auto val = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse(json)); + assert(val.has_value()); + assert(val.value().has_value()); + return data_store::FlagDescriptor{val.value().value()}; +} + +data_store::SegmentDescriptor Segment(char const* json) { + auto val = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse(json)); + assert(val.has_value()); + assert(val.value().has_value()); + return data_store::SegmentDescriptor{val.value().value()}; +} + +std::unique_ptr TestData() { + auto store = std::make_unique(); + store->Init({}); + + store->Upsert("segmentWithNoRules", Segment(R"({ + "key": "segmentWithNoRules", + "included": ["alice"], + "excluded": [], + "rules": [], + "salt": "salty", + "version": 1 + })")); + store->Upsert("segmentWithRuleMatchesUserAlice", Segment(R"({ + "key": "segmentWithRuleMatchesUserAlice", + "included": [], + "excluded": [], + "rules": [{ + "id": "rule-1", + "clauses": [{ + "attribute": "key", + "negate": false, + "op": "in", + "values": ["alice"], + "contextKind": "user" + }] + }], + "salt": "salty", + "version": 1 + })")); + + store->Upsert("flagWithTarget", Flag(R"( + { + "key": "flagWithTarget", + "version": 42, + "on": false, + "targets": [{ + "values": ["bob"], + "variation": 0 + }], + "rules": [], + "prerequisites": [], + "fallthrough": {"variation": 1}, + "offVariation": 0, + "variations": [false, true], + "clientSide": true, + "clientSideAvailability": { + "usingEnvironmentId": true, + "usingMobileKey": true + }, + "salt": "salty" + })")); + + store->Upsert("flagWithMatchesOpOnGroups", Flag(R"({ + "key": "flagWithMatchesOpOnGroups", + "version": 42, + "on": true, + "targets": [], + "rules": [ + { + "variation": 0, + "id": "6a7755ac-e47a-40ea-9579-a09dd5f061bd", + "clauses": [ + { + "attribute": "groups", + "op": "matches", + "values": [ + "^\\w+" + ], + "negate": false + } + ], + "trackEvents": true + } + ], + "prerequisites": [], + "fallthrough": {"variation": 1}, + "offVariation": 0, + "variations": [false, true], + "clientSide": true, + "clientSideAvailability": { + "usingEnvironmentId": true, + "usingMobileKey": true + }, + "salt": "salty", + "trackEvents": false, + "trackEventsFallthrough": true, + "debugEventsUntilDate": 1500000000 + })")); + + store->Upsert("flagWithMatchesOpOnKinds", Flag(R"({ + "key": "flagWithMatchesOpOnKinds", + "version": 42, + "on": true, + "targets": [], + "rules": [ + { + "variation": 0, + "id": "6a7755ac-e47a-40ea-9579-a09dd5f061bd", + "clauses": [ + { + "attribute": "kind", + "op": "matches", + "values": [ + "^[ou]" + ], + "negate": false + } + ], + "trackEvents": true + } + ], + "prerequisites": [], + "fallthrough": {"variation": 1}, + "offVariation": 0, + "variations": [false, true], + "clientSide": true, + "clientSideAvailability": { + "usingEnvironmentId": true, + "usingMobileKey": true + }, + "salt": "salty", + "trackEvents": false, + "trackEventsFallthrough": true, + "debugEventsUntilDate": 1500000000 + })")); + + store->Upsert("cycleFlagA", Flag(R"({ + "key": "cycleFlagA", + "targets": [], + "rules": [], + "salt": "salty", + "prerequisites": [{ + "key": "cycleFlagB", + "variation": 0 + }], + "on": true, + "fallthrough": {"variation": 0}, + "offVariation": 1, + "variations": [true, false] + })")); + store->Upsert("cycleFlagB", Flag(R"({ + "key": "cycleFlagB", + "targets": [], + "rules": [], + "salt": "salty", + "prerequisites": [{ + "key": "cycleFlagA", + "variation": 0 + }], + "on": true, + "fallthrough": {"variation": 0}, + "offVariation": 1, + "variations": [true, false] + })")); + + store->Upsert("flagWithExperiment", Flag(R"({ + "key": "flagWithExperiment", + "version": 42, + "on": true, + "targets": [], + "rules": [], + "prerequisites": [], + "fallthrough": { + "rollout": { + "kind": "experiment", + "seed": 61, + "variations": [ + {"variation": 0, "weight": 10000, "untracked": false}, + {"variation": 1, "weight": 20000, "untracked": false}, + {"variation": 0, "weight": 70000, "untracked": true} + ] + } + }, + "offVariation": 0, + "variations": [false, true], + "clientSide": true, + "clientSideAvailability": { + "usingEnvironmentId": true, + "usingMobileKey": true + }, + "salt": "salty", + "trackEvents": false, + "trackEventsFallthrough": false, + "debugEventsUntilDate": 1500000000 + })")); + store->Upsert("flagWithExperimentTargetingContext", Flag(R"({ + "key": "flagWithExperimentTargetingContext", + "version": 42, + "on": true, + "targets": [], + "rules": [], + "prerequisites": [], + "fallthrough": { + "rollout": { + "kind": "experiment", + "contextKind": "org", + "seed": 61, + "variations": [ + {"variation": 0, "weight": 10000, "untracked": false}, + {"variation": 1, "weight": 20000, "untracked": false}, + {"variation": 0, "weight": 70000, "untracked": true} + ] + } + }, + "offVariation": 0, + "variations": [false, true], + "clientSide": true, + "clientSideAvailability": { + "usingEnvironmentId": true, + "usingMobileKey": true + }, + "salt": "salty", + "trackEvents": false, + "trackEventsFallthrough": false, + "debugEventsUntilDate": 1500000000 + })")); + store->Upsert("flagWithSegmentMatchRule", Flag(R"({ + "key": "flagWithSegmentMatchRule", + "version": 42, + "on": true, + "targets": [], + "rules": [{ + "id": "match-rule", + "clauses": [{ + "contextKind": "user", + "attribute": "key", + "negate": false, + "op": "segmentMatch", + "values": ["segmentWithNoRules"] + }], + "variation": 0, + "trackEvents": false + }], + "prerequisites": [], + "fallthrough": {"variation": 1}, + "offVariation": 0, + "variations": [false, true], + "clientSide": true, + "clientSideAvailability": { + "usingEnvironmentId": true, + "usingMobileKey": true + }, + "salt": "salty" + })")); + + store->Upsert("flagWithSegmentMatchesUserAlice", Flag(R"({ + "key": "flagWithSegmentMatchesUserAlice", + "version": 42, + "on": true, + "targets": [], + "rules": [{ + "id": "match-rule", + "clauses": [{ + "op": "segmentMatch", + "values": ["segmentWithRuleMatchesUserAlice"] + }], + "variation": 0, + "trackEvents": false + }], + "prerequisites": [], + "fallthrough": {"variation": 1}, + "offVariation": 0, + "variations": [false, true], + "clientSide": true, + "clientSideAvailability": { + "usingEnvironmentId": true, + "usingMobileKey": true + }, + "salt": "salty" + })")); + return store; +} + +} // namespace launchdarkly::server_side::test_store diff --git a/libs/server-sdk/tests/test_store.hpp b/libs/server-sdk/tests/test_store.hpp new file mode 100644 index 000000000..bfccc008e --- /dev/null +++ b/libs/server-sdk/tests/test_store.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include "data_store/data_store.hpp" + +#include + +namespace launchdarkly::server_side::test_store { + +/** + * @return A data store preloaded with flags/segments for unit tests. + */ +std::unique_ptr TestData(); + +/** + * @return An initialized, but empty, data store. + */ +std::unique_ptr Empty(); + +/** + * Returns a flag suitable for inserting into a memory store, parsed from the + * given JSON representation. + */ +data_store::FlagDescriptor Flag(char const* json); + +/** + * Returns a segment suitable for inserting into a memory store, parsed from the + * given JSON representation. + */ +data_store::SegmentDescriptor Segment(char const* json); + +} // namespace launchdarkly::server_side::test_store diff --git a/libs/server-sdk/tests/timestamp_tests.cpp b/libs/server-sdk/tests/timestamp_tests.cpp new file mode 100644 index 000000000..f448d1c94 --- /dev/null +++ b/libs/server-sdk/tests/timestamp_tests.cpp @@ -0,0 +1,88 @@ +#include "evaluation/detail/timestamp_operations.hpp" + +#include + +#include + +using namespace launchdarkly::server_side::evaluation::detail; +using namespace std::chrono_literals; + +static Timepoint BasicDate() { + return std::chrono::system_clock::from_time_t(1577836800); +} + +struct TimestampTest { + launchdarkly::Value input; + char const* explanation; + std::optional expected; +}; + +class TimestampTests : public ::testing::TestWithParam {}; +TEST_P(TimestampTests, ExpectedTimestampIsParsed) { + auto const& param = GetParam(); + + std::optional result = ToTimepoint(param.input); + + constexpr auto print_tp = + [](std::optional const& expected) -> std::string { + if (expected) { + return std::to_string(expected.value().time_since_epoch().count()); + } else { + return "(none)"; + } + }; + + ASSERT_EQ(result, param.expected) + << param.explanation << ": input was " << param.input << ", expected " + << print_tp(param.expected) << " but got " << print_tp(result); +} + +INSTANTIATE_TEST_SUITE_P( + ValidTimestamps, + TimestampTests, + ::testing::ValuesIn({ + TimestampTest{0.0, "default constructed", Timepoint{}}, + TimestampTest{1000.0, "1 second", Timepoint{1s}}, + TimestampTest{1000.0 * 60, "60 seconds", Timepoint{60s}}, + TimestampTest{1000.0 * 60 * 60, "1 hour", Timepoint{60min}}, + TimestampTest{"2020-01-01T00:00:00Z", "with Zulu offset", BasicDate()}, + TimestampTest{"2020-01-01T00:00:00+00:00", "with normal offset", + BasicDate()}, + TimestampTest{"2020-01-01T01:00:00+01:00", "with 1hr offset", + BasicDate()}, + TimestampTest{"2020-01-01T01:00:00+01:00", + "with colon-delimited offset", BasicDate()}, + + TimestampTest{"2020-01-01T00:00:00.123Z", "with milliseconds", + BasicDate() + 123ms}, + TimestampTest{"2020-01-01T00:00:00.123+00:00", + "with milliseconds and offset", BasicDate() + 123ms}, + TimestampTest{"2020-01-01T00:00:00.000123Z", "with microseconds ", + BasicDate() + 123us}, + TimestampTest{"2020-01-01T00:00:00.000123+00:00", + "with microseconds and offset", BasicDate() + 123us}, + + })); + +INSTANTIATE_TEST_SUITE_P( + InvalidTimestamps, + TimestampTests, + ::testing::ValuesIn({ + TimestampTest{0.1, "not an integer", std::nullopt}, + TimestampTest{1000.2, "not an integer", std::nullopt}, + TimestampTest{123456.789, "not an integer", std::nullopt}, + TimestampTest{-1000.5, "not an integer", std::nullopt}, + TimestampTest{-1000.0, "negative integer", std::nullopt}, + TimestampTest{"", "empty string", std::nullopt}, + TimestampTest{"2020-01-01T00:00:00/foo", "invalid offset", + std::nullopt}, + TimestampTest{"2020-01-01T00:00:00.0000000001Z", + "more than 9 digits of precision", std::nullopt}, + TimestampTest{launchdarkly::Value::Null(), "not a number or string", + std::nullopt}, + TimestampTest{launchdarkly::Value::Array(), "not a number or string", + std::nullopt}, + TimestampTest{launchdarkly::Value::Object(), "not a number or string", + std::nullopt}, + + })); From 994aa498e3ad724c3fc3d72b11bcb0d739b240e0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 11:42:38 -0700 Subject: [PATCH 18/21] chore: release main (#266) :robot: I have created a release *beep* *boop* ---
launchdarkly-cpp-client: 3.2.0 ## [3.2.0](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-client-v3.1.1...launchdarkly-cpp-client-v3.2.0) (2023-10-23) ### Features * server-side SDK ([#160](https://github.com/launchdarkly/cpp-sdks/issues/160)) ([75eece3](https://github.com/launchdarkly/cpp-sdks/commit/75eece3a46870fdb6bf4384c112700558099c4d1)) ### Dependencies * The following workspace dependencies were updated * dependencies * launchdarkly-cpp-internal bumped from 0.2.0 to 0.3.0 * launchdarkly-cpp-common bumped from 0.4.0 to 0.5.0
launchdarkly-cpp-common: 0.5.0 ## [0.5.0](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-common-v0.4.0...launchdarkly-cpp-common-v0.5.0) (2023-10-23) ### Features * server-side SDK ([#160](https://github.com/launchdarkly/cpp-sdks/issues/160)) ([75eece3](https://github.com/launchdarkly/cpp-sdks/commit/75eece3a46870fdb6bf4384c112700558099c4d1))
launchdarkly-cpp-internal: 0.3.0 ## [0.3.0](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-internal-v0.2.0...launchdarkly-cpp-internal-v0.3.0) (2023-10-23) ### Features * server-side SDK ([#160](https://github.com/launchdarkly/cpp-sdks/issues/160)) ([75eece3](https://github.com/launchdarkly/cpp-sdks/commit/75eece3a46870fdb6bf4384c112700558099c4d1)) ### Dependencies * The following workspace dependencies were updated * dependencies * launchdarkly-cpp-common bumped from 0.4.0 to 0.5.0
--- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .release-please-manifest.json | 6 +++--- libs/client-sdk/CHANGELOG.md | 15 +++++++++++++++ libs/client-sdk/CMakeLists.txt | 2 +- .../include/launchdarkly/client_side/client.hpp | 2 +- libs/client-sdk/package.json | 6 +++--- libs/client-sdk/tests/client_c_bindings_test.cpp | 2 +- libs/client-sdk/tests/client_test.cpp | 2 +- libs/common/CHANGELOG.md | 7 +++++++ libs/common/package.json | 2 +- libs/internal/CHANGELOG.md | 14 ++++++++++++++ libs/internal/package.json | 4 ++-- 11 files changed, 49 insertions(+), 13 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index d437e2da7..c330a45ad 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,6 +1,6 @@ { - "libs/client-sdk": "3.1.1", + "libs/client-sdk": "3.2.0", "libs/server-sent-events": "0.2.0", - "libs/common": "0.4.0", - "libs/internal": "0.2.0" + "libs/common": "0.5.0", + "libs/internal": "0.3.0" } diff --git a/libs/client-sdk/CHANGELOG.md b/libs/client-sdk/CHANGELOG.md index c0a8d5f43..eaa8a2509 100644 --- a/libs/client-sdk/CHANGELOG.md +++ b/libs/client-sdk/CHANGELOG.md @@ -8,6 +8,21 @@ All notable changes to the LaunchDarkly Client-Side SDK for C/C++ will be docume * dependencies * launchdarkly-cpp-internal bumped from 0.1.9 to 0.1.10 +## [3.2.0](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-client-v3.1.1...launchdarkly-cpp-client-v3.2.0) (2023-10-23) + + +### Features + +* server-side SDK ([#160](https://github.com/launchdarkly/cpp-sdks/issues/160)) ([75eece3](https://github.com/launchdarkly/cpp-sdks/commit/75eece3a46870fdb6bf4384c112700558099c4d1)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * launchdarkly-cpp-internal bumped from 0.2.0 to 0.3.0 + * launchdarkly-cpp-common bumped from 0.4.0 to 0.5.0 + ## [3.1.1](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-client-v3.1.0...launchdarkly-cpp-client-v3.1.1) (2023-10-19) diff --git a/libs/client-sdk/CMakeLists.txt b/libs/client-sdk/CMakeLists.txt index a812e1f69..409fd277c 100644 --- a/libs/client-sdk/CMakeLists.txt +++ b/libs/client-sdk/CMakeLists.txt @@ -6,7 +6,7 @@ cmake_minimum_required(VERSION 3.19) project( LaunchDarklyCPPClient - VERSION 3.1.1 # {x-release-please-version} + VERSION 3.2.0 # {x-release-please-version} DESCRIPTION "LaunchDarkly C++ Client SDK" LANGUAGES CXX C ) diff --git a/libs/client-sdk/include/launchdarkly/client_side/client.hpp b/libs/client-sdk/include/launchdarkly/client_side/client.hpp index 3250f2e6a..e0c1c3679 100644 --- a/libs/client-sdk/include/launchdarkly/client_side/client.hpp +++ b/libs/client-sdk/include/launchdarkly/client_side/client.hpp @@ -324,7 +324,7 @@ class Client : public IClient { private: inline static char const* const kVersion = - "3.1.1"; // {x-release-please-version} + "3.2.0"; // {x-release-please-version} std::unique_ptr client; }; diff --git a/libs/client-sdk/package.json b/libs/client-sdk/package.json index 5656f0123..4ec3178c5 100644 --- a/libs/client-sdk/package.json +++ b/libs/client-sdk/package.json @@ -1,11 +1,11 @@ { "name": "launchdarkly-cpp-client", "description": "This package.json exists for modeling dependencies for the release process.", - "version": "3.1.1", + "version": "3.2.0", "private": true, "dependencies": { - "launchdarkly-cpp-internal": "0.2.0", - "launchdarkly-cpp-common": "0.4.0", + "launchdarkly-cpp-internal": "0.3.0", + "launchdarkly-cpp-common": "0.5.0", "launchdarkly-cpp-sse-client": "0.2.0" } } diff --git a/libs/client-sdk/tests/client_c_bindings_test.cpp b/libs/client-sdk/tests/client_c_bindings_test.cpp index ed594f4d8..0121b2d0c 100644 --- a/libs/client-sdk/tests/client_c_bindings_test.cpp +++ b/libs/client-sdk/tests/client_c_bindings_test.cpp @@ -27,7 +27,7 @@ TEST(ClientBindings, MinimalInstantiation) { char const* version = LDClientSDK_Version(); ASSERT_TRUE(version); - ASSERT_STREQ(version, "3.1.1"); // {x-release-please-version} + ASSERT_STREQ(version, "3.2.0"); // {x-release-please-version} LDClientSDK_Free(sdk); } diff --git a/libs/client-sdk/tests/client_test.cpp b/libs/client-sdk/tests/client_test.cpp index f84aca6b7..84292efba 100644 --- a/libs/client-sdk/tests/client_test.cpp +++ b/libs/client-sdk/tests/client_test.cpp @@ -16,7 +16,7 @@ TEST(ClientTest, ClientConstructedWithMinimalConfigAndContext) { char const* version = client.Version(); ASSERT_TRUE(version); - ASSERT_STREQ(version, "3.1.1"); // {x-release-please-version} + ASSERT_STREQ(version, "3.2.0"); // {x-release-please-version} } TEST(ClientTest, AllFlagsIsEmpty) { diff --git a/libs/common/CHANGELOG.md b/libs/common/CHANGELOG.md index e964afa2d..22448156e 100644 --- a/libs/common/CHANGELOG.md +++ b/libs/common/CHANGELOG.md @@ -12,6 +12,13 @@ * dependencies * launchdarkly-cpp-sse-client bumped from 0.1.1 to 0.1.2 +## [0.5.0](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-common-v0.4.0...launchdarkly-cpp-common-v0.5.0) (2023-10-23) + + +### Features + +* server-side SDK ([#160](https://github.com/launchdarkly/cpp-sdks/issues/160)) ([75eece3](https://github.com/launchdarkly/cpp-sdks/commit/75eece3a46870fdb6bf4384c112700558099c4d1)) + ## [0.4.0](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-common-v0.3.7...launchdarkly-cpp-common-v0.4.0) (2023-10-13) diff --git a/libs/common/package.json b/libs/common/package.json index ca700b009..9fe6295df 100644 --- a/libs/common/package.json +++ b/libs/common/package.json @@ -1,6 +1,6 @@ { "name": "launchdarkly-cpp-common", "description": "This package.json exists for modeling dependencies for the release process.", - "version": "0.4.0", + "version": "0.5.0", "private": true } diff --git a/libs/internal/CHANGELOG.md b/libs/internal/CHANGELOG.md index 6ae7dd859..6298b1fa8 100644 --- a/libs/internal/CHANGELOG.md +++ b/libs/internal/CHANGELOG.md @@ -46,6 +46,20 @@ * dependencies * launchdarkly-cpp-common bumped from 0.3.5 to 0.3.6 +## [0.3.0](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-internal-v0.2.0...launchdarkly-cpp-internal-v0.3.0) (2023-10-23) + + +### Features + +* server-side SDK ([#160](https://github.com/launchdarkly/cpp-sdks/issues/160)) ([75eece3](https://github.com/launchdarkly/cpp-sdks/commit/75eece3a46870fdb6bf4384c112700558099c4d1)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * launchdarkly-cpp-common bumped from 0.4.0 to 0.5.0 + ## [0.2.0](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-internal-v0.1.11...launchdarkly-cpp-internal-v0.2.0) (2023-10-13) diff --git a/libs/internal/package.json b/libs/internal/package.json index 24ec5c2bd..4c46323b5 100644 --- a/libs/internal/package.json +++ b/libs/internal/package.json @@ -1,9 +1,9 @@ { "name": "launchdarkly-cpp-internal", "description": "This package.json exists for modeling dependencies for the release process.", - "version": "0.2.0", + "version": "0.3.0", "private": true, "dependencies": { - "launchdarkly-cpp-common": "0.4.0" + "launchdarkly-cpp-common": "0.5.0" } } From fe08c3c14600c712ba6480f671fc306eca320044 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 23 Oct 2023 15:43:03 -0700 Subject: [PATCH 19/21] fix: allow for installing only the client or server SDK independently (#269) Latest client release failed because I failed to consider that the `install` step currently requires all artifacts (both client, and now server) to be built. Since we use `cmake --build . --target "$clientOrServer"`, it failed on the install step because server isn't built. This commit makes installation optional for either. --- CMakeLists.txt | 5 +++++ libs/client-sdk/src/CMakeLists.txt | 3 ++- libs/server-sdk/src/CMakeLists.txt | 3 ++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6b2e811f8..e9f9e29b0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -53,6 +53,11 @@ option(LD_DYNAMIC_LINK_OPENSSL option(LD_BUILD_EXAMPLES "Build hello-world examples." ON) +# If using 'make' as the build system, CMake causes the 'install' target to have a dependency on 'all', meaning +# it will cause a full build. This disables that, allowing us to build piecemeal instead. This is useful +# so that we only need to build the client or server for a given release (if only the client or server were affected.) +set(CMAKE_SKIP_INSTALL_ALL_DEPENDENCY true) + # All projects in this repo should share the same version of 3rd party depends. # It's the only way to remain sane. set(CMAKE_FILES "${CMAKE_CURRENT_SOURCE_DIR}/cmake") diff --git a/libs/client-sdk/src/CMakeLists.txt b/libs/client-sdk/src/CMakeLists.txt index 7e1b894ce..c24a4b414 100644 --- a/libs/client-sdk/src/CMakeLists.txt +++ b/libs/client-sdk/src/CMakeLists.txt @@ -63,7 +63,8 @@ add_library(launchdarkly::client ALIAS ${LIBNAME}) set_property(TARGET ${LIBNAME} PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL") -install(TARGETS ${LIBNAME}) +# Optional in case only the server SDK is being built. +install(TARGETS ${LIBNAME} OPTIONAL) if (LD_BUILD_SHARED_LIBS AND MSVC) install(FILES $ DESTINATION bin OPTIONAL) endif () diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index 220dad451..00569be24 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -80,7 +80,8 @@ add_library(launchdarkly::server ALIAS ${LIBNAME}) set_property(TARGET ${LIBNAME} PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL") -install(TARGETS ${LIBNAME}) +# Optional in case only the client SDK is being built. +install(TARGETS ${LIBNAME} OPTIONAL) if (LD_BUILD_SHARED_LIBS AND MSVC) install(FILES $ DESTINATION bin OPTIONAL) endif () From 82d72df338a2a89d6f55e3ebe21b4c987a9cc2c0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 15:46:56 -0700 Subject: [PATCH 20/21] chore: release main (#270) :robot: I have created a release *beep* *boop* ---
launchdarkly-cpp-client: 3.2.1 ## [3.2.1](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-client-v3.2.0...launchdarkly-cpp-client-v3.2.1) (2023-10-23) ### Bug Fixes * allow for installing only the client or server SDK independently ([#269](https://github.com/launchdarkly/cpp-sdks/issues/269)) ([fe08c3c](https://github.com/launchdarkly/cpp-sdks/commit/fe08c3c14600c712ba6480f671fc306eca320044))
--- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .release-please-manifest.json | 2 +- libs/client-sdk/CHANGELOG.md | 7 +++++++ libs/client-sdk/CMakeLists.txt | 2 +- .../client-sdk/include/launchdarkly/client_side/client.hpp | 2 +- libs/client-sdk/package.json | 2 +- libs/client-sdk/tests/client_c_bindings_test.cpp | 2 +- libs/client-sdk/tests/client_test.cpp | 2 +- 7 files changed, 13 insertions(+), 6 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index c330a45ad..abc4fec96 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,5 +1,5 @@ { - "libs/client-sdk": "3.2.0", + "libs/client-sdk": "3.2.1", "libs/server-sent-events": "0.2.0", "libs/common": "0.5.0", "libs/internal": "0.3.0" diff --git a/libs/client-sdk/CHANGELOG.md b/libs/client-sdk/CHANGELOG.md index eaa8a2509..7cd27507d 100644 --- a/libs/client-sdk/CHANGELOG.md +++ b/libs/client-sdk/CHANGELOG.md @@ -8,6 +8,13 @@ All notable changes to the LaunchDarkly Client-Side SDK for C/C++ will be docume * dependencies * launchdarkly-cpp-internal bumped from 0.1.9 to 0.1.10 +## [3.2.1](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-client-v3.2.0...launchdarkly-cpp-client-v3.2.1) (2023-10-23) + + +### Bug Fixes + +* allow for installing only the client or server SDK independently ([#269](https://github.com/launchdarkly/cpp-sdks/issues/269)) ([fe08c3c](https://github.com/launchdarkly/cpp-sdks/commit/fe08c3c14600c712ba6480f671fc306eca320044)) + ## [3.2.0](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-client-v3.1.1...launchdarkly-cpp-client-v3.2.0) (2023-10-23) diff --git a/libs/client-sdk/CMakeLists.txt b/libs/client-sdk/CMakeLists.txt index 409fd277c..c0822bcfc 100644 --- a/libs/client-sdk/CMakeLists.txt +++ b/libs/client-sdk/CMakeLists.txt @@ -6,7 +6,7 @@ cmake_minimum_required(VERSION 3.19) project( LaunchDarklyCPPClient - VERSION 3.2.0 # {x-release-please-version} + VERSION 3.2.1 # {x-release-please-version} DESCRIPTION "LaunchDarkly C++ Client SDK" LANGUAGES CXX C ) diff --git a/libs/client-sdk/include/launchdarkly/client_side/client.hpp b/libs/client-sdk/include/launchdarkly/client_side/client.hpp index e0c1c3679..d8be9404d 100644 --- a/libs/client-sdk/include/launchdarkly/client_side/client.hpp +++ b/libs/client-sdk/include/launchdarkly/client_side/client.hpp @@ -324,7 +324,7 @@ class Client : public IClient { private: inline static char const* const kVersion = - "3.2.0"; // {x-release-please-version} + "3.2.1"; // {x-release-please-version} std::unique_ptr client; }; diff --git a/libs/client-sdk/package.json b/libs/client-sdk/package.json index 4ec3178c5..e5bd535cc 100644 --- a/libs/client-sdk/package.json +++ b/libs/client-sdk/package.json @@ -1,7 +1,7 @@ { "name": "launchdarkly-cpp-client", "description": "This package.json exists for modeling dependencies for the release process.", - "version": "3.2.0", + "version": "3.2.1", "private": true, "dependencies": { "launchdarkly-cpp-internal": "0.3.0", diff --git a/libs/client-sdk/tests/client_c_bindings_test.cpp b/libs/client-sdk/tests/client_c_bindings_test.cpp index 0121b2d0c..a96c62124 100644 --- a/libs/client-sdk/tests/client_c_bindings_test.cpp +++ b/libs/client-sdk/tests/client_c_bindings_test.cpp @@ -27,7 +27,7 @@ TEST(ClientBindings, MinimalInstantiation) { char const* version = LDClientSDK_Version(); ASSERT_TRUE(version); - ASSERT_STREQ(version, "3.2.0"); // {x-release-please-version} + ASSERT_STREQ(version, "3.2.1"); // {x-release-please-version} LDClientSDK_Free(sdk); } diff --git a/libs/client-sdk/tests/client_test.cpp b/libs/client-sdk/tests/client_test.cpp index 84292efba..10d0ca1d5 100644 --- a/libs/client-sdk/tests/client_test.cpp +++ b/libs/client-sdk/tests/client_test.cpp @@ -16,7 +16,7 @@ TEST(ClientTest, ClientConstructedWithMinimalConfigAndContext) { char const* version = client.Version(); ASSERT_TRUE(version); - ASSERT_STREQ(version, "3.2.0"); // {x-release-please-version} + ASSERT_STREQ(version, "3.2.1"); // {x-release-please-version} } TEST(ClientTest, AllFlagsIsEmpty) { From 17668f94699f45086b5da97b5c2b249bf7905fcb Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 23 Oct 2023 16:39:38 -0700 Subject: [PATCH 21/21] chore: add server-side SDK to release-please config (#268) Adds the server-side SDK into release please @ `0.1.0`. --- .github/workflows/manual-publish-doc.yml | 1 + .github/workflows/manual-sdk-release-artifacts.yml | 1 + .release-please-manifest.json | 1 + libs/server-sdk/CMakeLists.txt | 2 +- libs/server-sdk/package.json | 10 ++++++++++ release-please-config.json | 10 ++++++++++ 6 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 libs/server-sdk/package.json diff --git a/.github/workflows/manual-publish-doc.yml b/.github/workflows/manual-publish-doc.yml index 78911c8cd..535888d59 100644 --- a/.github/workflows/manual-publish-doc.yml +++ b/.github/workflows/manual-publish-doc.yml @@ -8,6 +8,7 @@ on: type: choice options: - libs/client-sdk + - libs/server-sdk name: Publish Documentation jobs: build-publish: diff --git a/.github/workflows/manual-sdk-release-artifacts.yml b/.github/workflows/manual-sdk-release-artifacts.yml index 792f514bf..99e9f5db7 100644 --- a/.github/workflows/manual-sdk-release-artifacts.yml +++ b/.github/workflows/manual-sdk-release-artifacts.yml @@ -14,6 +14,7 @@ on: type: choice options: - libs/client-sdk:launchdarkly-cpp-client + - libs/server-sdk:launchdarkly-cpp-server name: Publish SDK Artifacts diff --git a/.release-please-manifest.json b/.release-please-manifest.json index abc4fec96..5169be222 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,5 +1,6 @@ { "libs/client-sdk": "3.2.1", + "libs/server-sdk": "0.1.0", "libs/server-sent-events": "0.2.0", "libs/common": "0.5.0", "libs/internal": "0.3.0" diff --git a/libs/server-sdk/CMakeLists.txt b/libs/server-sdk/CMakeLists.txt index c1de8c375..d1e7f9376 100644 --- a/libs/server-sdk/CMakeLists.txt +++ b/libs/server-sdk/CMakeLists.txt @@ -6,7 +6,7 @@ cmake_minimum_required(VERSION 3.19) project( LaunchDarklyCPPServer - VERSION 0.1 + VERSION 0.1.0 # {x-release-please-version} DESCRIPTION "LaunchDarkly C++ Server SDK" LANGUAGES CXX C ) diff --git a/libs/server-sdk/package.json b/libs/server-sdk/package.json new file mode 100644 index 000000000..c6b6bc948 --- /dev/null +++ b/libs/server-sdk/package.json @@ -0,0 +1,10 @@ +{ + "name": "launchdarkly-cpp-server", + "description": "This package.json exists for modeling dependencies for the release process.", + "version": "0.1.0", + "private": true, + "dependencies": { + "launchdarkly-cpp-internal": "0.3.0", + "launchdarkly-cpp-common": "0.5.0" + } +} diff --git a/release-please-config.json b/release-please-config.json index 810048113..b9b9fd9b5 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -11,6 +11,16 @@ "CMakeLists.txt" ] }, + "libs/server-sdk": { + "extra-files": [ + "include/launchdarkly/server_side/client.hpp", + "tests/server_c_bindings_test.cpp", + "tests/client_test.cpp", + "CMakeLists.txt" + ], + "prerelease": true, + "bump-minor-pre-major": true + }, "libs/server-sent-events": { "initial-version": "0.1.0" },