diff --git a/README.md b/README.md index 1327f10..e0d734b 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ The below table shows what features the SDKs support or plan to support. - [x] Unleash context - [x] Strategy constrains - [x] Application registration -- [ ] Variants (WIP) +- [x] Variants - [ ] Usage Metrics - [ ] Custom stickiness @@ -69,6 +69,29 @@ unleashClient.isEnabled("feature.toogle"); unleashClient.isEnabled("feature.toogle", context); ``` +### Getting a Variant + +``` + #include "unleash/context.h" + ... + unleash::Context context{"userId"}; + auto variant = unleashClient.variant("feature.toogle", context); + ... + /* + The variant response is an instance of the following structure: + { + std::string name; + unsigned int weight; + bool enabled; + bool feature_enabled; + std::string payload; + } + */ +``` + +For more information about variants, see the [Variant documentation](https://docs.getunleash.io/advanced/toggle_variants). + + ## Integration ### Building with CMake diff --git a/include/unleash/feature.h b/include/unleash/feature.h index 32edcd9..f49c6d6 100644 --- a/include/unleash/feature.h +++ b/include/unleash/feature.h @@ -1,6 +1,7 @@ #ifndef UNLEASH_FEATURE_H #define UNLEASH_FEATURE_H #include "unleash/strategies/strategy.h" +#include "unleash/variants/variant.h" #include #include #include @@ -10,12 +11,17 @@ class Context; class Feature { public: Feature(std::string name, std::vector> strategies, bool enable); + void setVariants(std::pair>, unsigned int> variants); bool isEnabled(const Context &context) const; + variant_t getVariant(const Context &context) const; private: + bool checkVariant(const unleash::Variant &variantInput, variant_t &variantResponse, const Context &context) const; + std::string m_name; bool m_enabled; std::vector> m_strategies; + std::pair>, unsigned int> m_variants; }; } // namespace unleash #endif //UNLEASH_FEATURE_H diff --git a/include/unleash/unleashclient.h b/include/unleash/unleashclient.h index 4611c5a..4f9c53a 100644 --- a/include/unleash/unleashclient.h +++ b/include/unleash/unleashclient.h @@ -14,6 +14,7 @@ namespace unleash { class UnleashClientBuilder; class Context; +struct variant_t; class UNLEASH_EXPORT UnleashClient { public: @@ -27,6 +28,8 @@ class UNLEASH_EXPORT UnleashClient { void initializeClient(); bool isEnabled(const std::string &flag); bool isEnabled(const std::string &flag, const Context &context); + variant_t variant(const std::string &flag, const Context &context); + private: UnleashClient(std::string name, std::string url); diff --git a/include/unleash/strategies/murmur3hash.h b/include/unleash/utils/murmur3hash.h similarity index 82% rename from include/unleash/strategies/murmur3hash.h rename to include/unleash/utils/murmur3hash.h index 31b5894..8285abb 100644 --- a/include/unleash/strategies/murmur3hash.h +++ b/include/unleash/utils/murmur3hash.h @@ -4,14 +4,14 @@ // MurmurHash3 was written by Austin Appleby, and is placed in the public // domain. The author hereby disclaims copyright to this source code. //----------------------------------------------------------------------------- -#include +#include #include namespace unleash { void murmurHash3X8632(const void *key, int len, uint32_t seed, void *out); -uint32_t normalizedMurmur3(const std::string &key, uint32_t seed = 0); +uint32_t normalizedMurmur3(const std::string &key, uint32_t modulus = 100, uint32_t seed = 0); } // namespace unleash diff --git a/include/unleash/variants/variant.h b/include/unleash/variants/variant.h new file mode 100644 index 0000000..3fa9ca4 --- /dev/null +++ b/include/unleash/variants/variant.h @@ -0,0 +1,40 @@ +#ifndef UNLEASH_VARIANTS_H +#define UNLEASH_VARIANTS_H +#include +#include + +namespace unleash { + +class Context; + +struct Override { + std::string contextName; + std::vector values; +}; + +struct variant_t { + std::string name; + unsigned int weight; + bool enabled; + bool feature_enabled; + std::string payload; +}; + +class Variant { +public: + Variant(std::string name, unsigned int weight, std::string_view payload, std::string_view overrides); + unsigned int getWeight() const { return m_weight; } + const std::string &getName() const { return m_name; } + const std::vector &getOverrides() const { return m_overrides; } + const std::string &getPayload() const { return m_payload; } + +private: + std::string m_name; + unsigned int m_weight; + std::string m_payload; + std::vector m_overrides; +}; +} // namespace unleash + + +#endif //UNLEASH_VARIANTS_H \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4b679e4..e7ac306 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,27 +1,28 @@ add_library( - unleash - unleashclient.cpp - feature.cpp - api/cprclient.cpp - strategies/strategy.cpp - strategies/default.cpp - strategies/userwithid.cpp - strategies/applicationhostname.cpp - strategies/flexiblerollout.cpp - strategies/remoteaddress.cpp - strategies/murmur3hash.cpp - strategies/gradualrolloutuserid.cpp - strategies/gradualrolloutsessionid.cpp - strategies/gradualrolloutrandom.cpp) + unleash + unleashclient.cpp + feature.cpp + api/cprclient.cpp + strategies/strategy.cpp + strategies/default.cpp + strategies/userwithid.cpp + strategies/applicationhostname.cpp + strategies/flexiblerollout.cpp + strategies/remoteaddress.cpp + utils/murmur3hash.cpp + strategies/gradualrolloutuserid.cpp + strategies/gradualrolloutsessionid.cpp + strategies/gradualrolloutrandom.cpp + variants/variant.cpp) add_library(unleash::unleash ALIAS unleash) target_include_directories( - unleash PUBLIC "$") + unleash PUBLIC "$") target_link_libraries(unleash PRIVATE cpr::cpr - nlohmann_json::nlohmann_json) + nlohmann_json::nlohmann_json) set_target_properties(unleash PROPERTIES VERSION ${unleash_VERSION} - SOVERSION ${unleash_VERSION_MAJOR}) + SOVERSION ${unleash_VERSION_MAJOR}) target_compile_features(unleash PUBLIC cxx_std_17) diff --git a/src/feature.cpp b/src/feature.cpp index 68a7763..40029b2 100644 --- a/src/feature.cpp +++ b/src/feature.cpp @@ -1,8 +1,12 @@ #include "unleash/feature.h" +#include "unleash/utils/murmur3hash.h" +#include +#include namespace unleash { Feature::Feature(std::string name, std::vector> strategies, bool enable) : m_name(std::move(name)), m_enabled(enable), m_strategies(std::move(strategies)) {} + bool Feature::isEnabled(const Context &context) const { if (m_enabled) { if (m_strategies.empty()) return true; @@ -12,4 +16,49 @@ bool Feature::isEnabled(const Context &context) const { } return false; } + +void Feature::setVariants(std::pair>, unsigned int> variants) { + m_variants = std::move(variants); +} + +variant_t Feature::getVariant(const unleash::Context &context) const { + variant_t variant{"disabled", 0, false, false}; + if (!isEnabled(context)) { return variant; } + + variant.feature_enabled = true; + if (m_variants.first.empty()) { return variant; } + + variant.enabled = true; + constexpr uint32_t seed = 86028157; + auto normalizedValue = normalizedMurmur3(m_name + ":" + context.userId, m_variants.second, seed); + unsigned int weight = 0; + for (auto &eachVariant : m_variants.first) { + if (!eachVariant->getOverrides().empty() && checkVariant(*eachVariant, variant, context)) break; + + weight += eachVariant->getWeight(); + if (normalizedValue <= weight) { + variant.name = eachVariant->getName(); + variant.payload = eachVariant->getPayload(); + break; + } + } + + return variant; +} + +bool Feature::checkVariant(const unleash::Variant &variantInput, variant_t &variantResponse, + const unleash::Context &context) const { + if (auto contextIt = std::find_if(variantInput.getOverrides().begin(), variantInput.getOverrides().end(), + [](const Override &o) { return o.contextName == "userId"; }); + contextIt != variantInput.getOverrides().end()) { + auto valuesIt = std::find((*contextIt).values.begin(), (*contextIt).values.end(), context.userId); + + if (valuesIt != (*contextIt).values.end()) { + variantResponse.name = variantInput.getName(); + variantResponse.payload = variantInput.getPayload(); + return true; + } + } + return false; +}; } // namespace unleash \ No newline at end of file diff --git a/src/strategies/flexiblerollout.cpp b/src/strategies/flexiblerollout.cpp index 9334053..953daf1 100644 --- a/src/strategies/flexiblerollout.cpp +++ b/src/strategies/flexiblerollout.cpp @@ -1,5 +1,5 @@ #include "unleash/strategies/flexiblerollout.h" -#include "unleash/strategies/murmur3hash.h" +#include "unleash/utils/murmur3hash.h" #include #include diff --git a/src/strategies/gradualrolloutsessionid.cpp b/src/strategies/gradualrolloutsessionid.cpp index 8d0cfc7..31202d7 100644 --- a/src/strategies/gradualrolloutsessionid.cpp +++ b/src/strategies/gradualrolloutsessionid.cpp @@ -1,5 +1,5 @@ #include "unleash/strategies/gradualrolloutsessionid.h" -#include "unleash/strategies/murmur3hash.h" +#include "unleash/utils/murmur3hash.h" #include namespace unleash { diff --git a/src/strategies/gradualrolloutuserid.cpp b/src/strategies/gradualrolloutuserid.cpp index 7ad2df8..2da7919 100644 --- a/src/strategies/gradualrolloutuserid.cpp +++ b/src/strategies/gradualrolloutuserid.cpp @@ -1,5 +1,5 @@ #include "unleash/strategies/gradualrolloutuserid.h" -#include "unleash/strategies/murmur3hash.h" +#include "unleash/utils/murmur3hash.h" #include namespace unleash { diff --git a/src/unleashclient.cpp b/src/unleashclient.cpp index dd06950..f03a292 100644 --- a/src/unleashclient.cpp +++ b/src/unleashclient.cpp @@ -110,20 +110,50 @@ bool UnleashClient::isEnabled(const std::string &flag, const Context &context) { return false; } +variant_t UnleashClient::variant(const std::string &flag, const unleash::Context &context) { + variant_t variant{"disabled", 0, false, false}; + if (m_isInitialized) { + variant.feature_enabled = isEnabled(flag, context); + if (auto search = m_features.find(flag); search != m_features.end()) { + std::cout << "variant" << std::endl; + return m_features.at(flag).getVariant(context); + } + } + return variant; +} + UnleashClient::featuresMap_t UnleashClient::loadFeatures(std::string_view features) const { const auto featuresJson = nlohmann::json::parse(features); featuresMap_t featuresMap; for (const auto &[key, value] : featuresJson["features"].items()) { - std::vector> m_strategies; + // Load strategies + std::vector> strategies; for (const auto &[strategyKey, strategyValue] : value["strategies"].items()) { std::string strategyParameters; if (strategyValue.contains("parameters")) strategyParameters = strategyValue["parameters"].dump(); std::string strategyConstraints; if (strategyValue.contains("constraints")) { strategyConstraints = strategyValue["constraints"].dump(); } - m_strategies.push_back(Strategy::createStrategy(strategyValue["name"].get(), - strategyParameters, strategyConstraints)); + strategies.push_back(Strategy::createStrategy(strategyValue["name"].get(), strategyParameters, + strategyConstraints)); + } + Feature newFeature(value["name"], std::move(strategies), value["enabled"]); + // Load variants + std::pair>, unsigned int> variants; + if (value.contains("variants")) { + unsigned int totalWeight = 0; + for (const auto &[variantKey, variantValue] : value["variants"].items()) { + std::string variantPayload; + if (variantValue.contains("payload")) variantPayload = variantValue["payload"].dump(); + std::string variantOverrides; + if (variantValue.contains("overrides")) variantOverrides = variantValue["overrides"].dump(); + variants.first.push_back(std::make_unique(variantValue["name"], variantValue["weight"], + variantPayload, variantOverrides)); + totalWeight += variantValue["weight"].get(); + } + variants.second = totalWeight; + newFeature.setVariants(std::move(variants)); } - featuresMap.try_emplace(value["name"], value["name"], std::move(m_strategies), value["enabled"]); + featuresMap.try_emplace(value["name"], std::move(newFeature)); } return featuresMap; } diff --git a/src/strategies/murmur3hash.cpp b/src/utils/murmur3hash.cpp similarity index 83% rename from src/strategies/murmur3hash.cpp rename to src/utils/murmur3hash.cpp index b4dc447..d93fcb8 100644 --- a/src/strategies/murmur3hash.cpp +++ b/src/utils/murmur3hash.cpp @@ -1,4 +1,5 @@ -#include "unleash/strategies/murmur3hash.h" +#include "unleash/utils/murmur3hash.h" +#include //----------------------------------------------------------------------------- // MurmurHash3 was written by Austin Appleby, and is placed in the public @@ -69,22 +70,11 @@ FORCE_INLINE uint32_t fmix32(uint32_t h) { return h; } -//---------- - -FORCE_INLINE uint64_t fmix64(uint64_t k) { - k ^= k >> 33; - k *= BIG_CONSTANT(0xff51afd7ed558ccd); - k ^= k >> 33; - k *= BIG_CONSTANT(0xc4ceb9fe1a85ec53); - k ^= k >> 33; - - return k; -} //----------------------------------------------------------------------------- void murmurHash3X8632(const void *key, int len, uint32_t seed, void *out) { - const uint8_t *data = (const uint8_t *) key; + const auto *data = (const uint8_t *) key; const int nblocks = len / 4; uint32_t h1 = seed; @@ -95,7 +85,7 @@ void murmurHash3X8632(const void *key, int len, uint32_t seed, void *out) { //---------- // body - const uint32_t *blocks = (const uint32_t *) (data + nblocks * 4); + const auto *blocks = (const uint32_t *) (data + nblocks * 4); for (int i = -nblocks; i; i++) { uint32_t k1 = getblock32(blocks, i); @@ -112,7 +102,7 @@ void murmurHash3X8632(const void *key, int len, uint32_t seed, void *out) { //---------- // tail - const uint8_t *tail = (const uint8_t *) (data + nblocks * 4); + const auto *tail = data + nblocks * 4; uint32_t k1 = 0; @@ -127,7 +117,7 @@ void murmurHash3X8632(const void *key, int len, uint32_t seed, void *out) { k1 = ROTL32(k1, 15); k1 *= c2; h1 ^= k1; - }; + } //---------- // finalization @@ -139,10 +129,10 @@ void murmurHash3X8632(const void *key, int len, uint32_t seed, void *out) { *(uint32_t *) out = h1; } -uint32_t normalizedMurmur3(const std::string &key, uint32_t seed) { +uint32_t normalizedMurmur3(const std::string &key, uint32_t modulus, uint32_t seed) { uint32_t murmur3Hash; - murmurHash3X8632(key.c_str(), key.length(), seed, &murmur3Hash); - murmur3Hash %= 100; + murmurHash3X8632(key.c_str(), static_cast(key.length()), seed, &murmur3Hash); + murmur3Hash %= modulus; murmur3Hash += 1; return murmur3Hash; } diff --git a/src/variants/variant.cpp b/src/variants/variant.cpp new file mode 100644 index 0000000..38372c9 --- /dev/null +++ b/src/variants/variant.cpp @@ -0,0 +1,23 @@ +#include "unleash/variants/variant.h" +#include + + +namespace unleash { +Variant::Variant(std::string name, unsigned int weight, std::string_view payload, std::string_view overrides) + : m_name(std::move(name)), m_weight(weight), m_payload(payload) { + if (overrides.empty()) return; + + auto overrides_json = nlohmann::json::parse(overrides); + for (const auto &[key, value] : overrides_json.items()) { + if (value.contains("contextName") && value.contains("values")) { + Override variantOverride{value["contextName"]}; + for (const auto &[valuesKey, valuesValue] : value["values"].items()) { + variantOverride.values.push_back(valuesValue); + } + m_overrides.push_back(variantOverride); + } + } +} + + +} // namespace unleash diff --git a/test/tests.cpp b/test/tests.cpp index 64c93e1..de94dcb 100644 --- a/test/tests.cpp +++ b/test/tests.cpp @@ -19,7 +19,7 @@ class ApiClientMock : public unleash::ApiClient { MOCK_METHOD(bool, registration, (unsigned int), (override)); }; -using TestParam = std::pair; +using TestParam = std::tuple; std::vector readSpecificationTestFromDisk(const std::string &testPath) { std::vector values; @@ -41,7 +41,10 @@ std::vector readSpecificationTestFromDisk(const std::string &testPath std::ifstream testFile(testPath + element.get()); nlohmann::json testJson; testFile >> testJson; - values.push_back(std::pair(testJson["state"].dump(), testJson["tests"].dump())); + if (testJson.contains("tests")) + values.push_back(std::make_tuple(testJson["state"].dump(), testJson["tests"].dump(), false)); + else if (testJson.contains("variantTests")) + values.push_back(std::make_tuple(testJson["state"].dump(), testJson["variantTests"].dump(), true)); } } return values; @@ -101,10 +104,10 @@ TEST_P(UnleashSpecificationTest, TestSet) { .refreshInterval(refreshInterval) .authentication("clientToken") .registration(true); - EXPECT_CALL(*apiMock, features()).WillRepeatedly(Return(testData.first)); + EXPECT_CALL(*apiMock, features()).WillRepeatedly(Return(std::get<0>(testData))); EXPECT_CALL(*apiMock, registration(refreshInterval)).WillRepeatedly(Return(true)); unleashClient.initializeClient(); - nlohmann::json testSet = nlohmann::json::parse(testData.second); + nlohmann::json testSet = nlohmann::json::parse(std::get<1>(testData)); unleashClient.initializeClient(); // Retry initialization to check nothing happens for (const auto &[key, value] : testSet.items()) { auto contextJson = value["context"]; @@ -116,7 +119,17 @@ TEST_P(UnleashSpecificationTest, TestSet) { testContext.properties.try_emplace(propertyKey, propertyValue); } } - EXPECT_EQ(unleashClient.isEnabled(value["toggleName"], testContext), value["expectedResult"].get()); + if (!std::get<2>(testData)) { + EXPECT_EQ(unleashClient.isEnabled(value["toggleName"], testContext), value["expectedResult"].get()); + } else { + std::cout << value["toggleName"] << std::endl; + nlohmann::json expectedResult = value["expectedResult"]; + auto variant = unleashClient.variant(value["toggleName"], testContext); + EXPECT_EQ(expectedResult["feature_enabled"], variant.feature_enabled); + EXPECT_EQ(expectedResult["enabled"], variant.enabled); + EXPECT_EQ(expectedResult["name"], variant.name); + if (expectedResult.contains("payload")) EXPECT_EQ(expectedResult["payload"].dump(), variant.payload); + } } }