From 0fbdfc546b6615fd8798e559a93c9d41d208e1c0 Mon Sep 17 00:00:00 2001 From: Bee Klimt Date: Wed, 2 Sep 2026 04:16:04 +0000 Subject: [PATCH] feat: Add C bindings for the client FDv2 configuration --- .../client_side/bindings/c/config/builder.h | 20 ++ .../c/config/fdv2_builder/fdv2_builder.h | 337 ++++++++++++++++++ libs/client-sdk/src/CMakeLists.txt | 1 + libs/client-sdk/src/bindings/c/builder.cpp | 13 + .../src/bindings/c/fdv2_builder.cpp | 239 +++++++++++++ libs/client-sdk/tests/CMakeLists.txt | 7 + libs/client-sdk/tests/c_headers_smoke_test.c | 58 +++ .../client-sdk/tests/fdv2_c_bindings_test.cpp | 177 +++++++++ 8 files changed, 852 insertions(+) create mode 100644 libs/client-sdk/include/launchdarkly/client_side/bindings/c/config/fdv2_builder/fdv2_builder.h create mode 100644 libs/client-sdk/src/bindings/c/fdv2_builder.cpp create mode 100644 libs/client-sdk/tests/c_headers_smoke_test.c create mode 100644 libs/client-sdk/tests/fdv2_c_bindings_test.cpp diff --git a/libs/client-sdk/include/launchdarkly/client_side/bindings/c/config/builder.h b/libs/client-sdk/include/launchdarkly/client_side/bindings/c/config/builder.h index e36282a96..197301faa 100644 --- a/libs/client-sdk/include/launchdarkly/client_side/bindings/c/config/builder.h +++ b/libs/client-sdk/include/launchdarkly/client_side/bindings/c/config/builder.h @@ -4,6 +4,7 @@ #pragma once #include +#include #include #include @@ -321,6 +322,25 @@ LDClientConfigBuilder_DataSource_MethodPoll( LDClientConfigBuilder b, LDDataSourcePollBuilder poll_builder); +/** + * Set the FDv2 configuration for the builder. The builder is automatically + * freed. + * + * FDv2 is the current flag delivery protocol. It supersedes the streaming and + * polling methods above, which speak FDv1, and configures both transports + * itself through named connection modes. + * + * WARNING: Do not call any other LDClientFDv2Builder function on the provided + * LDClientFDv2Builder after calling this function. It is undefined behavior. + * + * @param b Client config builder. Must not be NULL. + * @param fdv2_builder The FDv2 builder. The builder is consumed; do not free + * it. Must not be NULL. + */ +LD_EXPORT(void) +LDClientConfigBuilder_DataSource_MethodFDv2(LDClientConfigBuilder b, + LDClientFDv2Builder fdv2_builder); + /** * Creates a new DataSource builder for the Streaming method. * diff --git a/libs/client-sdk/include/launchdarkly/client_side/bindings/c/config/fdv2_builder/fdv2_builder.h b/libs/client-sdk/include/launchdarkly/client_side/bindings/c/config/fdv2_builder/fdv2_builder.h new file mode 100644 index 000000000..101f723b9 --- /dev/null +++ b/libs/client-sdk/include/launchdarkly/client_side/bindings/c/config/fdv2_builder/fdv2_builder.h @@ -0,0 +1,337 @@ +/** @file fdv2_builder.h */ +// NOLINTBEGIN modernize-use-using + +#pragma once + +#include + +#include + +#ifdef __cplusplus +extern "C" { // only need to export C interface if used by C++ source code +#endif + +typedef struct _LDClientFDv2Builder* LDClientFDv2Builder; +typedef struct _LDClientFDv2ModeBuilder* LDClientFDv2ModeBuilder; +typedef struct _LDClientFDv2StreamingBuilder* LDClientFDv2StreamingBuilder; +typedef struct _LDClientFDv2PollingBuilder* LDClientFDv2PollingBuilder; +typedef struct _LDClientFDv2FDv1FallbackBuilder* + LDClientFDv2FDv1FallbackBuilder; + +/** + * A named data system configuration: which sources the SDK uses to load flag + * data, and which it uses to keep that data current. + */ +enum LDClientConnectionMode { + /** Stream updates, falling back to polling. */ + LD_CLIENT_CONNECTION_MODE_STREAMING = 0, + /** Poll for updates on an interval. */ + LD_CLIENT_CONNECTION_MODE_POLLING = 1, + /** Evaluate against whatever is cached, and make no requests. */ + LD_CLIENT_CONNECTION_MODE_OFFLINE = 2 +}; + +/** + * Creates a new FDv2 builder. It starts from the configuration the SDK uses + * when the application customizes nothing: streaming mode, with streaming, + * polling, and offline modes available. + * + * If not passed into the config builder, must be manually freed with + * LDClientFDv2Builder_Free. + * + * @return A new FDv2 builder. + */ +LD_EXPORT(LDClientFDv2Builder) +LDClientFDv2Builder_New(void); + +/** + * Frees an FDv2 builder. Do not call if the builder was consumed by the + * config builder. + * + * @param b Builder to free. + */ +LD_EXPORT(void) +LDClientFDv2Builder_Free(LDClientFDv2Builder b); + +/** + * Sets the connection mode the SDK starts in. Defaults to streaming. + * + * @param b FDv2 builder. Must not be NULL. + * @param mode The mode to start in. + */ +LD_EXPORT(void) +LDClientFDv2Builder_InitialMode(LDClientFDv2Builder b, + enum LDClientConnectionMode mode); + +/** + * Sends the evaluation context in a request body rather than encoded into the + * request path. This keeps the context out of URL-based request logs and CDN + * logs, at the cost of CDN caching. + * + * @param b FDv2 builder. Must not be NULL. + * @param use_post True to send the context in a request body. + */ +LD_EXPORT(void) +LDClientFDv2Builder_UsePost(LDClientFDv2Builder b, bool use_post); + +/** + * Replaces what the given connection mode does. Modes left uncustomized keep + * their built-in definitions. The mode builder is automatically freed. + * + * WARNING: Do not call any other LDClientFDv2ModeBuilder function on the + * provided builder after calling this function. It is undefined behavior. + * + * @param b FDv2 builder. Must not be NULL. + * @param mode The mode to customize. + * @param mode_builder The mode definition. The builder is consumed; do not + * free it. Must not be NULL. + */ +LD_EXPORT(void) +LDClientFDv2Builder_CustomizeMode(LDClientFDv2Builder b, + enum LDClientConnectionMode mode, + LDClientFDv2ModeBuilder mode_builder); + +/** + * Creates a new builder for one connection mode's sources. + * + * A mode built this way replaces the SDK's built-in definition entirely, so it + * should list every source the mode needs, cache included. + * + * If not passed into an FDv2 builder, must be manually freed with + * LDClientFDv2ModeBuilder_Free. + * + * @return A new connection mode builder. + */ +LD_EXPORT(LDClientFDv2ModeBuilder) +LDClientFDv2ModeBuilder_New(void); + +/** + * Frees a connection mode builder. Do not call if the builder was consumed by + * an FDv2 builder. + * + * @param b Builder to free. + */ +LD_EXPORT(void) +LDClientFDv2ModeBuilder_Free(LDClientFDv2ModeBuilder b); + +/** + * Appends the local cache to the mode's initializer list. Reading persisted + * flag data lets evaluation begin before the network answers. + * + * @param b Connection mode builder. Must not be NULL. + */ +LD_EXPORT(void) +LDClientFDv2ModeBuilder_Initializer_Cache(LDClientFDv2ModeBuilder b); + +/** + * Appends a polling source to the mode's initializer list. The source builder + * is automatically freed. + * + * Initializers run in order until one loads a complete data set. + * + * WARNING: Do not call any other LDClientFDv2PollingBuilder function on the + * provided builder after calling this function. It is undefined behavior. + * + * @param b Connection mode builder. Must not be NULL. + * @param polling The polling source builder. The builder is consumed; do not + * free it. Must not be NULL. + */ +LD_EXPORT(void) +LDClientFDv2ModeBuilder_Initializer_Polling(LDClientFDv2ModeBuilder b, + LDClientFDv2PollingBuilder polling); + +/** + * Appends a streaming source to the mode's synchronizer list. The source + * builder is automatically freed. + * + * Order is preference. The first entry is the primary, and the SDK falls back + * to later entries when it cannot keep the primary running. + * + * WARNING: Do not call any other LDClientFDv2StreamingBuilder function on the + * provided builder after calling this function. It is undefined behavior. + * + * @param b Connection mode builder. Must not be NULL. + * @param streaming The streaming source builder. The builder is consumed; do + * not free it. Must not be NULL. + */ +LD_EXPORT(void) +LDClientFDv2ModeBuilder_Synchronizer_Streaming( + LDClientFDv2ModeBuilder b, + LDClientFDv2StreamingBuilder streaming); + +/** + * Appends a polling source to the mode's synchronizer list. See + * LDClientFDv2ModeBuilder_Synchronizer_Streaming for ordering semantics. The + * source builder is automatically freed. + * + * WARNING: Do not call any other LDClientFDv2PollingBuilder function on the + * provided builder after calling this function. It is undefined behavior. + * + * @param b Connection mode builder. Must not be NULL. + * @param polling The polling source builder. The builder is consumed; do not + * free it. Must not be NULL. + */ +LD_EXPORT(void) +LDClientFDv2ModeBuilder_Synchronizer_Polling( + LDClientFDv2ModeBuilder b, + LDClientFDv2PollingBuilder polling); + +/** + * Sets the FDv1 source the mode uses if the service directs the SDK to it. The + * SDK returns to FDv2 on its own once the service's fallback period has + * elapsed. The source builder is automatically freed. + * + * WARNING: Do not call any other LDClientFDv2FDv1FallbackBuilder function on + * the provided builder after calling this function. It is undefined behavior. + * + * @param b Connection mode builder. Must not be NULL. + * @param fallback The FDv1 fallback source builder. The builder is consumed; + * do not free it. Must not be NULL. + */ +LD_EXPORT(void) +LDClientFDv2ModeBuilder_FallbackToFDv1( + LDClientFDv2ModeBuilder b, + LDClientFDv2FDv1FallbackBuilder fallback); + +/** + * Leaves the mode with no FDv1 source. A fallback directive then stops the + * mode's synchronizer until the SDK returns to FDv2. + * + * @param b Connection mode builder. Must not be NULL. + */ +LD_EXPORT(void) +LDClientFDv2ModeBuilder_DisableFDv1Fallback(LDClientFDv2ModeBuilder b); + +/** + * Creates a new FDv2 streaming source builder. + * + * If not passed into a connection mode builder, must be manually freed with + * LDClientFDv2StreamingBuilder_Free. + * + * @return A new FDv2 streaming source builder. + */ +LD_EXPORT(LDClientFDv2StreamingBuilder) +LDClientFDv2StreamingBuilder_New(void); + +/** + * Sets where the reconnection backoff starts. The delay for the first + * reconnection starts near this value and grows exponentially for subsequent + * failures. + * + * @param b FDv2 streaming source builder. Must not be NULL. + * @param milliseconds Initial delay for a reconnection attempt. + */ +LD_EXPORT(void) +LDClientFDv2StreamingBuilder_InitialReconnectDelayMs( + LDClientFDv2StreamingBuilder b, + unsigned int milliseconds); + +/** + * Sends this source's requests to the given URL instead of the streaming URL + * the rest of the SDK uses. Useful for routing one tier to different + * infrastructure, such as a Relay Proxy used only as a fallback. + * + * @param b FDv2 streaming source builder. Must not be NULL. + * @param base_url Target URL. Must not be NULL. + */ +LD_EXPORT(void) +LDClientFDv2StreamingBuilder_BaseURL(LDClientFDv2StreamingBuilder b, + char const* base_url); + +/** + * Frees an FDv2 streaming source builder. Do not call if the builder was + * consumed by a connection mode builder. + * + * @param b Builder to free. + */ +LD_EXPORT(void) +LDClientFDv2StreamingBuilder_Free(LDClientFDv2StreamingBuilder b); + +/** + * Creates a new FDv2 polling source builder. + * + * If not passed into a connection mode builder, must be manually freed with + * LDClientFDv2PollingBuilder_Free. + * + * @return A new FDv2 polling source builder. + */ +LD_EXPORT(LDClientFDv2PollingBuilder) +LDClientFDv2PollingBuilder_New(void); + +/** + * Sets how long to wait between polls. Intervals shorter than the minimum the + * SDK permits are raised to it. + * + * @param b FDv2 polling source builder. Must not be NULL. + * @param seconds Polling interval in seconds. + */ +LD_EXPORT(void) +LDClientFDv2PollingBuilder_IntervalS(LDClientFDv2PollingBuilder b, + unsigned int seconds); + +/** + * Sends this source's requests to the given URL instead of the polling URL + * the rest of the SDK uses. + * + * @param b FDv2 polling source builder. Must not be NULL. + * @param base_url Target URL. Must not be NULL. + */ +LD_EXPORT(void) +LDClientFDv2PollingBuilder_BaseURL(LDClientFDv2PollingBuilder b, + char const* base_url); + +/** + * Frees an FDv2 polling source builder. Do not call if the builder was + * consumed by a connection mode builder. + * + * @param b Builder to free. + */ +LD_EXPORT(void) +LDClientFDv2PollingBuilder_Free(LDClientFDv2PollingBuilder b); + +/** + * Creates a new FDv1 fallback source builder. + * + * If not passed into a connection mode builder, must be manually freed with + * LDClientFDv2FDv1FallbackBuilder_Free. + * + * @return A new FDv1 fallback source builder. + */ +LD_EXPORT(LDClientFDv2FDv1FallbackBuilder) +LDClientFDv2FDv1FallbackBuilder_New(void); + +/** + * Sets how long to wait between polls while on FDv1. + * + * @param b FDv1 fallback source builder. Must not be NULL. + * @param seconds Polling interval in seconds. + */ +LD_EXPORT(void) +LDClientFDv2FDv1FallbackBuilder_IntervalS(LDClientFDv2FDv1FallbackBuilder b, + unsigned int seconds); + +/** + * Sends the fallback's requests to the given URL instead of the polling URL + * the rest of the SDK uses. + * + * @param b FDv1 fallback source builder. Must not be NULL. + * @param base_url Target URL. Must not be NULL. + */ +LD_EXPORT(void) +LDClientFDv2FDv1FallbackBuilder_BaseURL(LDClientFDv2FDv1FallbackBuilder b, + char const* base_url); + +/** + * Frees an FDv1 fallback source builder. Do not call if the builder was + * consumed by a connection mode builder. + * + * @param b Builder to free. + */ +LD_EXPORT(void) +LDClientFDv2FDv1FallbackBuilder_Free(LDClientFDv2FDv1FallbackBuilder b); + +#ifdef __cplusplus +} +#endif + +// NOLINTEND modernize-use-using diff --git a/libs/client-sdk/src/CMakeLists.txt b/libs/client-sdk/src/CMakeLists.txt index 8db6518b4..f59d0c7ad 100644 --- a/libs/client-sdk/src/CMakeLists.txt +++ b/libs/client-sdk/src/CMakeLists.txt @@ -61,6 +61,7 @@ target_sources(${LIBNAME} PRIVATE flag_manager/flag_updater.hpp bindings/c/sdk.cpp bindings/c/builder.cpp + bindings/c/fdv2_builder.cpp bindings/c/config.cpp data_sources/null_data_source.cpp flag_manager/context_index.cpp diff --git a/libs/client-sdk/src/bindings/c/builder.cpp b/libs/client-sdk/src/bindings/c/builder.cpp index 898ce2417..4549b373d 100644 --- a/libs/client-sdk/src/bindings/c/builder.cpp +++ b/libs/client-sdk/src/bindings/c/builder.cpp @@ -21,6 +21,8 @@ using namespace launchdarkly::client_side; #define TO_POLL_BUILDER(ptr) \ (reinterpret_cast(ptr)) +#define TO_FDV2_BUILDER(ptr) (reinterpret_cast(ptr)) + #define FROM_POLL_BUILDER(ptr) (reinterpret_cast(ptr)) #define TO_BASIC_LOGGING_BUILDER(ptr) \ @@ -248,6 +250,17 @@ LDClientConfigBuilder_DataSource_MethodPoll( LDDataSourcePollBuilder_Free(poll_builder); } +LD_EXPORT(void) +LDClientConfigBuilder_DataSource_MethodFDv2(LDClientConfigBuilder b, + LDClientFDv2Builder fdv2_builder) { + LD_ASSERT_NOT_NULL(b); + LD_ASSERT_NOT_NULL(fdv2_builder); + + DataSourceBuilder::FDv2* fb = TO_FDV2_BUILDER(fdv2_builder); + TO_BUILDER(b)->DataSource().Method(*fb); + LDClientFDv2Builder_Free(fdv2_builder); +} + LD_EXPORT(LDDataSourceStreamBuilder) LDDataSourceStreamBuilder_New() { return FROM_STREAM_BUILDER(new DataSourceBuilder::Streaming()); } diff --git a/libs/client-sdk/src/bindings/c/fdv2_builder.cpp b/libs/client-sdk/src/bindings/c/fdv2_builder.cpp new file mode 100644 index 000000000..6f1f6ba17 --- /dev/null +++ b/libs/client-sdk/src/bindings/c/fdv2_builder.cpp @@ -0,0 +1,239 @@ +// NOLINTBEGIN cppcoreguidelines-pro-type-reinterpret-cast +// NOLINTBEGIN OCInconsistentNamingInspection + +#include +#include + +#include +#include + +#include +#include + +using namespace launchdarkly::client_side; + +#define TO_FDV2_BUILDER(ptr) (reinterpret_cast(ptr)) +#define FROM_FDV2_BUILDER(ptr) (reinterpret_cast(ptr)) + +#define TO_MODE_BUILDER(ptr) (reinterpret_cast(ptr)) +#define FROM_MODE_BUILDER(ptr) (reinterpret_cast(ptr)) + +#define TO_STREAM_BUILDER(ptr) (reinterpret_cast(ptr)) +#define FROM_STREAM_BUILDER(ptr) \ + (reinterpret_cast(ptr)) + +#define TO_POLL_BUILDER(ptr) (reinterpret_cast(ptr)) +#define FROM_POLL_BUILDER(ptr) \ + (reinterpret_cast(ptr)) + +#define TO_FALLBACK_BUILDER(ptr) \ + (reinterpret_cast(ptr)) +#define FROM_FALLBACK_BUILDER(ptr) \ + (reinterpret_cast(ptr)) + +namespace { + +ConnectionMode ToConnectionMode(enum LDClientConnectionMode mode) { + switch (mode) { + case LD_CLIENT_CONNECTION_MODE_POLLING: + return ConnectionMode::kPolling; + case LD_CLIENT_CONNECTION_MODE_OFFLINE: + return ConnectionMode::kOffline; + case LD_CLIENT_CONNECTION_MODE_STREAMING: + default: + return ConnectionMode::kStreaming; + } +} + +} // namespace + +LD_EXPORT(LDClientFDv2Builder) +LDClientFDv2Builder_New(void) { + return FROM_FDV2_BUILDER(new FDv2Builder()); +} + +LD_EXPORT(void) +LDClientFDv2Builder_Free(LDClientFDv2Builder b) { + delete TO_FDV2_BUILDER(b); +} + +LD_EXPORT(void) +LDClientFDv2Builder_InitialMode(LDClientFDv2Builder b, + enum LDClientConnectionMode mode) { + LD_ASSERT_NOT_NULL(b); + + TO_FDV2_BUILDER(b)->InitialMode(ToConnectionMode(mode)); +} + +LD_EXPORT(void) +LDClientFDv2Builder_UsePost(LDClientFDv2Builder b, bool use_post) { + LD_ASSERT_NOT_NULL(b); + + TO_FDV2_BUILDER(b)->UsePost(use_post); +} + +LD_EXPORT(void) +LDClientFDv2Builder_CustomizeMode(LDClientFDv2Builder b, + enum LDClientConnectionMode mode, + LDClientFDv2ModeBuilder mode_builder) { + LD_ASSERT_NOT_NULL(b); + LD_ASSERT_NOT_NULL(mode_builder); + + TO_FDV2_BUILDER(b)->CustomizeMode(ToConnectionMode(mode), + *TO_MODE_BUILDER(mode_builder)); + LDClientFDv2ModeBuilder_Free(mode_builder); +} + +LD_EXPORT(LDClientFDv2ModeBuilder) +LDClientFDv2ModeBuilder_New(void) { + return FROM_MODE_BUILDER(new FDv2Builder::Mode()); +} + +LD_EXPORT(void) +LDClientFDv2ModeBuilder_Free(LDClientFDv2ModeBuilder b) { + delete TO_MODE_BUILDER(b); +} + +LD_EXPORT(void) +LDClientFDv2ModeBuilder_Initializer_Cache(LDClientFDv2ModeBuilder b) { + LD_ASSERT_NOT_NULL(b); + + TO_MODE_BUILDER(b)->Initializer(FDv2Builder::Cache()); +} + +LD_EXPORT(void) +LDClientFDv2ModeBuilder_Initializer_Polling( + LDClientFDv2ModeBuilder b, + LDClientFDv2PollingBuilder polling) { + LD_ASSERT_NOT_NULL(b); + LD_ASSERT_NOT_NULL(polling); + + TO_MODE_BUILDER(b)->Initializer(*TO_POLL_BUILDER(polling)); + LDClientFDv2PollingBuilder_Free(polling); +} + +LD_EXPORT(void) +LDClientFDv2ModeBuilder_Synchronizer_Streaming( + LDClientFDv2ModeBuilder b, + LDClientFDv2StreamingBuilder streaming) { + LD_ASSERT_NOT_NULL(b); + LD_ASSERT_NOT_NULL(streaming); + + TO_MODE_BUILDER(b)->Synchronizer(*TO_STREAM_BUILDER(streaming)); + LDClientFDv2StreamingBuilder_Free(streaming); +} + +LD_EXPORT(void) +LDClientFDv2ModeBuilder_Synchronizer_Polling( + LDClientFDv2ModeBuilder b, + LDClientFDv2PollingBuilder polling) { + LD_ASSERT_NOT_NULL(b); + LD_ASSERT_NOT_NULL(polling); + + TO_MODE_BUILDER(b)->Synchronizer(*TO_POLL_BUILDER(polling)); + LDClientFDv2PollingBuilder_Free(polling); +} + +LD_EXPORT(void) +LDClientFDv2ModeBuilder_FallbackToFDv1( + LDClientFDv2ModeBuilder b, + LDClientFDv2FDv1FallbackBuilder fallback) { + LD_ASSERT_NOT_NULL(b); + LD_ASSERT_NOT_NULL(fallback); + + TO_MODE_BUILDER(b)->FallbackToFDv1(*TO_FALLBACK_BUILDER(fallback)); + LDClientFDv2FDv1FallbackBuilder_Free(fallback); +} + +LD_EXPORT(void) +LDClientFDv2ModeBuilder_DisableFDv1Fallback(LDClientFDv2ModeBuilder b) { + LD_ASSERT_NOT_NULL(b); + + TO_MODE_BUILDER(b)->DisableFDv1Fallback(); +} + +LD_EXPORT(LDClientFDv2StreamingBuilder) +LDClientFDv2StreamingBuilder_New(void) { + return FROM_STREAM_BUILDER(new FDv2Builder::Streaming()); +} + +LD_EXPORT(void) +LDClientFDv2StreamingBuilder_InitialReconnectDelayMs( + LDClientFDv2StreamingBuilder b, + unsigned int milliseconds) { + LD_ASSERT_NOT_NULL(b); + + TO_STREAM_BUILDER(b)->InitialReconnectDelay( + std::chrono::milliseconds{milliseconds}); +} + +LD_EXPORT(void) +LDClientFDv2StreamingBuilder_BaseURL(LDClientFDv2StreamingBuilder b, + char const* base_url) { + LD_ASSERT_NOT_NULL(b); + LD_ASSERT_NOT_NULL(base_url); + + TO_STREAM_BUILDER(b)->BaseUrl(base_url); +} + +LD_EXPORT(void) +LDClientFDv2StreamingBuilder_Free(LDClientFDv2StreamingBuilder b) { + delete TO_STREAM_BUILDER(b); +} + +LD_EXPORT(LDClientFDv2PollingBuilder) +LDClientFDv2PollingBuilder_New(void) { + return FROM_POLL_BUILDER(new FDv2Builder::Polling()); +} + +LD_EXPORT(void) +LDClientFDv2PollingBuilder_IntervalS(LDClientFDv2PollingBuilder b, + unsigned int seconds) { + LD_ASSERT_NOT_NULL(b); + + TO_POLL_BUILDER(b)->PollInterval(std::chrono::seconds{seconds}); +} + +LD_EXPORT(void) +LDClientFDv2PollingBuilder_BaseURL(LDClientFDv2PollingBuilder b, + char const* base_url) { + LD_ASSERT_NOT_NULL(b); + LD_ASSERT_NOT_NULL(base_url); + + TO_POLL_BUILDER(b)->BaseUrl(base_url); +} + +LD_EXPORT(void) +LDClientFDv2PollingBuilder_Free(LDClientFDv2PollingBuilder b) { + delete TO_POLL_BUILDER(b); +} + +LD_EXPORT(LDClientFDv2FDv1FallbackBuilder) +LDClientFDv2FDv1FallbackBuilder_New(void) { + return FROM_FALLBACK_BUILDER(new FDv2Builder::FDv1Fallback()); +} + +LD_EXPORT(void) +LDClientFDv2FDv1FallbackBuilder_IntervalS(LDClientFDv2FDv1FallbackBuilder b, + unsigned int seconds) { + LD_ASSERT_NOT_NULL(b); + + TO_FALLBACK_BUILDER(b)->PollInterval(std::chrono::seconds{seconds}); +} + +LD_EXPORT(void) +LDClientFDv2FDv1FallbackBuilder_BaseURL(LDClientFDv2FDv1FallbackBuilder b, + char const* base_url) { + LD_ASSERT_NOT_NULL(b); + LD_ASSERT_NOT_NULL(base_url); + + TO_FALLBACK_BUILDER(b)->BaseUrl(base_url); +} + +LD_EXPORT(void) +LDClientFDv2FDv1FallbackBuilder_Free(LDClientFDv2FDv1FallbackBuilder b) { + delete TO_FALLBACK_BUILDER(b); +} + +// NOLINTEND OCInconsistentNamingInspection +// NOLINTEND cppcoreguidelines-pro-type-reinterpret-cast diff --git a/libs/client-sdk/tests/CMakeLists.txt b/libs/client-sdk/tests/CMakeLists.txt index 0f3f2c8a2..e55642434 100644 --- a/libs/client-sdk/tests/CMakeLists.txt +++ b/libs/client-sdk/tests/CMakeLists.txt @@ -19,3 +19,10 @@ add_executable(gtest_${LIBNAME} target_link_libraries(gtest_${LIBNAME} launchdarkly::client launchdarkly::internal launchdarkly::sse GTest::gtest_main) gtest_discover_tests(gtest_${LIBNAME}) + +# Compiled as C so that C++-only syntax cannot leak into a header that C +# consumers include. +add_library(c_headers_smoke_${LIBNAME} OBJECT + "${PROJECT_SOURCE_DIR}/tests/c_headers_smoke_test.c") +target_link_libraries(c_headers_smoke_${LIBNAME} PRIVATE launchdarkly::client) +set_target_properties(c_headers_smoke_${LIBNAME} PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED ON) diff --git a/libs/client-sdk/tests/c_headers_smoke_test.c b/libs/client-sdk/tests/c_headers_smoke_test.c new file mode 100644 index 000000000..986df07b1 --- /dev/null +++ b/libs/client-sdk/tests/c_headers_smoke_test.c @@ -0,0 +1,58 @@ +/* + * Compiled as C so that C++-only syntax cannot leak into a header a C + * consumer includes. It exercises the declarations rather than the behavior, + * which the C++ binding tests cover. + */ + +#include +#include +#include +#include + +void LDClientFDv2HeadersSmokeTest(void) { + LDClientFDv2Builder fdv2 = LDClientFDv2Builder_New(); + LDClientFDv2ModeBuilder mode = LDClientFDv2ModeBuilder_New(); + + LDClientFDv2Builder_InitialMode(fdv2, LD_CLIENT_CONNECTION_MODE_STREAMING); + LDClientFDv2Builder_UsePost(fdv2, true); + + LDClientFDv2ModeBuilder_Initializer_Cache(mode); + LDClientFDv2ModeBuilder_Initializer_Polling( + mode, LDClientFDv2PollingBuilder_New()); + LDClientFDv2ModeBuilder_Synchronizer_Streaming( + mode, LDClientFDv2StreamingBuilder_New()); + LDClientFDv2ModeBuilder_Synchronizer_Polling( + mode, LDClientFDv2PollingBuilder_New()); + LDClientFDv2ModeBuilder_FallbackToFDv1( + mode, LDClientFDv2FDv1FallbackBuilder_New()); + LDClientFDv2ModeBuilder_DisableFDv1Fallback(mode); + + LDClientFDv2Builder_CustomizeMode(fdv2, LD_CLIENT_CONNECTION_MODE_POLLING, + mode); + + LDClientConfigBuilder config = LDClientConfigBuilder_New("sdk-key"); + LDClientConfigBuilder_DataSource_MethodFDv2(config, fdv2); + LDClientConfigBuilder_Free(config); +} + +void LDClientFDv2SourceBuilderFreeSmokeTest(void) { + LDClientFDv2StreamingBuilder streaming = LDClientFDv2StreamingBuilder_New(); + LDClientFDv2PollingBuilder polling = LDClientFDv2PollingBuilder_New(); + LDClientFDv2FDv1FallbackBuilder fallback = + LDClientFDv2FDv1FallbackBuilder_New(); + LDClientFDv2ModeBuilder mode = LDClientFDv2ModeBuilder_New(); + LDClientFDv2Builder fdv2 = LDClientFDv2Builder_New(); + + LDClientFDv2StreamingBuilder_InitialReconnectDelayMs(streaming, 1000); + LDClientFDv2StreamingBuilder_BaseURL(streaming, "https://example.com"); + LDClientFDv2PollingBuilder_IntervalS(polling, 300); + LDClientFDv2PollingBuilder_BaseURL(polling, "https://example.com"); + LDClientFDv2FDv1FallbackBuilder_IntervalS(fallback, 300); + LDClientFDv2FDv1FallbackBuilder_BaseURL(fallback, "https://example.com"); + + LDClientFDv2StreamingBuilder_Free(streaming); + LDClientFDv2PollingBuilder_Free(polling); + LDClientFDv2FDv1FallbackBuilder_Free(fallback); + LDClientFDv2ModeBuilder_Free(mode); + LDClientFDv2Builder_Free(fdv2); +} diff --git a/libs/client-sdk/tests/fdv2_c_bindings_test.cpp b/libs/client-sdk/tests/fdv2_c_bindings_test.cpp new file mode 100644 index 000000000..a7a2a900d --- /dev/null +++ b/libs/client-sdk/tests/fdv2_c_bindings_test.cpp @@ -0,0 +1,177 @@ +#include + +#include +#include +#include + +#include +#include + +using launchdarkly::client_side::ConnectionMode; +using launchdarkly::config::shared::ClientSDK; + +using FDv2Config = launchdarkly::config::shared::built::FDv2Config; + +using namespace std::chrono_literals; + +namespace { + +// Builds a config through the C bindings and reads back the FDv2 settings. +FDv2Config BuildFDv2(LDClientFDv2Builder fdv2) { + LDClientConfigBuilder builder = LDClientConfigBuilder_New("sdk-key"); + LDClientConfigBuilder_DataSource_MethodFDv2(builder, fdv2); + + LDClientConfig config = nullptr; + LDStatus status = LDClientConfigBuilder_Build(builder, &config); + EXPECT_TRUE(LDStatus_Ok(status)); + LDStatus_Free(status); + + auto const* built = + reinterpret_cast(config); + auto result = std::get(built->DataSourceConfig().method); + LDClientConfig_Free(config); + return result; +} + +} // namespace + +TEST(FDv2Bindings, BuilderNewFree) { + LDClientFDv2Builder builder = LDClientFDv2Builder_New(); + ASSERT_TRUE(builder); + LDClientFDv2Builder_Free(builder); +} + +TEST(FDv2Bindings, SourceBuilderNewFree) { + LDClientFDv2StreamingBuilder streaming = LDClientFDv2StreamingBuilder_New(); + LDClientFDv2PollingBuilder polling = LDClientFDv2PollingBuilder_New(); + LDClientFDv2FDv1FallbackBuilder fallback = + LDClientFDv2FDv1FallbackBuilder_New(); + LDClientFDv2ModeBuilder mode = LDClientFDv2ModeBuilder_New(); + + ASSERT_TRUE(streaming); + ASSERT_TRUE(polling); + ASSERT_TRUE(fallback); + ASSERT_TRUE(mode); + + LDClientFDv2StreamingBuilder_Free(streaming); + LDClientFDv2PollingBuilder_Free(polling); + LDClientFDv2FDv1FallbackBuilder_Free(fallback); + LDClientFDv2ModeBuilder_Free(mode); +} + +TEST(FDv2Bindings, SelectingFDv2ReplacesTheFDv1Method) { + auto const config = BuildFDv2(LDClientFDv2Builder_New()); + + EXPECT_EQ(ConnectionMode::kStreaming, config.initial_mode); + EXPECT_EQ(3u, config.modes.size()); +} + +TEST(FDv2Bindings, InitialModeIsSelectable) { + LDClientFDv2Builder builder = LDClientFDv2Builder_New(); + LDClientFDv2Builder_InitialMode(builder, LD_CLIENT_CONNECTION_MODE_OFFLINE); + + auto const config = BuildFDv2(builder); + + EXPECT_EQ(ConnectionMode::kOffline, config.initial_mode); +} + +TEST(FDv2Bindings, UsePostIsSettable) { + LDClientFDv2Builder builder = LDClientFDv2Builder_New(); + LDClientFDv2Builder_UsePost(builder, true); + + auto const config = BuildFDv2(builder); + + EXPECT_TRUE(config.use_post); +} + +TEST(FDv2Bindings, CustomizingAModeReplacesItsPipeline) { + LDClientFDv2StreamingBuilder streaming = LDClientFDv2StreamingBuilder_New(); + LDClientFDv2StreamingBuilder_InitialReconnectDelayMs(streaming, 2000); + LDClientFDv2StreamingBuilder_BaseURL(streaming, + "https://stream.example.com"); + + LDClientFDv2ModeBuilder mode = LDClientFDv2ModeBuilder_New(); + LDClientFDv2ModeBuilder_Initializer_Cache(mode); + LDClientFDv2ModeBuilder_Synchronizer_Streaming(mode, streaming); + LDClientFDv2ModeBuilder_DisableFDv1Fallback(mode); + + LDClientFDv2Builder builder = LDClientFDv2Builder_New(); + LDClientFDv2Builder_CustomizeMode( + builder, LD_CLIENT_CONNECTION_MODE_STREAMING, mode); + + auto const config = BuildFDv2(builder); + auto const& definition = config.modes.at(ConnectionMode::kStreaming); + + ASSERT_EQ(1u, definition.initializers.size()); + EXPECT_TRUE(std::holds_alternative( + definition.initializers[0])); + ASSERT_EQ(1u, definition.synchronizers.size()); + auto const* built = + std::get_if(&definition.synchronizers[0]); + ASSERT_NE(nullptr, built); + EXPECT_EQ(2s, built->initial_reconnect_delay); + ASSERT_TRUE(built->base_url_override.has_value()); + EXPECT_EQ("https://stream.example.com", *built->base_url_override); + EXPECT_FALSE(definition.fdv1_fallback.has_value()); +} + +TEST(FDv2Bindings, PollingSourcesAreConfigurable) { + LDClientFDv2PollingBuilder initializer = LDClientFDv2PollingBuilder_New(); + LDClientFDv2PollingBuilder_IntervalS(initializer, 60); + + LDClientFDv2PollingBuilder synchronizer = LDClientFDv2PollingBuilder_New(); + LDClientFDv2PollingBuilder_IntervalS(synchronizer, 120); + LDClientFDv2PollingBuilder_BaseURL(synchronizer, + "https://poll.example.com"); + + LDClientFDv2ModeBuilder mode = LDClientFDv2ModeBuilder_New(); + LDClientFDv2ModeBuilder_Initializer_Polling(mode, initializer); + LDClientFDv2ModeBuilder_Synchronizer_Polling(mode, synchronizer); + + LDClientFDv2Builder builder = LDClientFDv2Builder_New(); + LDClientFDv2Builder_CustomizeMode(builder, + LD_CLIENT_CONNECTION_MODE_POLLING, mode); + + auto const config = BuildFDv2(builder); + auto const& definition = config.modes.at(ConnectionMode::kPolling); + + ASSERT_EQ(1u, definition.initializers.size()); + auto const* first = + std::get_if(&definition.initializers[0]); + ASSERT_NE(nullptr, first); + EXPECT_EQ(60s, first->poll_interval); + + ASSERT_EQ(1u, definition.synchronizers.size()); + auto const* second = + std::get_if(&definition.synchronizers[0]); + ASSERT_NE(nullptr, second); + EXPECT_EQ(120s, second->poll_interval); + ASSERT_TRUE(second->base_url_override.has_value()); + EXPECT_EQ("https://poll.example.com", *second->base_url_override); +} + +TEST(FDv2Bindings, TheFDv1FallbackIsConfigurable) { + LDClientFDv2FDv1FallbackBuilder fallback = + LDClientFDv2FDv1FallbackBuilder_New(); + LDClientFDv2FDv1FallbackBuilder_IntervalS(fallback, 600); + LDClientFDv2FDv1FallbackBuilder_BaseURL(fallback, + "https://relay.example.com"); + + LDClientFDv2ModeBuilder mode = LDClientFDv2ModeBuilder_New(); + LDClientFDv2ModeBuilder_Synchronizer_Streaming( + mode, LDClientFDv2StreamingBuilder_New()); + LDClientFDv2ModeBuilder_FallbackToFDv1(mode, fallback); + + LDClientFDv2Builder builder = LDClientFDv2Builder_New(); + LDClientFDv2Builder_CustomizeMode( + builder, LD_CLIENT_CONNECTION_MODE_STREAMING, mode); + + auto const config = BuildFDv2(builder); + auto const& built = + config.modes.at(ConnectionMode::kStreaming).fdv1_fallback; + + ASSERT_TRUE(built.has_value()); + EXPECT_EQ(600s, built->poll_interval); + ASSERT_TRUE(built->base_url_override.has_value()); + EXPECT_EQ("https://relay.example.com", *built->base_url_override); +}