diff --git a/.clang-format b/.clang-format index 4eba9f053..c6fa7e954 100644 --- a/.clang-format +++ b/.clang-format @@ -2,4 +2,5 @@ BasedOnStyle: Chromium IndentWidth: 4 QualifierAlignment: Right +NamespaceIndentation: None ... diff --git a/.github/ISSUE_TEMPLATE/client-sdk--bug-report.md b/.github/ISSUE_TEMPLATE/client-sdk--bug-report.md index 44d5a2bbf..906fc815e 100644 --- a/.github/ISSUE_TEMPLATE/client-sdk--bug-report.md +++ b/.github/ISSUE_TEMPLATE/client-sdk--bug-report.md @@ -22,13 +22,17 @@ assignees: '' A clear and concise description of what you expected to happen. **Logs** - If applicable, add any log output related to your problem. + If applicable, add any log output related to your problem. + To get more logs from the SDK, change the log level using environment variable `LD_LOG_LEVEL`. For example: + ``` + LD_LOG_LEVEL=debug ./your-application + ``` **SDK version** The version of this SDK that you are using. **Language version, developer tools** - For instance, Go 1.11 or Ruby 2.5.3. If you are using a language that requires a separate compiler, such as C, please include the name and version of the compiler too. + For instance, C++17 or C11. If you are using a language that requires a separate compiler, such as C, please include the name and version of the compiler too. **OS/platform** For instance, Ubuntu 16.04, Windows 10, or Android 4.0.3. If your code is running in a browser, please also include the browser type and version. diff --git a/.github/actions/sdk-release/action.yml b/.github/actions/sdk-release/action.yml index f6e789648..3c4eb762e 100644 --- a/.github/actions/sdk-release/action.yml +++ b/.github/actions/sdk-release/action.yml @@ -92,11 +92,25 @@ runs: if: runner.os == 'Windows' uses: ilammy/msvc-dev-cmd@v1 + - name: Upgrade OpenSSL + if: runner.os == 'Windows' + shell: bash + run: | + choco upgrade openssl --no-progress + - name: Determine OpenSSL Installation Directory + if: runner.os == 'Windows' + shell: bash + run: | + if [ -d "C:\Program Files\OpenSSL-Win64" ]; then + echo "OPENSSL_ROOT_DIR=C:\Program Files\OpenSSL-Win64" >> "$GITHUB_ENV" + else + echo "OPENSSL_ROOT_DIR=C:\Program Files\OpenSSL" >> "$GITHUB_ENV" + fi - name: Build Windows Artifacts if: runner.os == 'Windows' shell: bash env: - OPENSSL_ROOT_DIR: 'C:\Program Files\OpenSSL' + OPENSSL_ROOT_DIR: ${{ env.OPENSSL_ROOT_DIR }} BOOST_LIBRARY_DIR: 'C:\local\boost_1_81_0\lib64-msvc-14.3' BOOST_LIBRARYDIR: 'C:\local\boost_1_81_0\lib64-msvc-14.3' BOOST_ROOT: ${{ steps.install-boost.outputs.BOOST_ROOT }} @@ -155,9 +169,8 @@ runs: if: runner.os == 'macOS' shell: bash run: | - brew link --overwrite openssl@1.1 - echo "OPENSSL_ROOT_DIR=$(brew --prefix openssl@1.1)" >> "$GITHUB_ENV" - export OPENSSL_ROOT_DIR=$(brew --prefix openssl@1.1) + echo "OPENSSL_ROOT_DIR=$(brew --prefix openssl@3)" >> "$GITHUB_ENV" + export OPENSSL_ROOT_DIR=$(brew --prefix openssl@3) ./scripts/build-release.sh ${{ inputs.sdk_cmake_target }} env: diff --git a/.github/workflows/client.yml b/.github/workflows/client.yml index 173969727..75dc6f508 100644 --- a/.github/workflows/client.yml +++ b/.github/workflows/client.yml @@ -6,7 +6,7 @@ on: paths-ignore: - '**.md' #Do not need to run CI for markdown changes. pull_request: - branches: [ main, server-side ] + branches: [ main ] paths-ignore: - '**.md' @@ -16,12 +16,12 @@ jobs: env: # Port the test service (implemented in this repo) should bind to. TEST_SERVICE_PORT: 8123 - TEST_SERVICE_BINARY: ./build/contract-tests/client-contract-tests/client-tests + TEST_SERVICE_BINARY: ./build/contract-tests/sdk-contract-tests/sdk-tests steps: - uses: actions/checkout@v3 - uses: ./.github/actions/ci with: - cmake_target: client-tests + cmake_target: sdk-tests run_tests: false - name: 'Launch test service as background task' run: $TEST_SERVICE_BINARY $TEST_SERVICE_PORT 2>&1 & @@ -29,8 +29,7 @@ jobs: with: # Inform the test harness of test service's port. test_service_port: ${{ env.TEST_SERVICE_PORT }} - extra_params: '-skip-from ./contract-tests/client-contract-tests/test-suppressions.txt' - build-test-client: + build-test: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 @@ -41,10 +40,9 @@ jobs: runs-on: macos-12 steps: - run: | - brew link --overwrite openssl@1.1 - echo "OPENSSL_ROOT_DIR=$(brew --prefix openssl@1.1)" >> "$GITHUB_ENV" + echo "OPENSSL_ROOT_DIR=$(brew --prefix openssl@3)" >> "$GITHUB_ENV" # For debugging - echo "OPENSSL_ROOT_DIR=$(brew --prefix openssl@1.1)" + echo "OPENSSL_ROOT_DIR=$(brew --prefix openssl@3)" - uses: actions/checkout@v3 - uses: ./.github/actions/ci env: @@ -55,11 +53,23 @@ jobs: build-test-client-windows: runs-on: windows-2022 steps: + - name: Upgrade OpenSSL + shell: bash + run: | + choco upgrade openssl --no-progress + - name: Determine OpenSSL Installation Directory + shell: bash + run: | + if [ -d "C:\Program Files\OpenSSL-Win64" ]; then + echo "OPENSSL_ROOT_DIR=C:\Program Files\OpenSSL-Win64" >> "$GITHUB_ENV" + else + echo "OPENSSL_ROOT_DIR=C:\Program Files\OpenSSL" >> "$GITHUB_ENV" + fi - uses: actions/checkout@v3 - uses: ilammy/msvc-dev-cmd@v1 - uses: ./.github/actions/ci env: - OPENSSL_ROOT_DIR: 'C:\Program Files\OpenSSL' + OPENSSL_ROOT_DIR: ${{ env.OPENSSL_ROOT_DIR }} BOOST_LIBRARY_DIR: 'C:\local\boost_1_81_0\lib64-msvc-14.3' BOOST_LIBRARYDIR: 'C:\local\boost_1_81_0\lib64-msvc-14.3' with: diff --git a/.github/workflows/manual-publish-doc.yml b/.github/workflows/manual-publish-doc.yml index 78911c8cd..535888d59 100644 --- a/.github/workflows/manual-publish-doc.yml +++ b/.github/workflows/manual-publish-doc.yml @@ -8,6 +8,7 @@ on: type: choice options: - libs/client-sdk + - libs/server-sdk name: Publish Documentation jobs: build-publish: diff --git a/.github/workflows/manual-sdk-release-artifacts.yml b/.github/workflows/manual-sdk-release-artifacts.yml index fab15f26b..99e9f5db7 100644 --- a/.github/workflows/manual-sdk-release-artifacts.yml +++ b/.github/workflows/manual-sdk-release-artifacts.yml @@ -1,5 +1,6 @@ # Checks out the tag, builds release builds, and attaches them to the release for the tag. # If you need to change build scripts, then update the tag to include the modifications. +# NOTE: This workflow uses sdk-release/action.yml @ the tag specified in the workflow_dispatch input. on: workflow_dispatch: inputs: @@ -13,6 +14,7 @@ on: type: choice options: - libs/client-sdk:launchdarkly-cpp-client + - libs/server-sdk:launchdarkly-cpp-server name: Publish SDK Artifacts @@ -55,18 +57,18 @@ jobs: sdk_path: ${{ needs.split-input.outputs.sdk_path}} sdk_cmake_target: ${{ needs.split-input.outputs.sdk_cmake_target}} release-sdk-provenance: - needs: ['release-sdk'] + needs: [ 'release-sdk' ] strategy: matrix: # Generates a combined attestation for each platform os: [ linux, windows, macos ] - permissions: + permissions: actions: read id-token: write contents: write uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.7.0 with: base64-subjects: "${{ needs.release-sdk.outputs[format('hashes-{0}', matrix.os)] }}" - upload-assets: true + upload-assets: true upload-tag-name: ${{ inputs.tag }} provenance-name: ${{ format('{0}-multiple-provenance.intoto.jsonl', matrix.os) }} diff --git a/.github/workflows/server.yml b/.github/workflows/server.yml index 1ae481daf..1e9fec2f8 100644 --- a/.github/workflows/server.yml +++ b/.github/workflows/server.yml @@ -41,10 +41,9 @@ jobs: runs-on: macos-12 steps: - run: | - brew link --overwrite openssl@1.1 - echo "OPENSSL_ROOT_DIR=$(brew --prefix openssl@1.1)" >> "$GITHUB_ENV" + echo "OPENSSL_ROOT_DIR=$(brew --prefix openssl@3)" >> "$GITHUB_ENV" # For debugging - echo "OPENSSL_ROOT_DIR=$(brew --prefix openssl@1.1)" + echo "OPENSSL_ROOT_DIR=$(brew --prefix openssl@3)" - uses: actions/checkout@v3 - uses: ./.github/actions/ci env: @@ -55,14 +54,26 @@ jobs: build-test-server-windows: runs-on: windows-2022 steps: + - name: Upgrade OpenSSL + shell: bash + run: | + choco upgrade openssl --no-progress + - name: Determine OpenSSL Installation Directory + shell: bash + run: | + if [ -d "C:\Program Files\OpenSSL-Win64" ]; then + echo "OPENSSL_ROOT_DIR=C:\Program Files\OpenSSL-Win64" >> "$GITHUB_ENV" + else + echo "OPENSSL_ROOT_DIR=C:\Program Files\OpenSSL" >> "$GITHUB_ENV" + fi - uses: actions/checkout@v3 - uses: ilammy/msvc-dev-cmd@v1 - uses: ./.github/actions/ci env: - OPENSSL_ROOT_DIR: 'C:\Program Files\OpenSSL' + OPENSSL_ROOT_DIR: ${{ env.OPENSSL_ROOT_DIR }} BOOST_LIBRARY_DIR: 'C:\local\boost_1_81_0\lib64-msvc-14.3' BOOST_LIBRARYDIR: 'C:\local\boost_1_81_0\lib64-msvc-14.3' with: - cmake_target: launchdarkly-cpp-server + cmake_target: launchdarkly-cpp-client platform_version: 2022 toolset: msvc diff --git a/.release-please-manifest.json b/.release-please-manifest.json index db3c31f38..5169be222 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,6 +1,7 @@ { - "libs/client-sdk": "3.0.8", - "libs/server-sent-events": "0.1.3", - "libs/common": "0.3.6", - "libs/internal": "0.1.9" + "libs/client-sdk": "3.2.1", + "libs/server-sdk": "0.1.0", + "libs/server-sent-events": "0.2.0", + "libs/common": "0.5.0", + "libs/internal": "0.3.0" } diff --git a/CMakeLists.txt b/CMakeLists.txt index cec73e149..e9f9e29b0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,6 +3,7 @@ # Required for Apple Silicon support. cmake_minimum_required(VERSION 3.19) +include(CMakeDependentOption) project( LaunchDarklyCPPSDKs @@ -13,6 +14,50 @@ project( include(GNUInstallDirs) +option(BUILD_TESTING "Top-level switch for testing. Turn off to disable unit and contract tests." ON) + +option(LD_BUILD_SHARED_LIBS "Build the SDKs as shared libraries" OFF) + +cmake_dependent_option(LD_BUILD_UNIT_TESTS + "Build the C++ unit tests." + ON # default to enabling unit tests + "BUILD_TESTING;NOT LD_BUILD_SHARED_LIBS" # only exposed if top-level switch is on, and also only when building + # static libs. This is because we have hidden visibility of symbols by default (to only expose our C API.) + OFF # otherwise, off +) + +# If you want to run the unit tests with valgrind, then LD_TESTING_SANITIZERS must of OFF. +cmake_dependent_option(LD_TESTING_SANITIZERS + "Enable sanitizers for unit tests." + ON # default to enabling sanitizers + "LD_BUILD_UNIT_TESTS" # only expose if unit tests enabled.. + OFF # otherwise, off +) + +cmake_dependent_option(LD_BUILD_CONTRACT_TESTS + "Build contract test service." + OFF # default to disabling contract tests, since they require running a service + "BUILD_TESTING;NOT LD_BUILD_SHARED_LIBS" # only expose if top-level switch is on and using static libs, since C++ symbols needed would be hidden. + OFF # otherwise, off +) + +# The general strategy is to produce a fat artifact containing all of our dependencies so users +# only have a single thing to link. We should support this either being a static or shared library. +# Because OpenSSL is a large, and security relevant dependency, we should have a separate option +# to link against that statically or dynamically. + +option(LD_DYNAMIC_LINK_OPENSSL + "Dynamically link OpenSSL instead of building with static library" + OFF # default to linking OpenSSL statically +) + +option(LD_BUILD_EXAMPLES "Build hello-world examples." ON) + +# If using 'make' as the build system, CMake causes the 'install' target to have a dependency on 'all', meaning +# it will cause a full build. This disables that, allowing us to build piecemeal instead. This is useful +# so that we only need to build the client or server for a given release (if only the client or server were affected.) +set(CMAKE_SKIP_INSTALL_ALL_DEPENDENCY true) + # All projects in this repo should share the same version of 3rd party depends. # It's the only way to remain sane. set(CMAKE_FILES "${CMAKE_CURRENT_SOURCE_DIR}/cmake") @@ -20,15 +65,11 @@ set(CMAKE_CXX_STANDARD 17) set(CMAKE_POSITION_INDEPENDENT_CODE ON) -option(BUILD_TESTING "Enable C++ unit tests." ON) - -# If you want to run the unit tests with valgrind, then TESTING_SANITIZERS must of OFF. -option(TESTING_SANITIZERS "Enable sanitizers for unit tests." ON) - -if (BUILD_TESTING) +if (LD_BUILD_UNIT_TESTS) + message(STATUS "LaunchDarkly: building unit tests") set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -D_GLIBCXX_DEBUG") add_compile_definitions(LAUNCHDARKLY_USE_ASSERT) - if (TESTING_SANITIZERS) + if (LD_TESTING_SANITIZERS) if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fsanitize=undefined -fsanitize=leak") elseif (CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang") @@ -53,14 +94,22 @@ if (BUILD_TESTING) enable_testing() endif () -set(OPENSSL_USE_STATIC_LIBS ON) -set(OPENSSL_ROOT_DIR "/opt/homebrew/opt/openssl@1.1") +if (LD_DYNAMIC_LINK_OPENSSL) + message(STATUS "LaunchDarkly: searching for shared OpenSSL library") + set(OPENSSL_USE_STATIC_LIBS OFF) +else () + message(STATUS "LaunchDarkly: searching for static OpenSSL library") + set(OPENSSL_USE_STATIC_LIBS ON) +endif () + find_package(OpenSSL REQUIRED) message(STATUS "LaunchDarkly: using OpenSSL v${OPENSSL_VERSION}") +# Even though the main SDK might be a static or shared lib, boost should always statically +# linked into the binary. set(Boost_USE_STATIC_LIBS ON) -if (BUILD_SHARED_LIBS) +if (LD_BUILD_SHARED_LIBS) # When building a shared library we hide all symbols # aside from this we have specifically exported for the C-API. set(CMAKE_CXX_VISIBILITY_PRESET hidden) @@ -72,26 +121,25 @@ set(Boost_USE_STATIC_RUNTIME OFF) find_package(Boost 1.81 REQUIRED COMPONENTS json url coroutine) message(STATUS "LaunchDarkly: using Boost v${Boost_VERSION}") -add_subdirectory(libs/client-sdk) -add_subdirectory(libs/server-sdk) - -set(ORIGINAL_BUILD_SHARED_LIBS "${BUILD_SHARED_LIBS}") -set(BUILD_SHARED_LIBS OFF) +include(${CMAKE_FILES}/certify.cmake) +add_subdirectory(vendor/foxy) -# Always build the common libraries as static libs. +# Common, internal, and server-sent-events are built as "object" libraries. add_subdirectory(libs/common) add_subdirectory(libs/internal) add_subdirectory(libs/server-sent-events) -set(ORIGINAL_BUILD_SHARED_LIBS "${BUILD_SHARED_LIBS}") - -set(BUILD_TESTING OFF) -include(${CMAKE_FILES}/certify.cmake) -add_subdirectory(vendor/foxy) - -set(BUILD_TESTING "${ORIGINAL_BUILD_TESTING}") +# Built as static or shared depending on LD_BUILD_SHARED_LIBS variable. +# This target "links" in common, internal, and sse as object libraries. +add_subdirectory(libs/client-sdk) +add_subdirectory(libs/server-sdk) -set(BUILD_SHARED_LIBS "${ORIGINAL_BUILD_SHARED_LIBS}") +if (LD_BUILD_CONTRACT_TESTS) + message(STATUS "LaunchDarkly: building contract tests") + add_subdirectory(contract-tests) +endif () -add_subdirectory(contract-tests) -add_subdirectory(examples) +if (LD_BUILD_EXAMPLES) + message(STATUS "LaunchDarkly: building examples") + add_subdirectory(examples) +endif () diff --git a/CODEOWNERS b/CODEOWNERS index 7d0dac3c8..412f35ea2 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,2 +1,2 @@ # Repository Maintainers -* @launchdarkly/team-sdk +* @launchdarkly/team-sdk-c diff --git a/README.md b/README.md index 1c0969f34..714d2d15a 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,40 @@ GoogleTest is used for testing. For information on integrating an SDK package please refer to the SDK specific README. +## CMake Usage + +Various CMake options are available to customize the client/server SDK builds. + +| Option | Description | Default | Requires | +|---------------------------|----------------------------------------------------------------------------------------|--------------------|-------------------------------------------| +| `BUILD_TESTING` | Coarse-grained switch; turn off to disable all testing and only build the SDK targets. | On | N/A | +| `LD_BUILD_UNIT_TESTS` | Whether C++ unit tests are built. | On | `BUILD_TESTING; NOT LD_BUILD_SHARED_LIBS` | +| `LD_TESTING_SANITIZERS` | Whether sanitizers should be enabled. | On | `LD_BUILD_UNIT_TESTS` | +| `LD_BUILD_CONTRACT_TESTS` | Whether the contract test service (used in CI) is built. | Off | `BUILD_TESTING` | +| `LD_BUILD_EXAMPLES` | Whether example apps (hello world) are built. | On | N/A | +| `LD_BUILD_SHARED_LIBS` | Whether the SDKs are built as static or shared libraries. | Off (static lib) | N/A | +| `LD_DYNAMIC_LINK_OPENSSL` | Whether OpenSSL be dynamically linked. | Off (static link) | N/A | + +**Note:** _if building the SDKs as shared libraries, then unit tests won't be able to link correctly since the SDK's C++ +symbols aren't exposed. To run unit tests, build a static library._ + +Basic usage example: + +```bash +mkdir -p build && cd build +cmake -G"Unix Makefiles" .. +``` + +Slightly more advanced example - build shared libraries, and don't build any of the testing components: + +```bash +mkdir -p build && cd build +cmake -G"Unix Makefiles" -DLD_BUILD_SHARED_LIBS=On -DBUILD_TESTING=Off .. +``` + +The example uses `make`, but you might instead use [Ninja](https://ninja-build.org/), +MSVC, [etc.](https://cmake.org/cmake/help/latest/manual/cmake-generators.7.html) + ## LaunchDarkly overview [LaunchDarkly](https://www.launchdarkly.com) is a feature management platform that serves trillions of feature flags @@ -76,7 +110,8 @@ our [contributing guidelines](CONTRIBUTING.md) for instructions on how to contri - Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). Disable parts of your application to facilitate maintenance, without taking everything offline. -- LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Read [our documentation](https://docs.launchdarkly.com/sdk) for a complete list. +- LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. + Read [our documentation](https://docs.launchdarkly.com/sdk) for a complete list. - Explore LaunchDarkly - [launchdarkly.com](https://www.launchdarkly.com/ 'LaunchDarkly Main Website') for more information - [docs.launchdarkly.com](https://docs.launchdarkly.com/ 'LaunchDarkly Documentation') for our documentation and SDK diff --git a/cmake/certify.cmake b/cmake/certify.cmake index 32235ec67..f28da6fba 100644 --- a/cmake/certify.cmake +++ b/cmake/certify.cmake @@ -10,16 +10,14 @@ endif () FetchContent_Declare(boost_certify GIT_REPOSITORY https://github.com/djarek/certify.git GIT_TAG 97f5eebfd99a5d6e99d07e4820240994e4e59787 - ) +) set(BUILD_TESTING OFF) FetchContent_GetProperties(boost_certify) -if(NOT boost_certify_POPULATED) +if (NOT boost_certify_POPULATED) FetchContent_Populate(boost_certify) add_subdirectory(${boost_certify_SOURCE_DIR} ${boost_certify_BINARY_DIR} EXCLUDE_FROM_ALL) -endif() +endif () set(BUILD_TESTING "${ORIGINAL_BUILD_TESTING}") - -set(BUILD_SHARED_LIBS "${ORIGINAL_BUILD_SHARED_LIBS}") diff --git a/cmake/expected.cmake b/cmake/expected.cmake index 18864faa6..e2bbf4dde 100644 --- a/cmake/expected.cmake +++ b/cmake/expected.cmake @@ -7,9 +7,10 @@ if (${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.24") cmake_policy(SET CMP0135 NEW) endif () +set(EXPECTED_BUILD_TESTS OFF) FetchContent_Declare(tl-expected GIT_REPOSITORY https://github.com/TartanLlama/expected.git GIT_TAG 292eff8bd8ee230a7df1d6a1c00c4ea0eb2f0362 - ) +) FetchContent_MakeAvailable(tl-expected) diff --git a/contract-tests/client-contract-tests/test-suppressions.txt b/contract-tests/client-contract-tests/test-suppressions.txt index cba85bdd6..e69de29bb 100644 --- a/contract-tests/client-contract-tests/test-suppressions.txt +++ b/contract-tests/client-contract-tests/test-suppressions.txt @@ -1,33 +0,0 @@ -# The Client doesn't need to know how to deserialize users. -context type/convert/old user to context/{"key": ""} -context type/convert/old user to context/{"key": "a"} -context type/convert/old user to context/{"key": "a"} -context type/convert/old user to context/{"key": "a", "custom": {"b": true}} -context type/convert/old user to context/{"key": "a", "custom": {"b": 1}} -context type/convert/old user to context/{"key": "a", "custom": {"b": "c"}} -context type/convert/old user to context/{"key": "a", "custom": {"b": [1, 2]}} -context type/convert/old user to context/{"key": "a", "custom": {"b": {"c": 1}}} -context type/convert/old user to context/{"key": "a", "custom": {"b": 1, "c": 2}} -context type/convert/old user to context/{"key": "a", "custom": {"b": 1, "c": null}} -context type/convert/old user to context/{"key": "a", "custom": {}} -context type/convert/old user to context/{"key": "a", "custom": null} -context type/convert/old user to context/{"key": "a", "anonymous": true} -context type/convert/old user to context/{"key": "a", "anonymous": false} -context type/convert/old user to context/{"key": "a", "anonymous": null} -context type/convert/old user to context/{"key": "a", "privateAttributeNames": ["b"]} -context type/convert/old user to context/{"key": "a", "privateAttributeNames": []} -context type/convert/old user to context/{"key": "a", "privateAttributeNames": null} -context type/convert/old user to context/{"key": "a", "name": "b"} -context type/convert/old user to context/{"key": "a", "name": null} -context type/convert/old user to context/{"key": "a", "firstName": "b"} -context type/convert/old user to context/{"key": "a", "firstName": null} -context type/convert/old user to context/{"key": "a", "lastName": "b"} -context type/convert/old user to context/{"key": "a", "lastName": null} -context type/convert/old user to context/{"key": "a", "email": "b"} -context type/convert/old user to context/{"key": "a", "email": null} -context type/convert/old user to context/{"key": "a", "country": "b"} -context type/convert/old user to context/{"key": "a", "country": null} -context type/convert/old user to context/{"key": "a", "avatar": "b"} -context type/convert/old user to context/{"key": "a", "avatar": null} -context type/convert/old user to context/{"key": "a", "ip": "b"} -context type/convert/old user to context/{"key": "a", "ip": null} diff --git a/contract-tests/server-contract-tests/test-suppressions.txt b/contract-tests/server-contract-tests/test-suppressions.txt index da9feca4f..460d051db 100644 --- a/contract-tests/server-contract-tests/test-suppressions.txt +++ b/contract-tests/server-contract-tests/test-suppressions.txt @@ -5,37 +5,3 @@ streaming/validation/drop and reconnect if stream event has malformed JSON/delet 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 be37f398d..138ec4942 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -2,5 +2,4 @@ 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 index ceb971190..92b393183 100644 --- a/examples/client-and-server-coexistence/CMakeLists.txt +++ b/examples/client-and-server-coexistence/CMakeLists.txt @@ -12,4 +12,4 @@ 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) +target_link_libraries(c-client-and-server PRIVATE launchdarkly::client launchdarkly::server Threads::Threads) diff --git a/examples/client-and-server-coexistence/main.c b/examples/client-and-server-coexistence/main.c index 88c7190ab..935a0a328 100644 --- a/examples/client-and-server-coexistence/main.c +++ b/examples/client-and-server-coexistence/main.c @@ -12,6 +12,8 @@ #include +#include + int main() { LDContextBuilder context_builder = LDContextBuilder_New(); LDContextBuilder_AddKind(context_builder, "user", "example-user-key"); @@ -27,6 +29,7 @@ int main() { if (LDStatus_Ok(client_config_status)) { LDClientSDK client_sdk = LDClientSDK_New(client_config, context); + printf("Created client SDK\n"); LDClientSDK_Free(client_sdk); } @@ -39,8 +42,10 @@ int main() { if (LDStatus_Ok(server_config_status)) { LDServerSDK server_sdk = LDServerSDK_New(server_config); + printf("Created server SDK\n"); LDServerSDK_Free(server_sdk); } return 0; } +>>>>>>> main diff --git a/examples/hello-c-client/CMakeLists.txt b/examples/hello-c-client/CMakeLists.txt index 4f6e4cc1c..f8fc4ac90 100644 --- a/examples/hello-c-client/CMakeLists.txt +++ b/examples/hello-c-client/CMakeLists.txt @@ -12,4 +12,4 @@ set(THREADS_PREFER_PTHREAD_FLAG ON) find_package(Threads REQUIRED) add_executable(hello-c-client main.c) -target_link_libraries(hello-c-client PRIVATE launchdarkly::client launchdarkly::sse launchdarkly::common Threads::Threads) +target_link_libraries(hello-c-client PRIVATE launchdarkly::client Threads::Threads) diff --git a/examples/hello-c-server/CMakeLists.txt b/examples/hello-c-server/CMakeLists.txt index edd7e0c3e..c3ea8e824 100644 --- a/examples/hello-c-server/CMakeLists.txt +++ b/examples/hello-c-server/CMakeLists.txt @@ -12,4 +12,4 @@ 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) +target_link_libraries(hello-c-server PRIVATE launchdarkly::server Threads::Threads) diff --git a/libs/client-sdk/CHANGELOG.md b/libs/client-sdk/CHANGELOG.md index 216b092c0..7cd27507d 100644 --- a/libs/client-sdk/CHANGELOG.md +++ b/libs/client-sdk/CHANGELOG.md @@ -2,6 +2,72 @@ All notable changes to the LaunchDarkly Client-Side SDK for C/C++ will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org). +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * launchdarkly-cpp-internal bumped from 0.1.9 to 0.1.10 + +## [3.2.1](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-client-v3.2.0...launchdarkly-cpp-client-v3.2.1) (2023-10-23) + + +### Bug Fixes + +* allow for installing only the client or server SDK independently ([#269](https://github.com/launchdarkly/cpp-sdks/issues/269)) ([fe08c3c](https://github.com/launchdarkly/cpp-sdks/commit/fe08c3c14600c712ba6480f671fc306eca320044)) + +## [3.2.0](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-client-v3.1.1...launchdarkly-cpp-client-v3.2.0) (2023-10-23) + + +### Features + +* server-side SDK ([#160](https://github.com/launchdarkly/cpp-sdks/issues/160)) ([75eece3](https://github.com/launchdarkly/cpp-sdks/commit/75eece3a46870fdb6bf4384c112700558099c4d1)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * launchdarkly-cpp-internal bumped from 0.2.0 to 0.3.0 + * launchdarkly-cpp-common bumped from 0.4.0 to 0.5.0 + +## [3.1.1](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-client-v3.1.0...launchdarkly-cpp-client-v3.1.1) (2023-10-19) + + +### Bug Fixes + +* LD_BUILD_SHARED_LIBS build flag usage ([#260](https://github.com/launchdarkly/cpp-sdks/issues/260)) ([8dd473f](https://github.com/launchdarkly/cpp-sdks/commit/8dd473f825d4d05f1bc4f94621f7e4a4fefab929)) + +## [3.1.0](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-client-v3.0.10...launchdarkly-cpp-client-v3.1.0) (2023-10-13) + + +### Features + +* clean up LD CMake variables & allow for OpenSSL dynamic link ([#255](https://github.com/launchdarkly/cpp-sdks/issues/255)) ([ed23c9a](https://github.com/launchdarkly/cpp-sdks/commit/ed23c9a347665529a09d18111bb9d3b699381728)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * launchdarkly-cpp-internal bumped from 0.1.11 to 0.2.0 + * launchdarkly-cpp-common bumped from 0.3.7 to 0.4.0 + * launchdarkly-cpp-sse-client bumped from 0.1.3 to 0.2.0 + +## [3.0.10](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-client-v3.0.9...launchdarkly-cpp-client-v3.0.10) (2023-10-11) + + +### Bug Fixes + +* treat warnings as errors in CI ([#253](https://github.com/launchdarkly/cpp-sdks/issues/253)) ([7f4f168](https://github.com/launchdarkly/cpp-sdks/commit/7f4f168f47619d7fa8b8952feade485261c69049)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * launchdarkly-cpp-internal bumped from 0.1.10 to 0.1.11 + * launchdarkly-cpp-common bumped from 0.3.6 to 0.3.7 + ## [3.0.8](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-client-v3.0.7...launchdarkly-cpp-client-v3.0.8) (2023-09-13) diff --git a/libs/client-sdk/CMakeLists.txt b/libs/client-sdk/CMakeLists.txt index 0d4dffd46..c0822bcfc 100644 --- a/libs/client-sdk/CMakeLists.txt +++ b/libs/client-sdk/CMakeLists.txt @@ -6,7 +6,7 @@ cmake_minimum_required(VERSION 3.19) project( LaunchDarklyCPPClient - VERSION 3.0.8 # {x-release-please-version} + VERSION 3.2.1 # {x-release-please-version} DESCRIPTION "LaunchDarkly C++ Client SDK" LANGUAGES CXX C ) @@ -30,6 +30,6 @@ include(FetchContent) # Add main SDK sources. add_subdirectory(src) -if (BUILD_TESTING) +if (LD_BUILD_UNIT_TESTS) add_subdirectory(tests) endif () diff --git a/libs/client-sdk/README.md b/libs/client-sdk/README.md index aee4dd3b0..da2948e26 100644 --- a/libs/client-sdk/README.md +++ b/libs/client-sdk/README.md @@ -71,6 +71,22 @@ gcc -I $(pwd)/include -Llib -fPIE -g main.c liblaunchdarkly-cpp-client.so The examples here are to help with getting started, but generally speaking the SDK should be incorporated using your build system (CMake for instance). +### CMake Usage + +First, add the SDK to your project: + +```cmake +add_subdirectory(path-to-sdk-repo) +``` + +Currently `find_package` is not yet supported. + +This will expose the `launchdarkly::client` target. Next, link the target to your executable or library: + +```cmake +target_link_libraries(my-target PRIVATE launchdarkly::client) +``` + Learn more ----------- diff --git a/libs/client-sdk/include/launchdarkly/client_side/client.hpp b/libs/client-sdk/include/launchdarkly/client_side/client.hpp index a4eb5866d..d8be9404d 100644 --- a/libs/client-sdk/include/launchdarkly/client_side/client.hpp +++ b/libs/client-sdk/include/launchdarkly/client_side/client.hpp @@ -324,7 +324,7 @@ class Client : public IClient { private: inline static char const* const kVersion = - "3.0.8"; // {x-release-please-version} + "3.2.1"; // {x-release-please-version} std::unique_ptr client; }; diff --git a/libs/client-sdk/include/launchdarkly/client_side/data_source_status.hpp b/libs/client-sdk/include/launchdarkly/client_side/data_source_status.hpp index cdf005a35..4c14572c5 100644 --- a/libs/client-sdk/include/launchdarkly/client_side/data_source_status.hpp +++ b/libs/client-sdk/include/launchdarkly/client_side/data_source_status.hpp @@ -15,7 +15,7 @@ namespace launchdarkly::client_side::data_sources { /** * Enumeration of possible data source states. */ -enum class ClientDataSourceState { +enum class DataSourceState { /** * The initial state of the data source when the SDK is being * initialized. @@ -72,7 +72,7 @@ enum class ClientDataSourceState { }; using DataSourceStatus = - common::data_sources::DataSourceStatusBase; + common::data_sources::DataSourceStatusBase; /** * Interface for accessing and listening to the data source status. diff --git a/libs/client-sdk/package.json b/libs/client-sdk/package.json index d1128f73d..e5bd535cc 100644 --- a/libs/client-sdk/package.json +++ b/libs/client-sdk/package.json @@ -1,11 +1,11 @@ { "name": "launchdarkly-cpp-client", "description": "This package.json exists for modeling dependencies for the release process.", - "version": "3.0.8", + "version": "3.2.1", "private": true, "dependencies": { - "launchdarkly-cpp-internal": "0.1.9", - "launchdarkly-cpp-common": "0.3.6", - "launchdarkly-cpp-sse-client": "0.1.3" + "launchdarkly-cpp-internal": "0.3.0", + "launchdarkly-cpp-common": "0.5.0", + "launchdarkly-cpp-sse-client": "0.2.0" } } diff --git a/libs/client-sdk/src/CMakeLists.txt b/libs/client-sdk/src/CMakeLists.txt index f4a5081f7..c24a4b414 100644 --- a/libs/client-sdk/src/CMakeLists.txt +++ b/libs/client-sdk/src/CMakeLists.txt @@ -3,9 +3,15 @@ file(GLOB HEADER_LIST CONFIGURE_DEPENDS "${LaunchDarklyCPPClient_SOURCE_DIR}/include/launchdarkly/client_side/*.hpp" ) -# Automatic library: static or dynamic based on user config. +if (LD_BUILD_SHARED_LIBS) + message(STATUS "LaunchDarkly: building client-sdk as shared library") + add_library(${LIBNAME} SHARED) +else () + message(STATUS "LaunchDarkly: building client-sdk as static library") + add_library(${LIBNAME} STATIC) +endif () -add_library(${LIBNAME} +target_sources(${LIBNAME} PRIVATE ${HEADER_LIST} data_sources/streaming_data_source.cpp data_sources/data_source_event_handler.cpp @@ -31,14 +37,16 @@ add_library(${LIBNAME} flag_manager/context_index.cpp flag_manager/flag_manager.cpp flag_manager/flag_persistence.cpp - bindings/c/sdk.cpp) + bindings/c/sdk.cpp +) + -if (MSVC OR (NOT BUILD_SHARED_LIBS)) +if (MSVC OR (NOT LD_BUILD_SHARED_LIBS)) target_link_libraries(${LIBNAME} PUBLIC launchdarkly::common PRIVATE Boost::headers Boost::json Boost::url launchdarkly::sse launchdarkly::internal foxy) else () - # The default static lib builds, for linux, are positition independent. + # The default static lib builds, for linux, are position independent. # So they do not link into a shared object without issues. So, when # building shared objects do not link the static libraries and instead # use the "src.hpp" files for required libraries. @@ -55,8 +63,9 @@ add_library(launchdarkly::client ALIAS ${LIBNAME}) set_property(TARGET ${LIBNAME} PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL") -install(TARGETS ${LIBNAME}) -if (BUILD_SHARED_LIBS AND MSVC) +# Optional in case only the server SDK is being built. +install(TARGETS ${LIBNAME} OPTIONAL) +if (LD_BUILD_SHARED_LIBS AND MSVC) install(FILES $ DESTINATION bin OPTIONAL) endif () # Using PUBLIC_HEADERS would flatten the include. diff --git a/libs/client-sdk/src/bindings/c/builder.cpp b/libs/client-sdk/src/bindings/c/builder.cpp index 8f88b59ee..0259ba83b 100644 --- a/libs/client-sdk/src/bindings/c/builder.cpp +++ b/libs/client-sdk/src/bindings/c/builder.cpp @@ -93,7 +93,7 @@ LDClientConfigBuilder_Build(LDClientConfigBuilder b, LD_ASSERT_NOT_NULL(b); LD_ASSERT_NOT_NULL(out_config); - return launchdarkly::ConsumeBuilder(b, out_config); + return launchdarkly::detail::ConsumeBuilder(b, out_config); } LD_EXPORT(void) diff --git a/libs/client-sdk/src/client_impl.cpp b/libs/client-sdk/src/client_impl.cpp index 2b4aae39a..9d464673a 100644 --- a/libs/client-sdk/src/client_impl.cpp +++ b/libs/client-sdk/src/client_impl.cpp @@ -48,7 +48,6 @@ MakeDataSource(HttpProperties const& http_properties, auto data_source_properties = builder.Build(); if (config.DataSourceConfig().method.index() == 0) { - // TODO: use initial reconnect delay. return std::make_shared< launchdarkly::client_side::data_sources::StreamingDataSource>( config.ServiceEndpoints(), config.DataSourceConfig(), @@ -387,7 +386,7 @@ void ClientImpl::UpdateContextSynchronized(Context context) { ClientImpl::~ClientImpl() { ioc_.stop(); - // TODO: Probably not the best. + // TODO(SC-219101) run_thread_.join(); } diff --git a/libs/client-sdk/src/data_sources/streaming_data_source.cpp b/libs/client-sdk/src/data_sources/streaming_data_source.cpp index 4d1caf6f5..fd8c4e29a 100644 --- a/libs/client-sdk/src/data_sources/streaming_data_source.cpp +++ b/libs/client-sdk/src/data_sources/streaming_data_source.cpp @@ -1,3 +1,11 @@ +#include "streaming_data_source.hpp" + +#include +#include +#include +#include +#include + #include #include #include @@ -6,13 +14,6 @@ #include -#include "streaming_data_source.hpp" - -#include -#include -#include -#include - namespace launchdarkly::client_side::data_sources { static char const* const kCouldNotParseEndpoint = @@ -30,6 +31,7 @@ static char const* DataSourceErrorToString(launchdarkly::sse::Error error) { case sse::Error::ReadTimeout: return "read timeout reached"; } + launchdarkly::detail::unreachable(); } StreamingDataSource::StreamingDataSource( @@ -125,8 +127,6 @@ void StreamingDataSource::Start() { client_builder.header(header.first, header.second); } - // TODO: Handle proxy support. sc-204386 - auto weak_self = weak_from_this(); client_builder.receiver([weak_self](launchdarkly::sse::Event const& event) { diff --git a/libs/client-sdk/tests/client_c_bindings_test.cpp b/libs/client-sdk/tests/client_c_bindings_test.cpp index cd027b737..a96c62124 100644 --- a/libs/client-sdk/tests/client_c_bindings_test.cpp +++ b/libs/client-sdk/tests/client_c_bindings_test.cpp @@ -27,7 +27,7 @@ TEST(ClientBindings, MinimalInstantiation) { char const* version = LDClientSDK_Version(); ASSERT_TRUE(version); - ASSERT_STREQ(version, "3.0.8"); // {x-release-please-version} + ASSERT_STREQ(version, "3.2.1"); // {x-release-please-version} LDClientSDK_Free(sdk); } diff --git a/libs/client-sdk/tests/client_test.cpp b/libs/client-sdk/tests/client_test.cpp index c2daffe20..10d0ca1d5 100644 --- a/libs/client-sdk/tests/client_test.cpp +++ b/libs/client-sdk/tests/client_test.cpp @@ -16,7 +16,7 @@ TEST(ClientTest, ClientConstructedWithMinimalConfigAndContext) { char const* version = client.Version(); ASSERT_TRUE(version); - ASSERT_STREQ(version, "3.0.8"); // {x-release-please-version} + ASSERT_STREQ(version, "3.2.1"); // {x-release-please-version} } TEST(ClientTest, AllFlagsIsEmpty) { @@ -30,7 +30,7 @@ TEST(ClientTest, BoolVariationDefaultPassesThrough) { Client client(ConfigBuilder("sdk-123").Build().value(), ContextBuilder().Kind("cat", "shadow").Build()); - const std::string flag = "extra-cat-food"; + std::string const flag = "extra-cat-food"; std::vector values = {true, false}; for (auto const& v : values) { ASSERT_EQ(client.BoolVariation(flag, v), v); @@ -41,7 +41,7 @@ TEST(ClientTest, BoolVariationDefaultPassesThrough) { TEST(ClientTest, StringVariationDefaultPassesThrough) { Client client(ConfigBuilder("sdk-123").Build().value(), ContextBuilder().Kind("cat", "shadow").Build()); - const std::string flag = "treat"; + std::string const flag = "treat"; std::vector values = {"chicken", "fish", "cat-grass"}; for (auto const& v : values) { ASSERT_EQ(client.StringVariation(flag, v), v); @@ -52,7 +52,7 @@ TEST(ClientTest, StringVariationDefaultPassesThrough) { TEST(ClientTest, IntVariationDefaultPassesThrough) { Client client(ConfigBuilder("sdk-123").Build().value(), ContextBuilder().Kind("cat", "shadow").Build()); - const std::string flag = "weight"; + std::string const flag = "weight"; std::vector values = {0, 12, 13, 24, 1000}; for (auto const& v : values) { ASSERT_EQ(client.IntVariation("weight", v), v); @@ -63,7 +63,7 @@ TEST(ClientTest, IntVariationDefaultPassesThrough) { TEST(ClientTest, DoubleVariationDefaultPassesThrough) { Client client(ConfigBuilder("sdk-123").Build().value(), ContextBuilder().Kind("cat", "shadow").Build()); - const std::string flag = "weight"; + std::string const flag = "weight"; std::vector values = {0.0, 12.0, 13.0, 24.0, 1000.0}; for (auto const& v : values) { ASSERT_EQ(client.DoubleVariation(flag, v), v); @@ -75,7 +75,7 @@ TEST(ClientTest, JsonVariationDefaultPassesThrough) { Client client(ConfigBuilder("sdk-123").Build().value(), ContextBuilder().Kind("cat", "shadow").Build()); - const std::string flag = "assorted-values"; + std::string const flag = "assorted-values"; std::vector values = { Value({"running", "jumping"}), Value(3), Value(1.0), Value(true), Value(std::map{{"weight", 20}})}; @@ -84,3 +84,29 @@ TEST(ClientTest, JsonVariationDefaultPassesThrough) { ASSERT_EQ(*client.JsonVariationDetail(flag, v), v); } } + +// This test mainly serves to catch any changes made to the types in the Data +// Source Status API that are not backwards-compatible. +TEST(ClientTest, DataSourceStatus) { + Client client(ConfigBuilder("sdk-123").Build().value(), + ContextBuilder().Kind("cat", "shadow").Build()); + + client_side::data_sources::DataSourceStatus ds_status = + client.DataSourceStatus().Status(); + + std::optional + last_err = ds_status.LastError(); + + ASSERT_FALSE(last_err); + + client_side::data_sources::DataSourceStatus::DataSourceState state = + ds_status.State(); + + ASSERT_EQ(state, client_side::data_sources::DataSourceStatus:: + DataSourceState::kInitializing); + + client_side::data_sources::DataSourceStatus::DateTime date = + ds_status.StateSince(); + + ASSERT_NE(date, client_side::data_sources::DataSourceStatus::DateTime{}); +} diff --git a/libs/common/CHANGELOG.md b/libs/common/CHANGELOG.md index 51015db42..22448156e 100644 --- a/libs/common/CHANGELOG.md +++ b/libs/common/CHANGELOG.md @@ -12,6 +12,27 @@ * dependencies * launchdarkly-cpp-sse-client bumped from 0.1.1 to 0.1.2 +## [0.5.0](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-common-v0.4.0...launchdarkly-cpp-common-v0.5.0) (2023-10-23) + + +### Features + +* server-side SDK ([#160](https://github.com/launchdarkly/cpp-sdks/issues/160)) ([75eece3](https://github.com/launchdarkly/cpp-sdks/commit/75eece3a46870fdb6bf4384c112700558099c4d1)) + +## [0.4.0](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-common-v0.3.7...launchdarkly-cpp-common-v0.4.0) (2023-10-13) + + +### Features + +* clean up LD CMake variables & allow for OpenSSL dynamic link ([#255](https://github.com/launchdarkly/cpp-sdks/issues/255)) ([ed23c9a](https://github.com/launchdarkly/cpp-sdks/commit/ed23c9a347665529a09d18111bb9d3b699381728)) + +## [0.3.7](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-common-v0.3.6...launchdarkly-cpp-common-v0.3.7) (2023-10-11) + + +### Bug Fixes + +* treat warnings as errors in CI ([#253](https://github.com/launchdarkly/cpp-sdks/issues/253)) ([7f4f168](https://github.com/launchdarkly/cpp-sdks/commit/7f4f168f47619d7fa8b8952feade485261c69049)) + ## [0.3.6](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-common-v0.3.5...launchdarkly-cpp-common-v0.3.6) (2023-09-13) diff --git a/libs/common/CMakeLists.txt b/libs/common/CMakeLists.txt index 08e8d6e1f..0423c4b74 100644 --- a/libs/common/CMakeLists.txt +++ b/libs/common/CMakeLists.txt @@ -30,6 +30,6 @@ include(${CMAKE_FILES}/expected.cmake) # Add main SDK sources. add_subdirectory(src) -if (BUILD_TESTING) +if (LD_BUILD_UNIT_TESTS) add_subdirectory(tests) endif () diff --git a/libs/common/include/launchdarkly/attributes.hpp b/libs/common/include/launchdarkly/attributes.hpp index f37ab2a69..b9e68a850 100644 --- a/libs/common/include/launchdarkly/attributes.hpp +++ b/libs/common/include/launchdarkly/attributes.hpp @@ -118,8 +118,8 @@ class Attributes final { : key_(std::move(key)), name_(std::move(name)), anonymous_(anonymous), - custom_attributes_(std::move(attributes)), - private_attributes_(std::move(private_attributes)) {} + private_attributes_(std::move(private_attributes)), + custom_attributes_(std::move(attributes)) {} friend std::ostream& operator<<(std::ostream& out, Attributes const& attrs) { @@ -142,9 +142,13 @@ class Attributes final { } Attributes(Attributes const& context) = default; + Attributes(Attributes&& context) = default; + ~Attributes() = default; + Attributes& operator=(Attributes const&) = default; + Attributes& operator=(Attributes&&) = default; private: diff --git a/libs/common/include/launchdarkly/data/evaluation_detail.hpp b/libs/common/include/launchdarkly/data/evaluation_detail.hpp index d32428d05..f784e7428 100644 --- a/libs/common/include/launchdarkly/data/evaluation_detail.hpp +++ b/libs/common/include/launchdarkly/data/evaluation_detail.hpp @@ -55,6 +55,11 @@ class EvaluationDetail { */ [[nodiscard]] std::optional VariationIndex() const; + /** + * @return True if the evaluation resulted in an error. + */ + [[nodiscard]] bool IsError() const; + /** * @return A reference to the reason for the results. */ diff --git a/libs/common/include/launchdarkly/detail/c_binding_helpers.hpp b/libs/common/include/launchdarkly/detail/c_binding_helpers.hpp index 99772b9b6..2f170ec9b 100644 --- a/libs/common/include/launchdarkly/detail/c_binding_helpers.hpp +++ b/libs/common/include/launchdarkly/detail/c_binding_helpers.hpp @@ -7,7 +7,7 @@ #include #include -namespace launchdarkly { +namespace launchdarkly::detail { template struct has_result_type : std::false_type {}; @@ -106,5 +106,5 @@ bool OptReturnReinterpretCast(std::optional& opt, #define LD_ASSERT_NOT_NULL(param) LD_ASSERT(param != nullptr) -} // namespace launchdarkly +} // namespace launchdarkly::detail // NOLINTEND cppcoreguidelines-pro-type-reinterpret-cast diff --git a/libs/common/include/launchdarkly/detail/unreachable.hpp b/libs/common/include/launchdarkly/detail/unreachable.hpp new file mode 100644 index 000000000..f0dff1993 --- /dev/null +++ b/libs/common/include/launchdarkly/detail/unreachable.hpp @@ -0,0 +1,14 @@ +namespace launchdarkly::detail { + +// This may be replaced with a standard routine when C++23 is available. +[[noreturn]] inline void unreachable() { +// Uses compiler specific extensions if possible. +// Even if no extension is used, undefined behavior is still raised by +// an empty function body and the noreturn attribute. +#if defined(__GNUC__) // GCC, Clang, ICC + __builtin_unreachable(); +#elif defined(_MSC_VER) // MSVC + __assume(false); +#endif +} +} // namespace launchdarkly::detail diff --git a/libs/common/include/launchdarkly/value.hpp b/libs/common/include/launchdarkly/value.hpp index 92a026d1c..845453451 100644 --- a/libs/common/include/launchdarkly/value.hpp +++ b/libs/common/include/launchdarkly/value.hpp @@ -119,7 +119,7 @@ class Value final { using iterator_category = std::forward_iterator_tag; using difference_type = std::ptrdiff_t; - using value_type = std::pair; + using value_type = std::pair; using pointer = value_type const*; using reference = value_type const&; @@ -374,7 +374,7 @@ class Value final { static Value const& Null(); friend std::ostream& operator<<(std::ostream& out, Value const& value) { - switch (value.type_) { + switch (value.Type()) { case Type::kNull: out << "null()"; break; @@ -409,16 +409,17 @@ class Value final { operator int() const { return AsInt(); } private: - std::variant storage_; - enum Type type_; + struct null_type {}; + + std::variant storage_; // Empty constants used when accessing the wrong type. // These are not inline static const because of this bug: // https://developercommunity.visualstudio.com/t/inline-static-destructors-are-called-multiple-time/1157794 - static const std::string empty_string_; - static const Array empty_vector_; - static const Object empty_map_; - static const Value null_value_; + static std::string const empty_string_; + static Array const empty_vector_; + static Object const empty_map_; + static Value const null_value_; }; bool operator==(Value const& lhs, Value const& rhs); diff --git a/libs/common/package.json b/libs/common/package.json index a5898337e..9fe6295df 100644 --- a/libs/common/package.json +++ b/libs/common/package.json @@ -1,6 +1,6 @@ { "name": "launchdarkly-cpp-common", "description": "This package.json exists for modeling dependencies for the release process.", - "version": "0.3.6", + "version": "0.5.0", "private": true } diff --git a/libs/common/src/attribute_reference.cpp b/libs/common/src/attribute_reference.cpp index c0594a5e3..3a8fcf095 100644 --- a/libs/common/src/attribute_reference.cpp +++ b/libs/common/src/attribute_reference.cpp @@ -235,10 +235,11 @@ AttributeReference::AttributeReference() : AttributeReference("") {} std::string AttributeReference::PathToStringReference( std::vector path) { // Approximate size to reduce resizes. - auto size = std::accumulate(path.begin(), path.end(), 0, - [](auto sum, auto const& component) { - return sum + component.size() + 1; - }); + std::size_t size = + std::accumulate(path.begin(), path.end(), std::size_t{0}, + [](std::size_t sum, auto const& component) { + return sum + component.size() + 1; + }); std::string redaction_name; redaction_name.reserve(size); diff --git a/libs/common/src/bindings/c/data/evaluation_detail.cpp b/libs/common/src/bindings/c/data/evaluation_detail.cpp index c91c17f1d..c565e70e4 100644 --- a/libs/common/src/bindings/c/data/evaluation_detail.cpp +++ b/libs/common/src/bindings/c/data/evaluation_detail.cpp @@ -11,6 +11,7 @@ #define FROM_REASON(ptr) (reinterpret_cast(ptr)); using namespace launchdarkly; +using namespace launchdarkly::detail; LD_EXPORT(void) LDEvalDetail_Free(LDEvalDetail detail) { diff --git a/libs/common/src/bindings/c/value.cpp b/libs/common/src/bindings/c/value.cpp index ffbc75153..484d9897f 100644 --- a/libs/common/src/bindings/c/value.cpp +++ b/libs/common/src/bindings/c/value.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include using launchdarkly::Value; @@ -59,7 +60,7 @@ LD_EXPORT(enum LDValueType) LDValue_Type(LDValue val) { case Value::Type::kArray: return LDValueType_Array; } - LD_ASSERT(!"Unsupported value type."); + launchdarkly::detail::unreachable(); } LD_EXPORT(bool) LDValue_GetBool(LDValue val) { diff --git a/libs/common/src/data/evaluation_detail.cpp b/libs/common/src/data/evaluation_detail.cpp index c6415848d..22e383f03 100644 --- a/libs/common/src/data/evaluation_detail.cpp +++ b/libs/common/src/data/evaluation_detail.cpp @@ -35,11 +35,6 @@ 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_; @@ -54,11 +49,6 @@ template return reason_.has_value() && reason_->ErrorKind().has_value(); } -template -EvaluationDetail::operator bool() const { - return !IsError(); -} - template class EvaluationDetail; template class EvaluationDetail; template class EvaluationDetail; diff --git a/libs/common/src/value.cpp b/libs/common/src/value.cpp index 46b8b85c0..780d1f352 100644 --- a/libs/common/src/value.cpp +++ b/libs/common/src/value.cpp @@ -1,5 +1,3 @@ -#pragma clang diagnostic push - #include #include @@ -7,57 +5,76 @@ namespace launchdarkly { -const std::string Value::empty_string_; -const Value::Array Value::empty_vector_; -const Value::Object Value::empty_map_; -const Value Value::null_value_; +std::string const Value::empty_string_; +Value::Array const Value::empty_vector_; +Value::Object const Value::empty_map_; +Value const Value::null_value_; + +Value::Value() : storage_{null_type{}} {} -Value::Value() : type_(Value::Type::kNull), storage_{0.0} {} +Value::Value(bool boolean) : storage_{boolean} {} -Value::Value(bool boolean) : type_(Value::Type::kBool), storage_{boolean} {} +Value::Value(double num) : storage_{num} {} -Value::Value(double num) : type_(Value::Type::kNumber), storage_{num} {} +Value::Value(int num) : storage_{(double)num} {} -Value::Value(int num) : type_(Value::Type::kNumber), storage_{(double)num} {} +Value::Value(std::string str) : storage_{std::move(str)} {} -Value::Value(std::string str) - : type_(Value::Type::kString), storage_{std::move(str)} {} +Value::Value(char const* str) : storage_{std::string(str)} {} -Value::Value(char const* str) - : type_(Value::Type::kString), storage_{std::string(str)} {} +Value::Value(std::vector arr) : storage_{std::move(arr)} {} -Value::Value(std::vector arr) - : type_(Value::Type::kArray), storage_{std::move(arr)} {} +Value::Value(std::map obj) : storage_{std::move(obj)} {} -Value::Value(std::map obj) - : type_(Value::Type::kObject), storage_{std::move(obj)} {} +template +inline constexpr bool always_false_v = false; enum Value::Type Value::Type() const { - return type_; + return std::visit( + [](auto const& arg) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + return Type::kNull; + } else if constexpr (std::is_same_v) { + return Type::kBool; + } else if constexpr (std::is_same_v) { + return Type::kNumber; + } else if constexpr (std::is_same_v) { + return Type::kString; + } else if constexpr (std::is_same_v) { + return Type::kArray; + } else if constexpr (std::is_same_v) { + return Type::kObject; + } else { + static_assert(always_false_v, + "all value types must be visited"); + } + }, + storage_); } bool Value::IsNull() const { - return type_ == Type::kNull; + return std::holds_alternative(storage_); } bool Value::IsBool() const { - return type_ == Type::kBool; + return std::holds_alternative(storage_); } bool Value::IsNumber() const { - return type_ == Type::kNumber; + return std::holds_alternative(storage_); } bool Value::IsString() const { - return type_ == Type::kString; + return std::holds_alternative(storage_); } bool Value::IsArray() const { - return type_ == Type::kArray; + return std::holds_alternative(storage_); } bool Value::IsObject() const { - return type_ == Type::kObject; + return std::holds_alternative(storage_); } Value const& Value::Null() { @@ -67,42 +84,42 @@ Value const& Value::Null() { } bool Value::AsBool() const { - if (type_ == Type::kBool) { + if (IsBool()) { return std::get(storage_); } return false; } int Value::AsInt() const { - if (type_ == Type::kNumber) { + if (IsNumber()) { return static_cast(std::get(storage_)); } return 0; } double Value::AsDouble() const { - if (type_ == Type::kNumber) { + if (IsNumber()) { return std::get(storage_); } return 0.0; } std::string const& Value::AsString() const { - if (type_ == Type::kString) { + if (IsString()) { return std::get(storage_); } return empty_string_; } Value::Array const& Value::AsArray() const { - if (type_ == Type::kArray) { + if (IsArray()) { return std::get(storage_); } return empty_vector_; } Value::Object const& Value::AsObject() const { - if (type_ == Type::kObject) { + if (IsObject()) { return std::get(storage_); } return empty_map_; @@ -110,19 +127,17 @@ Value::Object const& Value::AsObject() const { Value::Value(std::optional opt_str) : storage_{0.0} { if (opt_str.has_value()) { - type_ = Type::kString; storage_ = opt_str.value(); } else { - type_ = Type::kNull; + storage_ = null_type{}; } } Value::Value(std::initializer_list values) - : type_(Type::kArray), storage_(std::vector(values)) {} + : storage_(std::vector(values)) {} + +Value::Value(Value::Array arr) : storage_(std::move(arr)) {} -Value::Value(Value::Array arr) - : storage_(std::move(arr)), type_(Type::kArray) {} -Value::Value(Value::Object obj) - : storage_(std::move(obj)), type_(Type::kObject) {} +Value::Value(Value::Object obj) : storage_(std::move(obj)) {} Value::Array::Iterator::Iterator(std::vector::const_iterator iterator) : iterator_(iterator) {} diff --git a/libs/internal/CHANGELOG.md b/libs/internal/CHANGELOG.md index 5feb50541..6298b1fa8 100644 --- a/libs/internal/CHANGELOG.md +++ b/libs/internal/CHANGELOG.md @@ -46,6 +46,55 @@ * dependencies * launchdarkly-cpp-common bumped from 0.3.5 to 0.3.6 +## [0.3.0](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-internal-v0.2.0...launchdarkly-cpp-internal-v0.3.0) (2023-10-23) + + +### Features + +* server-side SDK ([#160](https://github.com/launchdarkly/cpp-sdks/issues/160)) ([75eece3](https://github.com/launchdarkly/cpp-sdks/commit/75eece3a46870fdb6bf4384c112700558099c4d1)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * launchdarkly-cpp-common bumped from 0.4.0 to 0.5.0 + +## [0.2.0](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-internal-v0.1.11...launchdarkly-cpp-internal-v0.2.0) (2023-10-13) + + +### Features + +* clean up LD CMake variables & allow for OpenSSL dynamic link ([#255](https://github.com/launchdarkly/cpp-sdks/issues/255)) ([ed23c9a](https://github.com/launchdarkly/cpp-sdks/commit/ed23c9a347665529a09d18111bb9d3b699381728)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * launchdarkly-cpp-common bumped from 0.3.7 to 0.4.0 + +## [0.1.11](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-internal-v0.1.10...launchdarkly-cpp-internal-v0.1.11) (2023-10-11) + + +### Bug Fixes + +* treat warnings as errors in CI ([#253](https://github.com/launchdarkly/cpp-sdks/issues/253)) ([7f4f168](https://github.com/launchdarkly/cpp-sdks/commit/7f4f168f47619d7fa8b8952feade485261c69049)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * launchdarkly-cpp-common bumped from 0.3.6 to 0.3.7 + +## [0.1.10](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-internal-v0.1.9...launchdarkly-cpp-internal-v0.1.10) (2023-09-21) + + +### Bug Fixes + +* catch exception if en_US.utf8-locale missing when parsing datetime headers ([#251](https://github.com/launchdarkly/cpp-sdks/issues/251)) ([eb2a8f0](https://github.com/launchdarkly/cpp-sdks/commit/eb2a8f093996361541e11659165cbecc94c15346)) + ## [0.1.5](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-internal-v0.1.4...launchdarkly-cpp-internal-v0.1.5) (2023-06-30) diff --git a/libs/internal/CMakeLists.txt b/libs/internal/CMakeLists.txt index 1df835d0f..2be93f630 100644 --- a/libs/internal/CMakeLists.txt +++ b/libs/internal/CMakeLists.txt @@ -28,6 +28,6 @@ include(FetchContent) # Add main SDK sources. add_subdirectory(src) -if (BUILD_TESTING) +if (LD_BUILD_UNIT_TESTS) add_subdirectory(tests) endif () diff --git a/libs/internal/include/launchdarkly/data_model/segment.hpp b/libs/internal/include/launchdarkly/data_model/segment.hpp index 559eb0098..74405c3df 100644 --- a/libs/internal/include/launchdarkly/data_model/segment.hpp +++ b/libs/internal/include/launchdarkly/data_model/segment.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -16,9 +17,8 @@ namespace launchdarkly::data_model { struct Segment { - using Kind = std::string; struct Target { - Kind contextKind; + ContextKind contextKind; std::vector values; }; @@ -43,12 +43,9 @@ struct Segment { std::vector rules; std::optional salt; bool unbounded; - std::optional unboundedContextKind; + std::optional unboundedContextKind; std::optional generation; - // TODO(sc209882): in data model, ensure empty Kind string is error - // condition. - /** * Returns the segment's version. Satisfies ItemDescriptor template * constraints. diff --git a/libs/internal/include/launchdarkly/events/data/common_events.hpp b/libs/internal/include/launchdarkly/events/data/common_events.hpp index b8740319d..c9670df30 100644 --- a/libs/internal/include/launchdarkly/events/data/common_events.hpp +++ b/libs/internal/include/launchdarkly/events/data/common_events.hpp @@ -33,7 +33,8 @@ struct TrackEventParams { std::optional metric_value; }; -struct ServerTrackEventParams : public TrackEventParams { +struct ServerTrackEventParams { + TrackEventParams base; Context context; }; @@ -78,11 +79,13 @@ struct FeatureEventBase { explicit FeatureEventBase(FeatureEventParams const& params); }; -struct FeatureEvent : public FeatureEventBase { +struct FeatureEvent { + FeatureEventBase base; ContextKeys context_keys; }; -struct DebugEvent : public FeatureEventBase { +struct DebugEvent { + FeatureEventBase base; EventContext context; }; diff --git a/libs/internal/include/launchdarkly/events/detail/parse_date_header.hpp b/libs/internal/include/launchdarkly/events/detail/parse_date_header.hpp index 972270c0b..65c91151e 100644 --- a/libs/internal/include/launchdarkly/events/detail/parse_date_header.hpp +++ b/libs/internal/include/launchdarkly/events/detail/parse_date_header.hpp @@ -8,14 +8,17 @@ namespace launchdarkly::events::detail { template + static std::optional ParseDateHeader( - std::string const& datetime) { + std::string const& datetime, + std::locale const& locale) { // The following comments may not be entirely accurate. // TODO: There must be a better way. std::tm gmt_tm = {}; + std::istringstream string_stream(datetime); - string_stream.imbue(std::locale("en_US.utf-8")); + string_stream.imbue(locale); string_stream >> std::get_time(&gmt_tm, "%a, %d %b %Y %H:%M:%S GMT"); if (string_stream.fail()) { return std::nullopt; diff --git a/libs/internal/include/launchdarkly/events/detail/request_worker.hpp b/libs/internal/include/launchdarkly/events/detail/request_worker.hpp index d4fac42f3..9046d49eb 100644 --- a/libs/internal/include/launchdarkly/events/detail/request_worker.hpp +++ b/libs/internal/include/launchdarkly/events/detail/request_worker.hpp @@ -24,7 +24,7 @@ enum class State { PermanentlyFailed = 4, }; -std::ostream& operator<<(std::ostream& out, State const& s); +std::ostream& operator<<(std::ostream& out, State const& state); enum class Action { /* No action necessary. */ @@ -39,7 +39,7 @@ enum class Action { NotifyPermanentFailure = 4, }; -std::ostream& operator<<(std::ostream& out, Action const& s); +std::ostream& operator<<(std::ostream& out, Action const& state); /** * Computes the next (state, action) pair from an existing state and an HTTP @@ -99,12 +99,13 @@ class RequestWorker { RequestWorker(boost::asio::any_io_executor io, std::chrono::milliseconds retry_after, std::size_t id, + std::optional date_header_locale, Logger& logger); /** * Returns true if the worker is available for delivery. */ - bool Available() const; + [[nodiscard]] bool Available() const; /** * Passes an EventBatch to the worker for delivery. The delivery may be @@ -135,10 +136,10 @@ class RequestWorker { << batch_->Target() << " with payload: " << batch_->Request().Body().value_or("(no body)"); - requester_.Request( - batch_->Request(), [this, handler](network::HttpResult result) { - OnDeliveryAttempt(std::move(result), std::move(handler)); - }); + requester_.Request(batch_->Request(), + [this, handler](network::HttpResult const& result) { + OnDeliveryAttempt(result, std::move(handler)); + }); return result.get(); } @@ -163,9 +164,14 @@ class RequestWorker { /* Tag used in logs. */ std::string tag_; + /* The en_US locale is used to parse the Date header from HTTP responses. + * On some platforms, this may not be available hence the optional. */ + std::optional date_header_locale_; + Logger& logger_; - void OnDeliveryAttempt(network::HttpResult request, ResultCallback cb); + void OnDeliveryAttempt(network::HttpResult const& request, + ResultCallback cb); }; } // namespace launchdarkly::events::detail diff --git a/libs/internal/include/launchdarkly/events/detail/worker_pool.hpp b/libs/internal/include/launchdarkly/events/detail/worker_pool.hpp index 9b29e32df..a6a08441d 100644 --- a/libs/internal/include/launchdarkly/events/detail/worker_pool.hpp +++ b/libs/internal/include/launchdarkly/events/detail/worker_pool.hpp @@ -23,8 +23,6 @@ namespace launchdarkly::events::detail { */ class WorkerPool { public: - using ServerTimeCallback = - std::function; /** * Constructs a new WorkerPool. * @param io The executor used for all workers. diff --git a/libs/internal/include/launchdarkly/network/asio_requester.hpp b/libs/internal/include/launchdarkly/network/asio_requester.hpp index bc231b2c0..5c5e56ea7 100644 --- a/libs/internal/include/launchdarkly/network/asio_requester.hpp +++ b/libs/internal/include/launchdarkly/network/asio_requester.hpp @@ -2,6 +2,8 @@ #include "http_requester.hpp" +#include + #include #include #include @@ -44,6 +46,10 @@ static bool NeedsRedirect(HttpResult const& res) { res.Status() == 308 && res.Headers().count("location") != 0; } +/** + * Converts the given HttpMethod to a boost beast HTTP verb. + * If the verb is unrecognized, returns http::verb::get. + */ static http::verb ConvertMethod(HttpMethod method) { switch (method) { case HttpMethod::kPost: @@ -55,7 +61,7 @@ static http::verb ConvertMethod(HttpMethod method) { case HttpMethod::kPut: return http::verb::put; } - assert(!"Method not found. Ensure all method cases covered."); + launchdarkly::detail::unreachable(); } static http::request MakeBeastRequest( diff --git a/libs/internal/include/launchdarkly/serialization/json_context_kind.hpp b/libs/internal/include/launchdarkly/serialization/json_context_kind.hpp index c7046a00b..2551a65c9 100644 --- a/libs/internal/include/launchdarkly/serialization/json_context_kind.hpp +++ b/libs/internal/include/launchdarkly/serialization/json_context_kind.hpp @@ -16,4 +16,13 @@ tl::expected, JsonError> tag_invoke( unused, boost::json::value const& json_value); +// Serializers need to be in launchdarkly::data_model for ADL. +namespace data_model { + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::ContextKind const& segment); + +} + } // namespace launchdarkly diff --git a/libs/internal/include/launchdarkly/serialization/value_mapping.hpp b/libs/internal/include/launchdarkly/serialization/value_mapping.hpp index 753146d1a..09d90b4e2 100644 --- a/libs/internal/include/launchdarkly/serialization/value_mapping.hpp +++ b/libs/internal/include/launchdarkly/serialization/value_mapping.hpp @@ -162,7 +162,7 @@ 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()); + obj.emplace(key, boost::json::value_from(val.value())); } } diff --git a/libs/internal/package.json b/libs/internal/package.json index c474b1b36..4c46323b5 100644 --- a/libs/internal/package.json +++ b/libs/internal/package.json @@ -1,9 +1,9 @@ { "name": "launchdarkly-cpp-internal", "description": "This package.json exists for modeling dependencies for the release process.", - "version": "0.1.9", + "version": "0.3.0", "private": true, "dependencies": { - "launchdarkly-cpp-common": "0.3.6" + "launchdarkly-cpp-common": "0.5.0" } } diff --git a/libs/internal/src/CMakeLists.txt b/libs/internal/src/CMakeLists.txt index e156a8e72..03d499c20 100644 --- a/libs/internal/src/CMakeLists.txt +++ b/libs/internal/src/CMakeLists.txt @@ -50,6 +50,11 @@ add_library(${LIBNAME} OBJECT add_library(launchdarkly::internal ALIAS ${LIBNAME}) +# TODO(SC-209963): Remove once OpenSSL deprecated hash function usage has been updated +target_compile_options(${LIBNAME} PRIVATE + $<$,$,$>: + -Wno-deprecated-declarations> +) set_property(TARGET ${LIBNAME} PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL") diff --git a/libs/internal/src/events/asio_event_processor.cpp b/libs/internal/src/events/asio_event_processor.cpp index c9bfd8cc4..d8512af47 100644 --- a/libs/internal/src/events/asio_event_processor.cpp +++ b/libs/internal/src/events/asio_event_processor.cpp @@ -280,15 +280,12 @@ std::vector AsioEventProcessor::Process( if (!context_key_cache_.Notice( event.context.CanonicalKey())) { out.emplace_back(server_side::IndexEvent{ - event.creation_date, + event.base.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)); + out.emplace_back(std::move(event.base)); }}, std::move(input_event)); diff --git a/libs/internal/src/events/request_worker.cpp b/libs/internal/src/events/request_worker.cpp index ac8c32d79..b39090294 100644 --- a/libs/internal/src/events/request_worker.cpp +++ b/libs/internal/src/events/request_worker.cpp @@ -6,13 +6,15 @@ namespace launchdarkly::events::detail { RequestWorker::RequestWorker(boost::asio::any_io_executor io, std::chrono::milliseconds retry_after, std::size_t id, + std::optional date_header_locale, Logger& logger) - : timer_(io), + : timer_(std::move(io)), retry_delay_(retry_after), state_(State::Idle), requester_(timer_.get_executor()), batch_(std::nullopt), tag_("flush-worker[" + std::to_string(id) + "]: "), + date_header_locale_(std::move(date_header_locale)), logger_(logger) {} bool RequestWorker::Available() const { @@ -47,7 +49,7 @@ static bool IsSuccess(network::HttpResult const& result) { http::status_class::successful; } -void RequestWorker::OnDeliveryAttempt(network::HttpResult result, +void RequestWorker::OnDeliveryAttempt(network::HttpResult const& result, ResultCallback callback) { auto [next_state, action] = NextState(state_, result); @@ -81,11 +83,15 @@ void RequestWorker::OnDeliveryAttempt(network::HttpResult result, batch_.reset(); break; case Action::ParseDateAndReset: { + if (!date_header_locale_) { + batch_.reset(); + break; + } auto headers = result.Headers(); if (auto date = headers.find("Date"); date != headers.end()) { if (auto server_time = ParseDateHeader( - date->second)) { + date->second, *date_header_locale_)) { callback(batch_->Count(), *server_time); } } diff --git a/libs/internal/src/events/worker_pool.cpp b/libs/internal/src/events/worker_pool.cpp index 105495310..caa4a2b70 100644 --- a/libs/internal/src/events/worker_pool.cpp +++ b/libs/internal/src/events/worker_pool.cpp @@ -5,14 +5,37 @@ namespace launchdarkly::events::detail { +std::optional GetLocale(std::string const& locale, + std::string const& tag, + Logger& logger) { + try { + return std::locale(locale); + } catch (std::runtime_error) { + LD_LOG(logger, LogLevel::kWarn) + << tag << " couldn't load " << locale + << " locale. If debug events are enabled, they may be emitted for " + "longer than expected"; + return std::nullopt; + } +} + WorkerPool::WorkerPool(boost::asio::any_io_executor io, std::size_t pool_size, std::chrono::milliseconds delivery_retry_delay, Logger& logger) : io_(io), workers_() { + // The en_US.utf-8 locale is used whenever a date is parsed from the HTTP + // headers returned by the event-delivery endpoints. If the locale is + // unavailable, then the workers will skip the parsing step. + // + // This may result in debug events being emitted for longer than expected + // if the host's time is way out of sync. + std::optional date_header_locale = + GetLocale("en_US.utf-8", "event-processor", logger); + for (std::size_t i = 0; i < pool_size; i++) { workers_.emplace_back(std::make_unique( - io_, delivery_retry_delay, i, logger)); + io_, delivery_retry_delay, i, date_header_locale, logger)); } } diff --git a/libs/internal/src/serialization/events/json_events.cpp b/libs/internal/src/serialization/events/json_events.cpp index 36a9b4553..a7ccaf303 100644 --- a/libs/internal/src/serialization/events/json_events.cpp +++ b/libs/internal/src/serialization/events/json_events.cpp @@ -6,7 +6,7 @@ namespace launchdarkly::events { void tag_invoke(boost::json::value_from_tag const& tag, boost::json::value& json_value, FeatureEvent const& event) { - auto base = boost::json::value_from(event); + auto base = boost::json::value_from(event.base); base.as_object().emplace("kind", "feature"); base.as_object().emplace("contextKeys", boost::json::value_from(event.context_keys)); @@ -16,7 +16,7 @@ void tag_invoke(boost::json::value_from_tag const& tag, void tag_invoke(boost::json::value_from_tag const& tag, boost::json::value& json_value, DebugEvent const& event) { - auto base = boost::json::value_from(event); + auto base = boost::json::value_from(event.base); base.as_object().emplace("kind", "debug"); base.as_object().emplace("context", boost::json::value_from(event.context)); json_value = std::move(base); diff --git a/libs/internal/src/serialization/json_context_kind.cpp b/libs/internal/src/serialization/json_context_kind.cpp index 96f8e6114..b69aaa226 100644 --- a/libs/internal/src/serialization/json_context_kind.cpp +++ b/libs/internal/src/serialization/json_context_kind.cpp @@ -21,4 +21,14 @@ tl::expected, JsonError> tag_invoke( return data_model::ContextKind(str.c_str()); } + +namespace data_model { +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::ContextKind const& context_kind) { + boost::ignore_unused(unused); + json_value.emplace_string() = context_kind.t; +} +} // namespace data_model } // namespace launchdarkly +>>>>>>> main diff --git a/libs/internal/src/serialization/json_flag.cpp b/libs/internal/src/serialization/json_flag.cpp index 47429e2bd..1d040c1fa 100644 --- a/libs/internal/src/serialization/json_flag.cpp +++ b/libs/internal/src/serialization/json_flag.cpp @@ -1,8 +1,6 @@ #include #include -#include #include -#include #include #include #include @@ -228,7 +226,7 @@ void tag_invoke(boost::json::value_from_tag const& unused, data_model::Flag::Rollout::Kind const& kind) { switch (kind) { case Flag::Rollout::Kind::kUnrecognized: - // TODO: Should we be preserving the original string. + // TODO(SC-222050) break; case Flag::Rollout::Kind::kExperiment: json_value.emplace_string() = "experiment"; @@ -246,7 +244,7 @@ void tag_invoke(boost::json::value_from_tag const& unused, 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. + // TODO(SC-222050) obj.emplace("kind", boost::json::value_from(rollout.kind)); } WriteMinimal(obj, "seed", rollout.seed); diff --git a/libs/internal/src/serialization/json_segment.cpp b/libs/internal/src/serialization/json_segment.cpp index 157d26521..fa927b57a 100644 --- a/libs/internal/src/serialization/json_segment.cpp +++ b/libs/internal/src/serialization/json_segment.cpp @@ -20,7 +20,10 @@ tl::expected, JsonError> tag_invoke( data_model::Segment::Target target{}; - PARSE_FIELD_DEFAULT(target.contextKind, obj, "contextKind", "user"); + // The zero value of a ContextKind ("" - empty string) is not valid in the + // domain of possible contexts. This field is parsed as REQUIRED to + // specify that fact. + PARSE_REQUIRED_FIELD(target.contextKind, obj, "contextKind"); PARSE_FIELD(target.values, obj, "values"); @@ -125,7 +128,7 @@ void tag_invoke(boost::json::value_from_tag const& unused, 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); + obj.emplace("contextKind", boost::json::value_from(target.contextKind)); } void tag_invoke(boost::json::value_from_tag const& unused, diff --git a/libs/internal/src/serialization/json_value.cpp b/libs/internal/src/serialization/json_value.cpp index c04fae3d2..2e0b76ca0 100644 --- a/libs/internal/src/serialization/json_value.cpp +++ b/libs/internal/src/serialization/json_value.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -62,7 +63,7 @@ tl::expected, JsonError> tag_invoke( } // The above switch is exhaustive, so this can only happen if a new // type is added to boost::json::value. - assert(!"All types need to be handled."); + launchdarkly::detail::unreachable(); } Value tag_invoke(boost::json::value_to_tag const&, diff --git a/libs/internal/tests/data_model_serialization_test.cpp b/libs/internal/tests/data_model_serialization_test.cpp index 02cf3c90e..c8965e16d 100644 --- a/libs/internal/tests/data_model_serialization_test.cpp +++ b/libs/internal/tests/data_model_serialization_test.cpp @@ -637,7 +637,7 @@ TEST(FlagTests, SerializeAll) { } TEST(SegmentTargetTests, SerializeAll) { - data_model::Segment::Target target{"bob", {"bill", "sam"}}; + data_model::Segment::Target target{ContextKind("bob"), {"bill", "sam"}}; auto json = boost::json::value_from(target); auto expected = boost::json::parse( @@ -683,8 +683,8 @@ TEST(SegmentTests, SerializeBasicAll) { 87, {"bob", "sam"}, {"sally", "johan"}, - {{"vegetable", {"potato", "yam"}}}, - {{"material", {"cardboard", "plastic"}}}, + {{ContextKind("vegetable"), {"potato", "yam"}}}, + {{ContextKind("material"), {"cardboard", "plastic"}}}, {{{{data_model::Clause::Op::kIn, {"a", "b"}, true, @@ -730,8 +730,9 @@ TEST(SegmentTests, SerializeBasicAll) { } TEST(SegmentTests, SerializeUnbounded) { - data_model::Segment segment{"my-segment", 87, {}, {}, {}, {}, - {}, "salty", true, "company", 12}; + data_model::Segment segment{ + "my-segment", 87, {}, {}, {}, {}, {}, "salty", true, + ContextKind("company"), 12}; auto json = boost::json::value_from(segment); auto expected = boost::json::parse( diff --git a/libs/internal/tests/event_processor_test.cpp b/libs/internal/tests/event_processor_test.cpp index f8962ee08..226ebb3ef 100644 --- a/libs/internal/tests/event_processor_test.cpp +++ b/libs/internal/tests/event_processor_test.cpp @@ -58,9 +58,15 @@ TEST(WorkerPool, PoolReturnsNullptrWhenNoWorkerAvaialable) { ioc_thread.join(); } +class EventProcessorTests : public ::testing::Test { + public: + EventProcessorTests() : locale("en_US.utf-8") {} + std::locale locale; +}; + // This test is a temporary test that exists only to ensure the event processor // compiles; it should be replaced by more robust tests (and contract tests.) -TEST(EventProcessorTests, ProcessorCompiles) { +TEST_F(EventProcessorTests, ProcessorCompiles) { using namespace launchdarkly; Logger logger{ @@ -94,11 +100,12 @@ TEST(EventProcessorTests, ProcessorCompiles) { ioc_thread.join(); } -TEST(EventProcessorTests, ParseValidDateHeader) { +TEST_F(EventProcessorTests, ParseValidDateHeader) { using namespace launchdarkly; using Clock = std::chrono::system_clock; - auto date = detail::ParseDateHeader("Wed, 21 Oct 2015 07:28:00 GMT"); + auto date = events::detail::ParseDateHeader( + "Wed, 21 Oct 2015 07:28:00 GMT", locale); ASSERT_TRUE(date); @@ -106,21 +113,23 @@ TEST(EventProcessorTests, ParseValidDateHeader) { std::chrono::microseconds(1445412480000000)); } -TEST(EventProcessorTests, ParseInvalidDateHeader) { +TEST_F(EventProcessorTests, ParseInvalidDateHeader) { using namespace launchdarkly; - auto not_a_date = detail::ParseDateHeader( - "this is definitely not a date"); + auto not_a_date = + events::detail::ParseDateHeader( + "this is definitely not a date", locale); ASSERT_FALSE(not_a_date); - auto not_gmt = detail::ParseDateHeader( - "Wed, 21 Oct 2015 07:28:00 PST"); + auto not_gmt = events::detail::ParseDateHeader( + "Wed, 21 Oct 2015 07:28:00 PST", locale); ASSERT_FALSE(not_gmt); - auto missing_year = detail::ParseDateHeader( - "Wed, 21 Oct 07:28:00 GMT"); + auto missing_year = + events::detail::ParseDateHeader( + "Wed, 21 Oct 07:28:00 GMT", locale); ASSERT_FALSE(missing_year); } diff --git a/libs/server-sdk/CMakeLists.txt b/libs/server-sdk/CMakeLists.txt index 5f41ab6ba..312f24998 100644 --- a/libs/server-sdk/CMakeLists.txt +++ b/libs/server-sdk/CMakeLists.txt @@ -6,7 +6,7 @@ cmake_minimum_required(VERSION 3.19) project( LaunchDarklyCPPServer - VERSION 0.1 + VERSION 0.1.0 # {x-release-please-version} DESCRIPTION "LaunchDarkly C++ Server SDK" LANGUAGES CXX C ) @@ -24,7 +24,7 @@ endif () #set(CMAKE_FILES "${CMAKE_CURRENT_SOURCE_DIR}/cmake") #set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_FILES}) -# Needed to fetch external dependencies. +# Needed to fetch external dependencies. include(FetchContent) # Needed to parse RFC3339 dates in flag rules. @@ -33,6 +33,6 @@ include(${CMAKE_FILES}/rfc3339_timestamp.cmake) # Add main SDK sources. add_subdirectory(src) -if (BUILD_TESTING) +if (LD_BUILD_UNIT_TESTS) add_subdirectory(tests) endif () diff --git a/libs/server-sdk/README.md b/libs/server-sdk/README.md index 25ad3bf40..a758b532c 100644 --- a/libs/server-sdk/README.md +++ b/libs/server-sdk/README.md @@ -71,6 +71,22 @@ gcc -I $(pwd)/include -Llib -fPIE -g main.c liblaunchdarkly-cpp-server.so The examples here are to help with getting started, but generally speaking the SDK should be incorporated using your build system (CMake for instance). +### CMake Usage + +First, add the SDK to your project: + +```cmake +add_subdirectory(path-to-sdk-repo) +``` + +Currently `find_package` is not yet supported. + +This will expose the `launchdarkly::server` target. Next, link the target to your executable or library: + +```cmake +target_link_libraries(my-target PRIVATE launchdarkly::server) +``` + Learn more ----------- 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 a8336bf07..c6f16f7ea 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 @@ -18,7 +18,7 @@ namespace launchdarkly::server_side::data_sources { /** * Enumeration of possible data source states. */ -enum class ServerDataSourceState { +enum class DataSourceState { /** * The initial state of the data source when the SDK is being * initialized. @@ -65,7 +65,7 @@ enum class ServerDataSourceState { }; using DataSourceStatus = - common::data_sources::DataSourceStatusBase; + common::data_sources::DataSourceStatusBase; /** * Interface for accessing and listening to the data source status. diff --git a/libs/server-sdk/include/launchdarkly/server_side/integrations/persistent_store_core.hpp b/libs/server-sdk/include/launchdarkly/server_side/integrations/persistent_store_core.hpp new file mode 100644 index 000000000..a49f389be --- /dev/null +++ b/libs/server-sdk/include/launchdarkly/server_side/integrations/persistent_store_core.hpp @@ -0,0 +1,214 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include + +namespace launchdarkly::server_side::integrations { + +/** + * A versioned item which can be stored in a persistent store. + */ +struct SerializedItemDescriptor { + uint64_t version; + + /** + * During an Init/Upsert, when this is true, the serializedItem will + * contain a tombstone representation. If the persistence implementation + * can efficiently store the deletion state, and version, then it may + * choose to discard the item. + */ + bool deleted; + + /** + * When reading from a persistent store the serializedItem may be + * std::nullopt for deleted items. + */ + std::optional serializedItem; +}; + +/** + * Represents a namespace of persistent data. + */ +class IPersistentKind { + public: + /** + * The namespace for the data. + */ + [[nodiscard]] virtual std::string const& Namespace(); + + /** + * Deserialize data and return the version of the data. + * + * This is for cases where the persistent store cannot avoid deserializing + * data to determine its version. For instance a Redis store where + * the only columns are the prefixed key and the serialized data. + * + * @param data The data to deserialize. + * @return The version of the data. + */ + [[nodiscard]] virtual uint64_t Version(std::string const& data); + + IPersistentKind(IPersistentKind const& item) = delete; + IPersistentKind(IPersistentKind&& item) = delete; + IPersistentKind& operator=(IPersistentKind const&) = delete; + IPersistentKind& operator=(IPersistentKind&&) = delete; + virtual ~IPersistentKind() = default; + + protected: + IPersistentKind() = default; +}; + +/** + * Interface for a data store that holds feature flags and related data in a + * serialized form. + * + * This interface should be used for database integrations, or any other data + * store implementation that stores data in some external service. + * The SDK will take care of converting between its own internal data model and + * a serialized string form; the data store interacts only with the serialized + * form. + * + * The SDK will also provide its own caching layer on top of the persistent data + * store; the data store implementation should not provide caching, but simply + * do every query or update that the SDK tells it to do. + * + * Implementations must be thread-safe. + */ +class IPersistentStoreCore { + public: + enum class InitResult { + /** + * The init operation completed successfully. + */ + kSuccess, + + /** + * There was an error with the init operation. + */ + kError, + }; + + enum class UpsertResult { + /** + * The upsert completed successfully. + */ + kSuccess, + + /** + * There was an error with the upsert operation. + */ + kError, + + /** + * The upsert did not encounter errors, but the version of the + * existing item was greater than that the version of the upsert item. + */ + kNotUpdated + }; + + struct Error { + std::string message; + }; + + using GetResult = + tl::expected, Error>; + + using AllResult = + tl::expected, + Error>; + + using ItemKey = std::string; + using KeyItemPair = std::pair; + using OrderedNamepace = std::vector; + using KindCollectionPair = + std::pair; + using OrderedData = std::vector; + + /** + * Overwrites the store's contents with a set of items for each collection. + * + * All previous data should be discarded, regardless of versioning. + * + * The update should be done atomically. If it cannot be done atomically, + * then the store must first add or update each item in the same order that + * they are given in the input data, and then delete any previously stored + * items that were not in the input data. + * + * @param allData The ordered set of data to replace all current data with. + * @return The status of the init operation. + */ + virtual InitResult Init(OrderedData const& allData) = 0; + + /** + * Updates or inserts an item in the specified collection. For updates, the + * object will only be updated if the existing version is less than the new + * version. + * + * @param kind The collection kind to use. + * @param itemKey The unique key for the item within the collection. + * @param item The item to insert or update. + * + * @return The status of the operation. + */ + virtual UpsertResult Upsert(IPersistentKind const& kind, + std::string const& itemKey, + SerializedItemDescriptor const& item) = 0; + + /** + * Retrieves an item from the specified collection, if available. + * + * @param kind The kind of the item. + * @param itemKey The key for the item. + * @return A serialized item descriptor if the item existed, a std::nullopt + * if the item did not exist, or an error. For a deleted item the serialized + * item descriptor may contain a std::nullopt for the serializedItem. + */ + virtual GetResult Get(IPersistentKind const& kind, + std::string const& itemKey) const = 0; + + /** + * Retrieves all items from the specified collection. + * + * If the store contains placeholders for deleted items, it should include + * them in the results, not filter them out. + * @param kind The kind of data to get. + * @return Either all of the items of the type, or an error. If there are + * no items of the specified type, then return an empty collection. + */ + virtual AllResult All(IPersistentKind const& kind) const = 0; + + /** + * Returns true if this store has been initialized. + * + * In a shared data store, the implementation should be able to detect this + * state even if Init was called in a different process, i.e. it must query + * the underlying data store in some way. The method does not need to worry + * about caching this value; the SDK will call it rarely. + * + * @return True if the store has been initialized. + */ + virtual bool Initialized() const = 0; + + /** + * A short description of the store, for instance "Redis". May be used + * in diagnostic information and logging. + * + * @return A short description of the sore. + */ + virtual std::string const& Description() const = 0; + + IPersistentStoreCore(IPersistentStoreCore const& item) = delete; + IPersistentStoreCore(IPersistentStoreCore&& item) = delete; + IPersistentStoreCore& operator=(IPersistentStoreCore const&) = delete; + IPersistentStoreCore& operator=(IPersistentStoreCore&&) = delete; + virtual ~IPersistentStoreCore() = default; + + protected: + IPersistentStoreCore() = default; +}; +} // namespace launchdarkly::server_side::integrations diff --git a/libs/server-sdk/package.json b/libs/server-sdk/package.json new file mode 100644 index 000000000..c6b6bc948 --- /dev/null +++ b/libs/server-sdk/package.json @@ -0,0 +1,10 @@ +{ + "name": "launchdarkly-cpp-server", + "description": "This package.json exists for modeling dependencies for the release process.", + "version": "0.1.0", + "private": true, + "dependencies": { + "launchdarkly-cpp-internal": "0.3.0", + "launchdarkly-cpp-common": "0.5.0" + } +} diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index 5734c9818..00569be24 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -4,9 +4,17 @@ file(GLOB HEADER_LIST CONFIGURE_DEPENDS "${LaunchDarklyCPPServer_SOURCE_DIR}/include/launchdarkly/server_side/integrations/*.hpp" ) -# Automatic library: static or dynamic based on user config. +if (LD_BUILD_SHARED_LIBS) + message(STATUS "LaunchDarkly: building server-sdk as shared library") + add_library(${LIBNAME} SHARED) +else () + message(STATUS "LaunchDarkly: building server-sdk as static library") + add_library(${LIBNAME} STATIC) +endif () + -add_library(${LIBNAME} +target_sources(${LIBNAME} + PRIVATE ${HEADER_LIST} boost.cpp client.cpp @@ -50,7 +58,7 @@ add_library(${LIBNAME} bindings/c/all_flags_state/all_flags_state.cpp ) -if (MSVC OR (NOT BUILD_SHARED_LIBS)) +if (MSVC OR (NOT LD_BUILD_SHARED_LIBS)) target_link_libraries(${LIBNAME} PUBLIC launchdarkly::common PRIVATE Boost::headers Boost::json Boost::url launchdarkly::sse launchdarkly::internal foxy timestamp) @@ -72,8 +80,9 @@ add_library(launchdarkly::server ALIAS ${LIBNAME}) set_property(TARGET ${LIBNAME} PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL") -install(TARGETS ${LIBNAME}) -if (BUILD_SHARED_LIBS AND MSVC) +# Optional in case only the client SDK is being built. +install(TARGETS ${LIBNAME} OPTIONAL) +if (LD_BUILD_SHARED_LIBS AND MSVC) install(FILES $ DESTINATION bin OPTIONAL) endif () # Using PUBLIC_HEADERS would flatten the include. 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 index 222ac27f7..25a095fdd 100644 --- 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 @@ -4,6 +4,7 @@ #include #include +#include "launchdarkly/serialization/value_mapping.hpp" namespace launchdarkly::server_side { @@ -15,23 +16,15 @@ void tag_invoke(boost::json::value_from_tag const& unused, if (!state.OmitDetails()) { obj.emplace("version", state.Version()); - - if (auto const& reason = state.Reason()) { - obj.emplace("reason", boost::json::value_from(*reason)); - } + WriteMinimal(obj, "reason", state.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); - } + WriteMinimal(obj, "trackEvents", state.TrackEvents()); + WriteMinimal(obj, "trackReason", state.TrackReason()); if (auto const& date = state.DebugEventsUntilDate()) { if (*date > 0) { diff --git a/libs/server-sdk/src/bindings/c/builder.cpp b/libs/server-sdk/src/bindings/c/builder.cpp index e6a7c2f75..461bcf204 100644 --- a/libs/server-sdk/src/bindings/c/builder.cpp +++ b/libs/server-sdk/src/bindings/c/builder.cpp @@ -54,7 +54,7 @@ LDServerConfigBuilder_Build(LDServerConfigBuilder b, LD_ASSERT_NOT_NULL(b); LD_ASSERT_NOT_NULL(out_config); - return launchdarkly::ConsumeBuilder(b, out_config); + return launchdarkly::detail::ConsumeBuilder(b, out_config); } LD_EXPORT(void) diff --git a/libs/server-sdk/src/boost.cpp b/libs/server-sdk/src/boost.cpp index 5c9ea02b1..608953363 100644 --- a/libs/server-sdk/src/boost.cpp +++ b/libs/server-sdk/src/boost.cpp @@ -1,5 +1,6 @@ -// This file is used to include boost url/json when building a shared library on linux/mac. -// Windows links static libs in this case and does not include these src files, as there -// are issues compiling the value.ipp file from JSON with MSVC. -#include +// This file is used to include boost url/json when building a shared library on +// linux/mac. Windows links static libs in this case and does not include these +// src files, as there are issues compiling the value.ipp file from JSON with +// MSVC. #include +#include diff --git a/libs/server-sdk/src/client_impl.cpp b/libs/server-sdk/src/client_impl.cpp index 5d3f9c149..5dfc5b56b 100644 --- a/libs/server-sdk/src/client_impl.cpp +++ b/libs/server-sdk/src/client_impl.cpp @@ -51,7 +51,6 @@ MakeDataSource(HttpProperties const& http_properties, auto data_source_properties = builder.Build(); if (config.DataSourceConfig().method.index() == 0) { - // TODO: use initial reconnect delay. return std::make_shared< launchdarkly::server_side::data_sources::StreamingDataSource>( config.ServiceEndpoints(), config.DataSourceConfig(), @@ -129,20 +128,13 @@ ClientImpl::ClientImpl(Config config, std::string const& version) run_thread_ = std::move(std::thread([&]() { ioc_.run(); })); } -// TODO: audit if this is correct for server -// Was an attempt made to initialize the data source, and did that attempt -// succeed? The data source being connected, or not being connected due to -// offline mode, both represent successful terminal states. static bool IsInitializedSuccessfully(DataSourceStatus::DataSourceState state) { return state == DataSourceStatus::DataSourceState::kValid; } -// TODO: audit if this is correct for server -// Was any attempt made to initialize the data source (with a successful or -// permanent failure outcome?) static bool IsInitialized(DataSourceStatus::DataSourceState state) { return IsInitializedSuccessfully(state) || - (state == DataSourceStatus::DataSourceState::kOff); + (state != DataSourceStatus::DataSourceState::kInitializing); } void ClientImpl::Identify(Context context) { @@ -453,7 +445,7 @@ data_sources::IDataSourceStatusProvider& ClientImpl::DataSourceStatus() { ClientImpl::~ClientImpl() { ioc_.stop(); - // TODO: Probably not the best. + // TODO(SC-219101) run_thread_.join(); } } // namespace launchdarkly::server_side 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 6f0b720d3..0e8b36884 100644 --- a/libs/server-sdk/src/data_sources/streaming_data_source.cpp +++ b/libs/server-sdk/src/data_sources/streaming_data_source.cpp @@ -1,7 +1,6 @@ #include #include #include -#include #include #include @@ -12,144 +11,142 @@ namespace launchdarkly::server_side::data_sources { - static char const *const kCouldNotParseEndpoint = - "Could not parse streaming endpoint URL"; - - static char const *DataSourceErrorToString(launchdarkly::sse::Error error) { - switch (error) { - case sse::Error::NoContent: - return "server responded 204 (No Content), will not attempt to " - "reconnect"; - case sse::Error::InvalidRedirectLocation: - return "server responded with an invalid redirection"; - case sse::Error::UnrecoverableClientError: - return "unrecoverable client-side error"; - default: - return "unrecognized error"; - } +static char const* const kCouldNotParseEndpoint = + "Could not parse streaming endpoint URL"; + +static char const* DataSourceErrorToString(launchdarkly::sse::Error error) { + switch (error) { + case sse::Error::NoContent: + return "server responded 204 (No Content), will not attempt to " + "reconnect"; + case sse::Error::InvalidRedirectLocation: + return "server responded with an invalid redirection"; + case sse::Error::UnrecoverableClientError: + return "unrecoverable client-side error"; + default: + return "unrecognized error"; + } +} + +StreamingDataSource::StreamingDataSource( + config::shared::built::ServiceEndpoints const& endpoints, + config::shared::built::DataSourceConfig const& + data_source_config, + config::shared::built::HttpProperties http_properties, + boost::asio::any_io_executor ioc, + IDataSourceUpdateSink& handler, + DataSourceStatusManager& status_manager, + Logger const& logger) + : exec_(std::move(ioc)), + logger_(logger), + status_manager_(status_manager), + data_source_handler_( + DataSourceEventHandler(handler, logger, status_manager_)), + http_config_(std::move(http_properties)), + streaming_config_( + std::get>(data_source_config.method)), + streaming_endpoint_(endpoints.StreamingBaseUrl()) {} + +void StreamingDataSource::Start() { + status_manager_.SetState(DataSourceStatus::DataSourceState::kInitializing); + + auto updated_url = network::AppendUrl(streaming_endpoint_, + streaming_config_.streaming_path); + + // Bad URL, don't set the client. Start will then report the bad status. + if (!updated_url) { + LD_LOG(logger_, LogLevel::kError) << kCouldNotParseEndpoint; + status_manager_.SetState( + DataSourceStatus::DataSourceState::kOff, + DataSourceStatus::ErrorInfo::ErrorKind::kNetworkError, + kCouldNotParseEndpoint); + return; } - StreamingDataSource::StreamingDataSource( - config::shared::built::ServiceEndpoints const &endpoints, - config::shared::built::DataSourceConfig const & - data_source_config, - config::shared::built::HttpProperties http_properties, - boost::asio::any_io_executor ioc, - IDataSourceUpdateSink &handler, - DataSourceStatusManager &status_manager, - Logger const &logger) - : exec_(std::move(ioc)), - logger_(logger), - status_manager_(status_manager), - data_source_handler_( - DataSourceEventHandler(handler, logger, status_manager_)), - http_config_(std::move(http_properties)), - streaming_config_( - std::get>(data_source_config.method)), - streaming_endpoint_(endpoints.StreamingBaseUrl()) {} - - void StreamingDataSource::Start() { - status_manager_.SetState(DataSourceStatus::DataSourceState::kInitializing); - - auto updated_url = network::AppendUrl(streaming_endpoint_, - streaming_config_.streaming_path); - - // Bad URL, don't set the client. Start will then report the bad status. - if (!updated_url) { - LD_LOG(logger_, LogLevel::kError) << kCouldNotParseEndpoint; - status_manager_.SetState( - DataSourceStatus::DataSourceState::kOff, - DataSourceStatus::ErrorInfo::ErrorKind::kNetworkError, - kCouldNotParseEndpoint); - return; - } + auto uri_components = boost::urls::parse_uri(*updated_url); - auto uri_components = boost::urls::parse_uri(*updated_url); + // Unlikely that it could be parsed earlier, and it cannot be parsed now. + if (!uri_components) { + LD_LOG(logger_, LogLevel::kError) << kCouldNotParseEndpoint; + status_manager_.SetState( + DataSourceStatus::DataSourceState::kOff, + DataSourceStatus::ErrorInfo::ErrorKind::kNetworkError, + kCouldNotParseEndpoint); + return; + } - // Unlikely that it could be parsed earlier, and it cannot be parsed now. - if (!uri_components) { - LD_LOG(logger_, LogLevel::kError) << kCouldNotParseEndpoint; - status_manager_.SetState( - DataSourceStatus::DataSourceState::kOff, - DataSourceStatus::ErrorInfo::ErrorKind::kNetworkError, - kCouldNotParseEndpoint); - return; - } + boost::urls::url url = uri_components.value(); - boost::urls::url url = uri_components.value(); + auto client_builder = launchdarkly::sse::Builder(exec_, url.buffer()); - auto client_builder = launchdarkly::sse::Builder(exec_, url.buffer()); + client_builder.method(boost::beast::http::verb::get); - client_builder.method(boost::beast::http::verb::get); + // TODO: can the read timeout be shared with *all* http requests? Or should + // it have a default in defaults.hpp? This must be greater than the + // heartbeat interval of the streaming service. + client_builder.read_timeout(std::chrono::minutes(5)); - // TODO: can the read timeout be shared with *all* http requests? Or should - // it have a default in defaults.hpp? This must be greater than the - // heartbeat interval of the streaming service. - client_builder.read_timeout(std::chrono::minutes(5)); + client_builder.write_timeout(http_config_.WriteTimeout()); - client_builder.write_timeout(http_config_.WriteTimeout()); + client_builder.connect_timeout(http_config_.ConnectTimeout()); - client_builder.connect_timeout(http_config_.ConnectTimeout()); + client_builder.initial_reconnect_delay( + streaming_config_.initial_reconnect_delay); - client_builder.initial_reconnect_delay( - streaming_config_.initial_reconnect_delay); + for (auto const& header : http_config_.BaseHeaders()) { + client_builder.header(header.first, header.second); + } - for (auto const &header: http_config_.BaseHeaders()) { - client_builder.header(header.first, header.second); - } + auto weak_self = weak_from_this(); - // TODO: Handle proxy support. sc-204386 - - auto weak_self = weak_from_this(); - - client_builder.receiver([weak_self](launchdarkly::sse::Event const &event) { - if (auto self = weak_self.lock()) { - self->data_source_handler_.HandleMessage(event.type(), - event.data()); - // TODO: Use the result of handle message to restart the - // event source if we got bad data. sc-204387 - } - }); - - client_builder.logger([weak_self](auto msg) { - if (auto self = weak_self.lock()) { - LD_LOG(self->logger_, LogLevel::kDebug) << msg; - } - }); - - client_builder.errors([weak_self](auto error) { - if (auto self = weak_self.lock()) { - auto error_string = DataSourceErrorToString(error); - LD_LOG(self->logger_, LogLevel::kError) << error_string; - self->status_manager_.SetState( - DataSourceStatus::DataSourceState::kOff, - DataSourceStatus::ErrorInfo::ErrorKind::kErrorResponse, - error_string); - } - }); - - client_ = client_builder.build(); - - if (!client_) { - LD_LOG(logger_, LogLevel::kError) << kCouldNotParseEndpoint; - status_manager_.SetState( - DataSourceStatus::DataSourceState::kOff, - DataSourceStatus::ErrorInfo::ErrorKind::kNetworkError, - kCouldNotParseEndpoint); - return; + client_builder.receiver([weak_self](launchdarkly::sse::Event const& event) { + if (auto self = weak_self.lock()) { + self->data_source_handler_.HandleMessage(event.type(), + event.data()); + // TODO: Use the result of handle message to restart the + // event source if we got bad data. sc-204387 } - client_->async_connect(); - } + }); - void StreamingDataSource::ShutdownAsync(std::function completion) { - if (client_) { - status_manager_.SetState( - DataSourceStatus::DataSourceState::kInitializing); - return client_->async_shutdown(std::move(completion)); + client_builder.logger([weak_self](auto msg) { + if (auto self = weak_self.lock()) { + LD_LOG(self->logger_, LogLevel::kDebug) << msg; } - if (completion) { - boost::asio::post(exec_, completion); + }); + + client_builder.errors([weak_self](auto error) { + if (auto self = weak_self.lock()) { + auto error_string = DataSourceErrorToString(error); + LD_LOG(self->logger_, LogLevel::kError) << error_string; + self->status_manager_.SetState( + DataSourceStatus::DataSourceState::kOff, + DataSourceStatus::ErrorInfo::ErrorKind::kErrorResponse, + error_string); } + }); + + client_ = client_builder.build(); + + if (!client_) { + LD_LOG(logger_, LogLevel::kError) << kCouldNotParseEndpoint; + status_manager_.SetState( + DataSourceStatus::DataSourceState::kOff, + DataSourceStatus::ErrorInfo::ErrorKind::kNetworkError, + kCouldNotParseEndpoint); + return; + } + client_->async_connect(); +} + +void StreamingDataSource::ShutdownAsync(std::function completion) { + if (client_) { + status_manager_.SetState( + DataSourceStatus::DataSourceState::kInitializing); + return client_->async_shutdown(std::move(completion)); + } + if (completion) { + boost::asio::post(exec_, completion); } +} } // namespace launchdarkly::server_side::data_sources diff --git a/libs/server-sdk/src/data_store/persistent/expiration_tracker.hpp b/libs/server-sdk/src/data_store/persistent/expiration_tracker.hpp index 4086bb93d..bc57398c0 100644 --- a/libs/server-sdk/src/data_store/persistent/expiration_tracker.hpp +++ b/libs/server-sdk/src/data_store/persistent/expiration_tracker.hpp @@ -5,7 +5,6 @@ #include #include #include -#include #include #include @@ -123,7 +122,6 @@ class ExpirationTracker { std::array, static_cast>( DataKind::kKindCount)>; - void Set(DataKind kind, std::string const& key, TimePoint expiration); void Remove(DataKind kind, std::string const& key); std::optional Get(DataKind kind, diff --git a/libs/server-sdk/src/data_store/tagged_data.hpp b/libs/server-sdk/src/data_store/tagged_data.hpp index 6ff087860..320146721 100644 --- a/libs/server-sdk/src/data_store/tagged_data.hpp +++ b/libs/server-sdk/src/data_store/tagged_data.hpp @@ -34,4 +34,4 @@ class TaggedData { Storage storage_; }; -} +} // namespace launchdarkly::server_side::data_store diff --git a/libs/server-sdk/src/evaluation/evaluation_error.hpp b/libs/server-sdk/src/evaluation/evaluation_error.hpp index 69aa5ef51..7fb52d556 100644 --- a/libs/server-sdk/src/evaluation/evaluation_error.hpp +++ b/libs/server-sdk/src/evaluation/evaluation_error.hpp @@ -2,7 +2,6 @@ #include #include -#include namespace launchdarkly::server_side::evaluation { diff --git a/libs/server-sdk/tests/evaluator_tests.cpp b/libs/server-sdk/tests/evaluator_tests.cpp index 602a8dfe8..85807a848 100644 --- a/libs/server-sdk/tests/evaluator_tests.cpp +++ b/libs/server-sdk/tests/evaluator_tests.cpp @@ -62,7 +62,7 @@ TEST_F(EvaluatorTests, BasicChanges) { auto detail = eval_.Evaluate(flag, alice); - ASSERT_TRUE(detail); + ASSERT_FALSE(detail.IsError()); ASSERT_EQ(*detail, Value(false)); ASSERT_EQ(detail.VariationIndex(), 0); ASSERT_EQ(detail.Reason(), EvaluationReason::Off()); @@ -70,27 +70,27 @@ TEST_F(EvaluatorTests, BasicChanges) { // flip off variation flag.offVariation = 1; detail = eval_.Evaluate(flag, alice); - ASSERT_TRUE(detail); + ASSERT_FALSE(detail.IsError()); ASSERT_EQ(detail.VariationIndex(), 1); ASSERT_EQ(*detail, Value(true)); // off variation unspecified flag.offVariation = std::nullopt; detail = eval_.Evaluate(flag, alice); - ASSERT_TRUE(detail); + ASSERT_FALSE(detail.IsError()); ASSERT_EQ(detail.VariationIndex(), std::nullopt); ASSERT_EQ(*detail, Value::Null()); // flip targeting on flag.on = true; detail = eval_.Evaluate(flag, alice); - ASSERT_TRUE(detail); + ASSERT_FALSE(detail.IsError()); ASSERT_EQ(detail.VariationIndex(), 1); ASSERT_EQ(*detail, Value(true)); ASSERT_EQ(detail.Reason(), EvaluationReason::Fallthrough(false)); detail = eval_.Evaluate(flag, bob); - ASSERT_TRUE(detail); + ASSERT_FALSE(detail.IsError()); ASSERT_EQ(detail.VariationIndex(), 0); ASSERT_EQ(*detail, Value(false)); ASSERT_EQ(detail.Reason(), EvaluationReason::TargetMatch()); @@ -98,14 +98,14 @@ TEST_F(EvaluatorTests, BasicChanges) { // flip default variation flag.fallthrough = data_model::Flag::Variation{0}; detail = eval_.Evaluate(flag, alice); - ASSERT_TRUE(detail); + ASSERT_FALSE(detail.IsError()); ASSERT_EQ(detail.VariationIndex(), 0); ASSERT_EQ(*detail, Value(false)); // bob's reason should still be TargetMatch even though his value is now the // default detail = eval_.Evaluate(flag, bob); - ASSERT_TRUE(detail); + ASSERT_FALSE(detail.IsError()); ASSERT_EQ(detail.VariationIndex(), 0); ASSERT_EQ(*detail, Value(false)); ASSERT_EQ(detail.Reason(), EvaluationReason::TargetMatch()); @@ -121,12 +121,12 @@ TEST_F(EvaluatorTests, EvaluateWithMatchesOpGroups) { auto flag = store_->GetFlag("flagWithMatchesOpOnGroups")->item.value(); auto detail = eval_.Evaluate(flag, alice); - ASSERT_TRUE(detail); + ASSERT_FALSE(detail.IsError()); ASSERT_EQ(*detail, Value(true)); ASSERT_EQ(detail.Reason(), EvaluationReason::Fallthrough(false)); detail = eval_.Evaluate(flag, bob); - ASSERT_TRUE(detail); + ASSERT_FALSE(detail.IsError()); ASSERT_EQ(*detail, Value(false)); ASSERT_EQ(detail.VariationIndex(), 0); ASSERT_EQ(detail.Reason(), @@ -141,7 +141,7 @@ TEST_F(EvaluatorTests, EvaluateWithMatchesOpKinds) { auto flag = store_->GetFlag("flagWithMatchesOpOnKinds")->item.value(); auto detail = eval_.Evaluate(flag, alice); - ASSERT_TRUE(detail); + ASSERT_FALSE(detail.IsError()); ASSERT_EQ(*detail, Value(false)); ASSERT_EQ(detail.VariationIndex(), 0); ASSERT_EQ(detail.Reason(), @@ -149,13 +149,13 @@ TEST_F(EvaluatorTests, EvaluateWithMatchesOpKinds) { 0, "6a7755ac-e47a-40ea-9579-a09dd5f061bd", false)); detail = eval_.Evaluate(flag, bob); - ASSERT_TRUE(detail); + ASSERT_FALSE(detail.IsError()); ASSERT_EQ(*detail, Value(true)); ASSERT_EQ(detail.Reason(), EvaluationReason::Fallthrough(false)); auto new_bob = ContextBuilder().Kind("org", "bob").Build(); detail = eval_.Evaluate(flag, new_bob); - ASSERT_TRUE(detail); + ASSERT_FALSE(detail.IsError()); ASSERT_EQ(*detail, Value(false)); ASSERT_EQ(detail.VariationIndex(), 0); ASSERT_EQ(detail.Reason(), @@ -169,7 +169,7 @@ TEST_F(EvaluatorTestsWithLogs, PrerequisiteCycle) { auto flag = store_->GetFlag("cycleFlagA")->item.value(); auto detail = eval_.Evaluate(flag, alice); - ASSERT_FALSE(detail); + ASSERT_TRUE(detail.IsError()); ASSERT_EQ(detail.Reason(), EvaluationReason::MalformedFlag()); ASSERT_TRUE(messages_->Count(1)); ASSERT_TRUE(messages_->Contains(0, LogLevel::kError, "circular reference")); @@ -182,13 +182,13 @@ TEST_F(EvaluatorTests, FlagWithSegment) { auto flag = store_->GetFlag("flagWithSegmentMatchRule")->item.value(); auto detail = eval_.Evaluate(flag, alice); - ASSERT_TRUE(detail); + ASSERT_FALSE(detail.IsError()); ASSERT_EQ(*detail, Value(false)); ASSERT_EQ(detail.Reason(), EvaluationReason::RuleMatch(0, "match-rule", false)); detail = eval_.Evaluate(flag, bob); - ASSERT_TRUE(detail); + ASSERT_FALSE(detail.IsError()); ASSERT_EQ(*detail, Value(true)); ASSERT_EQ(detail.Reason(), EvaluationReason::Fallthrough(false)); } @@ -201,13 +201,13 @@ TEST_F(EvaluatorTests, FlagWithSegmentContainingRules) { store_->GetFlag("flagWithSegmentMatchesUserAlice")->item.value(); auto detail = eval_.Evaluate(flag, alice); - ASSERT_TRUE(detail); + ASSERT_FALSE(detail.IsError()); ASSERT_EQ(detail.Reason(), EvaluationReason::RuleMatch(0, "match-rule", false)); ASSERT_EQ(*detail, Value(false)); detail = eval_.Evaluate(flag, bob); - ASSERT_TRUE(detail); + ASSERT_FALSE(detail.IsError()); ASSERT_EQ(detail.Reason(), EvaluationReason::Fallthrough(false)); ASSERT_EQ(*detail, Value(true)); } @@ -220,17 +220,17 @@ TEST_F(EvaluatorTests, FlagWithExperiment) { auto flag = store_->GetFlag("flagWithExperiment")->item.value(); auto detail = eval_.Evaluate(flag, user_a); - ASSERT_TRUE(detail); + ASSERT_FALSE(detail.IsError()); ASSERT_EQ(*detail, Value(false)); ASSERT_TRUE(detail.Reason()->InExperiment()); detail = eval_.Evaluate(flag, user_b); - ASSERT_TRUE(detail); + ASSERT_FALSE(detail.IsError()); ASSERT_EQ(*detail, Value(true)); ASSERT_TRUE(detail.Reason()->InExperiment()); detail = eval_.Evaluate(flag, user_c); - ASSERT_TRUE(detail); + ASSERT_FALSE(detail.IsError()); ASSERT_EQ(*detail, Value(false)); ASSERT_FALSE(detail.Reason()->InExperiment()); } @@ -242,7 +242,7 @@ TEST_F(EvaluatorTests, FlagWithExperimentTargetingMissingContext) { auto user_a = ContextBuilder().Kind("user", "userKeyA").Build(); auto detail = eval_.Evaluate(flag, user_a); - ASSERT_TRUE(detail); + ASSERT_FALSE(detail.IsError()); ASSERT_EQ(*detail, Value(false)); ASSERT_EQ(detail.Reason(), EvaluationReason::Fallthrough(false)); } diff --git a/libs/server-sent-events/CHANGELOG.md b/libs/server-sent-events/CHANGELOG.md index f2947853c..8ac5d8d50 100644 --- a/libs/server-sent-events/CHANGELOG.md +++ b/libs/server-sent-events/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.2.0](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-sse-client-v0.1.3...launchdarkly-cpp-sse-client-v0.2.0) (2023-10-13) + + +### Features + +* clean up LD CMake variables & allow for OpenSSL dynamic link ([#255](https://github.com/launchdarkly/cpp-sdks/issues/255)) ([ed23c9a](https://github.com/launchdarkly/cpp-sdks/commit/ed23c9a347665529a09d18111bb9d3b699381728)) + ## [0.1.3](https://github.com/launchdarkly/cpp-sdks/compare/launchdarkly-cpp-sse-client-v0.1.2...launchdarkly-cpp-sse-client-v0.1.3) (2023-09-13) diff --git a/libs/server-sent-events/CMakeLists.txt b/libs/server-sent-events/CMakeLists.txt index 04e346659..a1daa302b 100644 --- a/libs/server-sent-events/CMakeLists.txt +++ b/libs/server-sent-events/CMakeLists.txt @@ -31,6 +31,6 @@ include(FetchContent) add_subdirectory(src) -if (BUILD_TESTING) +if (LD_BUILD_UNIT_TESTS) add_subdirectory(tests) endif () diff --git a/libs/server-sent-events/package.json b/libs/server-sent-events/package.json index fc34f0855..4984ca021 100644 --- a/libs/server-sent-events/package.json +++ b/libs/server-sent-events/package.json @@ -2,6 +2,6 @@ "name": "launchdarkly-cpp-sse-client", "description": "This package.json exists for modeling dependencies for the release process.", "private": true, - "version": "0.1.3", + "version": "0.2.0", "dependencies": {} } diff --git a/release-please-config.json b/release-please-config.json index 810048113..b9b9fd9b5 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -11,6 +11,16 @@ "CMakeLists.txt" ] }, + "libs/server-sdk": { + "extra-files": [ + "include/launchdarkly/server_side/client.hpp", + "tests/server_c_bindings_test.cpp", + "tests/client_test.cpp", + "CMakeLists.txt" + ], + "prerelease": true, + "bump-minor-pre-major": true + }, "libs/server-sent-events": { "initial-version": "0.1.0" }, diff --git a/scripts/build-release.sh b/scripts/build-release.sh index 922ab9554..c06d26824 100755 --- a/scripts/build-release.sh +++ b/scripts/build-release.sh @@ -17,7 +17,7 @@ cd .. # Build a dynamic release. mkdir -p build-dynamic && cd build-dynamic mkdir -p release -cmake -G Ninja -D CMAKE_BUILD_TYPE=Release -D BUILD_TESTING=OFF -D BUILD_SHARED_LIBS=ON -D CMAKE_INSTALL_PREFIX=./release .. +cmake -G Ninja -D CMAKE_BUILD_TYPE=Release -D BUILD_TESTING=OFF -D LD_BUILD_SHARED_LIBS=ON -D CMAKE_INSTALL_PREFIX=./release .. cmake --build . --target "$1" cmake --install . diff --git a/scripts/build-windows.sh b/scripts/build-windows.sh index faaad3f7e..226af72c8 100644 --- a/scripts/build-windows.sh +++ b/scripts/build-windows.sh @@ -20,7 +20,7 @@ cd .. # Build a dynamic debug release. mkdir -p build-dynamic-debug && cd build-dynamic-debug mkdir -p release -cmake -G Ninja -D CMAKE_BUILD_TYPE=Debug -D BUILD_TESTING=OFF -D BUILD_SHARED_LIBS=ON -D CMAKE_INSTALL_PREFIX=./release .. +cmake -G Ninja -D CMAKE_BUILD_TYPE=Debug -D BUILD_TESTING=OFF -D LD_BUILD_SHARED_LIBS=ON -D CMAKE_INSTALL_PREFIX=./release .. cmake --build . --target "$1" cmake --install . diff --git a/scripts/build.sh b/scripts/build.sh index 6b5527447..7b77203f0 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -5,7 +5,7 @@ # ./scripts/build.sh my-build-target ON # # $1 the name of the target. For example "launchdarkly-cpp-common". -# $2 ON/OFF which enables/disables building in a test configuration. +# $2 ON/OFF which enables/disables building in a test configuration (unit tests + contract tests.) function cleanup { cd .. @@ -17,6 +17,6 @@ cd build # script ends. trap cleanup EXIT -cmake -G Ninja -D BUILD_TESTING="$2" .. +cmake -G Ninja -D CMAKE_COMPILE_WARNING_AS_ERROR=TRUE -D BUILD_TESTING="$2" -D LD_BUILD_UNIT_TESTS="$2" -D LD_BUILD_CONTRACT_TESTS="$2" .. cmake --build . --target "$1"