diff --git a/libs/client-sdk/include/launchdarkly/client_side/bindings/c/sdk.h b/libs/client-sdk/include/launchdarkly/client_side/bindings/c/sdk.h index 82c886603..f184f589a 100644 --- a/libs/client-sdk/include/launchdarkly/client_side/bindings/c/sdk.h +++ b/libs/client-sdk/include/launchdarkly/client_side/bindings/c/sdk.h @@ -401,7 +401,7 @@ LDClientSDK_JsonVariationDetail(LDClientSDK sdk, * @code * LDValue all_flags = LDClientSDK_AllFlags(sdk); * LDValue_ObjectIter it; - * for (it = LDValue_CreateObjectIter(all_flags); + * for (it = LDValue_ObjectIter_New(all_flags); * !LDValue_ObjectIter_End(it); LDValue_ObjectIter_Next(it)) { char * const* flag_key = LDValue_ObjectIter_Key(it); LDValue flag_val_ref = * LDValue_ObjectIter_Value(it); diff --git a/libs/common/include/launchdarkly/detail/c_binding_helpers.hpp b/libs/common/include/launchdarkly/detail/c_binding_helpers.hpp index 2f170ec9b..273210ab0 100644 --- a/libs/common/include/launchdarkly/detail/c_binding_helpers.hpp +++ b/libs/common/include/launchdarkly/detail/c_binding_helpers.hpp @@ -1,3 +1,5 @@ +#pragma once + #include #include diff --git a/libs/server-sdk-redis-source/tests/c_bindings_test.cpp b/libs/server-sdk-redis-source/tests/c_bindings_test.cpp index 6e58372a7..3d9b2c26d 100644 --- a/libs/server-sdk-redis-source/tests/c_bindings_test.cpp +++ b/libs/server-sdk-redis-source/tests/c_bindings_test.cpp @@ -2,6 +2,16 @@ #include +#include +#include +#include + +#include + +#include "prefixed_redis_client.hpp" + +using namespace launchdarkly::data_model; + TEST(RedisBindings, SourcePointerIsStoredOnSuccessfulCreation) { LDServerLazyLoadRedisResult result; ASSERT_TRUE(LDServerLazyLoadRedisSource_New("tcp://localhost:1234", "foo", @@ -26,3 +36,79 @@ TEST(RedisBindings, SourcePointerIsNullptrOnFailure) { LDServerLazyLoadRedisSource_New("totally not a URI", "foo", &result)); ASSERT_EQ(result.source, nullptr); } + +// This is an end-to-end test that uses an actual Redis instance with +// provisioned flag data. The source is passed into the SDK's LazyLoad data +// system, and then AllFlags is used to verify that the data is read back from +// Redis correctly. +TEST(RedisBindings, CanUseInSDKLazyLoadDataSource) { + sw::redis::Redis redis("tcp://localhost:6379"); + redis.flushdb(); + + PrefixedClient client(redis, "testprefix"); + Flag flag_a{"foo", 1, false, std::nullopt, {true, false}}; + flag_a.offVariation = 0; // variation: true + Flag flag_b{"bar", 1, false, std::nullopt, {true, false}}; + flag_b.offVariation = 1; // variation: false + + client.PutFlag(flag_a); + client.PutFlag(flag_b); + client.Init(); + + LDServerLazyLoadRedisResult result; + ASSERT_TRUE(LDServerLazyLoadRedisSource_New("tcp://localhost:6379", + "testprefix", &result)); + + LDServerConfigBuilder cfg_builder = LDServerConfigBuilder_New("sdk-123"); + + LDServerLazyLoadBuilder lazy_builder = LDServerLazyLoadBuilder_New(); + LDServerLazyLoadBuilder_SourcePtr( + lazy_builder, + reinterpret_cast(result.source)); + LDServerConfigBuilder_DataSystem_LazyLoad(cfg_builder, lazy_builder); + LDServerConfigBuilder_Events_Enabled( + cfg_builder, false); // Don't want outbound connection to + // LD in test. + + LDServerConfig config; + LDStatus status = LDServerConfigBuilder_Build(cfg_builder, &config); + ASSERT_TRUE(LDStatus_Ok(status)); + + LDServerSDK sdk = LDServerSDK_New(config); + LDServerSDK_Start(sdk, LD_NONBLOCKING, nullptr); + + LDContextBuilder ctx_builder = LDContextBuilder_New(); + LDContextBuilder_AddKind(ctx_builder, "cat", "shadow"); + LDContext context = LDContextBuilder_Build(ctx_builder); + + LDAllFlagsState state = + LDServerSDK_AllFlagsState(sdk, context, LD_ALLFLAGSSTATE_DEFAULT); + + ASSERT_TRUE(LDAllFlagsState_Valid(state)); + LDValue all = LDAllFlagsState_Map(state); + + ASSERT_EQ(LDValue_Type(all), LDValueType_Object); + + std::unordered_map values; + LDValue_ObjectIter iter; + for (iter = LDValue_ObjectIter_New(all); + !LDValue_ObjectIter_End(iter); LDValue_ObjectIter_Next(iter)) { + char const* key = LDValue_ObjectIter_Key(iter); + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + auto value_ref = reinterpret_cast( + LDValue_ObjectIter_Value(iter)); + values.emplace(key, *value_ref); + } + + LDValue_ObjectIter_Free(iter); + + std::unordered_map expected = { + {"foo", true}, {"bar", false}}; + ASSERT_EQ(values, expected); + + LDValue_Free(all); + LDAllFlagsState_Free(state); + + LDContext_Free(context); + LDServerSDK_Free(sdk); +} diff --git a/libs/server-sdk-redis-source/tests/prefixed_redis_client.hpp b/libs/server-sdk-redis-source/tests/prefixed_redis_client.hpp new file mode 100644 index 000000000..95af81718 --- /dev/null +++ b/libs/server-sdk-redis-source/tests/prefixed_redis_client.hpp @@ -0,0 +1,67 @@ +#pragma once + +#include +#include + +#include +#include +#include + +#include + +class PrefixedClient { + public: + PrefixedClient(sw::redis::Redis& client, std::string prefix) + : client_(client), prefix_(std::move(prefix)) {} + + void Init() const { + try { + client_.set(Prefixed("$inited"), "true"); + } catch (sw::redis::Error const& e) { + FAIL() << e.what(); + } + } + + void PutFlag(launchdarkly::data_model::Flag const& flag) const { + try { + client_.hset(Prefixed("features"), flag.key, + serialize(boost::json::value_from(flag))); + } catch (sw::redis::Error const& e) { + FAIL() << e.what(); + } + } + + void PutDeletedFlag(std::string const& key, std::string const& ts) const { + try { + client_.hset(Prefixed("features"), key, ts); + } catch (sw::redis::Error const& e) { + FAIL() << e.what(); + } + } + + void PutDeletedSegment(std::string const& key, + std::string const& ts) const { + try { + client_.hset(Prefixed("segments"), key, ts); + } catch (sw::redis::Error const& e) { + FAIL() << e.what(); + } + } + + void PutSegment(launchdarkly::data_model::Segment const& segment) const { + try { + client_.hset(Prefixed("segments"), segment.key, + serialize(boost::json::value_from(segment))); + } catch (sw::redis::Error const& e) { + FAIL() << e.what(); + } + } + + private: + std::string Prefixed(std::string const& name) const { + return prefix_ + ":" + name; + } + + sw::redis::Redis& client_; + std::string const prefix_; +}; diff --git a/libs/server-sdk-redis-source/tests/redis_source_test.cpp b/libs/server-sdk-redis-source/tests/redis_source_test.cpp index 1064d8a4f..e69a6762b 100644 --- a/libs/server-sdk-redis-source/tests/redis_source_test.cpp +++ b/libs/server-sdk-redis-source/tests/redis_source_test.cpp @@ -3,72 +3,21 @@ #include #include -#include -#include +#include +#include +#include -#include +#include "prefixed_redis_client.hpp" #include -using namespace launchdarkly::server_side::integrations; -using namespace launchdarkly::data_model; - -class PrefixedClient { - public: - PrefixedClient(sw::redis::Redis& client, std::string const& prefix) - : client_(client), prefix_(prefix) {} - - void Init() const { - try { - client_.set(Prefixed("$inited"), "true"); - } catch (sw::redis::Error const& e) { - FAIL() << e.what(); - } - } - - void PutFlag(Flag const& flag) const { - try { - client_.hset(Prefixed("features"), flag.key, - serialize(boost::json::value_from(flag))); - } catch (sw::redis::Error const& e) { - FAIL() << e.what(); - } - } - - void PutDeletedFlag(std::string const& key, std::string const& ts) const { - try { - client_.hset(Prefixed("features"), key, ts); - } catch (sw::redis::Error const& e) { - FAIL() << e.what(); - } - } - - void PutDeletedSegment(std::string const& key, - std::string const& ts) const { - try { - client_.hset(Prefixed("segments"), key, ts); - } catch (sw::redis::Error const& e) { - FAIL() << e.what(); - } - } - - void PutSegment(Segment const& segment) const { - try { - client_.hset(Prefixed("segments"), segment.key, - serialize(boost::json::value_from(segment))); - } catch (sw::redis::Error const& e) { - FAIL() << e.what(); - } - } +#include - private: - std::string Prefixed(std::string const& name) const { - return prefix_ + ":" + name; - } +#include - sw::redis::Redis& client_; - std::string const& prefix_; -}; +using namespace launchdarkly::server_side::integrations; +using namespace launchdarkly::data_model; +using namespace launchdarkly::server_side; class RedisTests : public ::testing::Test { public: @@ -412,6 +361,39 @@ TEST_F(RedisTests, CanConvertRedisDataSourceToDataReader) { std::shared_ptr reader = std::move(*maybe_source); } +TEST_F(RedisTests, CanUseAsSDKLazyLoadDataSource) { + Flag flag_a{"foo", 1, false, std::nullopt, {true, false}}; + flag_a.offVariation = 0; // variation: true + Flag flag_b{"bar", 1, false, std::nullopt, {true, false}}; + flag_b.offVariation = 1; // variation: false + + PutFlag(flag_a); + PutFlag(flag_b); + Init(); + + auto cfg_builder = ConfigBuilder("sdk-123"); + cfg_builder.DataSystem().Method( + config::builders::LazyLoadBuilder().Source(source)); + cfg_builder.Events() + .Disable(); // Don't want outbound calls to LD in the test + auto config = cfg_builder.Build(); + + ASSERT_TRUE(config); + + auto client = Client(std::move(*config)); + client.StartAsync(); + + auto const context = + launchdarkly::ContextBuilder().Kind("cat", "shadow").Build(); + + auto const all_flags = client.AllFlagsState(context); + auto const expected = std::unordered_map{ + {"foo", true}, {"bar", false}}; + + ASSERT_TRUE(all_flags.Valid()); + ASSERT_EQ(all_flags.Values(), expected); +} + TEST(RedisErrorTests, InvalidURIs) { std::vector const uris = {"nope, not a redis URI", "http://foo", diff --git a/libs/server-sdk/include/launchdarkly/server_side/bindings/c/all_flags_state/all_flags_state.h b/libs/server-sdk/include/launchdarkly/server_side/bindings/c/all_flags_state/all_flags_state.h index ab8d42af9..d96231b47 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/bindings/c/all_flags_state/all_flags_state.h +++ b/libs/server-sdk/include/launchdarkly/server_side/bindings/c/all_flags_state/all_flags_state.h @@ -4,7 +4,6 @@ #pragma once #include -#include #include #ifdef __cplusplus @@ -15,14 +14,16 @@ extern "C" { // only need to export C interface if typedef struct _LDAllFlagsState* LDAllFlagsState; /** - * Frees an AllFlagsState. - * @param state The AllFlagState to free. + * Frees an @ref LDAllFlagsState. + * @param state The state to free. + * @return void */ -LD_EXPORT(void) LDAllFlagsState_Free(LDAllFlagsState state); +LD_EXPORT(void) +LDAllFlagsState_Free(LDAllFlagsState state); /** * True if the LDAllFlagsState is valid. False if there was - * an error, such as the data store being unavailable. + * an error, such as the data source being unavailable. * * An invalid LDAllFlagsState can still be serialized successfully to a JSON * string. @@ -30,7 +31,8 @@ LD_EXPORT(void) LDAllFlagsState_Free(LDAllFlagsState state); * @param state The LDAllFlagState to check for validity. Must not be NULL. * @return True if the state is valid, false otherwise. */ -LD_EXPORT(bool) LDAllFlagsState_Valid(LDAllFlagsState state); +LD_EXPORT(bool) +LDAllFlagsState_Valid(LDAllFlagsState state); /** * Serializes the LDAllFlagsState to a JSON string. @@ -39,7 +41,7 @@ LD_EXPORT(bool) LDAllFlagsState_Valid(LDAllFlagsState state); * * @param state The LDAllFlagState to serialize. Must not be NULL. * @return A JSON string representing the LDAllFlagsState. The caller must free - * the string using LDMemory_FreeString. + * the string using @ref LDMemory_FreeString. */ LD_EXPORT(char*) LDAllFlagsState_SerializeJSON(LDAllFlagsState state); @@ -49,18 +51,18 @@ LDAllFlagsState_SerializeJSON(LDAllFlagsState state); * LDAllFlagsState. * * In order to avoid copying when a large value is accessed, - * the returned LDValue is a reference and NOT DIRECTLY OWNED by the caller. Its - * lifetime is managed by the parent LDAllFlagsState object. + * the returned @ref LDValue is a reference and NOT DIRECTLY OWNED by the + * caller. Its lifetime is managed by the parent LDAllFlagsState object. * - * WARNING! + * *WARNING!* * Do not free the returned LDValue. * Do not in any way access the returned LDValue after the LDAllFlagsState has * been freed. * - * If the flag has no value, returns an LDValue of type LDValueType_Null. + * If the flag has no value, returns an LDValue of type @ref LDValueType_Null. * * To obtain a caller-owned copy of the LDValue not subject to these - * restrictions, call LDValue_NewValue on the result. + * restrictions, call @ref LDValue_NewValue on the result. * * @param state An LDAllFlagsState. Must not be NULL. * @param flag_key Key of the flag. Must not be NULL. @@ -72,8 +74,25 @@ LD_EXPORT(LDValue) LDAllFlagsState_Value(LDAllFlagsState state, char const* flag_key); /** - * Defines options that may be used with LDServerSDK_AllFlagsState. To - * obtain default behavior, pass LD_ALLFLAGSSTATE_DEFAULT. + * Returns an object-type @ref LDValue where the keys are flag keys + * and the values are the flag values for the @ref LDContext used to generate + * this state. + * + * The LDValue is owned by the caller and must be freed. This + * may cause a large heap allocation. If you're interested in bootstrapping + * a client-side SDK, this is not the right method: see @ref + * LDAllFlagsState_SerializeJSON. + * + * @param state An LDAllFlagsState. Must not be NULL. + * @return An object-type LDValue of flag-key/flag-value pairs. The caller MUST + * free this value using @ref LDValue_Free. + */ +LD_EXPORT(LDValue) +LDAllFlagsState_Map(LDAllFlagsState state); + +/** + * Defines options that may be used with @ref LDServerSDK_AllFlagsState. To + * obtain default behavior, pass `LD_ALLFLAGSSTATE_DEFAULT`. * * It is possible to combine multiple options by ORing them together. * diff --git a/libs/server-sdk/src/bindings/c/all_flags_state/all_flags_state.cpp b/libs/server-sdk/src/bindings/c/all_flags_state/all_flags_state.cpp index 112b18666..c546c27d5 100644 --- a/libs/server-sdk/src/bindings/c/all_flags_state/all_flags_state.cpp +++ b/libs/server-sdk/src/bindings/c/all_flags_state/all_flags_state.cpp @@ -6,7 +6,9 @@ #include #include -#include +#include + +// NOLINTBEGIN cppcoreguidelines-pro-type-reinterpret-cast #define TO_ALLFLAGS(ptr) (reinterpret_cast(ptr)) #define FROM_ALLFLAGS(ptr) (reinterpret_cast(ptr)) @@ -54,3 +56,19 @@ LDAllFlagsState_Value(LDAllFlagsState state, char const* flag_key) { return FROM_VALUE(const_cast(&val_ref)); } + +LD_EXPORT(LDValue) +LDAllFlagsState_Map(LDAllFlagsState state) { + LD_ASSERT_NOT_NULL(state); + + auto const& values = TO_ALLFLAGS(state)->Values(); + + std::map all_flags_ordered{values.begin(), + values.end()}; + + Value* val = new Value(Value::Object{std::move(all_flags_ordered)}); + + return FROM_VALUE(val); +} + +// NOLINTEND cppcoreguidelines-pro-type-reinterpret-cast diff --git a/libs/server-sdk/src/client_impl.cpp b/libs/server-sdk/src/client_impl.cpp index c8703ab15..53c399043 100644 --- a/libs/server-sdk/src/client_impl.cpp +++ b/libs/server-sdk/src/client_impl.cpp @@ -161,7 +161,7 @@ AllFlagsState ClientImpl::AllFlagsState(Context const& context, if (!Initialized()) { LD_LOG(logger_, LogLevel::kWarn) << "AllFlagsState() called before client has finished " - "initializing. Data store not available. Returning empty state"; + "initializing. Data source not available. Returning empty state"; return {}; } diff --git a/libs/server-sdk/tests/server_c_bindings_test.cpp b/libs/server-sdk/tests/server_c_bindings_test.cpp index 636751df6..bd23fcb7e 100644 --- a/libs/server-sdk/tests/server_c_bindings_test.cpp +++ b/libs/server-sdk/tests/server_c_bindings_test.cpp @@ -2,8 +2,8 @@ #include #include -#include #include +#include #include @@ -158,6 +158,17 @@ TEST(ClientBindings, AllFlagsState) { LDValue nonexistent_flag = LDAllFlagsState_Value(state, "nonexistent_flag"); ASSERT_EQ(LDValue_Type(nonexistent_flag), LDValueType_Null); + LDValue value_map = LDAllFlagsState_Map(state); + ASSERT_EQ(LDValue_Type(value_map), LDValueType_Object); + + LDValue_ObjectIter value_iter = LDValue_ObjectIter_New(value_map); + ASSERT_TRUE(value_iter); + + ASSERT_TRUE(LDValue_ObjectIter_End(value_iter)); + LDValue_ObjectIter_Free(value_iter); + + LDValue_Free(value_map); + LDAllFlagsState_Free(state); LDContext_Free(context); LDServerSDK_Free(sdk);