diff --git a/.github/actions/client-release/action.yml b/.github/actions/sdk-release/action.yml similarity index 85% rename from .github/actions/client-release/action.yml rename to .github/actions/sdk-release/action.yml index c6084d6e1..6e597f6a1 100644 --- a/.github/actions/client-release/action.yml +++ b/.github/actions/sdk-release/action.yml @@ -1,17 +1,23 @@ # This is a composite workflow that an generate all the release artifacts -# for the C++ client SDK. +# for the C++ client-side & server-side SDKs. # This can be ran automatically with the tag_name output from release-please, # or ran triggered manually with a user provided tag. -name: C++ Client Release -description: C++ Client Release Process +name: C++ SDK Release +description: C++ SDK Release Process inputs: tag_name: description: 'The tag name of the release to upload artifacts to.' required: true github_token: + description: 'The GitHub token to use for uploading artifacts.' required: true + sdk_path: + description: 'Path to the sdk, e.g. libs/client-sdk.' + required: true + sdk_cmake_target: + description: 'CMake target of the sdk, e.g. launchdarkly-cpp-client.' runs: using: composite @@ -28,10 +34,10 @@ runs: run: | sudo apt-get install doxygen sudo apt-get install graphviz - ./scripts/build-release.sh launchdarkly-cpp-client - ./scripts/build-docs.sh libs/client-sdk + ./scripts/build-release.sh ${{ inputs.sdk_cmake_target }} + ./scripts/build-docs.sh ${{ inputs.sdk_path }} env: - WORKSPACE: libs/client-sdk + WORKSPACE: ${{ inputs.sdk_path }} BOOST_ROOT: ${{ steps.install-boost.outputs.BOOST_ROOT }} - name: Archive Release Linux - GCC/x64/Static @@ -64,7 +70,7 @@ runs: if: runner.os == 'Linux' uses: ./.github/actions/publish-docs with: - workspace_path: libs/client-sdk + workspace_path: ${{ inputs.sdk_path }} - name: Configure MSVC if: runner.os == 'Windows' @@ -74,11 +80,11 @@ runs: if: runner.os == 'Windows' shell: bash env: - OPENSSL_ROOT_DIR: ${{ env.OPENSSL_ROOT_DIR }} + OPENSSL_ROOT_DIR: 'C:\Program Files\OpenSSL' 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 }} - run: ./scripts/build-windows.sh launchdarkly-cpp-client + run: ./scripts/build-windows.sh ${{ inputs.sdk_cmake_target }} - name: Archive Release Windows - MSVC/x64/Static if: runner.os == 'Windows' @@ -130,9 +136,9 @@ runs: echo "OPENSSL_ROOT_DIR=$(brew --prefix openssl@1.1)" >> "$GITHUB_ENV" export OPENSSL_ROOT_DIR=$(brew --prefix openssl@1.1) - ./scripts/build-release.sh launchdarkly-cpp-client + ./scripts/build-release.sh ${{ inputs.sdk_cmake_target }} env: - WORKSPACE: libs/client-sdk + WORKSPACE: ${{ inputs.sdk_path }} BOOST_ROOT: ${{ steps.install-boost.outputs.BOOST_ROOT }} - name: Archive Release Mac - AppleClang/x64/Static diff --git a/.github/workflows/client.yml b/.github/workflows/client.yml index fc40cca97..173969727 100644 --- a/.github/workflows/client.yml +++ b/.github/workflows/client.yml @@ -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 & @@ -29,15 +29,15 @@ 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: + extra_params: '-skip-from ./contract-tests/client-contract-tests/test-suppressions.txt' + build-test-client: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - uses: ./.github/actions/ci with: cmake_target: launchdarkly-cpp-client - build-test-mac: + build-test-client-mac: runs-on: macos-12 steps: - run: | @@ -52,14 +52,14 @@ jobs: with: cmake_target: launchdarkly-cpp-client platform_version: 12 - build-test-windows: + build-test-client-windows: runs-on: windows-2022 steps: - uses: actions/checkout@v3 - uses: ilammy/msvc-dev-cmd@v1 - uses: ./.github/actions/ci env: - OPENSSL_ROOT_DIR: ${{ env.OPENSSL_ROOT_DIR }} + OPENSSL_ROOT_DIR: 'C:\Program Files\OpenSSL' 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/common.yml b/.github/workflows/common.yml index d33bc4423..438bcdc6c 100644 --- a/.github/workflows/common.yml +++ b/.github/workflows/common.yml @@ -11,7 +11,7 @@ on: - '**.md' jobs: - build-test: + build-test-common: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/internal.yml b/.github/workflows/internal.yml index c8f998a4a..d116de8bb 100644 --- a/.github/workflows/internal.yml +++ b/.github/workflows/internal.yml @@ -11,7 +11,7 @@ on: - '**.md' jobs: - build-test: + build-test-internal: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/manual-client-release-artifacts.yml b/.github/workflows/manual-client-release-artifacts.yml deleted file mode 100644 index 12ba4d144..000000000 --- a/.github/workflows/manual-client-release-artifacts.yml +++ /dev/null @@ -1,29 +0,0 @@ -# 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. -on: - workflow_dispatch: - inputs: - tag: - description: 'The tag to create release artifacts for.' - required: true - -name: Publish Client Artifacts - -jobs: - release-client: - strategy: - matrix: - # Each of the platforms for which release-artifacts need generated. - os: [ ubuntu-latest, windows-2022, macos-12 ] - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v3 - with: - ref: ${{ inputs.tag }} - - id: release-client - name: Full release of libs/client-sdk - uses: ./.github/actions/client-release - with: - # The tag of the release to upload artifacts to. - tag_name: ${{ inputs.tag }} - github_token: ${{secrets.GITHUB_TOKEN}} diff --git a/.github/workflows/manual-sdk-release-artifacts.yml b/.github/workflows/manual-sdk-release-artifacts.yml new file mode 100644 index 000000000..fd55824cf --- /dev/null +++ b/.github/workflows/manual-sdk-release-artifacts.yml @@ -0,0 +1,52 @@ +# 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. +on: + workflow_dispatch: + inputs: + tag: + description: 'The tag to create release artifacts for.' + required: true + sdk_target: + description: 'The library/cmake target to release (delimited by ":").' + required: true + default: 'libs/client-sdk:launchdarkly-cpp-client' + type: choice + options: + - libs/client-sdk:launchdarkly-cpp-client + +name: Publish SDK Artifacts + +jobs: + split-input: + runs-on: ubuntu-latest + outputs: + sdk_path: ${{ steps.split-string.outputs.SDK_PATH }} + sdk_cmake_target: ${{ steps.split-string.outputs.SDK_CMAKE_TARGET }} + steps: + - name: Determine CMake target and SDK library path + id: split-string + run: | + INPUT="${{ inputs.sdk_target }}" + IFS=':' read -ra PATH_AND_TARGET <<< "$INPUT" + echo "SDK_PATH=${PATH_AND_TARGET[0]}" >> $GITHUB_OUTPUT + echo "SDK_CMAKE_TARGET=${PATH_AND_TARGET[1]}" >> $GITHUB_OUTPUT + release-sdk: + needs: split-input + strategy: + matrix: + # Each of the platforms for which release-artifacts need generated. + os: [ ubuntu-latest, windows-2022, macos-12 ] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v3 + with: + ref: ${{ inputs.tag }} + - id: release-sdk + name: Full release of ${{ needs.split-input.outputs.sdk_path }} + uses: ./.github/actions/sdk-release + with: + # The tag of the release to upload artifacts to. + tag_name: ${{ inputs.tag }} + github_token: ${{secrets.GITHUB_TOKEN}} + sdk_path: ${{ needs.split-input.outputs.sdk_path}} + sdk_cmake_target: ${{ needs.split-input.outputs.sdk_cmake_target}} diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 163ddeebc..1fca13e90 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -30,8 +30,10 @@ jobs: - uses: actions/checkout@v3 - id: release-client name: Full release of libs/client-sdk - uses: ./.github/actions/client-release + uses: ./.github/actions/sdk-release with: # The tag of the release to upload artifacts to. tag_name: ${{ needs.release-please.outputs.package-client-tag }} github_token: ${{secrets.GITHUB_TOKEN}} + sdk_path: 'libs/client-sdk' + sdk_cmake_target: 'launchdarkly-cpp-client' diff --git a/.github/workflows/server.yml b/.github/workflows/server.yml new file mode 100644 index 000000000..1ae481daf --- /dev/null +++ b/.github/workflows/server.yml @@ -0,0 +1,68 @@ +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: | + brew link --overwrite openssl@1.1 + echo "OPENSSL_ROOT_DIR=$(brew --prefix openssl@1.1)" >> "$GITHUB_ENV" + # For debugging + echo "OPENSSL_ROOT_DIR=$(brew --prefix openssl@1.1)" + - 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: + - uses: actions/checkout@v3 + - uses: ilammy/msvc-dev-cmd@v1 + - uses: ./.github/actions/ci + env: + OPENSSL_ROOT_DIR: 'C:\Program Files\OpenSSL' + 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-server + 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/.release-please-manifest.json b/.release-please-manifest.json index eb0caa68a..efd6372b6 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,6 +1,6 @@ { - "libs/client-sdk": "3.0.3", + "libs/client-sdk": "3.0.6", "libs/server-sent-events": "0.1.1", - "libs/common": "0.3.2", - "libs/internal": "0.1.5" + "libs/common": "0.3.4", + "libs/internal": "0.1.7" } diff --git a/CMakeLists.txt b/CMakeLists.txt index 6a58799ed..cec73e149 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,6 +31,8 @@ if (BUILD_TESTING) if (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") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address") elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fsanitize=leak") endif () @@ -67,7 +69,7 @@ endif () set(Boost_USE_MULTITHREADED ON) set(Boost_USE_STATIC_RUNTIME OFF) -find_package(Boost 1.80 REQUIRED COMPONENTS json url coroutine) +find_package(Boost 1.81 REQUIRED COMPONENTS json url coroutine) message(STATUS "LaunchDarkly: using Boost v${Boost_VERSION}") add_subdirectory(libs/client-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/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 100% rename from contract-tests/sdk-contract-tests/src/client_entity.cpp rename to contract-tests/client-contract-tests/src/client_entity.cpp diff --git a/contract-tests/sdk-contract-tests/src/entity_manager.cpp b/contract-tests/client-contract-tests/src/entity_manager.cpp similarity index 94% rename from contract-tests/sdk-contract-tests/src/entity_manager.cpp rename to contract-tests/client-contract-tests/src/entity_manager.cpp index 3b8b0d106..5a4d57c80 100644 --- a/contract-tests/sdk-contract-tests/src/entity_manager.cpp +++ b/contract-tests/client-contract-tests/src/entity_manager.cpp @@ -48,15 +48,20 @@ std::optional EntityManager::create(ConfigParams const& in) { 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)); + } } - auto& datasource = config_builder.DataSource(); - if (in.polling) { if (in.polling->baseUri) { endpoints.PollingBaseUrl(*in.polling->baseUri); 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/sdk-contract-tests/test-suppressions.txt b/contract-tests/client-contract-tests/test-suppressions.txt similarity index 100% rename from contract-tests/sdk-contract-tests/test-suppressions.txt rename to contract-tests/client-contract-tests/test-suppressions.txt 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..da9feca4f --- /dev/null +++ b/contract-tests/server-contract-tests/test-suppressions.txt @@ -0,0 +1,41 @@ +# 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 + +# The Server 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} diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 74d58b39a..be37f398d 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -1,2 +1,6 @@ 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..ceb971190 --- /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 launchdarkly::sse launchdarkly::common 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..88c7190ab --- /dev/null +++ b/examples/client-and-server-coexistence/main.c @@ -0,0 +1,46 @@ +/** + * 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 + +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); + 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); + LDServerSDK_Free(server_sdk); + } + + return 0; +} diff --git a/examples/hello-c-client/CMakeLists.txt b/examples/hello-c-client/CMakeLists.txt index 5d9ce32bf..4f6e4cc1c 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 launchdarkly::sse launchdarkly::common Threads::Threads) +add_executable(hello-c-client main.c) +target_link_libraries(hello-c-client PRIVATE launchdarkly::client launchdarkly::sse launchdarkly::common Threads::Threads) diff --git a/examples/hello-c-client/main.c b/examples/hello-c-client/main.c index 022f89b72..4915ff191 100644 --- a/examples/hello-c-client/main.c +++ b/examples/hello-c-client/main.c @@ -1,7 +1,7 @@ #include -#include #include +#include #include #include @@ -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..edd7e0c3e --- /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 launchdarkly::sse launchdarkly::common 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/CHANGELOG.md b/libs/client-sdk/CHANGELOG.md index 51c86eafc..abfb3430f 100644 --- a/libs/client-sdk/CHANGELOG.md +++ b/libs/client-sdk/CHANGELOG.md @@ -2,6 +2,40 @@ 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). +## [3.0.6](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-client-v3.0.5...launchdarkly-cpp-client-v3.0.6) (2023-08-29) + + +### Bug Fixes + +* LDDataSourceStatusListener_Init should take pointer ([#222](https://github.com/launchdarkly/cpp-sdks/issues/222)) ([0aa3d14](https://github.com/launchdarkly/cpp-sdks/commit/0aa3d1442cbfea3cc32d2ec981590137f0284a46)) + +## [3.0.5](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-client-v3.0.4...launchdarkly-cpp-client-v3.0.5) (2023-08-28) + + +### Bug Fixes + +* initialization of LDFlagListener ([#218](https://github.com/launchdarkly/cpp-sdks/issues/218)) ([6c263dd](https://github.com/launchdarkly/cpp-sdks/commit/6c263dd9110e4da188a56cabc54f783190e1114c)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * launchdarkly-cpp-internal bumped from 0.1.6 to 0.1.7 + * launchdarkly-cpp-common bumped from 0.3.3 to 0.3.4 + +## [3.0.4](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-client-v3.0.3...launchdarkly-cpp-client-v3.0.4) (2023-08-16) + +### Bug Fixes +Fixes required to run with msvc 14.1 (vs2017) (https://github.com/launchdarkly/cpp-sdks/issues/195) ([d16b2ea](https://github.com/launchdarkly/cpp-sdks/commit/d16b2ea1131b2a99efcec99b96c90b9384c33dc7)) + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * launchdarkly-cpp-internal bumped from 0.1.5 to 0.1.6 + * launchdarkly-cpp-common bumped from 0.3.2 to 0.3.3 + ## [3.0.3](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-client-v3.0.2...launchdarkly-cpp-client-v3.0.3) (2023-07-14) diff --git a/libs/client-sdk/CMakeLists.txt b/libs/client-sdk/CMakeLists.txt index 8ff95589b..71fe99a09 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.3 # {x-release-please-version} + VERSION 3.0.6 # {x-release-please-version} DESCRIPTION "LaunchDarkly C++ Client SDK" LANGUAGES CXX C ) diff --git a/libs/client-sdk/README.md b/libs/client-sdk/README.md index d9e8d557c..aee4dd3b0 100644 --- a/libs/client-sdk/README.md +++ b/libs/client-sdk/README.md @@ -4,11 +4,17 @@ LaunchDarkly Client-Side SDK for C/C++ [![Actions Status](https://github.com/launchdarkly/cpp-sdks/actions/workflows/client.yml/badge.svg)](https://github.com/launchdarkly/cpp-sdks/actions/workflows/client.yml) [![Documentation](https://img.shields.io/static/v1?label=GitHub+Pages&message=API+reference&color=00add8)](https://launchdarkly.github.io/cpp-sdks/libs/client-sdk/docs/html/) -The LaunchDarkly Client-Side SDK for C/C++ is designed primarily for use in desktop and embedded systems applications. 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. +The LaunchDarkly Client-Side SDK for C/C++ is designed primarily for use in desktop and embedded systems applications. +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 over 100 billion feature flags daily to help teams build better software, faster. [Get started](https://docs.launchdarkly.com/docs/getting-started) using LaunchDarkly today! +[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) @@ -20,33 +26,44 @@ This version of the LaunchDarkly SDK is compatible with POSIX environments (Linu Getting started --------------- -Download a release archive from the [Github releases]https://github.com/launchdarkly/cpp-sdks/releases?q=cpp-client&expanded=true) for use in your project. +Download a release archive from +the [Github releases](https://github.com/launchdarkly/cpp-sdks/releases?q=cpp-client&expanded=true) for use in your +project. -Refer to the [SDK documentation](https://docs.launchdarkly.com/sdk/client-side/c-c--) for complete instructions on installing and using the SDK. +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 on use cases and limitations. +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. +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. +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-client.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-client.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. +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-client.so ``` @@ -57,29 +74,48 @@ build system (CMake for instance). 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](TODO). +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. +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. Check out our [contributing guidelines](../../CONTRIBUTING.md) for instructions on how to contribute to this SDK. +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. +* 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 + * [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/client-side/c-c-- diff --git a/libs/client-sdk/include/launchdarkly/client_side/bindings/c/config/builder.h b/libs/client-sdk/include/launchdarkly/client_side/bindings/c/config/builder.h new file mode 100644 index 000000000..d1f92375e --- /dev/null +++ b/libs/client-sdk/include/launchdarkly/client_side/bindings/c/config/builder.h @@ -0,0 +1,487 @@ +/** @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 _LDClientConfigBuilder* LDClientConfigBuilder; +typedef struct _LDDataSourceStreamBuilder* LDDataSourceStreamBuilder; +typedef struct _LDDataSourcePollBuilder* LDDataSourcePollBuilder; +typedef struct _LDPersistenceCustomBuilder* LDPersistenceCustomBuilder; + +typedef void (*SetFn)(char const* storage_namespace, + char const* key, + char const* data, + void* user_data); + +typedef void (*RemoveFn)(char const* storage_namespace, + char const* key, + void* user_data); + +typedef size_t (*ReadFn)(char const* storage_namespace, + char const* key, + char const** read_value, + void* user_data); + +typedef void (*FreeFn)(char const* value, void* user_data); + +/** + * Defines a persistence interface suitable for use with SDK configuration. + */ +struct LDPersistence { + /** + * Add or update a value in the store. If the value cannot be set, then + * the function should complete normally. + * + * @param storage_namespace The namespace for the data. + * @param key The key for the data. + * @param value The data to add or update. + */ + SetFn Set; + + /** + * Remove a value from the store. If the value cannot be removed, then + * the function should complete normally. + * + * @param storage_namespace The namespace of the data. + * @param key The key of the data. + */ + RemoveFn Remove; + + /** + * Attempt to read a value from the store. + * @param storage_namespace The namespace of the data. + * @param key The key of the data. + * @param [out] read_value Out buffer containing the read string data. + * Should be set to null if no data was read. + * + * + * @return The number of characters read. Should be 0 if no data was read. + */ + ReadFn Read; + + /** + * The SDK will call this function after it has finished with the + * read_value from Read. + */ + FreeFn FreeRead; + + /** + * UserData is forwarded into all method calls in this struct. + */ + void* UserData; +}; + +/** + * Initializes a custom persistence implementation. Must be called before + * passing a custom implementation into configuration. + * @param backend Implementation to initialize. + */ +LD_EXPORT(void) LDPersistence_Init(struct LDPersistence* implementation); + +/** + * Constructs a client-side config builder. + */ +LD_EXPORT(LDClientConfigBuilder) LDClientConfigBuilder_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) +LDClientConfigBuilder_ServiceEndpoints_PollingBaseURL(LDClientConfigBuilder 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) +LDClientConfigBuilder_ServiceEndpoints_StreamingBaseURL(LDClientConfigBuilder 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) +LDClientConfigBuilder_ServiceEndpoints_EventsBaseURL(LDClientConfigBuilder 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) +LDClientConfigBuilder_ServiceEndpoints_RelayProxyBaseURL( + LDClientConfigBuilder 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) +LDClientConfigBuilder_AppInfo_Identifier(LDClientConfigBuilder 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) +LDClientConfigBuilder_AppInfo_Version(LDClientConfigBuilder 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) +LDClientConfigBuilder_Offline(LDClientConfigBuilder 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) +LDClientConfigBuilder_Events_Enabled(LDClientConfigBuilder 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) +LDClientConfigBuilder_Events_Capacity(LDClientConfigBuilder 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) +LDClientConfigBuilder_Events_FlushIntervalMs(LDClientConfigBuilder 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) +LDClientConfigBuilder_Events_AllAttributesPrivate(LDClientConfigBuilder 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) +LDClientConfigBuilder_Events_PrivateAttribute(LDClientConfigBuilder b, + char const* attribute_reference); +/** + * * Whether LaunchDarkly should provide additional information about how flag + * values were calculated. + * + * The additional information will then be available through the client's + * VariationDetail methods. Since this increases the size of network + * requests, such information is not sent unless you set this option to + * true. + * @param b Client config builder. Must not be NULL. + * @param with_reasons True to enable reasons. + */ +LD_EXPORT(void) +LDClientConfigBuilder_DataSource_WithReasons(LDClientConfigBuilder b, + bool with_reasons); + +/** + * Whether or not to use the REPORT verb to fetch flag settings. + * + * If this is true, flag settings will be fetched with a REPORT request + * including a JSON entity body with the context object. + * + * Otherwise (by default) a GET request will be issued with the context + * passed as a base64 URL-encoded path parameter. + * + * Do not use unless advised by LaunchDarkly. + * @param b Client config builder. Must not be NULL. + * @param use_reasons True to use the REPORT verb. + */ +LD_EXPORT(void) +LDClientConfigBuilder_DataSource_UseReport(LDClientConfigBuilder b, + bool use_report); +/** + * 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) +LDClientConfigBuilder_DataSource_MethodStream( + LDClientConfigBuilder b, + LDDataSourceStreamBuilder 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) +LDClientConfigBuilder_DataSource_MethodPoll( + LDClientConfigBuilder b, + LDDataSourcePollBuilder poll_builder); + +/** + * Creates a new DataSource builder for the Streaming method. + * + * If not passed into the config + * builder, must be manually freed with LDDataSourceStreamBuilder_Free. + * + * @return New builder for Streaming method. + */ +LD_EXPORT(LDDataSourceStreamBuilder) +LDDataSourceStreamBuilder_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) +LDDataSourceStreamBuilder_InitialReconnectDelayMs(LDDataSourceStreamBuilder 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) LDDataSourceStreamBuilder_Free(LDDataSourceStreamBuilder b); + +/** + * Creates a new DataSource builder for the Polling method. + * + * If not passed into the config + * builder, must be manually freed with LDDataSourcePollBuilder_Free. + * + * @return New builder for Polling method. + */ + +LD_EXPORT(LDDataSourcePollBuilder) +LDDataSourcePollBuilder_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) +LDDataSourcePollBuilder_IntervalS(LDDataSourcePollBuilder 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) LDDataSourcePollBuilder_Free(LDDataSourcePollBuilder 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) +LDClientConfigBuilder_HttpProperties_WrapperName(LDClientConfigBuilder 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) +LDClientConfigBuilder_HttpProperties_WrapperVersion( + LDClientConfigBuilder 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) +LDClientConfigBuilder_HttpProperties_Header(LDClientConfigBuilder b, + char const* key, + char const* value); + +/** + * Disables the default SDK logging. + * @param b Client config builder. Must not be NULL. + */ +LD_EXPORT(void) +LDClientConfigBuilder_Logging_Disable(LDClientConfigBuilder 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) +LDClientConfigBuilder_Logging_Basic(LDClientConfigBuilder 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) +LDClientConfigBuilder_Logging_Custom(LDClientConfigBuilder b, + LDLoggingCustomBuilder custom_builder); + +/** + * Creates a new builder for a custom, user-provided persistence. + * + * If not passed into the config builder, must be manually freed with + * LDPersistenceCustomBuilder_Free. + * @return New builder. + */ +LD_EXPORT(LDPersistenceCustomBuilder) LDPersistenceCustomBuilder_New(); + +/** + * Frees a custom persistence builder. Do not call if the builder was consumed + * by the config builder. + * @param b Builder to free. + */ +LD_EXPORT(void) LDPersistenceCustomBuilder_Free(LDPersistenceCustomBuilder b); + +/** + * Sets a custom persistence implementation. + * @param b Custom persistence builder. Must not be NULL. + * @param impl The implementation to use for persistence. Ensure the + * implementation was initialized with LDPersistence_Init. + */ +LD_EXPORT(void) +LDPersistenceCustomBuilder_Implementation(LDPersistenceCustomBuilder b, + struct LDPersistence impl); + +/** + * Configures the SDK with custom persistence. + * @param b Client config builder. Must not be NULL. + * @param custom_builder The custom persistence builder. Must not be NULL. + * @return + */ +LD_EXPORT(void) +LDClientConfigBuilder_Persistence_Custom( + LDClientConfigBuilder b, + LDPersistenceCustomBuilder custom_builder); + +/** + * Disables persistence. + * @param b Client config builder. Must not be NULL. + */ +LD_EXPORT(void) +LDClientConfigBuilder_Persistence_None(LDClientConfigBuilder b); + +/** + * Creates an LDClientConfig. The LDClientConfigBuilder 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) +LDClientConfigBuilder_Build(LDClientConfigBuilder builder, + LDClientConfig* out_config); + +/** + * Frees the builder; only necessary if not calling Build. + * @param builder Builder to free. + */ +LD_EXPORT(void) +LDClientConfigBuilder_Free(LDClientConfigBuilder builder); + +#ifdef __cplusplus +} +#endif + +// NOLINTEND modernize-use-using 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 new file mode 100644 index 000000000..1b8c154e4 --- /dev/null +++ b/libs/client-sdk/include/launchdarkly/client_side/bindings/c/config/config.h @@ -0,0 +1,28 @@ +/** @file */ +// NOLINTBEGIN modernize-use-using + +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { // only need to export C interface if +// used by C++ source code +#endif + +typedef struct _LDClientConfig* LDClientConfig; + +/** + * Free an unused configuration. Configurations used to construct an LDClientSDK + * must not be be freed. + * + * @param config Config to free. + */ +LD_EXPORT(void) LDClientConfig_Free(LDClientConfig config); + +#ifdef __cplusplus +} +#endif + +// NOLINTEND modernize-use-using 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 e353b79e7..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 @@ -4,12 +4,16 @@ // NOLINTBEGIN modernize-use-using #pragma once -#include +#include + #include #include +#include #include +#include #include #include +#include #include #include @@ -24,15 +28,17 @@ 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. - * @param config The configuration. Must not be NULL. - * @param context The initial context. Must not be NULL. - * @return New SDK instance. + * @param config The configuration. Ownership is transferred. Do not free or + * access the LDClientConfig in any way after this call; behavior is undefined. + * Must not be NULL. + * @param context The initial context. Ownership is transferred. Do not free or + * access the LDContext in any way after this call; behavior is undefined. Must + * not be NULL. + * @return New SDK instance. Must be freed with LDClientSDK_Free when no longer + * needed. */ LD_EXPORT(LDClientSDK) LDClientSDK_New(LDClientConfig config, LDContext context); @@ -133,7 +139,9 @@ LD_EXPORT(void) LDClientSDK_TrackEvent(LDClientSDK sdk, char const* event_name); * 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. Must not be NULL. + * event. Ownership is transferred. Do not free or + * access the LDValue in any way after this call; behavior is undefined. Must + * not be NULL. */ LD_EXPORT(void) LDClientSDK_TrackMetric(LDClientSDK sdk, @@ -147,7 +155,9 @@ LDClientSDK_TrackMetric(LDClientSDK sdk, * @param sdk SDK. Must not be NULL. * @param event_name Must not be NULL. * @param data A JSON value containing additional data associated with the - * event. Must not be NULL. + * event. Do not free or + * access the LDValue in any way after this call; behavior is undefined. Must + * not be NULL. */ LD_EXPORT(void) LDClientSDK_TrackData(LDClientSDK sdk, char const* event_name, LDValue data); @@ -352,7 +362,8 @@ LDClientSDK_DoubleVariationDetail(LDClientSDK sdk, * Returns the JSON value of a feature flag for a given flag key. * @param sdk SDK. 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. The value is copied. + * @param default_value The default value of the flag. Ownership is retained by + * the caller; a copy is made internally. Must not be NULL. * @return The variation for the current 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. @@ -367,7 +378,8 @@ LDClientSDK_JsonVariation(LDClientSDK sdk, * that also describes the way the value was determined. * @param sdk SDK. 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. The value is copied. + * @param default_value The default value of the flag. Ownership is retained by + * the caller; a copy is made internally. Must not be NULL. * @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. @@ -400,7 +412,7 @@ LDClientSDK_JsonVariationDetail(LDClientSDK sdk, * } * @endcode * @param sdk SDK. Must not be NULL. - * @return Value of type Object. + * @return Value of type Object. Must be freed with LDValue_Free. */ LD_EXPORT(LDValue) LDClientSDK_AllFlags(LDClientSDK sdk); @@ -411,52 +423,6 @@ LDClientSDK_AllFlags(LDClientSDK sdk); */ LD_EXPORT(void) LDClientSDK_Free(LDClientSDK sdk); -typedef void (*FlagChangedCallbackFn)(char const* flag_key, - LDValue new_value, - LDValue old_value, - bool deleted, - void* user_data); - -/** - * Defines a feature flag listener which may be used to listen for flag changes. - * The struct should be initialized using LDFlagListener_Init before use. - */ -struct LDFlagListener { - /** - * Callback function which is invoked for flag changes. - * - * The provided pointers are only valid for the duration of the function - * call (excluding UserData, whose lifetime is controlled by the caller). - * - * @param flag_key The name of the flag that changed. - * @param new_value The new value of the flag. If there was not an new - * value, because the flag was deleted, then the LDValue will be of a null - * type. Check the deleted parameter to see if a flag was deleted. - * @param old_value The old value of the flag. If there was not an old - * value, for instance a newly created flag, then the Value will be of a - * null type. - * @param deleted True if the flag has been deleted. - */ - FlagChangedCallbackFn FlagChanged; - - /** - * UserData is forwarded into callback functions. - */ - void* UserData; -}; - -/** - * Initializes a flag listener. Must be called before passing the listener - * to LDClientSDK_FlagNotifier_OnFlagChange. - * - * Create the struct, initialize the struct, set the FlagChanged handler - * and optionally UserData, and then pass the struct to - * LDClientSDK_FlagNotifier_OnFlagChange. - * - * @param listener Listener to initialize. - */ -LD_EXPORT(void) LDFlagListener_Init(struct LDFlagListener listener); - /** * Listen for changes for the specific flag. * @@ -480,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. @@ -536,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. @@ -616,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); @@ -686,7 +589,7 @@ struct LDDataSourceStatusListener { * @param listener Listener to initialize. */ LD_EXPORT(void) -LDDataSourceStatusListener_Init(struct LDDataSourceStatusListener listener); +LDDataSourceStatusListener_Init(struct LDDataSourceStatusListener* listener); /** * Listen for changes to the data source status. @@ -718,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/client.hpp b/libs/client-sdk/include/launchdarkly/client_side/client.hpp index 601d15780..50dd22549 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.3"; // {x-release-please-version} + "3.0.6"; // {x-release-please-version} std::unique_ptr client; }; diff --git a/libs/client-sdk/package.json b/libs/client-sdk/package.json index 0016b3e5a..7744ee229 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.3", + "version": "3.0.6", "private": true, "dependencies": { - "launchdarkly-cpp-internal": "0.1.5", - "launchdarkly-cpp-common": "0.3.2" + "launchdarkly-cpp-internal": "0.1.7", + "launchdarkly-cpp-common": "0.3.4" } } diff --git a/libs/client-sdk/src/CMakeLists.txt b/libs/client-sdk/src/CMakeLists.txt index aef6199ee..f4a5081f7 100644 --- a/libs/client-sdk/src/CMakeLists.txt +++ b/libs/client-sdk/src/CMakeLists.txt @@ -1,7 +1,7 @@ file(GLOB HEADER_LIST CONFIGURE_DEPENDS "${LaunchDarklyCPPClient_SOURCE_DIR}/include/launchdarkly/client_side/*.hpp" - ) +) # Automatic library: static or dynamic based on user config. @@ -25,6 +25,8 @@ add_library(${LIBNAME} flag_manager/flag_store.hpp flag_manager/flag_updater.hpp bindings/c/sdk.cpp + bindings/c/builder.cpp + bindings/c/config.cpp data_sources/null_data_source.cpp flag_manager/context_index.cpp flag_manager/flag_manager.cpp @@ -62,7 +64,7 @@ endif () install(DIRECTORY "${LaunchDarklyCPPClient_SOURCE_DIR}/include/launchdarkly" DESTINATION "include" - ) +) # Need the public headers to build. target_include_directories(${LIBNAME} PUBLIC ../include) diff --git a/libs/common/src/bindings/c/config/builder.cpp b/libs/client-sdk/src/bindings/c/builder.cpp similarity index 84% rename from libs/common/src/bindings/c/config/builder.cpp rename to libs/client-sdk/src/bindings/c/builder.cpp index 827b3ba3e..8f88b59ee 100644 --- a/libs/common/src/bindings/c/config/builder.cpp +++ b/libs/client-sdk/src/bindings/c/builder.cpp @@ -1,7 +1,8 @@ // NOLINTBEGIN cppcoreguidelines-pro-type-reinterpret-cast // NOLINTBEGIN OCInconsistentNamingInspection -#include +#include + #include #include #include @@ -40,27 +41,6 @@ using namespace launchdarkly::client_side; #define FROM_CUSTOM_PERSISTENCE_BUILDER(ptr) \ (reinterpret_cast(ptr)) -/** - * Utility class to allow user-provided backends to satisfy the ILogBackend - * interface. - */ -class LogBackendWrapper : public launchdarkly::ILogBackend { - public: - explicit LogBackendWrapper(LDLogBackend backend) : backend_(backend) {} - bool Enabled(launchdarkly::LogLevel level) noexcept override { - return backend_.Enabled(static_cast(level), - backend_.UserData); - } - void Write(launchdarkly::LogLevel level, - std::string message) noexcept override { - return backend_.Write(static_cast(level), message.c_str(), - backend_.UserData); - } - - private: - LDLogBackend backend_; -}; - class PersistenceImplementationWrapper : public IPersistence { public: explicit PersistenceImplementationWrapper(LDPersistence impl) @@ -326,14 +306,6 @@ LDClientConfigBuilder_HttpProperties_Header(LDClientConfigBuilder b, TO_BUILDER(b)->HttpProperties().Header(key, value); } -LD_EXPORT(LDLoggingBasicBuilder) LDLoggingBasicBuilder_New() { - return FROM_BASIC_LOGGING_BUILDER(new LoggingBuilder::BasicLogging()); -} - -LD_EXPORT(void) LDLoggingBasicBuilder_Free(LDLoggingBasicBuilder b) { - delete TO_BASIC_LOGGING_BUILDER(b); -} - LD_EXPORT(void) LDClientConfigBuilder_Logging_Disable(LDClientConfigBuilder b) { LD_ASSERT_NOT_NULL(b); @@ -341,22 +313,6 @@ LDClientConfigBuilder_Logging_Disable(LDClientConfigBuilder b) { TO_BUILDER(b)->Logging().Logging(LoggingBuilder::NoLogging()); } -LD_EXPORT(void) -LDLoggingBasicBuilder_Level(LDLoggingBasicBuilder b, enum LDLogLevel level) { - using launchdarkly::LogLevel; - LD_ASSERT_NOT_NULL(b); - - LoggingBuilder::BasicLogging* logger = TO_BASIC_LOGGING_BUILDER(b); - logger->Level(static_cast(level)); -} - -void LDLoggingBasicBuilder_Tag(LDLoggingBasicBuilder b, char const* tag) { - LD_ASSERT_NOT_NULL(b); - LD_ASSERT_NOT_NULL(tag); - - TO_BASIC_LOGGING_BUILDER(b)->Tag(tag); -} - LD_EXPORT(void) LDClientConfigBuilder_Logging_Basic(LDClientConfigBuilder b, LDLoggingBasicBuilder basic_builder) { @@ -368,12 +324,6 @@ LDClientConfigBuilder_Logging_Basic(LDClientConfigBuilder b, LDLoggingBasicBuilder_Free(basic_builder); } -LD_EXPORT(void) LDLogBackend_Init(struct LDLogBackend* backend) { - backend->Enabled = [](enum LDLogLevel, void*) { return false; }; - backend->Write = [](enum LDLogLevel, char const*, void*) {}; - backend->UserData = nullptr; -} - LD_EXPORT(void) LDPersistence_Init(struct LDPersistence* impl) { impl->Set = [](char const* storage_namespace, char const* key, char const* data, void* user_data) {}; @@ -388,14 +338,6 @@ LD_EXPORT(void) LDPersistence_Init(struct LDPersistence* impl) { impl->UserData = nullptr; } -LD_EXPORT(LDLoggingCustomBuilder) LDLoggingCustomBuilder_New() { - return FROM_CUSTOM_LOGGING_BUILDER(new LoggingBuilder::CustomLogging()); -} - -LD_EXPORT(void) LDLoggingCustomBuilder_Free(LDLoggingCustomBuilder b) { - delete TO_CUSTOM_LOGGING_BUILDER(b); -} - LD_EXPORT(void) LDClientConfigBuilder_Logging_Custom(LDClientConfigBuilder b, LDLoggingCustomBuilder custom_builder) { @@ -408,14 +350,6 @@ LDClientConfigBuilder_Logging_Custom(LDClientConfigBuilder b, LDLoggingCustomBuilder_Free(custom_builder); } -LD_EXPORT(void) -LDLoggingCustomBuilder_Backend(LDLoggingCustomBuilder b, LDLogBackend backend) { - LD_ASSERT_NOT_NULL(b); - - TO_CUSTOM_LOGGING_BUILDER(b)->Backend( - std::make_shared(backend)); -} - LD_EXPORT(LDPersistenceCustomBuilder) LDPersistenceCustomBuilder_New() { return FROM_CUSTOM_PERSISTENCE_BUILDER( new PersistenceBuilder::CustomBuilder()); diff --git a/libs/common/src/bindings/c/config/config.cpp b/libs/client-sdk/src/bindings/c/config.cpp similarity index 89% rename from libs/common/src/bindings/c/config/config.cpp rename to libs/client-sdk/src/bindings/c/config.cpp index d9dd3c476..61d3daa5a 100644 --- a/libs/common/src/bindings/c/config/config.cpp +++ b/libs/client-sdk/src/bindings/c/config.cpp @@ -1,7 +1,7 @@ // NOLINTBEGIN cppcoreguidelines-pro-type-reinterpret-cast // NOLINTBEGIN OCInconsistentNamingInspection -#include +#include #include #define TO_CONFIG(ptr) (reinterpret_cast(ptr)) diff --git a/libs/client-sdk/src/bindings/c/sdk.cpp b/libs/client-sdk/src/bindings/c/sdk.cpp index ec68b396a..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,46 +373,10 @@ LD_EXPORT(time_t) LDDataSourceStatus_StateSince(LDDataSourceStatus status) { .count(); } -LD_EXPORT(void) LDFlagListener_Init(struct LDFlagListener listener) { - listener.FlagChanged = nullptr; - listener.UserData = nullptr; -} - -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(LDDataSourceStatusListener listener) { - listener.StatusChanged = nullptr; - listener.UserData = nullptr; +LDDataSourceStatusListener_Init(struct LDDataSourceStatusListener* listener) { + listener->StatusChanged = nullptr; + listener->UserData = nullptr; } LD_EXPORT(LDListenerConnection) @@ -450,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 fee995486..2b4aae39a 100644 --- a/libs/client-sdk/src/client_impl.cpp +++ b/libs/client-sdk/src/client_impl.cpp @@ -252,7 +252,6 @@ EvaluationDetail ClientImpl::VariationInternal(FlagKey const& key, << "LaunchDarkly client has not yet been initialized. " "Returning default value"; - // TODO: SC-199918 auto error_reason = EvaluationReason(EvaluationReason::ErrorKind::kClientNotReady); if (eval_reasons_available_) { 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 faa7e33d4..9d7c504cc 100644 --- a/libs/client-sdk/src/data_sources/streaming_data_source.cpp +++ b/libs/client-sdk/src/data_sources/streaming_data_source.cpp @@ -91,7 +91,6 @@ void StreamingDataSource::Start() { return; } - // TODO: Initial reconnect delay. sc-204393 boost::urls::url url = uri_components.value(); if (data_source_config_.with_reasons) { @@ -117,6 +116,9 @@ void StreamingDataSource::Start() { 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); } diff --git a/libs/client-sdk/tests/client_c_bindings_test.cpp b/libs/client-sdk/tests/client_c_bindings_test.cpp index 44a98c45c..66d0e5272 100644 --- a/libs/client-sdk/tests/client_c_bindings_test.cpp +++ b/libs/client-sdk/tests/client_c_bindings_test.cpp @@ -1,8 +1,10 @@ #include -#include -#include +#include #include + +#include + #include #include @@ -25,7 +27,7 @@ TEST(ClientBindings, MinimalInstantiation) { char const* version = LDClientSDK_Version(); ASSERT_TRUE(version); - ASSERT_STREQ(version, "3.0.3"); // {x-release-please-version} + ASSERT_STREQ(version, "3.0.6"); // {x-release-please-version} LDClientSDK_Free(sdk); } @@ -47,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); @@ -57,8 +59,15 @@ TEST(ClientBindings, RegisterFlagListener) { LDClientSDK_Start(sdk, 3000, &success); EXPECT_TRUE(success); - struct LDFlagListener listener {}; - LDFlagListener_Init(listener); + struct LDFlagListener listener { + reinterpret_cast(0x123), + reinterpret_cast(0x456) + }; + + LDFlagListener_Init(&listener); + ASSERT_EQ(listener.FlagChanged, nullptr); + ASSERT_EQ(listener.UserData, nullptr); + listener.UserData = const_cast("Potato"); listener.FlagChanged = FlagListenerFunction; @@ -92,8 +101,14 @@ TEST(ClientBindings, RegisterDataSourceStatusChangeListener) { LDClientSDK sdk = LDClientSDK_New(config, context); - struct LDDataSourceStatusListener listener {}; - LDDataSourceStatusListener_Init(listener); + struct LDDataSourceStatusListener listener { + reinterpret_cast(0x123), + reinterpret_cast(0x456) + }; + LDDataSourceStatusListener_Init(&listener); + + ASSERT_EQ(listener.StatusChanged, nullptr); + ASSERT_EQ(listener.UserData, nullptr); listener.UserData = const_cast("Potato"); listener.StatusChanged = StatusListenerFunction; diff --git a/libs/common/tests/bindings/client_config_test.cpp b/libs/client-sdk/tests/client_config_test.cpp similarity index 98% rename from libs/common/tests/bindings/client_config_test.cpp rename to libs/client-sdk/tests/client_config_test.cpp index fba2d97b5..f9b923afe 100644 --- a/libs/common/tests/bindings/client_config_test.cpp +++ b/libs/client-sdk/tests/client_config_test.cpp @@ -1,6 +1,7 @@ #include -#include "launchdarkly/bindings/c/config/builder.h" -#include "launchdarkly/config/client.hpp" + +#include +#include TEST(ClientConfigBindings, ConfigBuilderNewFree) { LDClientConfigBuilder builder = LDClientConfigBuilder_New("sdk-123"); diff --git a/libs/client-sdk/tests/client_test.cpp b/libs/client-sdk/tests/client_test.cpp index 07888d711..2711e72b2 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.3"); // {x-release-please-version} + ASSERT_STREQ(version, "3.0.6"); // {x-release-please-version} } TEST(ClientTest, AllFlagsIsEmpty) { diff --git a/libs/common/CHANGELOG.md b/libs/common/CHANGELOG.md index 4c55a32d5..7e88c32cb 100644 --- a/libs/common/CHANGELOG.md +++ b/libs/common/CHANGELOG.md @@ -6,6 +6,20 @@ * dependencies * launchdarkly-cpp-sse-client bumped from 0.1.0 to 0.1.1 +## [0.3.4](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-common-v0.3.3...launchdarkly-cpp-common-v0.3.4) (2023-08-28) + + +### Bug Fixes + +* initialization of LDFlagListener ([#218](https://github.com/launchdarkly/cpp-sdks/issues/218)) ([6c263dd](https://github.com/launchdarkly/cpp-sdks/commit/6c263dd9110e4da188a56cabc54f783190e1114c)) + +## [0.3.3](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-common-v0.3.2...launchdarkly-cpp-common-v0.3.3) (2023-08-16) + + +### Bug Fixes + +* Fixes required to run with msvc 14.1 (vs2017) ([#195](https://github.com/launchdarkly/cpp-sdks/issues/195)) ([d16b2ea](https://github.com/launchdarkly/cpp-sdks/commit/d16b2ea1131b2a99efcec99b96c90b9384c33dc7)) + ## [0.3.1](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-common-v0.3.0...launchdarkly-cpp-common-v0.3.1) (2023-06-08) diff --git a/libs/common/include/launchdarkly/attribute_reference.hpp b/libs/common/include/launchdarkly/attribute_reference.hpp index c1e077e65..13ff74043 100644 --- a/libs/common/include/launchdarkly/attribute_reference.hpp +++ b/libs/common/include/launchdarkly/attribute_reference.hpp @@ -126,7 +126,7 @@ class AttributeReference { /** * Default constructs an invalid attribute reference. */ - explicit AttributeReference(); + AttributeReference(); bool operator==(AttributeReference const& other) const { return components_ == other.components_; diff --git a/libs/common/include/launchdarkly/attributes_builder.hpp b/libs/common/include/launchdarkly/attributes_builder.hpp index 3f959c168..bc0dc82c9 100644 --- a/libs/common/include/launchdarkly/attributes_builder.hpp +++ b/libs/common/include/launchdarkly/attributes_builder.hpp @@ -63,7 +63,9 @@ class AttributesBuilder final { AttributesBuilder& operator=(AttributesBuilder const&) = delete; AttributesBuilder& operator=(AttributesBuilder&&) = delete; - AttributesBuilder(AttributesBuilder&& builder) noexcept = default; + // This cannot be noexcept because of: + // https://developercommunity.visualstudio.com/t/bug-in-stdmapstdpair-implementation-with-move-only/840554 + AttributesBuilder(AttributesBuilder&& builder) = default; ~AttributesBuilder() = default; /** diff --git a/libs/common/include/launchdarkly/bindings/c/config/builder.h b/libs/common/include/launchdarkly/bindings/c/config/builder.h index 616ccc4c6..ac7b7e130 100644 --- a/libs/common/include/launchdarkly/bindings/c/config/builder.h +++ b/libs/common/include/launchdarkly/bindings/c/config/builder.h @@ -1,597 +1,14 @@ -/** @file */ -// NOLINTBEGIN modernize-use-using - -#pragma once - -#include -#include -#include - -#include -#include - -#ifdef __cplusplus -extern "C" { // only need to export C interface if -// used by C++ source code -#endif - -typedef struct _LDClientConfigBuilder* LDClientConfigBuilder; -typedef struct _LDDataSourceStreamBuilder* LDDataSourceStreamBuilder; -typedef struct _LDDataSourcePollBuilder* LDDataSourcePollBuilder; -typedef struct _LDLoggingCustomBuilder* LDLoggingCustomBuilder; -typedef struct _LDLoggingBasicBuilder* LDLoggingBasicBuilder; -typedef struct _LDPersistenceCustomBuilder* LDPersistenceCustomBuilder; - -/** - * Defines the log levels used with the SDK's default logger, or a user-provided - * custom logger. - */ -enum LDLogLevel { - LD_LOG_DEBUG = 0, - LD_LOG_INFO = 1, - LD_LOG_WARN = 2, - LD_LOG_ERROR = 3, -}; - -typedef bool (*EnabledFn)(enum LDLogLevel level, void* user_data); -typedef void (*WriteFn)(enum LDLogLevel level, - char const* msg, - void* user_data); - -/** - * Defines a logging interface suitable for use with SDK configuration. - */ -struct LDLogBackend { - /** - * Check if the specified log level is enabled. Must be thread safe. - * @param level The log level to check. - * @return Returns true if the level is enabled. - */ - EnabledFn Enabled; - - /** - * Write a message to the specified level. The message pointer is valid only - * for the duration of this function call. Must be thread safe. - * @param level The level to write the message to. - * @param message The message to write. - */ - WriteFn Write; - - /** - * UserData is forwarded into both Enabled and Write. - */ - void* UserData; -}; - -/** - * Initializes a custom log backend. Must be called before passing a custom - * backend into configuration. - * @param backend Backend to initialize. - */ -LD_EXPORT(void) LDLogBackend_Init(struct LDLogBackend* backend); - -typedef void (*SetFn)(char const* storage_namespace, - char const* key, - char const* data, - void* user_data); - -typedef void (*RemoveFn)(char const* storage_namespace, - char const* key, - void* user_data); - -typedef size_t (*ReadFn)(char const* storage_namespace, - char const* key, - char const** read_value, - void* user_data); - -typedef void (*FreeFn)(char const* value, void* user_data); - -/** - * Defines a persistence interface suitable for use with SDK configuration. - */ -struct LDPersistence { - /** - * Add or update a value in the store. If the value cannot be set, then - * the function should complete normally. - * - * @param storage_namespace The namespace for the data. - * @param key The key for the data. - * @param value The data to add or update. - */ - SetFn Set; - - /** - * Remove a value from the store. If the value cannot be removed, then - * the function should complete normally. - * - * @param storage_namespace The namespace of the data. - * @param key The key of the data. - */ - RemoveFn Remove; - - /** - * Attempt to read a value from the store. - * @param storage_namespace The namespace of the data. - * @param key The key of the data. - * @param [out] read_value Out buffer containing the read string data. - * Should be set to null if no data was read. - * - * - * @return The number of characters read. Should be 0 if no data was read. - */ - ReadFn Read; - - /** - * The SDK will call this function after it has finished with the - * read_value from Read. - */ - FreeFn FreeRead; - - /** - * UserData is forwarded into all method calls in this struct. - */ - void* UserData; -}; - -/** - * Initializes a custom persistence implementation. Must be called before - * passing a custom implementation into configuration. - * @param backend Implementation to initialize. - */ -LD_EXPORT(void) LDPersistence_Init(struct LDPersistence* implementation); - -/** - * Constructs a client-side config builder. - */ -LD_EXPORT(LDClientConfigBuilder) LDClientConfigBuilder_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) -LDClientConfigBuilder_ServiceEndpoints_PollingBaseURL(LDClientConfigBuilder 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) -LDClientConfigBuilder_ServiceEndpoints_StreamingBaseURL(LDClientConfigBuilder 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) -LDClientConfigBuilder_ServiceEndpoints_EventsBaseURL(LDClientConfigBuilder 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) -LDClientConfigBuilder_ServiceEndpoints_RelayProxyBaseURL( - LDClientConfigBuilder 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) -LDClientConfigBuilder_AppInfo_Identifier(LDClientConfigBuilder 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) -LDClientConfigBuilder_AppInfo_Version(LDClientConfigBuilder 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) -LDClientConfigBuilder_Offline(LDClientConfigBuilder 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) -LDClientConfigBuilder_Events_Enabled(LDClientConfigBuilder 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) -LDClientConfigBuilder_Events_Capacity(LDClientConfigBuilder 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) -LDClientConfigBuilder_Events_FlushIntervalMs(LDClientConfigBuilder 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) -LDClientConfigBuilder_Events_AllAttributesPrivate(LDClientConfigBuilder 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) -LDClientConfigBuilder_Events_PrivateAttribute(LDClientConfigBuilder b, - char const* attribute_reference); -/** - * * Whether LaunchDarkly should provide additional information about how flag - * values were calculated. - * - * The additional information will then be available through the client's - * VariationDetail methods. Since this increases the size of network - * requests, such information is not sent unless you set this option to - * true. - * @param b Client config builder. Must not be NULL. - * @param with_reasons True to enable reasons. - */ -LD_EXPORT(void) -LDClientConfigBuilder_DataSource_WithReasons(LDClientConfigBuilder b, - bool with_reasons); - -/** - * Whether or not to use the REPORT verb to fetch flag settings. - * - * If this is true, flag settings will be fetched with a REPORT request - * including a JSON entity body with the context object. - * - * Otherwise (by default) a GET request will be issued with the context - * passed as a base64 URL-encoded path parameter. - * - * Do not use unless advised by LaunchDarkly. - * @param b Client config builder. Must not be NULL. - * @param use_reasons True to use the REPORT verb. - */ -LD_EXPORT(void) -LDClientConfigBuilder_DataSource_UseReport(LDClientConfigBuilder b, - bool use_report); -/** - * 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) -LDClientConfigBuilder_DataSource_MethodStream( - LDClientConfigBuilder b, - LDDataSourceStreamBuilder stream_builder); - -/** - * Set the polling configuration for the builder. +/** @file + * This header for the LaunchDarkly C++ Client-side SDK has moved! * - * 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. + * Please use 'launchdarkly/client_side/bindings/c/config/builder.h' instead. * - * @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) -LDClientConfigBuilder_DataSource_MethodPoll( - LDClientConfigBuilder b, - LDDataSourcePollBuilder poll_builder); - -/** - * Creates a new DataSource builder for the Streaming method. - * - * If not passed into the config - * builder, must be manually freed with LDDataSourceStreamBuilder_Free. - * - * @return New builder for Streaming method. - */ -LD_EXPORT(LDDataSourceStreamBuilder) -LDDataSourceStreamBuilder_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) -LDDataSourceStreamBuilder_InitialReconnectDelayMs(LDDataSourceStreamBuilder 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) LDDataSourceStreamBuilder_Free(LDDataSourceStreamBuilder b); - -/** - * Creates a new DataSource builder for the Polling method. - * - * If not passed into the config - * builder, must be manually freed with LDDataSourcePollBuilder_Free. - * - * @return New builder for Polling method. - */ - -LD_EXPORT(LDDataSourcePollBuilder) -LDDataSourcePollBuilder_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) -LDDataSourcePollBuilder_IntervalS(LDDataSourcePollBuilder 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) LDDataSourcePollBuilder_Free(LDDataSourcePollBuilder 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) -LDClientConfigBuilder_HttpProperties_WrapperName(LDClientConfigBuilder 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) -LDClientConfigBuilder_HttpProperties_WrapperVersion( - LDClientConfigBuilder 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) -LDClientConfigBuilder_HttpProperties_Header(LDClientConfigBuilder b, - char const* key, - char const* value); - -/** - * Creates a new builder for LaunchDarkly's default logger. - * - * If not passed into the config - * builder, must be manually freed with LDLoggingBasicBuilder_Free. - * @return New builder. - */ -LD_EXPORT(LDLoggingBasicBuilder) LDLoggingBasicBuilder_New(); - -/** - * Frees a basic logging builder. Do not call if the builder was consumed by - * the config builder. - * @param b Builder to free. - */ -LD_EXPORT(void) LDLoggingBasicBuilder_Free(LDLoggingBasicBuilder b); - -/** - * Sets the enabled log level. The default level is LD_LOG_INFO. - * @param b Client config builder. Must not be NULL. - * @param level Level to set. - */ -LD_EXPORT(void) -LDLoggingBasicBuilder_Level(LDLoggingBasicBuilder b, enum LDLogLevel level); - -/** - * Set a tag for this logger. This tag will be included at the start - * of log entries in square brackets. - * - * If the name was "LaunchDarkly", then log entries will be prefixed - * with "[LaunchDarkly]". The default tag is "LaunchDarkly". - * @param b Client config builder. Must not be NULL. - * @param tag Tag to set. Must not be NULL. - */ -LD_EXPORT(void) -LDLoggingBasicBuilder_Tag(LDLoggingBasicBuilder b, char const* tag); - -/** - * Disables the default SDK logging. - * @param b Client config builder. Must not be NULL. - */ -LD_EXPORT(void) -LDClientConfigBuilder_Logging_Disable(LDClientConfigBuilder 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) -LDClientConfigBuilder_Logging_Basic(LDClientConfigBuilder b, - LDLoggingBasicBuilder basic_builder); - -/** - * Creates a new builder for a custom, user-provided logger. - * - * If not passed into the config - * builder, must be manually freed with LDLoggingCustomBuilder_Free. - * @return New builder. - */ -LD_EXPORT(LDLoggingCustomBuilder) LDLoggingCustomBuilder_New(); - -/** - * Frees a custom logging builder. Do not call if the builder was consumed by - * the config builder. - * @param b Builder to free. - */ -LD_EXPORT(void) LDLoggingCustomBuilder_Free(LDLoggingCustomBuilder b); - -/** - * Sets a custom log backend. - * @param b Custom logging builder. Must not be NULL. - * @param backend The backend to use for logging. Ensure the backend was - * initialized with LDLogBackend_Init. - */ -LD_EXPORT(void) -LDLoggingCustomBuilder_Backend(LDLoggingCustomBuilder b, - struct LDLogBackend backend); - -/** - * 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) -LDClientConfigBuilder_Logging_Custom(LDClientConfigBuilder b, - LDLoggingCustomBuilder custom_builder); - -/** - * Creates a new builder for a custom, user-provided persistence. - * - * If not passed into the config builder, must be manually freed with - * LDPersistenceCustomBuilder_Free. - * @return New builder. - */ -LD_EXPORT(LDPersistenceCustomBuilder) LDPersistenceCustomBuilder_New(); - -/** - * Frees a custom persistence builder. Do not call if the builder was consumed - * by the config builder. - * @param b Builder to free. - */ -LD_EXPORT(void) LDPersistenceCustomBuilder_Free(LDPersistenceCustomBuilder b); - -/** - * Sets a custom persistence implementation. - * @param b Custom persistence builder. Must not be NULL. - * @param impl The implementation to use for persistence. Ensure the - * implementation was initialized with LDPersistence_Init. - */ -LD_EXPORT(void) -LDPersistenceCustomBuilder_Implementation(LDPersistenceCustomBuilder b, - struct LDPersistence impl); - -/** - * Configures the SDK with custom persistence. - * @param b Client config builder. Must not be NULL. - * @param custom_builder The custom persistence builder. Must not be NULL. - * @return - */ -LD_EXPORT(void) -LDClientConfigBuilder_Persistence_Custom( - LDClientConfigBuilder b, - LDPersistenceCustomBuilder custom_builder); - -/** - * Disables persistence. - * @param b Client config builder. Must not be NULL. - */ -LD_EXPORT(void) -LDClientConfigBuilder_Persistence_None(LDClientConfigBuilder b); - -/** - * Creates an LDClientConfig. The LDClientConfigBuilder 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) -LDClientConfigBuilder_Build(LDClientConfigBuilder builder, - LDClientConfig* out_config); - -/** - * Frees the builder; only necessary if not calling Build. - * @param builder Builder to free. - */ -LD_EXPORT(void) -LDClientConfigBuilder_Free(LDClientConfigBuilder builder); - -#ifdef __cplusplus -} -#endif + * The original C++ Client-side 1.0 release shipped builder.h at this location, + * but it was moved to the client_side subdirectory to accommodate the C++ + * Server-side SDK. */ +#pragma once -// NOLINTEND modernize-use-using +#pragma message( \ + "LaunchDarkly Client-side C++ SDK: ACTION REQUIRED: This header has moved!") +#pragma message( \ + "LaunchDarkly Client-side C++ SDK: Please include 'launchdarkly/client_side/bindings/c/config/builder.h' instead") diff --git a/libs/common/include/launchdarkly/bindings/c/config/config.h b/libs/common/include/launchdarkly/bindings/c/config/config.h index d81ae5a8c..44c751716 100644 --- a/libs/common/include/launchdarkly/bindings/c/config/config.h +++ b/libs/common/include/launchdarkly/bindings/c/config/config.h @@ -1,27 +1,14 @@ -/** @file */ -// NOLINTBEGIN modernize-use-using - +/** @file + * This header for the LaunchDarkly C++ Client-side SDK has moved! + * + * Please use 'launchdarkly/client_side/bindings/c/config/config.h' instead. + * + * The original C++ Client-side 1.0 release shipped config.h at this location, + * but it was moved to the client_side subdirectory to accommodate the C++ + * Server-side SDK. */ #pragma once -#include -#include - -#ifdef __cplusplus -extern "C" { // only need to export C interface if -// used by C++ source code -#endif - -typedef struct _LDClientConfig* LDClientConfig; - -/** - * Free the configuration. Configurations passed into an LDClient do not need to - * be freed. - * @param config Config to free. - */ -LD_EXPORT(void) LDClientConfig_Free(LDClientConfig config); - -#ifdef __cplusplus -} -#endif - -// NOLINTEND modernize-use-using +#pragma message( \ + "LaunchDarkly Client-side C++ SDK: ACTION REQUIRED: This header has moved!") +#pragma message( \ + "LaunchDarkly Client-side C++ SDK: Please include 'launchdarkly/client_side/bindings/c/config/config.h' instead") diff --git a/libs/common/include/launchdarkly/bindings/c/config/logging_builder.h b/libs/common/include/launchdarkly/bindings/c/config/logging_builder.h new file mode 100644 index 000000000..caaa00033 --- /dev/null +++ b/libs/common/include/launchdarkly/bindings/c/config/logging_builder.h @@ -0,0 +1,134 @@ +/** @file */ +// 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 _LDLoggingBasicBuilder* LDLoggingBasicBuilder; +typedef struct _LDLoggingCustomBuilder* LDLoggingCustomBuilder; + +/** + * Defines the log levels used with the SDK's default logger, or a user-provided + * custom logger. + */ +enum LDLogLevel { + LD_LOG_DEBUG = 0, + LD_LOG_INFO = 1, + LD_LOG_WARN = 2, + LD_LOG_ERROR = 3, +}; + +typedef bool (*EnabledFn)(enum LDLogLevel level, void* user_data); +typedef void (*WriteFn)(enum LDLogLevel level, + char const* msg, + void* user_data); + +/** + * Defines a logging interface suitable for use with SDK configuration. + */ +struct LDLogBackend { + /** + * Check if the specified log level is enabled. Must be thread safe. + * @param level The log level to check. + * @return Returns true if the level is enabled. + */ + EnabledFn Enabled; + + /** + * Write a message to the specified level. The message pointer is valid only + * for the duration of this function call. Must be thread safe. + * @param level The level to write the message to. + * @param message The message to write. + */ + WriteFn Write; + + /** + * UserData is forwarded into both Enabled and Write. + */ + void* UserData; +}; + +/** + * Initializes a custom log backend. Must be called before passing a custom + * backend into configuration. + * @param backend Backend to initialize. + */ +LD_EXPORT(void) LDLogBackend_Init(struct LDLogBackend* backend); + +/** + * Creates a new builder for LaunchDarkly's default logger. + * + * If not passed into the config + * builder, must be manually freed with LDLoggingBasicBuilder_Free. + * @return New builder. + */ +LD_EXPORT(LDLoggingBasicBuilder) LDLoggingBasicBuilder_New(); + +/** + * Frees a basic logging builder. Do not call if the builder was consumed by + * the config builder. + * @param b Builder to free. + */ +LD_EXPORT(void) LDLoggingBasicBuilder_Free(LDLoggingBasicBuilder b); + +/** + * Sets the enabled log level. The default level is LD_LOG_INFO. + * @param b Client config builder. Must not be NULL. + * @param level Level to set. + */ +LD_EXPORT(void) +LDLoggingBasicBuilder_Level(LDLoggingBasicBuilder b, enum LDLogLevel level); + +/** + * Set a tag for this logger. This tag will be included at the start + * of log entries in square brackets. + * + * If the name was "LaunchDarkly", then log entries will be prefixed + * with "[LaunchDarkly]". The default tag is "LaunchDarkly". + * @param b Client config builder. Must not be NULL. + * @param tag Tag to set. Must not be NULL. + */ +LD_EXPORT(void) +LDLoggingBasicBuilder_Tag(LDLoggingBasicBuilder b, char const* tag); + +/** + * Creates a new builder for a custom, user-provided logger. + * + * If not passed into the config + * builder, must be manually freed with LDLoggingCustomBuilder_Free. + * @return New builder. + */ +LD_EXPORT(LDLoggingCustomBuilder) LDLoggingCustomBuilder_New(); + +/** + * Frees a custom logging builder. Do not call if the builder was consumed by + * the config builder. + * @param b Builder to free. + */ +LD_EXPORT(void) LDLoggingCustomBuilder_Free(LDLoggingCustomBuilder b); + +/** + * Sets a custom log backend. + * @param b Custom logging builder. Must not be NULL. + * @param backend The backend to use for logging. Ensure the backend was + * initialized with LDLogBackend_Init. + */ +LD_EXPORT(void) +LDLoggingCustomBuilder_Backend(LDLoggingCustomBuilder b, + struct LDLogBackend backend); + +#ifdef __cplusplus +} +#endif + +// NOLINTEND modernize-use-using 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/flag_listener.h b/libs/common/include/launchdarkly/bindings/c/flag_listener.h new file mode 100644 index 000000000..2e930129c --- /dev/null +++ b/libs/common/include/launchdarkly/bindings/c/flag_listener.h @@ -0,0 +1,64 @@ +/** @file */ +// NOLINTBEGIN modernize-use-using + +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { // only need to export C interface if +// used by C++ source code +#endif + +typedef void (*FlagChangedCallbackFn)(char const* flag_key, + LDValue new_value, + LDValue old_value, + bool deleted, + void* user_data); + +/** + * Defines a feature flag listener which may be used to listen for flag changes. + * The struct should be initialized using LDFlagListener_Init before use. + */ +struct LDFlagListener { + /** + * Callback function which is invoked for flag changes. + * + * The provided pointers are only valid for the duration of the function + * call (excluding UserData, whose lifetime is controlled by the caller). + * + * @param flag_key The name of the flag that changed. + * @param new_value The new value of the flag. If there was not an new + * value, because the flag was deleted, then the LDValue will be of a null + * type. Check the deleted parameter to see if a flag was deleted. + * @param old_value The old value of the flag. If there was not an old + * value, for instance a newly created flag, then the Value will be of a + * null type. + * @param deleted True if the flag has been deleted. + */ + FlagChangedCallbackFn FlagChanged; + + /** + * UserData is forwarded into callback functions. + */ + void* UserData; +}; + +/** + * Initializes a flag listener. Must be called before passing the listener + * to LDClientSDK_FlagNotifier_OnFlagChange. + * + * Create the struct, initialize the struct, set the FlagChanged handler + * and optionally UserData, and then pass the struct to + * LDClientSDK_FlagNotifier_OnFlagChange. + * + * @param listener Listener to initialize. + */ +LD_EXPORT(void) LDFlagListener_Init(struct LDFlagListener* listener); + +#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/data/evaluation_detail.hpp b/libs/common/include/launchdarkly/data/evaluation_detail.hpp index e0d2cceec..d32428d05 100644 --- a/libs/common/include/launchdarkly/data/evaluation_detail.hpp +++ b/libs/common/include/launchdarkly/data/evaluation_detail.hpp @@ -60,6 +60,14 @@ class EvaluationDetail { */ [[nodiscard]] std::optional const& Reason() const; + /** + * Check if an evaluation reason exists, and if so, if it is of a particular + * kind. + * @param kind Kind to check. + * @return True if a reason exists and matches the given kind. + */ + [[nodiscard]] bool ReasonKindIs(enum EvaluationReason::Kind kind) const; + /** * @return True if the evaluation resulted in an error. * TODO(sc209960) 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 index f7ddfa4d0..c1d6813c7 100644 --- 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 @@ -2,7 +2,9 @@ #include +#include #include +#include namespace launchdarkly::common::data_sources { @@ -11,7 +13,7 @@ namespace launchdarkly::common::data_sources { */ class DataSourceStatusErrorInfo { public: - using StatusCodeType = uint64_t; + using StatusCodeType = std::uint64_t; using ErrorKind = DataSourceStatusErrorKind; using DateTime = std::chrono::time_point; 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 index f540d3d65..6adc7d87e 100644 --- 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 @@ -1,5 +1,7 @@ #pragma once +#include + namespace launchdarkly::common::data_sources { /** diff --git a/libs/common/include/launchdarkly/persistence/persistence.hpp b/libs/common/include/launchdarkly/persistence/persistence.hpp index 0cb027eff..6cda9802c 100644 --- a/libs/common/include/launchdarkly/persistence/persistence.hpp +++ b/libs/common/include/launchdarkly/persistence/persistence.hpp @@ -1,5 +1,7 @@ #pragma once +#include + /** * Interface for a data store that holds feature flag data and other SDK * properties in a serialized form. diff --git a/libs/common/include/launchdarkly/value.hpp b/libs/common/include/launchdarkly/value.hpp index 1354c427d..92a026d1c 100644 --- a/libs/common/include/launchdarkly/value.hpp +++ b/libs/common/include/launchdarkly/value.hpp @@ -413,9 +413,11 @@ class Value final { enum Type type_; // Empty constants used when accessing the wrong type. - inline static const std::string empty_string_; - inline static const Array empty_vector_; - inline static const Object empty_map_; + // 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_; }; diff --git a/libs/common/package.json b/libs/common/package.json index cc60f7eba..e40b38a50 100644 --- a/libs/common/package.json +++ b/libs/common/package.json @@ -1,7 +1,7 @@ { "name": "launchdarkly-cpp-common", "description": "This package.json exists for modeling dependencies for the release process.", - "version": "0.3.2", + "version": "0.3.4", "private": true, "dependencies": { "launchdarkly-cpp-sse-client": "0.1.1" diff --git a/libs/common/src/CMakeLists.txt b/libs/common/src/CMakeLists.txt index 6b8fb67fc..0130ec962 100644 --- a/libs/common/src/CMakeLists.txt +++ b/libs/common/src/CMakeLists.txt @@ -12,7 +12,7 @@ file(GLOB HEADER_LIST CONFIGURE_DEPENDS "${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. add_library(${LIBNAME} OBJECT @@ -44,16 +44,18 @@ add_library(${LIBNAME} OBJECT bindings/c/context_builder.cpp bindings/c/status.cpp bindings/c/context.cpp - bindings/c/config/builder.cpp - bindings/c/config/config.cpp - log_level.cpp - config/persistence_builder.cpp + bindings/c/config/logging_builder.cpp bindings/c/data/evaluation_detail.cpp + 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 - bindings/c/listener_connection.cpp data_sources/data_source_status_error_kind.cpp - data_sources/data_source_status_error_info.cpp) + data_sources/data_source_status_error_info.cpp +) add_library(launchdarkly::common ALIAS ${LIBNAME}) @@ -66,7 +68,7 @@ install(TARGETS ${LIBNAME}) # This will preserve it, but dependencies must do the same. install(DIRECTORY "${LaunchDarklyCommonSdk_SOURCE_DIR}/include/launchdarkly" DESTINATION "include" - ) +) message(STATUS "LaunchDarklyCommonSdk_SOURCE_DIR=${LaunchDarklyCommonSdk_SOURCE_DIR}") diff --git a/libs/common/src/bindings/c/config/log_backend_wrapper.hpp b/libs/common/src/bindings/c/config/log_backend_wrapper.hpp new file mode 100644 index 000000000..1b835c5d3 --- /dev/null +++ b/libs/common/src/bindings/c/config/log_backend_wrapper.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include +#include +/** + * Utility class to allow user-provided backends to satisfy the ILogBackend + * interface. + */ +class LogBackendWrapper : public launchdarkly::ILogBackend { + public: + explicit LogBackendWrapper(LDLogBackend backend) : backend_(backend) {} + bool Enabled(launchdarkly::LogLevel level) noexcept override { + return backend_.Enabled(static_cast(level), + backend_.UserData); + } + void Write(launchdarkly::LogLevel level, + std::string message) noexcept override { + return backend_.Write(static_cast(level), message.c_str(), + backend_.UserData); + } + + private: + LDLogBackend backend_; +}; diff --git a/libs/common/src/bindings/c/config/logging_builder.cpp b/libs/common/src/bindings/c/config/logging_builder.cpp new file mode 100644 index 000000000..6872fc493 --- /dev/null +++ b/libs/common/src/bindings/c/config/logging_builder.cpp @@ -0,0 +1,66 @@ +#include +#include + +#include + +#include "log_backend_wrapper.hpp" + +using namespace launchdarkly::config::shared::builders; + +#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)) + +LD_EXPORT(void) LDLogBackend_Init(struct LDLogBackend* backend) { + backend->Enabled = [](enum LDLogLevel, void*) { return false; }; + backend->Write = [](enum LDLogLevel, char const*, void*) {}; + backend->UserData = nullptr; +} + +LD_EXPORT(LDLoggingBasicBuilder) LDLoggingBasicBuilder_New() { + return FROM_BASIC_LOGGING_BUILDER(new LoggingBuilder::BasicLogging()); +} + +LD_EXPORT(void) LDLoggingBasicBuilder_Free(LDLoggingBasicBuilder b) { + delete TO_BASIC_LOGGING_BUILDER(b); +} + +LD_EXPORT(void) +LDLoggingBasicBuilder_Level(LDLoggingBasicBuilder b, enum LDLogLevel level) { + using launchdarkly::LogLevel; + LD_ASSERT_NOT_NULL(b); + + LoggingBuilder::BasicLogging* logger = TO_BASIC_LOGGING_BUILDER(b); + logger->Level(static_cast(level)); +} + +void LDLoggingBasicBuilder_Tag(LDLoggingBasicBuilder b, char const* tag) { + LD_ASSERT_NOT_NULL(b); + LD_ASSERT_NOT_NULL(tag); + + TO_BASIC_LOGGING_BUILDER(b)->Tag(tag); +} + +LD_EXPORT(LDLoggingCustomBuilder) LDLoggingCustomBuilder_New() { + return FROM_CUSTOM_LOGGING_BUILDER(new LoggingBuilder::CustomLogging()); +} + +LD_EXPORT(void) LDLoggingCustomBuilder_Free(LDLoggingCustomBuilder b) { + delete TO_CUSTOM_LOGGING_BUILDER(b); +} + +LD_EXPORT(void) +LDLoggingCustomBuilder_Backend(LDLoggingCustomBuilder b, LDLogBackend backend) { + LD_ASSERT_NOT_NULL(b); + + TO_CUSTOM_LOGGING_BUILDER(b)->Backend( + std::make_shared(backend)); +} 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/bindings/c/flag_listener.cpp b/libs/common/src/bindings/c/flag_listener.cpp new file mode 100644 index 000000000..edd10e7ae --- /dev/null +++ b/libs/common/src/bindings/c/flag_listener.cpp @@ -0,0 +1,6 @@ +#include + +LD_EXPORT(void) LDFlagListener_Init(struct LDFlagListener* listener) { + listener->FlagChanged = nullptr; + listener->UserData = nullptr; +} diff --git a/libs/common/src/config/app_info_builder.cpp b/libs/common/src/config/app_info_builder.cpp index 8cd6c737d..69f163a86 100644 --- a/libs/common/src/config/app_info_builder.cpp +++ b/libs/common/src/config/app_info_builder.cpp @@ -21,7 +21,13 @@ tl::expected AppInfoBuilder::Tag::Build() const { } bool ValidChar(char c) { - return std::isalnum(c) != 0 || c == '-' || c == '.' || c == '_'; + if (c > 0) { + // The MSVC implementation of isalnum will assert if the number is + // outside its lookup table (0-0xFF, inclusive.) + // iswalnum would not, but is less restrictive than desired. + return std::isalnum(c) != 0 || c == '-' || c == '.' || c == '_'; + } + return false; } std::optional IsValidTag(std::string const& key, diff --git a/libs/common/src/data/evaluation_detail.cpp b/libs/common/src/data/evaluation_detail.cpp index 81a7aba3b..c6415848d 100644 --- a/libs/common/src/data/evaluation_detail.cpp +++ b/libs/common/src/data/evaluation_detail.cpp @@ -35,6 +35,11 @@ std::optional const& EvaluationDetail::Reason() const { return reason_; } +template +bool EvaluationDetail::ReasonKindIs(enum EvaluationReason::Kind kind) const { + return reason_.has_value() && reason_->Kind() == kind; +} + template std::optional EvaluationDetail::VariationIndex() const { return variation_index_; diff --git a/libs/common/src/value.cpp b/libs/common/src/value.cpp index 70bc20339..46b8b85c0 100644 --- a/libs/common/src/value.cpp +++ b/libs/common/src/value.cpp @@ -7,6 +7,9 @@ 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_; Value::Value() : type_(Value::Type::kNull), storage_{0.0} {} diff --git a/libs/internal/CHANGELOG.md b/libs/internal/CHANGELOG.md index bf568b165..a25507822 100644 --- a/libs/internal/CHANGELOG.md +++ b/libs/internal/CHANGELOG.md @@ -22,6 +22,18 @@ * dependencies * launchdarkly-cpp-common bumped from 0.3.0 to 0.3.1 +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * launchdarkly-cpp-common bumped from 0.3.2 to 0.3.3 + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * launchdarkly-cpp-common bumped from 0.3.3 to 0.3.4 + ## [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/include/launchdarkly/data_model/flag.hpp b/libs/internal/include/launchdarkly/data_model/flag.hpp index fc312d666..ebf9b24a9 100644 --- a/libs/internal/include/launchdarkly/data_model/flag.hpp +++ b/libs/internal/include/launchdarkly/data_model/flag.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -16,8 +17,10 @@ namespace launchdarkly::data_model { struct Flag { - using Variation = std::uint64_t; - using Weight = std::uint64_t; + 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 { @@ -31,7 +34,8 @@ struct Flag { Weight weight; bool untracked; - WeightedVariation() = default; + WeightedVariation(); + WeightedVariation(Variation index, Weight weight); static WeightedVariation Untracked(Variation index, Weight weight); @@ -48,19 +52,19 @@ struct Flag { DEFINE_CONTEXT_KIND_FIELD(contextKind) Rollout() = default; - Rollout(std::vector); + explicit Rollout(std::vector); }; - using VariationOrRollout = std::variant; + using VariationOrRollout = std::variant, Rollout>; struct Prerequisite { std::string key; - std::uint64_t variation; + Variation variation; }; struct Target { std::vector values; - std::uint64_t variation; + Variation variation; ContextKind contextKind; }; @@ -78,7 +82,7 @@ struct Flag { }; std::string key; - std::uint64_t version; + FlagVersion version; bool on; VariationOrRollout fallthrough; std::vector variations; @@ -87,13 +91,13 @@ struct Flag { std::vector targets; std::vector contextTargets; std::vector rules; - std::optional offVariation; + std::optional offVariation; bool clientSide; ClientSideAvailability clientSideAvailability; std::optional salt; bool trackEvents; bool trackEventsFallthrough; - std::optional debugEventsUntilDate; + std::optional debugEventsUntilDate; /** * Returns the flag's version. Satisfies ItemDescriptor template @@ -101,5 +105,8 @@ struct Flag { * @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_sources/data_source_status_manager_base.hpp b/libs/internal/include/launchdarkly/data_sources/data_source_status_manager_base.hpp index fdf0f4a59..b791d4e0c 100644 --- 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 @@ -2,6 +2,7 @@ #include #include +#include #include diff --git a/libs/internal/include/launchdarkly/events/data/common_events.hpp b/libs/internal/include/launchdarkly/events/data/common_events.hpp index 667567c30..b8740319d 100644 --- a/libs/internal/include/launchdarkly/events/data/common_events.hpp +++ b/libs/internal/include/launchdarkly/events/data/common_events.hpp @@ -33,7 +33,12 @@ struct TrackEventParams { std::optional metric_value; }; -// Track (custom) events are directly serialized from their parameters. +struct ServerTrackEventParams : public TrackEventParams { + Context context; +}; + +using ClientTrackEventParams = TrackEventParams; + using TrackEvent = TrackEventParams; struct IdentifyEventParams { @@ -57,6 +62,7 @@ struct FeatureEventParams { std::optional reason; bool require_full_event; std::optional debug_events_until_date; + std::optional prereq_of; }; struct FeatureEventBase { @@ -67,6 +73,7 @@ struct FeatureEventBase { Value value; std::optional reason; Value default_; + std::optional prereq_of; explicit FeatureEventBase(FeatureEventParams const& params); }; diff --git a/libs/internal/include/launchdarkly/events/data/events.hpp b/libs/internal/include/launchdarkly/events/data/events.hpp index 1f474f2de..a0e00eae6 100644 --- a/libs/internal/include/launchdarkly/events/data/events.hpp +++ b/libs/internal/include/launchdarkly/events/data/events.hpp @@ -5,8 +5,10 @@ namespace launchdarkly::events { -using InputEvent = - std::variant; +using InputEvent = std::variant; using OutputEvent = std::variant= 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_; diff --git a/libs/internal/include/launchdarkly/events/detail/summarizer.hpp b/libs/internal/include/launchdarkly/events/detail/summarizer.hpp index 37a23ef72..e87a6055c 100644 --- a/libs/internal/include/launchdarkly/events/detail/summarizer.hpp +++ b/libs/internal/include/launchdarkly/events/detail/summarizer.hpp @@ -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: diff --git a/libs/internal/include/launchdarkly/serialization/json_flag.hpp b/libs/internal/include/launchdarkly/serialization/json_flag.hpp index 88c1edd57..775e2cb15 100644 --- a/libs/internal/include/launchdarkly/serialization/json_flag.hpp +++ b/libs/internal/include/launchdarkly/serialization/json_flag.hpp @@ -62,4 +62,46 @@ tl::expected, JsonError> tag_invoke( 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_rule_clause.hpp b/libs/internal/include/launchdarkly/serialization/json_rule_clause.hpp index f0eab96a4..a66d85606 100644 --- a/libs/internal/include/launchdarkly/serialization/json_rule_clause.hpp +++ b/libs/internal/include/launchdarkly/serialization/json_rule_clause.hpp @@ -23,4 +23,17 @@ tl::expected tag_invoke( 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_segment.hpp b/libs/internal/include/launchdarkly/serialization/json_segment.hpp index ce73c719b..83987903e 100644 --- a/libs/internal/include/launchdarkly/serialization/json_segment.hpp +++ b/libs/internal/include/launchdarkly/serialization/json_segment.hpp @@ -23,4 +23,21 @@ tl::expected, JsonError> tag_invoke( 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/value_mapping.hpp b/libs/internal/include/launchdarkly/serialization/value_mapping.hpp index 5d7d7484a..753146d1a 100644 --- a/libs/internal/include/launchdarkly/serialization/value_mapping.hpp +++ b/libs/internal/include/launchdarkly/serialization/value_mapping.hpp @@ -156,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, 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/internal/package.json b/libs/internal/package.json index fabf596f3..44bbe8768 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.5", + "version": "0.1.7", "private": true, "dependencies": { - "launchdarkly-cpp-common": "0.3.2" + "launchdarkly-cpp-common": "0.3.4" } } diff --git a/libs/internal/src/data_model/flag.cpp b/libs/internal/src/data_model/flag.cpp index 78d4f4651..2a03074a7 100644 --- a/libs/internal/src/data_model/flag.cpp +++ b/libs/internal/src/data_model/flag.cpp @@ -16,6 +16,8 @@ Flag::Rollout::WeightedVariation Flag::Rollout::WeightedVariation::Untracked( 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_)), @@ -24,4 +26,26 @@ Flag::Rollout::Rollout(std::vector variations_) 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/events/asio_event_processor.cpp b/libs/internal/src/events/asio_event_processor.cpp index 734068735..c9bfd8cc4 100644 --- a/libs/internal/src/events/asio_event_processor.cpp +++ b/libs/internal/src/events/asio_event_processor.cpp @@ -271,7 +271,23 @@ std::vector AsioEventProcessor::Process( out.emplace_back(IdentifyEvent{event.creation_date, filter_.filter(event.context)}); }, - [&](TrackEventParams&& event) { + [&](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.creation_date, + filter_.filter(event.context)}); + } + } + + // Object slicing on purpose; the context will be stripped out + // of the ServerTrackEventParams when converted to a + // TrackEventParams. out.emplace_back(std::move(event)); }}, std::move(input_event)); diff --git a/libs/internal/src/events/common_events.cpp b/libs/internal/src/events/common_events.cpp index d3d324ec8..e6fdbc096 100644 --- a/libs/internal/src/events/common_events.cpp +++ b/libs/internal/src/events/common_events.cpp @@ -8,6 +8,6 @@ FeatureEventBase::FeatureEventBase(FeatureEventParams const& params) variation(params.variation), value(params.value), reason(params.reason), - default_(params.default_) {} - + default_(params.default_), + prereq_of(params.prereq_of) {} } // namespace launchdarkly::events diff --git a/libs/internal/src/events/outbox.cpp b/libs/internal/src/events/outbox.cpp index 7b131670a..33e95d008 100644 --- a/libs/internal/src/events/outbox.cpp +++ b/libs/internal/src/events/outbox.cpp @@ -34,7 +34,7 @@ std::vector Outbox::Consume() { return out; } -bool Outbox::Empty() { +bool Outbox::Empty() const { return items_.empty(); } diff --git a/libs/internal/src/events/summarizer.cpp b/libs/internal/src/events/summarizer.cpp index 1c3ad7202..3ca0ff236 100644 --- a/libs/internal/src/events/summarizer.cpp +++ b/libs/internal/src/events/summarizer.cpp @@ -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_; } diff --git a/libs/internal/src/serialization/events/json_events.cpp b/libs/internal/src/serialization/events/json_events.cpp index ad5f3b8ee..36a9b4553 100644 --- a/libs/internal/src/serialization/events/json_events.cpp +++ b/libs/internal/src/serialization/events/json_events.cpp @@ -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, @@ -131,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::detail diff --git a/libs/internal/src/serialization/json_flag.cpp b/libs/internal/src/serialization/json_flag.cpp index f25d48a8d..47429e2bd 100644 --- a/libs/internal/src/serialization/json_flag.cpp +++ b/libs/internal/src/serialization/json_flag.cpp @@ -19,7 +19,7 @@ tl::expected, JsonError> tag_invoke( REQUIRE_OBJECT(json_value); auto const& obj = json_value.as_object(); - data_model::Flag::Rollout rollout; + data_model::Flag::Rollout rollout{}; PARSE_FIELD(rollout.variations, obj, "variations"); PARSE_FIELD_DEFAULT(rollout.kind, obj, "kind", @@ -49,7 +49,7 @@ tag_invoke(boost::json::value_to_tag, JsonError> tag_invoke( REQUIRE_OBJECT(json_value); auto const& obj = json_value.as_object(); - data_model::Flag::Target target; + data_model::Flag::Target target{}; PARSE_FIELD(target.values, obj, "values"); PARSE_FIELD(target.variation, obj, "variation"); PARSE_FIELD_DEFAULT(target.contextKind, obj, "contextKind", @@ -115,7 +115,7 @@ tl::expected, JsonError> tag_invoke( REQUIRE_OBJECT(json_value); auto const& obj = json_value.as_object(); - data_model::Flag::Rule rule; + data_model::Flag::Rule rule{}; PARSE_FIELD(rule.trackEvents, obj, "trackEvents"); PARSE_FIELD(rule.clauses, obj, "clauses"); @@ -144,7 +144,7 @@ tag_invoke( REQUIRE_OBJECT(json_value); auto const& obj = json_value.as_object(); - data_model::Flag::ClientSideAvailability client_side_availability; + data_model::Flag::ClientSideAvailability client_side_availability{}; PARSE_FIELD(client_side_availability.usingEnvironmentId, obj, "usingEnvironmentId"); PARSE_FIELD(client_side_availability.usingMobileKey, obj, "usingMobileKey"); @@ -161,7 +161,7 @@ tl::expected, JsonError> tag_invoke( auto const& obj = json_value.as_object(); - data_model::Flag flag; + data_model::Flag flag{}; PARSE_REQUIRED_FIELD(flag.key, obj, "key"); @@ -195,17 +195,177 @@ tag_invoke(boost::json::value_to_tag< REQUIRE_OBJECT(json_value); auto const& obj = json_value.as_object(); - std::optional rollout; + std::optional rollout{}; PARSE_CONDITIONAL_FIELD(rollout, obj, "rollout"); if (rollout) { return std::make_optional(*rollout); } - data_model::Flag::Variation variation; - PARSE_REQUIRED_FIELD(variation, obj, "variation"); + std::optional variation; - return std::make_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: Should we be preserving the original string. + 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: Should we be preserving the original string and putting it in. + 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_rule_clause.cpp b/libs/internal/src/serialization/json_rule_clause.cpp index df6530d23..febc96f28 100644 --- a/libs/internal/src/serialization/json_rule_clause.cpp +++ b/libs/internal/src/serialization/json_rule_clause.cpp @@ -16,7 +16,7 @@ tl::expected, JsonError> tag_invoke( REQUIRE_OBJECT(json_value); auto const& obj = json_value.as_object(); - data_model::Clause clause; + data_model::Clause clause{}; PARSE_REQUIRED_FIELD(clause.op, obj, "op"); PARSE_FIELD(clause.values, obj, "values"); @@ -98,4 +98,81 @@ tl::expected tag_invoke( 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 index 84043c377..e7b894a0a 100644 --- a/libs/internal/src/serialization/json_sdk_data_set.cpp +++ b/libs/internal/src/serialization/json_sdk_data_set.cpp @@ -18,7 +18,7 @@ tl::expected, JsonError> tag_invoke( auto const& obj = json_value.as_object(); - data_model::SDKDataSet data_set; + data_model::SDKDataSet data_set{}; PARSE_FIELD(data_set.flags, obj, "flags"); PARSE_FIELD(data_set.segments, obj, "segments"); diff --git a/libs/internal/src/serialization/json_segment.cpp b/libs/internal/src/serialization/json_segment.cpp index 3ef22453b..157d26521 100644 --- a/libs/internal/src/serialization/json_segment.cpp +++ b/libs/internal/src/serialization/json_segment.cpp @@ -18,7 +18,7 @@ tl::expected, JsonError> tag_invoke( REQUIRE_OBJECT(json_value); auto const& obj = json_value.as_object(); - data_model::Segment::Target target; + data_model::Segment::Target target{}; PARSE_FIELD_DEFAULT(target.contextKind, obj, "contextKind", "user"); @@ -37,7 +37,7 @@ tl::expected, JsonError> tag_invoke( REQUIRE_OBJECT(json_value); auto const& obj = json_value.as_object(); - data_model::Segment::Rule rule; + data_model::Segment::Rule rule{}; PARSE_FIELD(rule.clauses, obj, "clauses"); @@ -77,7 +77,7 @@ tl::expected, JsonError> tag_invoke( auto const& obj = json_value.as_object(); - data_model::Segment segment; + data_model::Segment segment{}; PARSE_REQUIRED_FIELD(segment.key, obj, "key"); PARSE_REQUIRED_FIELD(segment.version, obj, "version"); @@ -97,4 +97,48 @@ tl::expected, JsonError> tag_invoke( 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", 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 704c71f70..c04fae3d2 100644 --- a/libs/internal/src/serialization/json_value.cpp +++ b/libs/internal/src/serialization/json_value.cpp @@ -104,11 +104,5 @@ void tag_invoke(boost::json::value_from_tag const&, } } -tl::expected tag_invoke( - boost::json::value_to_tag> const& tag, - boost::json::value const& json_value) { - return boost::json::value_to(json_value); -} - // NOLINTEND modernize-return-braced-init-list } // namespace launchdarkly diff --git a/libs/internal/src/serialization/value_mapping.cpp b/libs/internal/src/serialization/value_mapping.cpp index b83183725..7142d8bf7 100644 --- a/libs/internal/src/serialization/value_mapping.cpp +++ b/libs/internal/src/serialization/value_mapping.cpp @@ -70,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/internal/tests/data_model_serialization_test.cpp b/libs/internal/tests/data_model_serialization_test.cpp index 8e1feb5ee..02cf3c90e 100644 --- a/libs/internal/tests/data_model_serialization_test.cpp +++ b/libs/internal/tests/data_model_serialization_test.cpp @@ -1,5 +1,7 @@ #include +#include + #include #include #include @@ -281,11 +283,11 @@ TEST(PrerequisiteTests, DeserializesAllFields) { ASSERT_EQ(result->variation, 123); } -TEST(PrerequisiteTests, DeserializeFailsWithNegativeVariation) { +TEST(PrerequisiteTests, DeserializeSucceedsWithNegativeVariation) { auto result = boost::json::value_to< tl::expected>( boost::json::parse(R"({"key" : "foo", "variation" : -123})")); - ASSERT_FALSE(result); + ASSERT_TRUE(result); } TEST(TargetTests, DeserializesMinimumValid) { @@ -298,11 +300,11 @@ TEST(TargetTests, DeserializesMinimumValid) { ASSERT_TRUE(result->values.empty()); } -TEST(TargetTests, DeserializesFailsWithNegativeVariation) { +TEST(TargetTests, DeserializesSucceedsWithNegativeVariation) { auto result = boost::json::value_to< tl::expected>( boost::json::parse(R"({"variation" : -123})")); - ASSERT_FALSE(result); + ASSERT_TRUE(result); } TEST(TargetTests, DeserializesAllFields) { @@ -324,7 +326,8 @@ TEST(FlagRuleTests, DeserializesMinimumValid) { ASSERT_FALSE(result->trackEvents); ASSERT_TRUE(result->clauses.empty()); ASSERT_FALSE(result->id); - ASSERT_EQ(std::get(result->variationOrRollout), + ASSERT_EQ(std::get>( + result->variationOrRollout), data_model::Flag::Variation(123)); } @@ -346,7 +349,8 @@ TEST(FlagRuleTests, DeserializesAllFields) { ASSERT_TRUE(result->trackEvents); ASSERT_TRUE(result->clauses.empty()); ASSERT_EQ(result->id, "foo"); - ASSERT_EQ(std::get(result->variationOrRollout), + ASSERT_EQ(std::get>( + result->variationOrRollout), data_model::Flag::Variation(123)); } @@ -368,3 +372,381 @@ TEST(ClientSideAvailabilityTests, DeserializesAllFields) { 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{"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"}, + {{"vegetable", {"potato", "yam"}}}, + {{"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, "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/event_summarizer_test.cpp b/libs/internal/tests/event_summarizer_test.cpp index ad3e59e9e..9e657ef8c 100644 --- a/libs/internal/tests/event_summarizer_test.cpp +++ b/libs/internal/tests/event_summarizer_test.cpp @@ -26,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 { 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..25ad3bf40 --- /dev/null +++ b/libs/server-sdk/README.md @@ -0,0 +1,121 @@ +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). + +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/client.hpp b/libs/server-sdk/include/launchdarkly/server_side/client.hpp index 71614f4cd..e7f1b8056 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/client.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/client.hpp @@ -5,6 +5,9 @@ #include #include +#include +#include + #include #include #include @@ -12,7 +15,6 @@ #include namespace launchdarkly::server_side { - /** * Interface for the standard SDK client methods and properties. */ @@ -51,15 +53,26 @@ class IClient { [[nodiscard]] virtual bool Initialized() const = 0; /** - * Returns a map from feature flag keys to feature - * flag values for the current context. + * 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. * - * @return A map from feature flag keys to values for the current context. + * @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 std::unordered_map AllFlagsState() - const = 0; + [[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 @@ -234,6 +247,13 @@ class IClient { 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; @@ -258,8 +278,10 @@ class Client : public IClient { [[nodiscard]] bool Initialized() const override; using FlagKey = std::string; - [[nodiscard]] std::unordered_map AllFlagsState() - const override; + [[nodiscard]] class AllFlagsState AllFlagsState( + Context const& context, + enum AllFlagsState::Options options = + AllFlagsState::Options::Default) override; void Track(Context const& ctx, std::string event_name, @@ -316,6 +338,8 @@ class Client : public IClient { 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. 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 index b0b540d55..a8336bf07 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/data_source_status.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/data_source_status.hpp @@ -31,7 +31,8 @@ enum class ServerDataSourceState { /** * Indicates that the data source is currently operational and has not - * had any problems since the last time it received data. + * 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 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 index 0ff18837b..5734c9818 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -2,7 +2,7 @@ file(GLOB HEADER_LIST CONFIGURE_DEPENDS "${LaunchDarklyCPPServer_SOURCE_DIR}/include/launchdarkly/server_side/*.hpp" "${LaunchDarklyCPPServer_SOURCE_DIR}/include/launchdarkly/server_side/integrations/*.hpp" - ) +) # Automatic library: static or dynamic based on user config. @@ -11,6 +11,9 @@ add_library(${LIBNAME} 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 @@ -40,7 +43,12 @@ add_library(${LIBNAME} 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 BUILD_SHARED_LIBS)) target_link_libraries(${LIBNAME} @@ -73,7 +81,7 @@ endif () install(DIRECTORY "${LaunchDarklyCPPServer_SOURCE_DIR}/include/launchdarkly" DESTINATION "include" - ) +) # Need the public headers to build. target_include_directories(${LIBNAME} PUBLIC ../include) 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..222ac27f7 --- /dev/null +++ b/libs/server-sdk/src/all_flags_state/json_all_flags_state.cpp @@ -0,0 +1,56 @@ +#include +#include +#include + +#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) { + boost::ignore_unused(unused); + auto& obj = json_value.emplace_object(); + + if (!state.OmitDetails()) { + obj.emplace("version", state.Version()); + + if (auto const& reason = state.Reason()) { + obj.emplace("reason", boost::json::value_from(*reason)); + } + } + + if (auto const& variation = state.Variation()) { + obj.emplace("variation", *variation); + } + + if (state.TrackEvents()) { + obj.emplace("trackEvents", true); + } + + if (state.TrackReason()) { + obj.emplace("trackReason", true); + } + + 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..e6a7c2f75 --- /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::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/client.cpp b/libs/server-sdk/src/client.cpp index c9b84acde..ce992de85 100644 --- a/libs/server-sdk/src/client.cpp +++ b/libs/server-sdk/src/client.cpp @@ -4,6 +4,24 @@ 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)) {} @@ -16,8 +34,10 @@ std::future Client::StartAsync() { } using FlagKey = std::string; -[[nodiscard]] std::unordered_map Client::AllFlagsState() const { - return client->AllFlagsState(); +[[nodiscard]] AllFlagsState Client::AllFlagsState( + Context const& context, + enum AllFlagsState::Options options) { + return client->AllFlagsState(context, options); } void Client::Track(Context const& ctx, @@ -104,6 +124,10 @@ EvaluationDetail Client::JsonVariationDetail(Context const& ctx, return client->JsonVariationDetail(ctx, key, std::move(default_value)); } +data_sources::IDataSourceStatusProvider& Client::DataSourceStatus() { + return client->DataSourceStatus(); +} + char const* Client::Version() { return kVersion; } diff --git a/libs/server-sdk/src/client_impl.cpp b/libs/server-sdk/src/client_impl.cpp index 9cd59eb22..5d3f9c149 100644 --- a/libs/server-sdk/src/client_impl.cpp +++ b/libs/server-sdk/src/client_impl.cpp @@ -6,6 +6,7 @@ #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" @@ -14,7 +15,6 @@ #include #include #include -#include #include #include @@ -75,6 +75,29 @@ static Logger MakeLogger(config::shared::built::Logging const& config) { 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_( @@ -87,23 +110,22 @@ ClientImpl::ClientImpl(Config config, std::string const& version) 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(), - memory_store_, + data_store_updater_, status_manager_, logger_)), - event_processor_(nullptr), - evaluator_(logger_, memory_store_) { - if (config.Events().Enabled() && !config.Offline()) { - event_processor_ = - std::make_unique>( - ioc_.get_executor(), config.ServiceEndpoints(), config.Events(), - http_properties_, logger_); - } else { - event_processor_ = std::make_unique(); - } - + 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(); })); } @@ -124,8 +146,9 @@ static bool IsInitialized(DataSourceStatus::DataSourceState state) { } void ClientImpl::Identify(Context context) { - event_processor_->SendAsync(events::IdentifyEventParams{ - std::chrono::system_clock::now(), std::move(context)}); + events_default_.Send([&](EventFactory const& factory) { + return factory.Identify(std::move(context)); + }); } std::future ClientImpl::StartAsyncInternal( @@ -144,6 +167,8 @@ std::future ClientImpl::StartAsyncInternal( return false; /* keep the change listener */ }); + data_source_->Start(); + return fut; } @@ -155,24 +180,62 @@ bool ClientImpl::Initialized() const { return IsInitializedSuccessfully(status_manager_.Status().State()); } -std::unordered_map ClientImpl::AllFlagsState() const { +AllFlagsState ClientImpl::AllFlagsState(Context const& context, + AllFlagsState::Options options) { std::unordered_map result; - // TODO: implement all flags state (and update signature). - // for (auto& [key, descriptor] : memory_store_.AllFlags()) { - // if (descriptor->item) { - // result.try_emplace(key, descriptor->item->Value()); - // } - // } - return 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) { - event_processor_->SendAsync(events::TrackEventParams{ - std::chrono::system_clock::now(), std::move(event_name), - ctx.KindsToKeys(), std::move(data), 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, @@ -193,132 +256,197 @@ void ClientImpl::Track(Context const& ctx, std::string event_name) { } void ClientImpl::FlushAsync() { - event_processor_->FlushAsync(); + if (event_processor_) { + event_processor_->FlushAsync(); + } } -template -EvaluationDetail ClientImpl::VariationInternal(Context const& ctx, - FlagKey const& key, - Value default_value, - bool check_type) { - auto desc = memory_store_.GetFlag(key); - - if (!desc || !desc->item) { - if (!Initialized()) { - LD_LOG(logger_, LogLevel::kWarn) - << "LaunchDarkly client has not yet been initialized. " - "Returning default value"; - - auto error_reason = - EvaluationReason(EvaluationReason::ErrorKind::kClientNotReady); - return EvaluationDetail(std::move(default_value), std::nullopt, - std::move(error_reason)); +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"; + } + } +} - LD_LOG(logger_, LogLevel::kInfo) - << "Unknown feature flag " << key << "; returning default value"; - - auto error_reason = - EvaluationReason(EvaluationReason::ErrorKind::kFlagNotFound); - return EvaluationDetail(std::move(default_value), std::nullopt, - std::move(error_reason)); +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; +} - } else if (!Initialized()) { - LD_LOG(logger_, LogLevel::kInfo) - << "LaunchDarkly client has not yet been initialized. " - "Returning cached value"; +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); } - assert(desc->item); + auto flag_rule = memory_store_.GetFlag(key); - auto const& flag = *(desc->item); + bool flag_present = IsFlagPresent(flag_rule); - EvaluationDetail const detail = evaluator_.Evaluate(flag, ctx); + LogVariationCall(key, flag_present); - if (check_type && default_value.Type() != Value::Type::kNull && - detail.Value().Type() != default_value.Type()) { - auto error_reason = - EvaluationReason(EvaluationReason::ErrorKind::kWrongType); + 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); +} - return EvaluationDetail(std::move(default_value), std::nullopt, - error_reason); +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; +} - return EvaluationDetail(detail.Value(), detail.VariationIndex(), - detail.Reason()); +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 VariationInternal(ctx, key, default_value, true); + return VariationDetail(ctx, Value::Type::kBool, key, default_value); } bool ClientImpl::BoolVariation(Context const& ctx, IClient::FlagKey const& key, bool default_value) { - return *VariationInternal(ctx, key, default_value, true); + return Variation(ctx, Value::Type::kBool, key, default_value); } EvaluationDetail ClientImpl::StringVariationDetail( Context const& ctx, ClientImpl::FlagKey const& key, std::string default_value) { - return VariationInternal(ctx, key, std::move(default_value), - true); + 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 *VariationInternal(ctx, key, std::move(default_value), - true); + return Variation(ctx, Value::Type::kString, key, default_value); } EvaluationDetail ClientImpl::DoubleVariationDetail( Context const& ctx, ClientImpl::FlagKey const& key, double default_value) { - return VariationInternal(ctx, key, default_value, true); + return VariationDetail(ctx, Value::Type::kNumber, key, + default_value); } double ClientImpl::DoubleVariation(Context const& ctx, IClient::FlagKey const& key, double default_value) { - return *VariationInternal(ctx, key, default_value, true); + return Variation(ctx, Value::Type::kNumber, key, default_value); } EvaluationDetail ClientImpl::IntVariationDetail( Context const& ctx, IClient::FlagKey const& key, int default_value) { - return VariationInternal(ctx, key, default_value, true); + return VariationDetail(ctx, Value::Type::kNumber, key, default_value); } int ClientImpl::IntVariation(Context const& ctx, IClient::FlagKey const& key, int default_value) { - return *VariationInternal(ctx, key, default_value, true); + 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, std::move(default_value), false); + 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, std::move(default_value), false); + return *VariationInternal(ctx, key, default_value, events_default_); +} + +data_sources::IDataSourceStatusProvider& ClientImpl::DataSourceStatus() { + return status_manager_; } -// data_sources::IDataSourceStatusProvider& ClientImpl::DataSourceStatus() { -// return status_manager_; -// } -// // flag_manager::IFlagNotifier& ClientImpl::FlagNotifier() { // return flag_manager_.Notifier(); // } @@ -328,5 +456,4 @@ ClientImpl::~ClientImpl() { // TODO: Probably not the best. 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 index f5491527d..d6c77ecee 100644 --- a/libs/server-sdk/src/client_impl.hpp +++ b/libs/server-sdk/src/client_impl.hpp @@ -13,10 +13,13 @@ #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 @@ -44,8 +47,10 @@ class ClientImpl : public IClient { bool Initialized() const override; using FlagKey = std::string; - [[nodiscard]] std::unordered_map AllFlagsState() - const override; + [[nodiscard]] class AllFlagsState AllFlagsState( + Context const& context, + AllFlagsState::Options options = + AllFlagsState::Options::Default) override; void Track(Context const& ctx, std::string event_name, @@ -102,16 +107,52 @@ class ClientImpl : public IClient { 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 VariationInternal(Context const& ctx, - FlagKey const& key, - Value default_value, - bool check_type); + [[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, @@ -121,6 +162,8 @@ class ClientImpl : public IClient { std::function predicate); + void LogVariationCall(std::string const& key, bool flag_present) const; + Config config_; Logger logger_; @@ -132,6 +175,9 @@ class ClientImpl : public IClient { 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_; @@ -139,10 +185,11 @@ class ClientImpl : public IClient { mutable std::mutex init_mutex_; std::condition_variable init_waiter_; - data_sources::DataSourceStatusManager status_manager_; - 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 index c3a3d7970..2d8c436b3 100644 --- a/libs/server-sdk/src/data_sources/data_source_event_handler.cpp +++ b/libs/server-sdk/src/data_sources/data_source_event_handler.cpp @@ -62,7 +62,7 @@ tl::expected, JsonError> tag_invoke( 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 != "/") { + if (!(path == "/" || path.empty())) { return std::nullopt; } PARSE_FIELD(put.data, obj, "data"); @@ -148,7 +148,7 @@ DataSourceEventHandler::MessageStatus DataSourceEventHandler::HandleMessage( boost::json::value_to, JsonError>>( parsed); - if (!res.has_value()) { + if (!res) { LD_LOG(logger_, LogLevel::kError) << kErrorPutInvalid; status_manager_.SetError( DataSourceStatus::ErrorInfo::ErrorKind::kInvalidData, diff --git a/libs/server-sdk/src/data_sources/streaming_data_source.cpp b/libs/server-sdk/src/data_sources/streaming_data_source.cpp index b2f9e8a7d..2b76e05ac 100644 --- a/libs/server-sdk/src/data_sources/streaming_data_source.cpp +++ b/libs/server-sdk/src/data_sources/streaming_data_source.cpp @@ -24,6 +24,8 @@ static char const* DataSourceErrorToString(launchdarkly::sse::Error error) { return "server responded with an invalid redirection"; case sse::Error::UnrecoverableClientError: return "unrecoverable client-side error"; + default: + return "unrecognized error"; } } @@ -75,7 +77,6 @@ void StreamingDataSource::Start() { return; } - // TODO: Initial reconnect delay. sc-204393 boost::urls::url url = uri_components.value(); auto client_builder = launchdarkly::sse::Builder(exec_, url.buffer()); @@ -91,6 +92,9 @@ void StreamingDataSource::Start() { 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); } diff --git a/libs/server-sdk/src/data_store/dependency_tracker.hpp b/libs/server-sdk/src/data_store/dependency_tracker.hpp index 1dddb78b8..d62ba2c7c 100644 --- a/libs/server-sdk/src/data_store/dependency_tracker.hpp +++ b/libs/server-sdk/src/data_store/dependency_tracker.hpp @@ -1,7 +1,9 @@ #pragma once +#include #include #include +#include #include #include diff --git a/libs/server-sdk/src/data_store/persistent/expiration_tracker.hpp b/libs/server-sdk/src/data_store/persistent/expiration_tracker.hpp index 60bb4fc70..4086bb93d 100644 --- a/libs/server-sdk/src/data_store/persistent/expiration_tracker.hpp +++ b/libs/server-sdk/src/data_store/persistent/expiration_tracker.hpp @@ -1,9 +1,11 @@ #pragma once +#include #include #include #include #include +#include #include #include diff --git a/libs/server-sdk/src/evaluation/bucketing.cpp b/libs/server-sdk/src/evaluation/bucketing.cpp index 7ebd7fb8f..1333ee1c0 100644 --- a/libs/server-sdk/src/evaluation/bucketing.cpp +++ b/libs/server-sdk/src/evaluation/bucketing.cpp @@ -98,7 +98,7 @@ AttributeReference const& Key() { return key; } -std::optional ContextHash(Value const& value, BucketPrefix prefix) { +std::optional ContextHash(Value const& value, BucketPrefix prefix) { using namespace launchdarkly::encoding; std::optional id = BucketValue(value); @@ -160,8 +160,12 @@ tl::expected Variation( return std::visit( [&](auto&& arg) -> tl::expected { using T = std::decay_t; - if constexpr (std::is_same_v) { - return BucketResult(arg); + 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( diff --git a/libs/server-sdk/src/evaluation/bucketing.hpp b/libs/server-sdk/src/evaluation/bucketing.hpp index b536f1921..346374590 100644 --- a/libs/server-sdk/src/evaluation/bucketing.hpp +++ b/libs/server-sdk/src/evaluation/bucketing.hpp @@ -61,7 +61,7 @@ class BucketPrefix { std::variant prefix_; }; -using ContextHashValue = float; +using ContextHashValue = double; /** * Computes the context hash value for an attribute in the given context diff --git a/libs/server-sdk/src/evaluation/evaluator.cpp b/libs/server-sdk/src/evaluation/evaluator.cpp index 9241832a6..97057fbd2 100644 --- a/libs/server-sdk/src/evaluation/evaluator.cpp +++ b/libs/server-sdk/src/evaluation/evaluator.cpp @@ -23,15 +23,23 @@ Evaluator::Evaluator(Logger& logger, data_store::IDataStore const& store) : logger_(logger), store_(store), stack_() {} EvaluationDetail Evaluator::Evaluate( - Flag const& flag, + data_model::Flag const& flag, launchdarkly::Context const& context) { - return Evaluate("", flag, context); + return Evaluate(flag, context, EventScope{}); } EvaluationDetail Evaluator::Evaluate( - std::string const& parent_key, Flag const& flag, - launchdarkly::Context const& context) { + 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()); @@ -56,7 +64,7 @@ EvaluationDetail Evaluator::Evaluate( // Recursive call; cycles are detected by the guard. EvaluationDetail detailed_evaluation = - Evaluate(flag.key, *descriptor.item, context); + Evaluate(flag.key, *descriptor.item, context, event_scope); if (detailed_evaluation.IsError()) { return detailed_evaluation; @@ -65,7 +73,11 @@ EvaluationDetail Evaluator::Evaluate( std::optional variation_index = detailed_evaluation.VariationIndex(); - // TODO(209589) prerequisite events. + 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, @@ -73,8 +85,9 @@ EvaluationDetail Evaluator::Evaluate( } } } else { - LogError(parent_key, Error::CyclicPrerequisiteReference(flag.key)); - return OffValue(flag, EvaluationReason::MalformedFlag()); + 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 @@ -138,7 +151,7 @@ EvaluationDetail Evaluator::FlagVariation( Flag const& flag, Flag::Variation variation_index, EvaluationReason reason) const { - if (variation_index >= flag.variations.size()) { + if (variation_index < 0 || variation_index >= flag.variations.size()) { LogError(flag.key, Error::NonexistentVariationIndex(variation_index)); return EvaluationReason::MalformedFlag(); } diff --git a/libs/server-sdk/src/evaluation/evaluator.hpp b/libs/server-sdk/src/evaluation/evaluator.hpp index 6e22405b9..cf7a15468 100644 --- a/libs/server-sdk/src/evaluation/evaluator.hpp +++ b/libs/server-sdk/src/evaluation/evaluator.hpp @@ -7,6 +7,7 @@ #include #include "../data_store/data_store.hpp" +#include "../events/event_scope.hpp" #include "bucketing.hpp" #include "detail/evaluation_stack.hpp" #include "evaluation_error.hpp" @@ -22,6 +23,23 @@ class Evaluator { /** * 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, @@ -29,9 +47,10 @@ class Evaluator { private: [[nodiscard]] EvaluationDetail Evaluate( - std::string const& parent_key, + std::optional parent_key, data_model::Flag const& flag, - launchdarkly::Context const& context); + launchdarkly::Context const& context, + EventScope const& event_scope); [[nodiscard]] EvaluationDetail FlagVariation( data_model::Flag const& flag, diff --git a/libs/server-sdk/src/evaluation/operators.cpp b/libs/server-sdk/src/evaluation/operators.cpp index d5c98edb4..31ceb960f 100644 --- a/libs/server-sdk/src/evaluation/operators.cpp +++ b/libs/server-sdk/src/evaluation/operators.cpp @@ -89,9 +89,6 @@ bool RegexMatch(std::string const& context_value, // boost::bad_expression can be thrown by basic_regex when compiling a // regular expression. return false; - } catch (boost::regex_error) { - // boost::regex_error thrown on stack exhaustion - return false; } catch (std::runtime_error) { // std::runtime_error can be thrown when a call // to regex_search results in an "everlasting" search diff --git a/libs/server-sdk/src/evaluation/rules.cpp b/libs/server-sdk/src/evaluation/rules.cpp index b43a0ef89..313cc44b1 100644 --- a/libs/server-sdk/src/evaluation/rules.cpp +++ b/libs/server-sdk/src/evaluation/rules.cpp @@ -191,14 +191,9 @@ tl::expected Contains(Segment const& segment, } bool IsTargeted(Context const& context, - std::vector const& keys, - std::vector const& targets) { - if (IsUser(context) && targets.empty()) { - return std::find(keys.begin(), keys.end(), context.CanonicalKey()) != - keys.end(); - } - - for (auto const& target : targets) { + 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; @@ -209,12 +204,11 @@ bool IsTargeted(Context const& context, } } - return false; -} + if (auto key = context.Get("user", "key"); !key.IsNull()) { + return std::find(user_keys.begin(), user_keys.end(), key.AsString()) != + user_keys.end(); + } -bool IsUser(Context const& context) { - auto const& kinds = context.Kinds(); - return kinds.size() == 1 && kinds[0] == "user"; + return false; } - } // 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/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/client_test.cpp b/libs/server-sdk/tests/client_test.cpp index 315c0a40d..73d0c0dd7 100644 --- a/libs/server-sdk/tests/client_test.cpp +++ b/libs/server-sdk/tests/client_test.cpp @@ -68,3 +68,12 @@ TEST_F(ClientTest, JsonVariationDefaultPassesThrough) { 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/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/operator_tests.cpp b/libs/server-sdk/tests/operator_tests.cpp index 727fa66ce..e804c22e3 100644 --- a/libs/server-sdk/tests/operator_tests.cpp +++ b/libs/server-sdk/tests/operator_tests.cpp @@ -79,7 +79,11 @@ TEST(OpTests, DateComparisonMicrosecondPrecision) { } } -TEST(OpTests, DateComparisonFailsWithMoreThanMicrosecondPrecision) { +// 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"}, @@ -88,17 +92,15 @@ TEST(OpTests, DateComparisonFailsWithMoreThanMicrosecondPrecision) { "2023-10-08T02:00:00.00000000011+00:00"}}; for (auto const& [date1, date2] : dates) { - EXPECT_FALSE(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_FALSE(Match(Clause::Op::kAfter, date2, date1)) - << date2 << " > " << date1; + 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; + } } } diff --git a/libs/server-sdk/tests/rule_tests.cpp b/libs/server-sdk/tests/rule_tests.cpp index ca4ce7ae1..56d9d0f1b 100644 --- a/libs/server-sdk/tests/rule_tests.cpp +++ b/libs/server-sdk/tests/rule_tests.cpp @@ -67,7 +67,7 @@ TEST_P(AllOperatorsTest, Matches) { *store, stack); ASSERT_EQ(result, param.expected) << context.Get("user", "attr") << " " << clause.op << " " - << clause.values << " should be " << param.expected; + << Value(clause.values) << " should be " << param.expected; } #define MATCH true 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/test_store.hpp b/libs/server-sdk/tests/test_store.hpp index 4b0a37c44..bfccc008e 100644 --- a/libs/server-sdk/tests/test_store.hpp +++ b/libs/server-sdk/tests/test_store.hpp @@ -16,4 +16,16 @@ std::unique_ptr TestData(); */ 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 index 78db3107d..f448d1c94 100644 --- a/libs/server-sdk/tests/timestamp_tests.cpp +++ b/libs/server-sdk/tests/timestamp_tests.cpp @@ -61,12 +61,6 @@ INSTANTIATE_TEST_SUITE_P( BasicDate() + 123us}, TimestampTest{"2020-01-01T00:00:00.000123+00:00", "with microseconds and offset", BasicDate() + 123us}, - TimestampTest{"2020-01-01T00:00:00.123456789Z", - "floor nanoseconds with zulu offset", - BasicDate() + 123ms + 456us}, - TimestampTest{"2020-01-01T01:00:00.123456789+01:00", - "floor nanoseconds with offset", - BasicDate() + 123ms + 456us}, })); diff --git a/libs/server-sent-events/include/launchdarkly/sse/client.hpp b/libs/server-sent-events/include/launchdarkly/sse/client.hpp index 1d7046c08..904904a23 100644 --- a/libs/server-sent-events/include/launchdarkly/sse/client.hpp +++ b/libs/server-sent-events/include/launchdarkly/sse/client.hpp @@ -90,6 +90,14 @@ class Builder { */ Builder& write_timeout(std::chrono::milliseconds timeout); + /** + * Specifies the initial delay before reconnection when backoff takes place + * due to an error on the connection. + * @param timeout + * @return Reference to this builder. + */ + Builder& initial_reconnect_delay(std::chrono::milliseconds delay); + /** * Specify the method for the initial request. The default method is GET. * @param verb The HTTP method. @@ -138,6 +146,7 @@ class Builder { std::optional read_timeout_; std::optional write_timeout_; std::optional connect_timeout_; + std::optional initial_reconnect_delay_; LogCallback logging_cb_; EventReceiver receiver_; ErrorCallback error_cb_; diff --git a/libs/server-sent-events/src/client.cpp b/libs/server-sent-events/src/client.cpp index e15bd42d2..8d8432382 100644 --- a/libs/server-sent-events/src/client.cpp +++ b/libs/server-sent-events/src/client.cpp @@ -37,6 +37,13 @@ auto const kDefaultUserAgent = BOOST_BEAST_VERSION_STRING; // Time duration used when no timeout is specified (1 year). auto const kNoTimeout = std::chrono::hours(8760); +// Time duration that the backoff algorithm uses before initiating a new +// connection, the first time a failure is detected. +auto const kDefaultInitialReconnectDelay = std::chrono::seconds(1); + +// Maximum duration between backoff attempts. +auto const kDefaultMaxBackoffDelay = std::chrono::seconds(30); + static boost::optional ToOptRef( std::optional& maybe_val) { if (maybe_val) { @@ -60,6 +67,7 @@ class FoxyClient : public Client, std::optional connect_timeout, std::optional read_timeout, std::optional write_timeout, + std::optional initial_reconnect_delay, Builder::EventReceiver receiver, Builder::LogCallback logger, Builder::ErrorCallback errors, @@ -75,7 +83,9 @@ class FoxyClient : public Client, launchdarkly::foxy::session_opts{ ToOptRef(ssl_context_), connect_timeout.value_or(kNoTimeout)}), - backoff_(std::chrono::seconds(1), std::chrono::seconds(30)), + backoff_( + initial_reconnect_delay.value_or(kDefaultInitialReconnectDelay), + kDefaultMaxBackoffDelay), last_event_id_(std::nullopt), backoff_timer_(session_.get_executor()), event_receiver_(std::move(receiver)), @@ -347,6 +357,7 @@ Builder::Builder(net::any_io_executor ctx, std::string url) read_timeout_{std::nullopt}, write_timeout_{std::nullopt}, connect_timeout_{std::nullopt}, + initial_reconnect_delay_{std::nullopt}, logging_cb_([](auto msg) {}), receiver_([](launchdarkly::sse::Event const&) {}), error_cb_([](auto err) {}) { @@ -382,6 +393,11 @@ Builder& Builder::write_timeout(std::chrono::milliseconds timeout) { return *this; } +Builder& Builder::initial_reconnect_delay(std::chrono::milliseconds delay) { + initial_reconnect_delay_ = delay; + return *this; +} + Builder& Builder::method(http::verb verb) { request_.method(verb); return *this; @@ -441,8 +457,8 @@ std::shared_ptr Builder::build() { return std::make_shared( net::make_strand(executor_), request, host, service, connect_timeout_, - read_timeout_, write_timeout_, receiver_, logging_cb_, error_cb_, - std::move(ssl)); + read_timeout_, write_timeout_, initial_reconnect_delay_, receiver_, + logging_cb_, error_cb_, std::move(ssl)); } } // namespace launchdarkly::sse