Skip to content
7 changes: 7 additions & 0 deletions openfeature/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,13 @@ cc_library(
],
)

cc_library(
name = "hook_data",
srcs = ["hook_data.cpp"],
hdrs = ["hook_data.h"],
include_prefix = "openfeature",
)

cc_library(
name = "hook_hints",
hdrs = ["hook_hints.h"],
Expand Down
21 changes: 21 additions & 0 deletions openfeature/hook_data.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#include "openfeature/hook_data.h"

#include <any>
#include <string>
#include <unordered_map>

namespace openfeature {

void HookData::Set(std::string_view key, std::any value) {
data_.insert_or_assign(std::string(key), std::move(value));
}

const std::any* HookData::Get(std::string_view key) const {
auto it_key = data_.find(std::string(key));
if (it_key != data_.end()) {
return &it_key->second;
}
return nullptr;
}

} // namespace openfeature
36 changes: 36 additions & 0 deletions openfeature/hook_data.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#ifndef CPP_SDK_INCLUDE_OPENFEATURE_HOOK_DATA_H_
#define CPP_SDK_INCLUDE_OPENFEATURE_HOOK_DATA_H_

#include <any>
#include <string>
#include <string_view>
#include <unordered_map>

namespace openfeature {

// HookData provides a way for hooks to maintain state across their execution
// stages. Each hook instance gets its own isolated data store that persists
// only for the duration of a single flag evaluation.
class HookData {
public:
HookData() = default;

void Set(std::string_view key, std::any value);

const std::any* Get(std::string_view key) const;

template <typename T>
T* GetAs(std::string_view key) {
auto it_key = data_.find(std::string(key));
if (it_key != data_.end()) {
return std::any_cast<T>(&it_key->second);
}
return nullptr;
}

private:
std::unordered_map<std::string, std::any> data_;
};

} // namespace openfeature
#endif // CPP_SDK_INCLUDE_OPENFEATURE_HOOK_DATA_H_
9 changes: 9 additions & 0 deletions test/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,12 @@ cc_test(
"@googletest//:gtest_main",
],
)

cc_test(
name = "hook_data_test",
srcs = ["hook_data_test.cpp"],
deps = [
"//openfeature:hook_data",
"@googletest//:gtest_main",
],
)
159 changes: 159 additions & 0 deletions test/hook_data_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
#include "openfeature/hook_data.h"

#include <gtest/gtest.h>

#include <any>
#include <memory>
#include <string>

