Skip to content

Commit

Permalink
feat: add LDAllFlagsState_Map C binding (#350)
Browse files Browse the repository at this point in the history
Adds `LDAllFlagsState_Map(state)`, which returns a map of key/value
pairs encoded in an object-type `LDValue`. This is in contrast to
accessing individual values in the state.
  • Loading branch information
cwaldren-ld authored Dec 26, 2023
1 parent 34cafc6 commit 2aca898
Show file tree
Hide file tree
Showing 9 changed files with 263 additions and 78 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions libs/common/include/launchdarkly/detail/c_binding_helpers.hpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#pragma once

#include <launchdarkly/bindings/c/status.h>
#include <launchdarkly/error.hpp>

Expand Down
86 changes: 86 additions & 0 deletions libs/server-sdk-redis-source/tests/c_bindings_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

#include <launchdarkly/server_side/bindings/c/integrations/redis/redis_source.h>

Check failure on line 3 in libs/server-sdk-redis-source/tests/c_bindings_test.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

/libs/server-sdk-redis-source/tests/c_bindings_test.cpp:3:10 [clang-diagnostic-error]

'launchdarkly/server_side/bindings/c/integrations/redis/redis_source.h' file not found

#include <launchdarkly/bindings/c/context_builder.h>
#include <launchdarkly/server_side/bindings/c/config/builder.h>
#include <launchdarkly/server_side/bindings/c/sdk.h>

#include <launchdarkly/data_model/flag.hpp>

#include "prefixed_redis_client.hpp"

using namespace launchdarkly::data_model;

TEST(RedisBindings, SourcePointerIsStoredOnSuccessfulCreation) {
LDServerLazyLoadRedisResult result;

Check warning on line 16 in libs/server-sdk-redis-source/tests/c_bindings_test.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

/libs/server-sdk-redis-source/tests/c_bindings_test.cpp:16:33 [cppcoreguidelines-init-variables]

variable 'result' is not initialized
ASSERT_TRUE(LDServerLazyLoadRedisSource_New("tcp://localhost:1234", "foo",
Expand All @@ -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;

Check warning on line 58 in libs/server-sdk-redis-source/tests/c_bindings_test.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

/libs/server-sdk-redis-source/tests/c_bindings_test.cpp:58:33 [cppcoreguidelines-init-variables]

variable 'result' is not initialized
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<LDServerLazyLoadSourcePtr>(result.source));

Check warning on line 67 in libs/server-sdk-redis-source/tests/c_bindings_test.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

/libs/server-sdk-redis-source/tests/c_bindings_test.cpp:67:9 [cppcoreguidelines-pro-type-reinterpret-cast]

do not use reinterpret_cast
LDServerConfigBuilder_DataSystem_LazyLoad(cfg_builder, lazy_builder);
LDServerConfigBuilder_Events_Enabled(
cfg_builder, false); // Don't want outbound connection to
// LD in test.

LDServerConfig config;

Check warning on line 73 in libs/server-sdk-redis-source/tests/c_bindings_test.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

/libs/server-sdk-redis-source/tests/c_bindings_test.cpp:73:20 [cppcoreguidelines-init-variables]

variable 'config' is not initialized
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<std::string, launchdarkly::Value> values;

Check warning on line 92 in libs/server-sdk-redis-source/tests/c_bindings_test.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

/libs/server-sdk-redis-source/tests/c_bindings_test.cpp:92:58 [cppcoreguidelines-init-variables]

variable 'values' is not initialized
LDValue_ObjectIter iter;

Check warning on line 93 in libs/server-sdk-redis-source/tests/c_bindings_test.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

/libs/server-sdk-redis-source/tests/c_bindings_test.cpp:93:24 [cppcoreguidelines-init-variables]

variable 'iter' is not initialized
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<launchdarkly::Value const* const>(

Check warning on line 98 in libs/server-sdk-redis-source/tests/c_bindings_test.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

/libs/server-sdk-redis-source/tests/c_bindings_test.cpp:98:9 [readability-qualified-auto]

'auto value_ref' can be declared as 'const auto *value_ref'
LDValue_ObjectIter_Value(iter));
values.emplace(key, *value_ref);
}

LDValue_ObjectIter_Free(iter);

std::unordered_map<std::string, launchdarkly::Value> expected = {

Check warning on line 105 in libs/server-sdk-redis-source/tests/c_bindings_test.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

/libs/server-sdk-redis-source/tests/c_bindings_test.cpp:105:58 [cppcoreguidelines-init-variables]

variable 'expected' is not initialized
{"foo", true}, {"bar", false}};
ASSERT_EQ(values, expected);

LDValue_Free(all);
LDAllFlagsState_Free(state);

LDContext_Free(context);
LDServerSDK_Free(sdk);
}
67 changes: 67 additions & 0 deletions libs/server-sdk-redis-source/tests/prefixed_redis_client.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#pragma once

#include <launchdarkly/serialization/json_flag.hpp>
#include <launchdarkly/serialization/json_segment.hpp>

#include <gtest/gtest.h>
#include <redis++.h>

Check failure on line 7 in libs/server-sdk-redis-source/tests/prefixed_redis_client.hpp

View workflow job for this annotation

GitHub Actions / cpp-linter

/libs/server-sdk-redis-source/tests/prefixed_redis_client.hpp:7:10 [clang-diagnostic-error]

'redis++.h' file not found
#include <boost/json.hpp>

#include <string>

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_;
};
102 changes: 42 additions & 60 deletions libs/server-sdk-redis-source/tests/redis_source_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,72 +3,21 @@
#include <launchdarkly/server_side/integrations/data_reader/kinds.hpp>
#include <launchdarkly/server_side/integrations/redis/redis_source.hpp>

Check failure on line 4 in libs/server-sdk-redis-source/tests/redis_source_test.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

/libs/server-sdk-redis-source/tests/redis_source_test.cpp:4:10 [clang-diagnostic-error]

'launchdarkly/server_side/integrations/redis/redis_source.hpp' file not found

#include <launchdarkly/serialization/json_flag.hpp>
#include <launchdarkly/serialization/json_segment.hpp>
#include <launchdarkly/context_builder.hpp>
#include <launchdarkly/server_side/client.hpp>
#include <launchdarkly/server_side/config/config_builder.hpp>

#include <boost/json.hpp>
#include "prefixed_redis_client.hpp"

#include <redis++.h>

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 <boost/json.hpp>

private:
std::string Prefixed(std::string const& name) const {
return prefix_ + ":" + name;
}
#include <unordered_map>

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:
Expand Down Expand Up @@ -412,6 +361,39 @@ TEST_F(RedisTests, CanConvertRedisDataSourceToDataReader) {
std::shared_ptr<ISerializedDataReader> 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<std::string, launchdarkly::Value>{
{"foo", true}, {"bar", false}};

ASSERT_TRUE(all_flags.Valid());
ASSERT_EQ(all_flags.Values(), expected);
}

TEST(RedisErrorTests, InvalidURIs) {
std::vector<std::string> const uris = {"nope, not a redis URI",
"http://foo",
Expand Down
Loading

0 comments on commit 2aca898

Please sign in to comment.