Skip to content

Commit

Permalink
Config v6 (#23)
Browse files Browse the repository at this point in the history
* Update config JSON model to v6

* Use int32_t instead of int for Int setting values for better portability

* Improve ConfigCatClient public API by making EvaluationDetails generic + add missing getValueDetails overload w/o defaultValue + eliminate getValue code redundancy

* Change user param type to shared_ptr<ConfigCatUser> + revise shared_ptr<> param passing

* Handle unexpected errors in evaluation methods (to guard against the SDK bringing down the consumer's app) + add the captured exception to EvaluationDetails + add the exception message to the log + change the return type of getKeyAndValue to std::optional for consistency

* Remove ConfigCatLogger and SettingResult from public API

* Refactor evaluator and evaluation logging to prepare it for the new features

* Make onConfigChanged hook's argument const to prevent accidental modifications by end user

* Revise std::move usage

* Implement new comparison operators

* Implement segment condition evaluation

* Implement prerequisite flag condition evaluation

* Implement SDK key format validation + fix broken tests

* Improve message of error 1103 + improve error reporting + fix SDK not returning the error message when forceRefresh fails

* Improve code formatting consistency

* coding style

* NamespaceIndentation: None

* config V2 rollout integration tests

* evaluateSensitiveTextSliceEqualsAnyOf fix

* Correct mistakes in error messages

* Handle unexpected errors in forceRefresh (to guard against the SDK bringing down the consumer's app) + expose RefreshResult to end user by returning it from forceRefresh

* Adjust terminology to docs (eliminate the usage of term 'match' in the context of conditions)

* evaluation log test + evaluation fixes

* test fix

* specchar test + sdk validation test

* v2 eval fixes + tests

* etag fix

* We should go to the cache in all polling modes instead of using the in memory variable (see https://trello.com/c/rreKm64A)

* test fix

* windows regex fix

* ConfigCatUser::create

* ConfigV2EvaluationTest -> EvaluationTest

* timeutils

* OverrideValueTypeMismatchShouldBeHandledCorrectly_SimplifiedConfig

* remove "this->"s + warning fixes

* Revert "OverrideValueTypeMismatchShouldBeHandledCorrectly_SimplifiedConfig"

This reverts commit 1413fae.

* ValueAndDefaultValueTypeCompatibilityTest

* keep just datetime_to_isostring and make_datetime on public interface

* make_datetime fix

* version 4.0.0

* make_datetime millisec 0 default value

* Change the return type of ConfigCatClient.get to shared_ptr<ConfigCatClient> to prevent use after free issues

* Fix clang error

* Add noexcept to move constructors/operators

* Minor corrections, maintain naming convention

* Add a few addition tests for user attribute value conversion edge cases

* Update samples in README.md

* Add guard against nullptr dereference in ConfigCatClient::close

* Call days_from_civil with 32-bit integer

* Manual polling in EnsureCloseWorks on windows

* curl close fix

* Add missing synchronization to ConfigCatClient::instanceCount

* github CI: ctest -j4

* Improve assertions in EnsureCloseWorks test

* ResponseErrorCode + cancelled request handling

---------

Co-authored-by: kp-cat <[email protected]>
  • Loading branch information
adams85 and kp-cat authored Apr 4, 2024
1 parent 55cd33d commit ac7f452
Show file tree
Hide file tree
Showing 135 changed files with 10,123 additions and 2,475 deletions.
6 changes: 6 additions & 0 deletions .clang-format
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
BasedOnStyle: Chromium
IndentWidth: 4
ColumnLimit: 160
NamespaceIndentation: None
# The number of spaces before trailing line comments (// - comments).
SpacesBeforeTrailingComments: 1
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ if(CONFIGCAT_BUILD_TESTS)
)
target_include_directories(google_tests PRIVATE ${CONFIGCAT_INCLUDE_PATHS})
# $<TARGET_PROPERTY:configcat,LINK_LIBRARIES> explicitly propagates private dependencies
target_link_libraries(google_tests configcat gtest_main $<TARGET_PROPERTY:configcat,LINK_LIBRARIES>)
target_link_libraries(google_tests configcat gmock_main $<TARGET_PROPERTY:configcat,LINK_LIBRARIES>)

gtest_discover_tests(google_tests)
endif()
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,9 @@ Read more about Targeting [here](https://configcat.com/docs/advanced/targeting/)
Percentage and targeted rollouts are calculated by the user object passed to the configuration requests.
The user object must be created with a **mandatory** identifier parameter which uniquely identifies each user:
```cpp
auto user = ConfigCatUser("#USER-IDENTIFIER#");
auto user = ConfigCatUser::create("#USER-IDENTIFIER#");

bool isMyAwesomeFeatureEnabled = client->getValue("isMyAwesomeFeatureEnabled", false, &user);
bool isMyAwesomeFeatureEnabled = client->getValue("isMyAwesomeFeatureEnabled", false, user);
if (isMyAwesomeFeatureEnabled) {
doTheNewThing();
} else {
Expand Down
498 changes: 344 additions & 154 deletions include/configcat/config.h

Large diffs are not rendered by default.

80 changes: 49 additions & 31 deletions include/configcat/configcatclient.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
#include "keyvalue.h"
#include "configcatoptions.h"
#include "refreshresult.h"
#include "settingresult.h"
#include "evaluationdetails.h"


Expand All @@ -20,15 +19,21 @@ class ConfigFetcher;
class RolloutEvaluator;
class FlagOverrides;
class ConfigService;

struct SettingResult;

class ConfigCatClient {
public:
ConfigCatClient(const ConfigCatClient&) = delete; // Disable copy
ConfigCatClient& operator=(const ConfigCatClient&) = delete;

ConfigCatClient(ConfigCatClient&&) = delete; // Disable move
ConfigCatClient& operator=(ConfigCatClient&&) = delete;

// Creates a new or gets an already existing [ConfigCatClient] for the given [sdkKey].
static ConfigCatClient* get(const std::string& sdkKey, const ConfigCatOptions* options = nullptr);
static std::shared_ptr<ConfigCatClient> get(const std::string& sdkKey, const ConfigCatOptions* options = nullptr);

// Closes an individual [ConfigCatClient] instance.
static void close(ConfigCatClient* client);
static void close(const std::shared_ptr<ConfigCatClient>& client);

// Closes all [ConfigCatClient] instances.
static void closeAll();
Expand All @@ -43,17 +48,17 @@ class ConfigCatClient {
* Parameter [defaultValue]: in case of any failure, this value will be returned.
* Parameter [user]: the user object to identify the caller.
*/
bool getValue(const std::string& key, bool defaultValue, const ConfigCatUser* user = nullptr) const;
int getValue(const std::string& key, int defaultValue, const ConfigCatUser* user = nullptr) const;
double getValue(const std::string& key, double defaultValue, const ConfigCatUser* user = nullptr) const;
std::string getValue(const std::string& key, const char* defaultValue, const ConfigCatUser* user = nullptr) const;
std::string getValue(const std::string& key, const std::string& defaultValue, const ConfigCatUser* user = nullptr) const;
bool getValue(const std::string& key, bool defaultValue, const std::shared_ptr<ConfigCatUser>& user = nullptr) const;
int32_t getValue(const std::string& key, int32_t defaultValue, const std::shared_ptr<ConfigCatUser>& user = nullptr) const;
double getValue(const std::string& key, double defaultValue, const std::shared_ptr<ConfigCatUser>& user = nullptr) const;
std::string getValue(const std::string& key, const char* defaultValue, const std::shared_ptr<ConfigCatUser>& user = nullptr) const;
std::string getValue(const std::string& key, const std::string& defaultValue, const std::shared_ptr<ConfigCatUser>& user = nullptr) const;

/**
* Gets the value of a feature flag or setting as std::shared_ptr<Value> identified by the given [key].
* In case of any failure, nullptr will be returned. The [user] param identifies the caller.
* Gets the value of a feature flag or setting as std::optional<Value> identified by the given [key].
* In case of any failure, std::nullopt will be returned. The [user] param identifies the caller.
*/
std::shared_ptr<Value> getValue(const std::string& key, const ConfigCatUser* user = nullptr) const;
std::optional<Value> getValue(const std::string& key, const std::shared_ptr<ConfigCatUser>& user = nullptr) const;

/**
* Gets the value and evaluation details of a feature flag or setting identified by the given `key`.
Expand All @@ -62,34 +67,40 @@ class ConfigCatClient {
* Parameter [defaultValue]: in case of any failure, this value will be returned.
* Parameter [user]: the user object to identify the caller.
*/
EvaluationDetails getValueDetails(const std::string& key, bool defaultValue, const ConfigCatUser* user = nullptr) const;
EvaluationDetails getValueDetails(const std::string& key, int defaultValue, const ConfigCatUser* user = nullptr) const;
EvaluationDetails getValueDetails(const std::string& key, double defaultValue, const ConfigCatUser* user = nullptr) const;
EvaluationDetails getValueDetails(const std::string& key, const std::string& defaultValue, const ConfigCatUser* user = nullptr) const;
EvaluationDetails getValueDetails(const std::string& key, const char* defaultValue, const ConfigCatUser* user = nullptr) const;
EvaluationDetails<bool> getValueDetails(const std::string& key, bool defaultValue, const std::shared_ptr<ConfigCatUser>& user = nullptr) const;
EvaluationDetails<int32_t> getValueDetails(const std::string& key, int32_t defaultValue, const std::shared_ptr<ConfigCatUser>& user = nullptr) const;
EvaluationDetails<double> getValueDetails(const std::string& key, double defaultValue, const std::shared_ptr<ConfigCatUser>& user = nullptr) const;
EvaluationDetails<std::string> getValueDetails(const std::string& key, const std::string& defaultValue, const std::shared_ptr<ConfigCatUser>& user = nullptr) const;
EvaluationDetails<std::string> getValueDetails(const std::string& key, const char* defaultValue, const std::shared_ptr<ConfigCatUser>& user = nullptr) const;

/**
* Gets the value and evaluation details of a feature flag or setting identified by the given [key].
* In case of any failure, the [value] field of the returned EvaluationDetails struct will be set to std::nullopt. The [user] param identifies the caller.
*/
EvaluationDetails<std::optional<Value>> getValueDetails(const std::string& key, const std::shared_ptr<ConfigCatUser>& user = nullptr) const;

// Gets all the setting keys.
std::vector<std::string> getAllKeys() const;

// Gets the key of a setting and it's value identified by the given Variation ID (analytics)
std::shared_ptr<KeyValue> getKeyAndValue(const std::string& variationId) const;
std::optional<KeyValue> getKeyAndValue(const std::string& variationId) const;

// Gets the values of all feature flags or settings.
std::unordered_map<std::string, Value> getAllValues(const ConfigCatUser* user = nullptr) const;
std::unordered_map<std::string, Value> getAllValues(const std::shared_ptr<ConfigCatUser>& user = nullptr) const;

// Gets the values along with evaluation details of all feature flags and settings.
std::vector<EvaluationDetails> getAllValueDetails(const ConfigCatUser* user = nullptr) const;
std::vector<EvaluationDetails<Value>> getAllValueDetails(const std::shared_ptr<ConfigCatUser>& user = nullptr) const;

// Initiates a force refresh synchronously on the cached configuration.
void forceRefresh();
RefreshResult forceRefresh();

// Sets the default user.
void setDefaultUser(std::shared_ptr<ConfigCatUser> user) {
inline void setDefaultUser(const std::shared_ptr<ConfigCatUser>& user) {
defaultUser = user;
}

// Sets the default user to nullptr.
void clearDefaultUser() {
inline void clearDefaultUser() {
defaultUser.reset();
}

Expand All @@ -103,23 +114,30 @@ class ConfigCatClient {
bool isOffline() const;

// Gets the Hooks object for subscribing events.
std::shared_ptr<Hooks> getHooks() { return hooks; }
inline std::shared_ptr<Hooks> getHooks() { return hooks; }

private:
struct MakeSharedEnabler;

ConfigCatClient(const std::string& sdkKey, const ConfigCatOptions& options);

void closeResources();

template<typename ValueType>
ValueType _getValue(const std::string& key, const ValueType& defaultValue, const ConfigCatUser* user = nullptr) const;
ValueType _getValue(const std::string& key, const ValueType& defaultValue, const std::shared_ptr<ConfigCatUser>& user = nullptr) const;

template<typename ValueType>
EvaluationDetails _getValueDetails(const std::string& key, ValueType defaultValue, const ConfigCatUser* user = nullptr) const;
EvaluationDetails<ValueType> _getValueDetails(const std::string& key, const ValueType& defaultValue, const std::shared_ptr<ConfigCatUser>& user = nullptr) const;

SettingResult getSettings() const;

EvaluationDetails evaluate(const std::string& key,
const ConfigCatUser* user,
const Setting& setting,
double fetchTime) const;
template<typename ValueType>
EvaluationDetails<ValueType> evaluate(const std::string& key,
const std::optional<Value>& defaultValue,
const std::shared_ptr<ConfigCatUser>& effectiveUser,
const Setting& setting,
const std::shared_ptr<Settings>& settings,
double fetchTime) const;

std::shared_ptr<Hooks> hooks;
std::shared_ptr<ConfigCatLogger> logger;
Expand All @@ -129,7 +147,7 @@ class ConfigCatClient {
std::unique_ptr<ConfigService> configService;

static std::mutex instancesMutex;
static std::unordered_map<std::string, std::unique_ptr<ConfigCatClient>> instances;
static std::unordered_map<std::string, std::shared_ptr<ConfigCatClient>> instances;
};

} // namespace configcat
28 changes: 15 additions & 13 deletions include/configcat/configcatoptions.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
#include <functional>
#include <vector>
#include <mutex>
#include <exception>
#include <optional>
#include "datagovernance.h"
#include "pollingmode.h"
#include "configcache.h"
Expand All @@ -21,9 +23,9 @@ namespace configcat {
class Hooks {
public:
explicit Hooks(const std::function<void()>& onClientReady = nullptr,
const std::function<void(std::shared_ptr<Settings>)>& onConfigChanged = nullptr,
const std::function<void(const EvaluationDetails&)>& onFlagEvaluated = nullptr,
const std::function<void(const std::string&)>& onError = nullptr) {
const std::function<void(std::shared_ptr<const Settings>)>& onConfigChanged = nullptr,
const std::function<void(const EvaluationDetailsBase&)>& onFlagEvaluated = nullptr,
const std::function<void(const std::string&, const std::exception_ptr&)>& onError = nullptr) {
if (onClientReady) {
onClientReadyCallbacks.push_back(onClientReady);
}
Expand All @@ -43,17 +45,17 @@ class Hooks {
onClientReadyCallbacks.push_back(callback);
}

void addOnConfigChanged(const std::function<void(std::shared_ptr<Settings>)>& callback) {
void addOnConfigChanged(const std::function<void(std::shared_ptr<const Settings>)>& callback) {
std::lock_guard<std::mutex> lock(mutex);
onConfigChangedCallbacks.push_back(callback);
}

void addOnFlagEvaluated(const std::function<void(const EvaluationDetails&)>& callback) {
void addOnFlagEvaluated(const std::function<void(const EvaluationDetailsBase&)>& callback) {
std::lock_guard<std::mutex> lock(mutex);
onFlagEvaluatedCallbacks.push_back(callback);
}

void addOnError(const std::function<void(const std::string&)>& callback) {
void addOnError(const std::function<void(const std::string&, const std::exception_ptr&)>& callback) {
std::lock_guard<std::mutex> lock(mutex);
onErrorCallbacks.push_back(callback);
}
Expand All @@ -65,24 +67,24 @@ class Hooks {
}
}

void invokeOnConfigChanged(std::shared_ptr<Settings> config) {
void invokeOnConfigChanged(const std::shared_ptr<Settings>& config) {
std::lock_guard<std::mutex> lock(mutex);
for (auto& callback : onConfigChangedCallbacks) {
callback(config);
}
}

void invokeOnFlagEvaluated(const EvaluationDetails& details) {
void invokeOnFlagEvaluated(const EvaluationDetailsBase& details) {
std::lock_guard<std::mutex> lock(mutex);
for (auto& callback : onFlagEvaluatedCallbacks) {
callback(details);
}
}

void invokeOnError(const std::string& error) {
void invokeOnError(const std::string& message, const std::exception_ptr& exception) {
std::lock_guard<std::mutex> lock(mutex);
for (auto& callback : onErrorCallbacks) {
callback(error);
callback(message, exception);
}
}

Expand All @@ -97,9 +99,9 @@ class Hooks {
private:
std::mutex mutex;
std::vector<std::function<void()>> onClientReadyCallbacks;
std::vector<std::function<void(std::shared_ptr<Settings>)>> onConfigChangedCallbacks;
std::vector<std::function<void(const EvaluationDetails&)>> onFlagEvaluatedCallbacks;
std::vector<std::function<void(const std::string&)>> onErrorCallbacks;
std::vector<std::function<void(std::shared_ptr<const Settings>)>> onConfigChangedCallbacks;
std::vector<std::function<void(const EvaluationDetailsBase&)>> onFlagEvaluatedCallbacks;
std::vector<std::function<void(const std::string&, const std::exception_ptr&)>> onErrorCallbacks;
};

// Configuration options for ConfigCatClient.
Expand Down
91 changes: 83 additions & 8 deletions include/configcat/configcatuser.h
Original file line number Diff line number Diff line change
@@ -1,26 +1,101 @@
#pragma once

#include <chrono>
#include <memory>
#include <optional>
#include <string>
#include <unordered_map>
#include <variant>
#include <vector>
#include "timeutils.h"

namespace configcat {

// An object containing attributes to properly identify a given user for rollout evaluation.
class ConfigCatUser {
public:
struct AttributeValue : public std::variant<std::string, double, date_time_t, std::vector<std::string>> {
private:
using _Base = std::variant<std::string, double, date_time_t, std::vector<std::string>>;
public:
AttributeValue(const char* v) : _Base(std::string(v)) {}
// CLang number type conversion to variant<double> fix
AttributeValue(double value) : _Base(value) {}

// Disable the implicit conversion from pointer to bool: https://stackoverflow.com/a/59372958/8656352
template<typename T>
AttributeValue(T*) = delete;

using _Base::_Base;
using _Base::operator=;
};

static constexpr char kIdentifierAttribute[] = "Identifier";
static constexpr char kEmailAttribute[] = "Email";
static constexpr char kCountryAttribute[] = "Country";

/**
* Creates a new instance of the [ConfigCatUser] class.
*
* Parameter [id]: the unique identifier of the user or session (e.g. email address, primary key, session ID, etc.)
* Parameter [email]: email address of the user.
* Parameter [country]: country of the user.
* Parameter [custom]: custom attributes of the user for advanced targeting rule definitions (e.g. user role, subscription type, etc.)
*
* All comparators support `std::string` values as User Object attribute (in some cases they need to be provided in a specific format though, see below),
* but some of them also support other types of values. It depends on the comparator how the values will be handled. The following rules apply:
*
* **Text-based comparators** (EQUALS, IS ONE OF, etc.)
* * accept `std::string` values,
* * all other values are automatically converted to `std::string` (a warning will be logged but evaluation will continue as normal).
*
* **SemVer-based comparators** (IS ONE OF, &lt;, &gt;=, etc.)
* * accept `std::string` values containing a properly formatted, valid semver value,
* * all other values are considered invalid (a warning will be logged and the currently evaluated targeting rule will be skipped).
*
* **Number-based comparators** (=, &lt;, &gt;=, etc.)
* * accept `double` values,
* * accept `std::string` values containing a properly formatted, valid `double` value,
* * all other values are considered invalid (a warning will be logged and the currently evaluated targeting rule will be skipped).
*
* **Date time-based comparators** (BEFORE / AFTER)
* * accept `configcat::date_time_t` (`std::chrono::system_clock::time_point`) values,
which are automatically converted to a second-based Unix timestamp,
* * accept `double` values representing a second-based Unix timestamp,
* * accept `std::string` values containing a properly formatted, valid `double` value,
* * all other values are considered invalid (a warning will be logged and the currently evaluated targeting rule will be skipped).
*
* **String array-based comparators** (ARRAY CONTAINS ANY OF / ARRAY NOT CONTAINS ANY OF)
* * accept lists of `std::string` (i.e. `std::vector<std::string>`),
* * accept `std::string` values containing a valid JSON string which can be deserialized to a list of `std::string`,
* * all other values are considered invalid (a warning will be logged and the currently evaluated targeting rule will be skipped).
*/
ConfigCatUser(const std::string& id,
const std::string& email = {},
const std::string& country = {},
const std::unordered_map<std::string, std::string>& custom = {});
const std::optional<std::string>& email = std::nullopt,
const std::optional<std::string>& country = std::nullopt,
const std::unordered_map<std::string, ConfigCatUser::AttributeValue>& custom = {})
: identifier(id)
, email(email)
, country(country)
, custom(custom) {}

const std::string* getAttribute(const std::string& key) const;
static inline std::shared_ptr<ConfigCatUser> create(const std::string& id,
const std::optional<std::string>& email = std::nullopt,
const std::optional<std::string>& country = std::nullopt,
const std::unordered_map<std::string, ConfigCatUser::AttributeValue>& custom = {}) {
return std::make_shared<ConfigCatUser>(id, email, country, custom);
}

inline const std::string& getIdentifier() const { return std::get<std::string>(identifier); }
inline const ConfigCatUser::AttributeValue& getIdentifierAttribute() const { return identifier; }
const ConfigCatUser::AttributeValue* getAttribute(const std::string& key) const;
std::string toJson() const;

private:
std::unordered_map<std::string, std::string> attributes;

public:
const std::string& identifier;
ConfigCatUser::AttributeValue identifier;
std::optional<ConfigCatUser::AttributeValue> email;
std::optional<ConfigCatUser::AttributeValue> country;
std::unordered_map<std::string, AttributeValue> custom;
};

} // namespace configcat
3 changes: 2 additions & 1 deletion include/configcat/consolelogger.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ class ConsoleLogger : public ILogger {
public:
ConsoleLogger(LogLevel logLevel = LOG_LEVEL_WARNING): ILogger(logLevel) {}

void log(LogLevel level, const std::string& message) override {
void log(LogLevel level, const std::string& message, const std::exception_ptr& exception = nullptr) override {
printf("[%s]: %s\n", logLevelAsString(level), message.c_str());
if (exception) printf("Exception details: %s\n", unwrap_exception_message(exception).c_str());
}
};

Expand Down
Loading

0 comments on commit ac7f452

Please sign in to comment.