namespace openfeature {

constexpr std::string_view kNonExistentKey = "non_existent_key";

constexpr std::string_view kIntKey = "int_attr";
constexpr int kIntValue = 42;

constexpr std::string_view kStringKey = "string_attr";
constexpr std::string_view kStringValue = "hello world";

constexpr std::string_view kBoolKey = "bool_attr";
constexpr bool kBoolValue = true;

constexpr std::string_view kDoubleKey = "double_attr";
constexpr double kDoubleValue = 3.14159;

constexpr std::string_view kCounterKey = "counter";
constexpr int kInitialCounterValue = 100;
constexpr int kUpdatedCounterValue = 200;

constexpr std::string_view kDynamicKey = "dynamic_key";
constexpr int kDynamicIntValue = 12345;
constexpr std::string_view kDynamicStringValue = "now a string";

constexpr std::string_view kMyIntKey = "my_int";
constexpr int kMyIntValue = 99;

constexpr std::string_view kCustomStateKey = "custom_state";
constexpr int kInitialCallCount = 1;
constexpr std::string_view kBeforeStageName = "before_stage";

constexpr std::string_view kStageTrackerKey = "stage_tracker";
constexpr std::string_view kBeforeStage = "before";
constexpr std::string_view kAfterStage = "after";
constexpr int kUpdatedCallCount = 2;

constexpr std::string_view kSharedKey = "shared_key";
constexpr std::string_view kSharedDataValue = "shared data";
constexpr int kExpectedUseCount = 2;

class HookDataTest : public ::testing::Test {
protected:
HookData hook_data_;
};

TEST_F(HookDataTest, DefaultConstructorCreatesEmptyData) {
EXPECT_EQ(hook_data_.Get(kNonExistentKey), nullptr);
EXPECT_EQ(hook_data_.GetAs<int>(kNonExistentKey), nullptr);
}

TEST_F(HookDataTest, SetAndGetPrimitiveTypes) {
hook_data_.Set(kIntKey, kIntValue);
hook_data_.Set(kStringKey, std::string(kStringValue));
hook_data_.Set(kBoolKey, kBoolValue);
hook_data_.Set(kDoubleKey, kDoubleValue);

const std::any* int_any = hook_data_.Get(kIntKey);
ASSERT_NE(int_any, nullptr);
EXPECT_EQ(std::any_cast<int>(*int_any), kIntValue);

int* int_ptr = hook_data_.GetAs<int>(kIntKey);
ASSERT_NE(int_ptr, nullptr);
EXPECT_EQ(*int_ptr, kIntValue);

auto* str_ptr = hook_data_.GetAs<std::string>(kStringKey);
ASSERT_NE(str_ptr, nullptr);
EXPECT_EQ(*str_ptr, kStringValue);

bool* bool_ptr = hook_data_.GetAs<bool>(kBoolKey);
ASSERT_NE(bool_ptr, nullptr);
EXPECT_EQ(*bool_ptr, kBoolValue);

auto* double_ptr = hook_data_.GetAs<double>(kDoubleKey);
ASSERT_NE(double_ptr, nullptr);
EXPECT_DOUBLE_EQ(*double_ptr, kDoubleValue);
}

TEST_F(HookDataTest, SetOverwritesExistingKeyWithSameType) {
hook_data_.Set(kCounterKey, kInitialCounterValue);
ASSERT_NE(hook_data_.GetAs<int>(kCounterKey), nullptr);
EXPECT_EQ(*hook_data_.GetAs<int>(kCounterKey), kInitialCounterValue);

hook_data_.Set(kCounterKey, kUpdatedCounterValue);
ASSERT_NE(hook_data_.GetAs<int>(kCounterKey), nullptr);
EXPECT_EQ(*hook_data_.GetAs<int>(kCounterKey), kUpdatedCounterValue);
}

TEST_F(HookDataTest, SetOverwritesExistingKeyWithDifferentType) {
hook_data_.Set(kDynamicKey, kDynamicIntValue);
EXPECT_NE(hook_data_.GetAs<int>(kDynamicKey), nullptr);

hook_data_.Set(kDynamicKey, std::string(kDynamicStringValue));
EXPECT_EQ(hook_data_.GetAs<int>(kDynamicKey), nullptr);

auto* str_ptr = hook_data_.GetAs<std::string>(kDynamicKey);
ASSERT_NE(str_ptr, nullptr);
EXPECT_EQ(*str_ptr, kDynamicStringValue);
}

TEST_F(HookDataTest, GetAsReturnsNullptrOnTypeMismatch) {
hook_data_.Set(kMyIntKey, kMyIntValue);

EXPECT_EQ(hook_data_.GetAs<std::string>(kMyIntKey), nullptr);
EXPECT_EQ(hook_data_.GetAs<double>(kMyIntKey), nullptr);
EXPECT_EQ(hook_data_.GetAs<bool>(kMyIntKey), nullptr);
}

// Custom struct for testing user-defined type storage across hooks.
struct CustomEvaluationState {
int call_count;
std::string step_name;
};

TEST_F(HookDataTest, SetAndGetCustomStruct) {
CustomEvaluationState state{kInitialCallCount, std::string(kBeforeStageName)};
hook_data_.Set(kCustomStateKey, state);

auto* retrieved = hook_data_.GetAs<CustomEvaluationState>(kCustomStateKey);
ASSERT_NE(retrieved, nullptr);
EXPECT_EQ(retrieved->call_count, kInitialCallCount);
EXPECT_EQ(retrieved->step_name, kBeforeStageName);
}

TEST_F(HookDataTest, ModifyStoredValueInPlaceViaGetAs) {
hook_data_.Set(
kStageTrackerKey,
CustomEvaluationState{kInitialCallCount, std::string(kBeforeStage)});

auto* state_ptr = hook_data_.GetAs<CustomEvaluationState>(kStageTrackerKey);
ASSERT_NE(state_ptr, nullptr);
state_ptr->call_count++;
state_ptr->step_name = kAfterStage;

auto* updated_ptr = hook_data_.GetAs<CustomEvaluationState>(kStageTrackerKey);
ASSERT_NE(updated_ptr, nullptr);
EXPECT_EQ(updated_ptr->call_count, kUpdatedCallCount);
EXPECT_EQ(updated_ptr->step_name, kAfterStage);
}

TEST_F(HookDataTest, SetAndGetSharedPtr) {
auto ptr = std::make_shared<std::string>(kSharedDataValue);
hook_data_.Set(kSharedKey, ptr);

auto* retrieved = hook_data_.GetAs<std::shared_ptr<std::string>>(kSharedKey);
ASSERT_NE(retrieved, nullptr);
ASSERT_NE(*retrieved, nullptr);
EXPECT_EQ(**retrieved, kSharedDataValue);
EXPECT_EQ(retrieved->use_count(), kExpectedUseCount);
}

} // namespace openfeature