diff --git a/libs/common/CMakeLists.txt b/libs/common/CMakeLists.txt index 98e614f86..290a64fe5 100644 --- a/libs/common/CMakeLists.txt +++ b/libs/common/CMakeLists.txt @@ -21,12 +21,15 @@ if (CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) set_property(GLOBAL PROPERTY USE_FOLDERS ON) endif () -#set(CMAKE_FILES "${CMAKE_CURRENT_SOURCE_DIR}/cmake") -#set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_FILES}) - # Needed to fetch external dependencies. include(FetchContent) +set(Boost_USE_STATIC_LIBS OFF) +set(Boost_USE_MULTITHREADED ON) +set(Boost_USE_STATIC_RUNTIME OFF) +find_package(Boost 1.80 REQUIRED) +message(STATUS "LaunchDarkly: using Boost v${Boost_VERSION}") + # Add main SDK sources. add_subdirectory(src) diff --git a/libs/common/include/attribute_reference.hpp b/libs/common/include/attribute_reference.hpp index 40d181b98..f4aac60a7 100644 --- a/libs/common/include/attribute_reference.hpp +++ b/libs/common/include/attribute_reference.hpp @@ -1,9 +1,13 @@ #pragma once +#include #include #include +#include #include +#include + namespace launchdarkly { /** @@ -28,6 +32,19 @@ namespace launchdarkly { */ class AttributeReference { public: + /** + * Provides a hashing function for use with unordered sets. + */ + struct HashFunction { + std::size_t operator()(AttributeReference const& ref) const { + return boost::hash_range(ref.components_.begin(), + ref.components_.end()); + } + }; + + using SetType = std::unordered_set; + /** * Get the component of the attribute reference at the specified depth. * @@ -38,7 +55,7 @@ class AttributeReference { * @return The component at the specified depth or an empty string if the * depth is out of bounds. */ - std::string const& component(size_t depth) const; + std::string const& component(std::size_t depth) const; /** * Get the total depth of the reference. @@ -46,7 +63,7 @@ class AttributeReference { * For example, depth() on the reference `/a/b/c` would return 3. * @return */ - size_t depth() const; + std::size_t depth() const; /** * Check if the reference is a "kind" reference. Either `/kind` or `kind`. @@ -94,10 +111,34 @@ class AttributeReference { return os; } + /** + * Construct an attribute reference from a string. + * @param ref_str The string to make an attribute reference from. + */ + AttributeReference(std::string ref_str); + + /** + * Construct an attribute reference from a constant string. + * @param ref_str The string to make an attribute reference from. + */ + AttributeReference(char const* ref_str); + + bool operator==(AttributeReference const& other) const { + return components_ == other.components_; + } + + bool operator!=(AttributeReference const& other) const { + return !(*this == other); + } + + bool operator<(AttributeReference const& rhs) const { + return components_ < rhs.components_; + } + private: AttributeReference(std::string str, bool is_literal); - bool valid_; + bool valid_ = false; std::string redaction_name_; std::vector components_; diff --git a/libs/common/include/attributes.hpp b/libs/common/include/attributes.hpp new file mode 100644 index 000000000..c3651e5f1 --- /dev/null +++ b/libs/common/include/attributes.hpp @@ -0,0 +1,151 @@ +#pragma once + +#include +#include + +#include "attribute_reference.hpp" +#include "value.hpp" + +namespace launchdarkly { + +/** + * A collection of attributes that can be present within a context. + * A multi-context has multiple sets of attributes keyed by their "kind". + */ +class Attributes { + public: + /** + * Get the key for the context. + * @return A reference to the context key. + */ + std::string const& key() const; + + /** + * Get the name for the context. + * + * @return A reference to the context name, or an empty string if no name + * is set. + */ + std::string const& name() const; + + /** + * Is the context anonymous or not. Defaults to false. + * @return True if the context is anonymous. + */ + bool anonymous() const; + + /** + * Get a set of the private attributes for the context. + * @return The set of private attributes for the context. + */ + AttributeReference::SetType const& private_attributes() const { + return private_attributes_; + } + + /** + * Gets the item by the specified attribute reference, or returns a null + * Value. + * @param ref The reference to get an attribute by. + * @return A Value containing the requested field, or a Value representing + * null. + */ + launchdarkly::Value const& get( + launchdarkly::AttributeReference const& ref) const { + if (!ref.valid()) { + // Cannot index by invalid references. + return launchdarkly::Value::null(); + } + if (ref.is_kind()) { + // Cannot access kind. + return launchdarkly::Value::null(); + } + + if (ref.depth() == 1) { + // Handle built-in attributes. + if (ref.component(0) == "key") { + return key_; + } + if (ref.component(0) == "name") { + return name_; + } + if (ref.component(0) == "anonymous") { + return anonymous_; + } + } + + launchdarkly::Value const* node = &custom_attributes_; + bool found = true; + for (size_t index = 0; index < ref.depth(); index++) { + auto const& component = ref.component(index); + if (node->is_object()) { + auto const& map = node->as_object(); + if (auto search = map.find(component); search != map.end()) { + node = &search->second; + } else { + found = false; + break; + } + } else { + found = false; + } + } + if (!found) { + return launchdarkly::Value::null(); + } + return *node; + } + + /** + * Construct a set of attributes. This is used internally by the SDK + * but is not intended to used by consumers of the SDK. + * + * @param key The key for the context. + * @param name The name of the context. + * @param anonymous If the context is anonymous. + * @param attributes Additional attributes for the context. + * @param private_attributes A list of attributes that should be private. + */ + Attributes(std::string key, + std::optional name, + bool anonymous, + launchdarkly::Value attributes, + AttributeReference::SetType private_attributes = + AttributeReference::SetType()) + : key_(std::move(key)), + name_(std::move(name)), + anonymous_(anonymous), + custom_attributes_(std::move(attributes)), + private_attributes_(std::move(private_attributes)) {} + + friend std::ostream& operator<<(std::ostream& out, + Attributes const& attrs) { + out << "{key: " << attrs.key_ << ", " + << " name: " << attrs.name_ << " anonymous: " << attrs.anonymous_ + << " private: ["; + bool first = true; + for (auto const& private_attribute : attrs.private_attributes_) { + if (first) { + first = false; + } else { + out << ", "; + } + out << private_attribute; + } + out << "] " + << " custom: " << attrs.custom_attributes_ << "}"; + + return out; + } + + private: + // Built-in attributes. + launchdarkly::Value key_; + launchdarkly::Value name_; + launchdarkly::Value anonymous_; + AttributeReference::SetType private_attributes_; + + launchdarkly::Value custom_attributes_; + + // Kinds are contained at the context level, not inside attributes. +}; +} // namespace launchdarkly diff --git a/libs/common/include/attributes_builder.hpp b/libs/common/include/attributes_builder.hpp new file mode 100644 index 000000000..b26904a0c --- /dev/null +++ b/libs/common/include/attributes_builder.hpp @@ -0,0 +1,182 @@ +#pragma once + +#include + +#include "attribute_reference.hpp" +#include "attributes.hpp" +#include "value.hpp" + +namespace launchdarkly { + +/** + * This is used in the implementation of the context builder for setting + * attributes for a single context. This is not intended to be directly + * used by an SDK consumer. + * + * @tparam BuilderReturn The type of builder using the AttributesBuilder. + * @tparam BuildType The type of object being built. + */ +template +class AttributesBuilder { + public: + /** + * Create an attributes builder with the given key. + * @param key A unique string identifying a context. + */ + AttributesBuilder(BuilderReturn& builder, std::string kind, std::string key) + : key_(std::move(key)), kind_(std::move(kind)), builder_(builder) {} + + /** + * The context's name. + * + * You can search for contexts on the Contexts page by name. + * + * @param name + * @return A reference to the current builder. + */ + AttributesBuilder& name(std::string name); + + /** + * If true, the context will _not_ appear on the Contexts page in the + * LaunchDarkly dashboard. + * + * @param anonymous The value to set. + * @return A reference to the current builder. + */ + AttributesBuilder& anonymous(bool anonymous); + + /** + * Add or update an attribute in the context. + * + * This method cannot be used to set the key, kind, name, or anonymous + * property of a context. The specific methods on the context builder, or + * attributes builder, should be used. + * + * @param name The name of the attribute. + * @param value The value for the attribute. + * @param private_attribute If the attribute should be considered private: + * that is, the value will not be sent to LaunchDarkly in analytics events. + * @return A reference to the current builder. + */ + AttributesBuilder& set(std::string name, launchdarkly::Value value); + + /** + * Add or update a private attribute in the context. + * + * This method cannot be used to set the key, kind, name, or anonymous + * property of a context. The specific methods on the context builder, or + * attributes builder, should be used. + * + * Once you have set an attribute private it will remain in the private + * list even if you call `set` afterward. This method is just a convenience + * which also adds the attribute to the `private_attributes`. + * + * @param name The name of the attribute. + * @param value The value for the attribute. + * @param private_attribute If the attribute should be considered private: + * that is, the value will not be sent to LaunchDarkly in analytics events. + * @return A reference to the current builder. + */ + AttributesBuilder& set_private(std::string name, launchdarkly::Value value); + + /** + * Designate a context attribute, or properties within them, as private: + * that is, their values will not be sent to LaunchDarkly in analytics + * events. + * + * Each parameter can be a simple attribute name, such as "email". Or, if + * the first character is a slash, the parameter is interpreted as a + * slash-delimited path to a property within a JSON object, where the first + * path component is a Context attribute name and each following component + * is a nested property name: for example, suppose the attribute "address" + * had the following JSON object value: + * + * ``` + * {"street": {"line1": "abc", "line2": "def"}} + * ``` + * + * Using ["/address/street/line1"] in this case would cause the "line1" + * property to be marked as private. This syntax deliberately resembles JSON + * Pointer, but other JSON Pointer features such as array indexing are not + * supported for Private. + * + * This action only affects analytics events that involve this particular + * Context. To mark some (or all) Context attributes as private for all + * contexts, use the overall configuration for the SDK. See [TODO] and + * [TODO]. + * + * The attributes "kind" and "key", and the "_meta" attributes cannot be + * made private. + * + * In this example, firstName is marked as private, but lastName is not: + * + * ``` + * const context = { + * kind: 'org', + * key: 'my-key', + * firstName: 'Pierre', + * lastName: 'Menard', + * _meta: { + * privateAttributes: ['firstName'], + * } + * }; + * ``` + * + * This is a metadata property, rather than an attribute that can be + * addressed in evaluations: that is, a rule clause that references the + * attribute name "privateAttributes", will not use this value, but would + * use a "privateAttributes" attribute set on the context. + * @param ref The reference to set private. + * @return A reference to the current builder. + */ + AttributesBuilder& add_private_attribute(AttributeReference ref); + + /** + * Add items from an iterable collection. One that provides a begin/end + * iterator and iterates over AttributeReferences or a convertible type. + * @tparam IterType The type of iterable. + * @param attributes The attributes to add as private. + * @return A reference to the current builder. + */ + template + AttributesBuilder& add_private_attributes(IterType attributes) { + for (auto iter : attributes) { + private_attributes_.insert(iter); + } + return *this; + } + + AttributesBuilder kind(std::string kind, std::string key) { + builder_.internal_add_kind(kind_, build_attributes()); + return builder_.kind(kind, key); + } + + /** + * Build the context. This method should not be called more than once. + * It moves the builder content into the built context. + * + * @return The built context. + */ + [[nodiscard]] BuildType build() { + builder_.internal_add_kind(kind_, build_attributes()); + return builder_.build(); + } + + private: + BuilderReturn& builder_; + + Attributes build_attributes(); + + AttributesBuilder& set(std::string name, + launchdarkly::Value value, + bool private_attribute); + + std::string kind_; + std::string key_; + std::string name_; + bool anonymous_ = false; + + std::map values_; + AttributeReference::SetType private_attributes_; +}; +} // namespace launchdarkly diff --git a/libs/common/include/context.hpp b/libs/common/include/context.hpp new file mode 100644 index 000000000..1199bec82 --- /dev/null +++ b/libs/common/include/context.hpp @@ -0,0 +1,130 @@ +#pragma once + +#include +#include + +#include "attributes.hpp" +#include "value.hpp" + +namespace launchdarkly { + +struct ContextErrors { + inline static const std::string kInvalidKind = + "\"Kind contained invalid characters. A kind may contain ASCII letters " + "or " + "numbers, as well as '.', '-', and '_'.\""; + // For now disallow an empty key. This may need changed if anonymous key + // generation and persistence is added. + inline static const std::string kInvalidKey = + "\"The key for a context may not be empty.\""; +}; + +class ContextBuilder; + +/** + * A LaunchDarkly context. + */ +class Context { + friend class ContextBuilder; + + public: + /** + * Get the kinds the context contains. + * + * @return A vector of kinds. + */ + std::vector const& kinds(); + + /** + * Get a set of attributes associated with a kind. + * + * Only call this function if you have checked that the kind is present. + * + * @param kind The kind to get attributes for. + * @return The attributes if they exist. + */ + [[nodiscard]] Attributes const& attributes(std::string const& kind) const; + + /** + * Get an attribute value by kind and attribute reference. If the kind is + * not present, or the attribute not present in the kind, then + * Value::null() will be returned. + * + * @param kind The kind to get the value for. + * @param ref The reference to the desired attribute. + * @return The attribute Value or a Value representing null. + */ + Value const& get(std::string const& kind, + launchdarkly::AttributeReference const& ref); + + /** + * Check if a context is valid. + * + * @return Returns true if the context is valid. + */ + [[nodiscard]] bool valid() const { return valid_; } + + /** + * Get the canonical key for this context. + */ + [[nodiscard]] std::string const& canonical_key() const; + + /** + * Get a collection containing the kinds and their associated keys. + * + * @return Returns a map of kinds to keys. + */ + [[nodiscard]] std::map const& + keys_and_kinds() const; + + /** + * Get a string containing errors the context encountered during + * construction. + * + * @return A string containing errors, or an empty string if there are no + * errors. + */ + std::string const& errors() { return errors_; } + + friend std::ostream& operator<<(std::ostream& out, Context const& context) { + if (context.valid_) { + out << "{contexts: ["; + bool first = true; + for (auto const& kind : context.attributes_) { + if (first) { + first = false; + } else { + out << ", "; + } + out << "kind: " << kind.first << " attributes: " << kind.second; + } + out << "]"; + } else { + out << "{invalid: errors: [" << context.errors_ << "]"; + } + + return out; + } + + private: + /** + * Create an invalid context with the given error message. + */ + Context(std::string error_message); + + /** + * Create a valid context with the given attributes. + * @param attributes + */ + Context(std::map attributes); + std::map attributes_; + std::vector kinds_; + std::map keys_and_kinds_; + bool valid_ = false; + std::string errors_; + std::string canonical_key_; + + std::string make_canonical_key(); +}; + +} // namespace launchdarkly diff --git a/libs/common/include/context_builder.hpp b/libs/common/include/context_builder.hpp new file mode 100644 index 000000000..093d6be07 --- /dev/null +++ b/libs/common/include/context_builder.hpp @@ -0,0 +1,72 @@ +#pragma once + +#include +#include "attributes_builder.hpp" +#include "context.hpp" + +namespace launchdarkly { + +/** + * Class for building LaunchDarkly contexts. + * + * You cannot build a context until you have added at least one kind. + * + * Building a context with a single kind. + * @code + * auto context = ContextBuilder() + * .kind("user", "bobby-bobberson") + * .name("Bob") + * .anonymous(false) + * // Set a custom attribute. + * .set("likesCats", true) + * // Set a private custom attribute. + * .set_private("email", "email@email.email") + * .build(); + * @endcode + * + * Building a context with multiple kinds. + * @code + * auto context = ContextBuilder() + * .kind("user", "bobby-bobberson") + * .name("Bob") + * .anonymous(false) + * // Set a custom attribute. + * .set("likesCats", true) + * // Set a private custom attribute. + * .set_private("email", "email@email.email") + * // Add another kind to the context. + * .kind("org", "org-key") + * .anonymous(true) + * .set("goal", "money") + * .build(); + * @endcode + */ +class ContextBuilder { + friend AttributesBuilder; + + public: + /** + * Start adding a kind to the context. + * @param kind The kind being added. + * @param key The key for the kind. + * @return A builder which allows adding attributes for the kind. + */ + AttributesBuilder kind(std::string kind, + std::string key); + + private: + /** + * Build a context. This should only be called once, because + * the contents of the builder are moved. + * @return The built context. + */ + Context build(); + + void internal_add_kind(std::string kind, Attributes attrs); + + std::map kinds_; + bool valid_ = true; + std::string errors_; +}; + +} // namespace launchdarkly diff --git a/libs/common/include/value.hpp b/libs/common/include/value.hpp new file mode 100644 index 000000000..4f36d5f33 --- /dev/null +++ b/libs/common/include/value.hpp @@ -0,0 +1,424 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +namespace launchdarkly { + +/** + * Value represents any of the data types supported by JSON, all of which can be + * used for a LaunchDarkly feature flag variation, or for an attribute in an + * evaluation context. Value instances are immutable. + * + * # Uses of JSON types in LaunchDarkly + * + * LaunchDarkly feature flags can have variations of any JSON type other than + * null. If you want to evaluate a feature flag in a general way that does not + * have expectations about the variation type, or if the variation value is a + * complex data structure such as an array or object, you can use the SDK method + * [TODO] to get the value and then use Value methods to examine it. + * + * Similarly, attributes of an evaluation context ([TODO]) + * can have variations of any JSON type other than null. If you want to set a + * context attribute in a general way that will accept any type, or set the + * attribute value to a complex data structure such as an array or object, you + * can use the builder method [TODO]. + * + * Arrays and objects have special meanings in LaunchDarkly flag evaluation: + * - An array of values means "try to match any of these values to the + * targeting rule." + * - An object allows you to match a property within the object to the + * targeting rule. For instance, in the example above, a targeting rule could + * reference /objectAttr1/color to match the value "green". Nested property + * references like /objectAttr1/address/street are allowed if a property + * contains another JSON object. + * + * # Constructors and builders + * [TODO] + * + * # Comparisons + * [TODO] + */ +class Value { + public: + /** + * Array type for values. Provides const iteration and indexing. + */ + class Array { + public: + struct Iterator { + using iterator_category = std::forward_iterator_tag; + using difference_type = std::ptrdiff_t; + using value_type = Value; + using pointer = value_type const*; + using reference = value_type const&; + + Iterator(std::vector::const_iterator iterator); + + reference operator*() const; + pointer operator->(); + Iterator& operator++(); + Iterator operator++(int); + + friend bool operator==(Iterator const& lhs, Iterator const& rhs) { + return lhs.iterator_ == rhs.iterator_; + }; + + friend bool operator!=(Iterator const& lhs, Iterator const& rhs) { + return lhs.iterator_ != rhs.iterator_; + }; + + private: + std::vector::const_iterator iterator_; + }; + + friend std::ostream& operator<<(std::ostream& out, Array const& arr) { + out << "["; + bool first = true; + for (auto const& item : arr.vec_) { + if (first) { + first = false; + } else { + out << ", "; + } + out << item; + } + out << "]"; + return out; + } + + /** + * Create an array from a vector of Value. + * @param vec The vector to base the array on. + */ + Array(std::vector vec); + Array(std::initializer_list values) : vec_(values) {} + Array() = default; + + Value const& operator[](std::size_t index) const; + + [[nodiscard]] std::size_t size() const; + + [[nodiscard]] Iterator begin() const; + + [[nodiscard]] Iterator end() const; + + private: + std::vector vec_; + }; + + /** + * Object type for values. Provides const iteration and indexing. + */ + class Object { + public: + struct Iterator { + using iterator_category = std::forward_iterator_tag; + using difference_type = std::ptrdiff_t; + + using value_type = std::pair; + using pointer = value_type const*; + using reference = value_type const&; + + Iterator(std::map::const_iterator iterator); + + reference operator*() const; + pointer operator->(); + Iterator& operator++(); + Iterator operator++(int); + + friend bool operator==(Iterator const& lhs, Iterator const& rhs) { + return rhs.it_ == lhs.it_; + }; + friend bool operator!=(Iterator const& lhs, Iterator const& rhs) { + return lhs.it_ != rhs.it_; + }; + + private: + std::map::const_iterator it_; + }; + + friend std::ostream& operator<<(std::ostream& out, Object const& obj) { + out << "{"; + bool first = true; + for (auto const& pair : obj.map_) { + if (first) { + first = false; + } else { + out << ", "; + } + out << "{" << pair.first << ", " << pair.second << "}"; + } + out << "}"; + return out; + } + + /** + * Create an Object from a map of Values. + * @param map The map to base the object on. + */ + Object(std::map map) : map_(std::move(map)) {} + Object() = default; + Object(std::initializer_list> values) { + map_.insert(std::make_move_iterator(values.begin()), + std::make_move_iterator(values.end())); + } + + /* + * Get the Value with the specified key. + * + * This operates like `.at` on a map, and accessing out of bounds + * is invalid. + */ + Value const& operator[](std::string const& key) const { + return map_.at(key); + } + + /** + * The number of items in the Object. + * @return The number of items in the Object. + */ + [[nodiscard]] std::size_t size() const; + + /** + * Get the number of items with the given key. Will be 1 or 0. + * @param key The key to get a count for. + * @return The count of items with the given key. + */ + [[nodiscard]] std::size_t count(std::string const& key) const { + return map_.count(key); + } + + [[nodiscard]] Iterator begin() const; + + [[nodiscard]] Iterator end() const; + + /** + * Find a Value by key. Operates like `find` on a std::map. + * @param key The key to find a value for. + * @return The value, or the end iterator. + */ + [[nodiscard]] Iterator find(std::string const& key) const; + + private: + std::map map_; + }; + + /** + * Create a Value from a string constant. + * @param str The string constant to base the value on. + */ + Value(char const* str); + + enum class Type { kNull, kBool, kNumber, kString, kObject, kArray }; + + /** + * Construct a value representing null. + */ + Value(); + + Value(Value const& val) = default; + Value(Value&&) = default; + Value& operator=(Value const&) = default; + Value& operator=(Value&&) = default; + + /** + * Construct a boolean value. + * @param boolean + */ + Value(bool boolean); + + /** + * Construct a number value from a double. + * @param num + */ + Value(double num); + + /** + * Construct a number value from an integer. + * @param num + */ + Value(int num); + + /** + * Construct a string value. + * @param str + */ + Value(std::string str); + + /** + * Construct an array value from a vector of Value. + * @param arr + */ + Value(std::vector arr); + + Value(Array arr) : storage_(std::move(arr)), type_(Type::kArray) {} + + Value(Object obj) : storage_(std::move(obj)), type_(Type::kObject) {} + + /** + * Construct an object value from a map of Value. + * @param obj + */ + Value(std::map obj); + + /** + * Create an array type value from the given list. + * + * Cannot be used to create object type values. + * @param values + */ + Value(std::initializer_list values); + + /** + * Create either a value string, or null value, from an optional string. + * @param opt_string + */ + Value(std::optional opt_string); + + /** + * Get the type of the attribute. + */ + [[nodiscard]] Type type() const; + + /** + * Returns true if the value is a null. + * + * Unlike other variants there is not an as_null(). Instead use the return + * value from this function as a marker. + * @return True if the value is null. + */ + [[nodiscard]] bool is_null() const; + + /** + * Returns true if the value is a boolean. + * + * @return + */ + [[nodiscard]] bool is_bool() const; + + /** + * Returns true if the value is a number. + * + * Numbers are always stored as doubles, but can be accessed as either + * an int or double for convenience. + * @return True if the value is a number. + */ + [[nodiscard]] bool is_number() const; + + /** + * Returns true if the value is a string. + * + * @return True if the value is a string. + */ + [[nodiscard]] bool is_string() const; + + /** + * Returns true if the value is an array. + * + * @return True if the value is an array. + */ + [[nodiscard]] bool is_array() const; + + /** + * Returns true if the value is an object. + * + * @return True if the value is an object. + */ + [[nodiscard]] bool is_object() const; + + /** + * If the value is a boolean, then return the boolean, otherwise return + * false. + * + * @return The value of the boolean, or false. + */ + [[nodiscard]] bool as_bool() const; + + /** + * If the value is a number, then return the internal double value as an + * integer, otherwise return 0. + * + * @return The value as an integer, or 0. + */ + [[nodiscard]] int as_int() const; + + [[nodiscard]] double as_double() const; + + /** + * If the value is a string, then return a reference to that string, + * otherwise return a reference to an empty string. + * + * @return The value as a string, or an empty string. + */ + [[nodiscard]] std::string const& as_string() const; + + /** + * If the value is an array type, then return a reference to that array as a + * vector, otherwise return a reference to an empty vector. + * + * @return The value as a vector, or an empty vector. + */ + [[nodiscard]] Array const& as_array() const; + + /** + * if the value is an object type, then return a reference to that object + * as a map, otherwise return a reference to an empty map. + * + * @return The value as a map, or an empty map. + */ + [[nodiscard]] Object const& as_object() const; + + ~Value() = default; + + /** + * Get a null value. + * @return The null value. + */ + static Value const& null(); + + friend std::ostream& operator<<(std::ostream& out, Value const& value) { + switch (value.type_) { + case Type::kNull: + out << "null()"; + break; + case Type::kBool: + out << "bool(" + << (boost::get(value.storage_) ? "true" : "false") + << ")"; + break; + case Type::kNumber: + out << "number(" << boost::get(value.storage_) << ")"; + break; + case Type::kString: + out << "string(" << boost::get(value.storage_) + << ")"; + break; + case Type::kObject: + out << "object(" << boost::get(value.storage_) << ")"; + break; + case Type::kArray: + out << "array(" << boost::get(value.storage_) << ")"; + break; + } + return out; + } + + private: + boost::variant storage_; + Type type_; + + // Empty constants used when accessing the wrong type. + inline static const std::string empty_string_; + inline static const Array empty_vector_; + inline static const Object empty_map_; + static const Value null_value_; +}; + +} // namespace launchdarkly diff --git a/libs/common/src/CMakeLists.txt b/libs/common/src/CMakeLists.txt index d6aa90def..321dce69c 100644 --- a/libs/common/src/CMakeLists.txt +++ b/libs/common/src/CMakeLists.txt @@ -11,6 +11,11 @@ add_library(${LIBNAME} console_backend.cpp log_level.cpp attribute_reference.cpp + context.cpp + context_builder.cpp + attributes.cpp + value.cpp + attributes_builder.cpp config/service_endpoints.cpp config/endpoints_builder.cpp config/config_builder.cpp @@ -20,6 +25,8 @@ add_library(${LIBNAME} add_library(launchdarkly::common ALIAS ${LIBNAME}) +target_link_libraries(${LIBNAME} Boost::headers) + # Need the public headers to build. target_include_directories(${LIBNAME} PUBLIC ../include) diff --git a/libs/common/src/attribute_reference.cpp b/libs/common/src/attribute_reference.cpp index 9d489dc9a..428ca48b8 100644 --- a/libs/common/src/attribute_reference.cpp +++ b/libs/common/src/attribute_reference.cpp @@ -175,6 +175,7 @@ AttributeReference::AttributeReference(std::string str, bool literal) { } else { redaction_name_ = str; } + valid_ = true; } else { valid_ = ParseRef(str, components_); redaction_name_ = std::move(str); @@ -192,14 +193,14 @@ AttributeReference AttributeReference::from_reference_str(std::string ref_str) { return {std::move(ref_str), false}; } -std::string const& AttributeReference::component(size_t depth) const { +std::string const& AttributeReference::component(std::size_t depth) const { if (depth < components_.size()) { return components_[depth]; } return empty_; } -size_t AttributeReference::depth() const { +std::size_t AttributeReference::depth() const { return components_.size(); } @@ -215,4 +216,10 @@ std::string const& AttributeReference::redaction_name() const { return redaction_name_; } +AttributeReference::AttributeReference(std::string ref_str) + : AttributeReference(std::move(ref_str), false) {} + +AttributeReference::AttributeReference(char const* ref_str) + : AttributeReference(std::string(ref_str)) {} + } // namespace launchdarkly diff --git a/libs/common/src/attributes.cpp b/libs/common/src/attributes.cpp new file mode 100644 index 000000000..4d26b2f07 --- /dev/null +++ b/libs/common/src/attributes.cpp @@ -0,0 +1,16 @@ +#include "attributes.hpp" + +namespace launchdarkly { + +std::string const& Attributes::key() const { + return key_.as_string(); +} + +std::string const& Attributes::name() const { + return name_.as_string(); +} + +bool Attributes::anonymous() const { + return anonymous_.as_bool(); +} +} // namespace launchdarkly diff --git a/libs/common/src/attributes_builder.cpp b/libs/common/src/attributes_builder.cpp new file mode 100644 index 000000000..96ed128e6 --- /dev/null +++ b/libs/common/src/attributes_builder.cpp @@ -0,0 +1,64 @@ +#include "attributes_builder.hpp" +#include "context_builder.hpp" + +namespace launchdarkly { + +template <> +AttributesBuilder& +AttributesBuilder::name(std::string name) { + name_ = std::move(name); + return *this; +} + +template <> +AttributesBuilder& +AttributesBuilder::anonymous(bool anonymous) { + anonymous_ = anonymous; + return *this; +} + +template <> +AttributesBuilder& +AttributesBuilder::set(std::string name, + Value value, + bool private_attribute) { + if (name == "key" || name == "kind" || name == "anonymous" || + name == "name") { + return *this; + } + + if (private_attribute) { + this->private_attributes_.insert(name); + } + values_[std::move(name)] = std::move(value); + return *this; +} + +template <> +AttributesBuilder& +AttributesBuilder::set(std::string name, Value value) { + return set(std::move(name), std::move(value), false); +} + +template <> +AttributesBuilder& +AttributesBuilder::set_private(std::string name, + Value value) { + return set(std::move(name), std::move(value), true); +} + +template <> +AttributesBuilder& +AttributesBuilder::add_private_attribute( + AttributeReference ref) { + private_attributes_.insert(std::move(ref)); + return *this; +} + +template <> +Attributes AttributesBuilder::build_attributes() { + return {std::move(key_), std::move(name_), anonymous_, std::move(values_), + std::move(private_attributes_)}; +} + +} // namespace launchdarkly diff --git a/libs/common/src/context.cpp b/libs/common/src/context.cpp new file mode 100644 index 000000000..ca19ccacf --- /dev/null +++ b/libs/common/src/context.cpp @@ -0,0 +1,92 @@ +#include + +#include "context.hpp" + +namespace launchdarkly { + +static bool NeedsEscape(std::string_view to_check) { + return to_check.find_first_of("%:") != std::string_view::npos; +} + +static std::string EscapeKey(std::string_view const& to_escape) { + std::string escaped; + for (auto const& character : to_escape) { + if (character == '%') { + escaped.append("%3A"); + } else if (character == ':') { + escaped.append("%25"); + } else { + escaped.push_back(character); + } + } + return escaped; +} + +std::vector const& Context::kinds() { + return kinds_; +} + +Context::Context(std::string error_message) + : errors_(std::move(error_message)) {} + +Context::Context(std::map attributes) + : attributes_(std::move(attributes)), valid_(true) { + for (auto& pair : attributes_) { + kinds_.push_back(pair.first); + keys_and_kinds_[pair.first] = pair.second.key(); + } + + canonical_key_ = make_canonical_key(); +} + +Value const& Context::get(std::string const& kind, + AttributeReference const& ref) { + auto found = attributes_.find(kind); + if (found != attributes_.end()) { + return found->second.get(ref); + } + return Value::null(); +} + +Attributes const& Context::attributes(std::string const& kind) const { + return attributes_.at(kind); +} + +std::string const& Context::canonical_key() const { + return canonical_key_; +} + +std::map const& Context::keys_and_kinds() + const { + return keys_and_kinds_; +} + +std::string Context::make_canonical_key() { + if (keys_and_kinds_.size() == 1) { + if (auto iterator = keys_and_kinds_.find("user"); + iterator != keys_and_kinds_.end()) { + return std::string(iterator->second); + } + } + std::stringstream stream; + bool first = true; + // Maps are ordered, so keys and kinds will be in the correct order for + // the canonical key. + for (auto& pair : keys_and_kinds_) { + if (first) { + first = false; + } else { + stream << ":"; + } + if (NeedsEscape(pair.second)) { + std::string escaped = EscapeKey(pair.second); + stream << pair.first << ":" << escaped; + } else { + stream << pair.first << ":" << pair.second; + } + } + stream.flush(); + return stream.str(); +} + +} // namespace launchdarkly diff --git a/libs/common/src/context_builder.cpp b/libs/common/src/context_builder.cpp new file mode 100644 index 000000000..6bc463a8d --- /dev/null +++ b/libs/common/src/context_builder.cpp @@ -0,0 +1,63 @@ +#include + +#include "context_builder.hpp" + +namespace launchdarkly { + +/** + * Verify if a kind is valid. + * @param kind The kind to validate. + * @return True if the kind is valid. + */ +static bool ValidKind(std::string_view kind) { + if (kind.length() == 0) { + return false; + } + + return std::all_of(kind.begin(), kind.end(), [](auto character) { + return character == '.' || character == '-' || character == '_' || + (character >= '0' && character <= '9') || + (character >= 'A' && character <= 'Z') || + (character >= 'a' && character <= 'z'); + }); +} + +AttributesBuilder ContextBuilder::kind( + std::string kind, + std::string key) { + bool kind_valid = ValidKind(kind); + bool key_valid = !key.empty(); + if (!kind_valid || !key_valid) { + valid_ = false; + auto append = errors_.length() != 0; + std::stringstream stream(errors_); + if (append) { + stream << ", "; + } + if (!kind_valid) { + stream << kind << ": " << ContextErrors::kInvalidKind; + if (!key_valid) { + stream << ", "; + } + } + if (!key_valid) { + stream << kind << ": " << ContextErrors::kInvalidKey; + } + stream.flush(); + errors_ = stream.str(); + } + return {*this, std::move(kind), std::move(key)}; +} + +Context ContextBuilder::build() { + if (valid_) { + return {std::move(kinds_)}; + } + return {std::move(errors_)}; +} + +void ContextBuilder::internal_add_kind(std::string kind, Attributes attrs) { + kinds_.insert_or_assign(std::move(kind), std::move(attrs)); +} + +} // namespace launchdarkly diff --git a/libs/common/src/value.cpp b/libs/common/src/value.cpp new file mode 100644 index 000000000..12d250acb --- /dev/null +++ b/libs/common/src/value.cpp @@ -0,0 +1,197 @@ +#pragma clang diagnostic push + +#include "value.hpp" + +#include +#include + +namespace launchdarkly { + +const Value Value::null_value_; + +Value::Value() : type_(Value::Type::kNull), storage_{0.0} {} + +Value::Value(bool boolean) : type_(Value::Type::kBool), storage_{boolean} {} + +Value::Value(double num) : type_(Value::Type::kNumber), storage_{num} {} + +Value::Value(int num) : type_(Value::Type::kNumber), storage_{(double)num} {} + +Value::Value(std::string str) + : type_(Value::Type::kString), storage_{std::move(str)} {} + +Value::Value(char const* str) + : type_(Value::Type::kString), storage_{std::string(str)} {} + +Value::Value(std::vector arr) + : type_(Value::Type::kArray), storage_{std::move(arr)} {} + +Value::Value(std::map obj) + : type_(Value::Type::kObject), storage_{std::move(obj)} {} + +Value::Type Value::type() const { + return type_; +} + +bool Value::is_null() const { + return type_ == Type::kNull; +} + +bool Value::is_bool() const { + return type_ == Type::kBool; +} + +bool Value::is_number() const { + return type_ == Type::kNumber; +} + +bool Value::is_string() const { + return type_ == Type::kString; +} + +bool Value::is_array() const { + return type_ == Type::kArray; +} + +bool Value::is_object() const { + return type_ == Type::kObject; +} + +Value const& Value::null() { + // This still just constructs a value, but it may be more discoverable + // for people using the API. + return null_value_; +} + +bool Value::as_bool() const { + if (type_ == Type::kBool) { + return boost::get(storage_); + } + return false; +} + +int Value::as_int() const { + if (type_ == Type::kNumber) { + return static_cast(boost::get(storage_)); + } + return 0; +} + +double Value::as_double() const { + if (type_ == Type::kNumber) { + return boost::get(storage_); + } + return 0.0; +} + +std::string const& Value::as_string() const { + if (type_ == Type::kString) { + return boost::get(storage_); + } + return empty_string_; +} + +Value::Array const& Value::as_array() const { + if (type_ == Type::kArray) { + return boost::get(storage_); + } + return empty_vector_; +} + +Value::Object const& Value::as_object() const { + if (type_ == Type::kObject) { + return boost::get(storage_); + } + return empty_map_; +} + +Value::Value(std::optional opt_str) : storage_{0.0} { + if (opt_str.has_value()) { + type_ = Type::kString; + storage_ = opt_str.value(); + } else { + type_ = Type::kNull; + } +} +Value::Value(std::initializer_list values) + : type_(Type::kArray), storage_(std::vector(values)) {} + +Value::Array::Iterator::Iterator(std::vector::const_iterator iterator) + : iterator_(iterator) {} + +Value::Array::Iterator::reference Value::Array::Iterator::operator*() const { + return *iterator_; +} + +Value::Array::Iterator::pointer Value::Array::Iterator::operator->() { + return &*iterator_; +} + +Value::Array::Iterator& Value::Array::Iterator::operator++() { + iterator_++; + return *this; +} + +Value::Array::Iterator Value::Array::Iterator::operator++(int) { + Iterator tmp = *this; + ++(*this); + return tmp; +} + +Value const& Value::Array::operator[](std::size_t index) const { + return vec_[index]; +} + +std::size_t Value::Array::size() const { + return vec_.size(); +} + +Value::Array::Iterator Value::Array::begin() const { + return {vec_.begin()}; +} + +Value::Array::Iterator Value::Array::end() const { + return {vec_.end()}; +} + +Value::Array::Array(std::vector vec) : vec_(std::move(vec)) {} + +Value::Object::Iterator::Iterator( + std::map::const_iterator iterator) + : it_(iterator) {} + +Value::Object::Iterator::reference Value::Object::Iterator::operator*() const { + return *it_; +} + +Value::Object::Iterator::pointer Value::Object::Iterator::operator->() { + return &*it_; +} + +Value::Object::Iterator& Value::Object::Iterator::operator++() { + it_++; + return *this; +} + +Value::Object::Iterator Value::Object::Iterator::operator++(int) { + Iterator tmp = *this; + ++(*this); + return tmp; +} + +std::size_t Value::Object::size() const { + return map_.size(); +} + +Value::Object::Iterator Value::Object::begin() const { + return {map_.begin()}; +} + +Value::Object::Iterator Value::Object::end() const { + return {map_.end()}; +} + +Value::Object::Iterator Value::Object::find(std::string const& key) const { + return {map_.find(key)}; +} +} // namespace launchdarkly diff --git a/libs/common/tests/attribute_reference_test.cpp b/libs/common/tests/attribute_reference_test.cpp index cc648102a..d303c3e98 100644 --- a/libs/common/tests/attribute_reference_test.cpp +++ b/libs/common/tests/attribute_reference_test.cpp @@ -1,8 +1,10 @@ -#include "attribute_reference.hpp" #include + #include #include +#include "attribute_reference.hpp" + using launchdarkly::AttributeReference; class BadReferencesTestFixture : public ::testing::TestWithParam { @@ -83,3 +85,11 @@ TEST(AttributeReferenceTests, OstreamOperator) { stream << AttributeReference::from_reference_str("/~"); EXPECT_EQ("invalid(/~)", stream.str()); } + +TEST(AttributeReferenceTests, FromString) { + AttributeReference ref("/a"); + AttributeReference ref_b(std::string("/b")); + + EXPECT_EQ("a", ref.component(0)); + EXPECT_EQ("b", ref_b.component(0)); +} diff --git a/libs/common/tests/attributes_test.cpp b/libs/common/tests/attributes_test.cpp new file mode 100644 index 000000000..c45d25d0e --- /dev/null +++ b/libs/common/tests/attributes_test.cpp @@ -0,0 +1,103 @@ +#include + +#include "attributes.hpp" + +using launchdarkly::AttributeReference; +using launchdarkly::Attributes; +using launchdarkly::Value; + +TEST(AttributesTests, CanGetBuiltInAttributesByReference) { + Attributes attributes("the-key", "the-name", true, Value()); + + EXPECT_EQ("the-key", + attributes.get(AttributeReference::from_reference_str("/key")) + .as_string()); + + EXPECT_EQ("the-name", + attributes.get(AttributeReference::from_reference_str("/name")) + .as_string()); + + EXPECT_TRUE( + attributes.get(AttributeReference::from_reference_str("/anonymous")) + .as_bool()); +} + +TEST(AttributesTests, CanGetCustomAttributesByReference) { + Attributes attributes( + "the-key", std::nullopt, true, + Value(std::map( + {{"int", 42}, + {"double", 3.14}, + {"string", "potato"}, + {"bool", true}, + {"array", {true, false, "bacon"}}, + {"obj", std::map{{"string", "eggs"}}}}))); + + EXPECT_EQ(42, attributes.get(AttributeReference::from_reference_str("/int")) + .as_int()); + + EXPECT_EQ(3.14, + attributes.get(AttributeReference::from_reference_str("/double")) + .as_double()); + + EXPECT_EQ("potato", + attributes.get(AttributeReference::from_reference_str("/string")) + .as_string()); + + EXPECT_TRUE(attributes.get(AttributeReference::from_reference_str("/bool")) + .as_bool()); + + EXPECT_TRUE(attributes.get(AttributeReference::from_reference_str("/array")) + .as_array()[0] + .as_bool()); + + EXPECT_FALSE( + attributes.get(AttributeReference::from_reference_str("/array")) + .as_array()[1] + .as_bool()); + + EXPECT_EQ("bacon", + attributes.get(AttributeReference::from_reference_str("/array")) + .as_array()[2] + .as_string()); + + EXPECT_EQ( + "eggs", + attributes.get(AttributeReference::from_reference_str("/obj/string")) + .as_string()); +} + +TEST(AttributesTests, CanGetSomethingThatDoesNotExist) { + Attributes attributes("the-key", std::nullopt, true, + Value(std::map({{"int", 42}}))); + + EXPECT_TRUE( + attributes.get(AttributeReference::from_reference_str("/missing")) + .is_null()); +} + +std::string ProduceString(Attributes const& attrs) { + std::stringstream stream; + stream << attrs; + stream.flush(); + return stream.str(); +} + +TEST(AttributesTests, OStreamOperator) { + Attributes attributes("the-key", std::nullopt, true, + Value(std::map({{"int", 42}}))); + EXPECT_EQ( + "{key: string(the-key), name: null() anonymous: bool(true) private: " + "[] custom: object({{int, number(42)}})}", + ProduceString(attributes)); + + Attributes attributes2("the-key", "the-name", true, + Value(std::map({{"int", 42}})), + AttributeReference::SetType{"/potato", "/bacon"}); + + EXPECT_EQ( + "{key: string(the-key), name: string(the-name) anonymous: bool(true) " + "private: [valid(/bacon), valid(/potato)] custom: object({{int, " + "number(42)}})}", + ProduceString(attributes2)); +} diff --git a/libs/common/tests/context_builder_test.cpp b/libs/common/tests/context_builder_test.cpp new file mode 100644 index 000000000..8d48d69fa --- /dev/null +++ b/libs/common/tests/context_builder_test.cpp @@ -0,0 +1,129 @@ +#include + +#include "context_builder.hpp" + +using launchdarkly::ContextBuilder; + +using launchdarkly::Value; +using Object = launchdarkly::Value::Object; +using Array = launchdarkly::Value::Array; +using launchdarkly::AttributeReference; + +TEST(ContextBuilderTests, CanMakeBasicContext) { + auto context = ContextBuilder().kind("user", "user-key").build(); + + EXPECT_TRUE(context.valid()); + + EXPECT_EQ(1, context.kinds().size()); + EXPECT_EQ("user", context.kinds()[0]); + EXPECT_EQ("user-key", context.get("user", "/key").as_string()); + + EXPECT_EQ("user-key", context.attributes("user").key()); + EXPECT_FALSE(context.attributes("user").anonymous()); +} + +TEST(ContextBuilderTests, CanMakeSingleContextWithCustomAttributes) { + auto context = ContextBuilder() + .kind("user", "bobby-bobberson") + .name("Bob") + .anonymous(true) + // Set a custom attribute. + .set("likesCats", true) + // Set a private custom attribute. + .set_private("email", "email@email.email") + .build(); + + EXPECT_TRUE(context.valid()); + + EXPECT_EQ("user", context.kinds()[0]); + EXPECT_EQ("bobby-bobberson", context.get("user", "/key").as_string()); + EXPECT_EQ("Bob", context.get("user", "name").as_string()); + + EXPECT_EQ("email@email.email", context.get("user", "email").as_string()); + EXPECT_TRUE(context.get("user", "likesCats").as_bool()); + + EXPECT_EQ("bobby-bobberson", context.attributes("user").key()); + EXPECT_TRUE(context.attributes("user").anonymous()); + EXPECT_EQ(1, context.attributes("user").private_attributes().size()); + EXPECT_EQ(1, + context.attributes("user").private_attributes().count("email")); +} + +TEST(ContextBuilderTests, CanBuildComplexMultiContext) { + auto context = + ContextBuilder() + .kind("user", "user-key") + .anonymous(true) + .name("test") + .set("string", "potato") + .set("int", 42) + .set("double", 3.14) + .set("array", {false, true, 42}) + + .set_private("private", "this is private") + .add_private_attribute("double") + .add_private_attributes(std::vector{"string", "int"}) + .add_private_attributes( + std::vector{"explicitArray"}) + // Start the org kind. + .kind("org", "org-key") + .name("Macdonwalds") + .set("explicitArray", Array{"egg", "ham"}) + .set("object", Object{{"string", "bacon"}, {"boolean", false}}) + .build(); + + EXPECT_TRUE(context.valid()); + + EXPECT_TRUE(context.get("user", "/anonymous").as_bool()); + EXPECT_EQ("test", context.get("user", "/name").as_string()); + EXPECT_EQ("potato", context.get("user", "/string").as_string()); + EXPECT_EQ(42, context.get("user", "int").as_int()); + EXPECT_EQ(3.14, context.get("user", "double").as_double()); + EXPECT_EQ(42, context.get("user", "array").as_array()[2].as_int()); + EXPECT_EQ("ham", + context.get("org", "explicitArray").as_array()[1].as_string()); + EXPECT_EQ("bacon", + context.get("org", "object").as_object()["string"].as_string()); + + EXPECT_EQ(5, context.attributes("user").private_attributes().size()); + EXPECT_EQ(1, context.attributes("user").private_attributes().count("int")); + EXPECT_EQ(1, + context.attributes("user").private_attributes().count("double")); + EXPECT_EQ(1, context.attributes("user").private_attributes().count( + "explicitArray")); + EXPECT_EQ(1, + context.attributes("user").private_attributes().count("string")); + EXPECT_EQ(1, + context.attributes("user").private_attributes().count("private")); + EXPECT_EQ("Macdonwalds", context.get("org", "/name").as_string()); +} + +TEST(ContextBuilderTests, HandlesInvalidKinds) { + auto context_bad_kind = ContextBuilder().kind("#$#*(", "valid-key").build(); + EXPECT_FALSE(context_bad_kind.valid()); + + EXPECT_EQ( + "#$#*(: \"Kind contained invalid characters. A kind may contain ASCII " + "letters or numbers, as well as '.', '-', and '_'.\"", + context_bad_kind.errors()); +} + +TEST(ContextBuilderTests, HandlesInvalidKeys) { + auto context_bad_key = ContextBuilder().kind("user", "").build(); + EXPECT_FALSE(context_bad_key.valid()); + + EXPECT_EQ("user: \"The key for a context may not be empty.\"", + context_bad_key.errors()); +} + +TEST(ContextBuilderTests, HandlesMultipleErrors) { + auto context = ContextBuilder().kind("#$#*(", "").build(); + EXPECT_FALSE(context.valid()); + + EXPECT_EQ( + "#$#*(: \"Kind contained invalid characters. A kind may contain ASCII " + "letters or numbers, as well as '.', '-', and '_'.\", #$#*(: \"The key " + "for " + "a context may not be empty.\"", + context.errors()); +} diff --git a/libs/common/tests/context_tests.cpp b/libs/common/tests/context_tests.cpp new file mode 100644 index 000000000..e2acf6ad4 --- /dev/null +++ b/libs/common/tests/context_tests.cpp @@ -0,0 +1,87 @@ +#include + +#include "context_builder.hpp" + +using launchdarkly::Context; +using launchdarkly::ContextBuilder; + +TEST(ContextTests, CanonicalKeyForUser) { + auto context = ContextBuilder().kind("user", "user-key").build(); + EXPECT_EQ("user-key", context.canonical_key()); +} + +TEST(ContextTests, CanonicalKeyForNonUser) { + auto context = ContextBuilder().kind("org", "org-key").build(); + EXPECT_EQ("org:org-key", context.canonical_key()); +} + +TEST(ContextTests, CanonicalKeyForMultiContext) { + auto context = ContextBuilder() + .kind("user", "user-key") + .kind("org", "org-key") + .build(); + + EXPECT_EQ("org:org-key:user:user-key", context.canonical_key()); +} + +TEST(ContextTests, EscapesCanonicalKey) { + auto context = ContextBuilder() + .kind("user", "user:key") + .kind("org", "org%key") + .build(); + + EXPECT_EQ("org:org%3Akey:user:user%25key", context.canonical_key()); +} + +TEST(ContextTests, CanGetKeysAndKinds) { + auto context = ContextBuilder() + .kind("user", "user-key") + .kind("org", "org-key") + .build(); + EXPECT_EQ(2, context.keys_and_kinds().size()); + EXPECT_EQ("user-key", context.keys_and_kinds().find("user")->second); + EXPECT_EQ("org-key", context.keys_and_kinds().find("org")->second); +} + +std::string ProduceString(Context const& ctx) { + std::stringstream stream; + stream << ctx; + stream.flush(); + return stream.str(); +} + +TEST(ContextTests, OstreamOperatorValidContext) { + auto context = ContextBuilder().kind("user", "user-key").build(); + EXPECT_EQ( + "{contexts: [kind: user attributes: {key: string(user-key), name: " + "string() anonymous: bool(false) private: [] custom: object({})}]", + ProduceString(context)); + + auto context_2 = ContextBuilder() + .kind("user", "user-key") + .kind("org", "org-key") + .name("Sam") + .set_private("test", true) + .set("string", "potato") + .build(); + + EXPECT_EQ( + "{contexts: [kind: org attributes: {key: string(org-key), name: " + "string(Sam) anonymous: bool(false) private: [valid(test)] custom: " + "object({{string, string(potato)}, {test, bool(true)}})}, kind: user " + "attributes: {key: string(user-key), name: string() anonymous: " + "bool(false) private: [] custom: object({})}]", + ProduceString(context_2)); +} + +TEST(ContextTests, OstreamOperatorInvalidContext) { + auto context = ContextBuilder().kind("#$#*(", "").build(); + EXPECT_FALSE(context.valid()); + + EXPECT_EQ( + "{invalid: errors: [#$#*(: \"Kind contained invalid characters. A kind " + "may contain ASCII letters or numbers, as well as '.', '-', and " + "'_'.\", " + "#$#*(: \"The key for a context may not be empty.\"]", + ProduceString(context)); +} diff --git a/libs/common/tests/value_test.cpp b/libs/common/tests/value_test.cpp new file mode 100644 index 000000000..5dbef0e27 --- /dev/null +++ b/libs/common/tests/value_test.cpp @@ -0,0 +1,216 @@ +#include +#include +#include +#include +#include + +#include "value.hpp" + +// NOLINTBEGIN cppcoreguidelines-avoid-magic-numbers + +using launchdarkly::Value; +TEST(ValueTests, CanMakeNullValue) { + Value null_val; + EXPECT_TRUE(null_val.is_null()); + EXPECT_EQ(Value::Type::kNull, null_val.type()); +} + +TEST(ValueTests, CanMoveNullValue) { + Value null_val(std::move(Value())); + EXPECT_TRUE(null_val.is_null()); + EXPECT_EQ(Value::Type::kNull, null_val.type()); +} + +TEST(ValueTests, CanMakeBoolValue) { + Value attr_bool(false); + EXPECT_TRUE(attr_bool.is_bool()); + EXPECT_FALSE(attr_bool.as_bool()); + EXPECT_EQ(Value::Type::kBool, attr_bool.type()); +} + +TEST(ValueTests, CanMoveBoolValue) { + Value attr_bool(std::move(Value(false))); + EXPECT_TRUE(attr_bool.is_bool()); + EXPECT_FALSE(attr_bool.as_bool()); + EXPECT_EQ(Value::Type::kBool, attr_bool.type()); +} + +TEST(ValueTests, CanMakeDoubleValue) { + Value attr_double(3.14159); + EXPECT_TRUE(attr_double.is_number()); + EXPECT_EQ(3.14159, attr_double.as_double()); + EXPECT_EQ(3, attr_double.as_int()); + EXPECT_EQ(Value::Type::kNumber, attr_double.type()); +} + +TEST(ValueTests, CanMoveDoubleValue) { + // Not repeated for int, as they use the same data type. + Value attr_double(std::move(Value(3.14159))); + EXPECT_TRUE(attr_double.is_number()); + EXPECT_EQ(3.14159, attr_double.as_double()); + EXPECT_EQ(3, attr_double.as_int()); + EXPECT_EQ(Value::Type::kNumber, attr_double.type()); +} + +TEST(ValueTests, CanMakeIntValue) { + Value attr_int(42); + EXPECT_TRUE(attr_int.is_number()); + EXPECT_EQ(42, attr_int.as_double()); + EXPECT_EQ(42, attr_int.as_int()); + EXPECT_EQ(Value::Type::kNumber, attr_int.type()); +} + +TEST(ValueTests, CanMakeStringValue) { + Value attr_str(std::string("potato")); + EXPECT_TRUE(attr_str.is_string()); + EXPECT_EQ("potato", attr_str.as_string()); + EXPECT_EQ(Value::Type::kString, attr_str.type()); +} + +TEST(ValueTests, CanMoveStringValue) { + Value attr_str(std::move(Value(std::string("potato")))); + EXPECT_TRUE(attr_str.is_string()); + EXPECT_EQ("potato", attr_str.as_string()); + EXPECT_EQ(Value::Type::kString, attr_str.type()); +} + +void VectorAssertions(Value attr_arr) { + EXPECT_TRUE(attr_arr.is_array()); + EXPECT_FALSE(attr_arr.as_array()[0].as_bool()); + EXPECT_TRUE(attr_arr.as_array()[1].as_bool()); + EXPECT_EQ("potato", attr_arr.as_array()[2].as_string()); + EXPECT_EQ(42, attr_arr.as_array()[3].as_int()); + EXPECT_EQ(3.14, attr_arr.as_array()[4].as_double()); + + EXPECT_EQ("a", attr_arr.as_array()[5].as_array()[0].as_string()); + EXPECT_EQ(21, attr_arr.as_array()[5].as_array()[1].as_int()); + + EXPECT_EQ( + "bacon", + attr_arr.as_array()[6].as_object().find("string")->second.as_string()); + + EXPECT_EQ(Value::Type::kArray, attr_arr.type()); +} + +TEST(ValueTests, CanMakeFromVector) { + Value attr_arr(std::vector{ + false, true, "potato", 42, 3.14, std::vector{"a", 21}, + std::map{{"string", "bacon"}}}); + + VectorAssertions(attr_arr); +} + +TEST(ValueTests, CanMoveVectorValue) { + Value attr_arr(std::move(Value(std::vector{ + false, true, "potato", 42, 3.14, std::vector{"a", 21}, + std::map{{"string", "bacon"}}}))); + VectorAssertions(attr_arr); +} + +TEST(ValueTests, CanMakeFromInitListVector) { + Value init_list = {false, + true, + "potato", + 42, + 3.14, + {"a", 21}, + std::map{{"string", "bacon"}}}; + + VectorAssertions(init_list); +} + +void MapAssertions(Value const& attr_map) { + EXPECT_TRUE(attr_map.is_object()); + EXPECT_TRUE(attr_map.as_object()["bool"].as_bool()); + + EXPECT_TRUE(attr_map.is_object()); + // Can index. + EXPECT_EQ(3.14, attr_map.as_object()["double"].as_double()); + // Can use find. + EXPECT_EQ(42, attr_map.as_object().find("int")->second.as_int()); + EXPECT_EQ("potato", + attr_map.as_object().find("string")->second.as_string()); + EXPECT_TRUE( + attr_map.as_object().find("array")->second.as_array()[0].as_bool()); + EXPECT_FALSE( + attr_map.as_object().find("array")->second.as_array()[1].as_bool()); + EXPECT_EQ( + "bacon", + attr_map.as_object().find("array")->second.as_array()[2].as_string()); + + EXPECT_EQ("eggs", attr_map.as_object() + .find("obj") + ->second.as_object() + .find("string") + ->second.as_string()); +} + +TEST(ValueTests, CanMakeFromMap) { + Value attr_map(std::map( + {{"int", 42}, + {"double", 3.14}, + {"string", "potato"}, + {"bool", true}, + {"array", {true, false, "bacon"}}, + {"obj", std::map{{"string", "eggs"}}}})); + + MapAssertions(attr_map); +} + +TEST(ValueTests, CanMoveMap) { + Value attr_map(std::move(Value(std::map( + {{"int", 42}, + {"double", 3.14}, + {"string", "potato"}, + {"bool", true}, + {"array", {true, false, "bacon"}}, + {"obj", std::map{{"string", "eggs"}}}})))); + + MapAssertions(attr_map); +} + +TEST(ValueTests, AssignToExistingValue) { + // Running this test with valgrind should being to light any issues with + // cleaning things up during moves and copies. + auto str_value = Value("test"); + EXPECT_EQ("test", str_value.as_string()); + auto second_str = Value("second"); + str_value = std::move(second_str); + // Move assignment. + EXPECT_EQ("second", str_value.as_string()); + + auto third_str = Value("third"); + // Copy assignment + str_value = third_str; + EXPECT_EQ("third", str_value.as_string()); +} + +std::string ProduceString(Value val) { + std::stringstream stream; + stream << val; + stream.flush(); + return stream.str(); +} + +TEST(ValueTests, OstreamOperator) { + EXPECT_EQ("null()", ProduceString(Value())); + + EXPECT_EQ("bool(false)", ProduceString(Value(false))); + EXPECT_EQ("bool(true)", ProduceString(Value(true))); + + EXPECT_EQ("number(3.14)", ProduceString(Value(3.14))); + EXPECT_EQ("number(42)", ProduceString(Value(42))); + + EXPECT_EQ("string(potato)", ProduceString(Value("potato"))); + + EXPECT_EQ("string(potato)", ProduceString(Value("potato"))); + + EXPECT_EQ("array([string(ham), string(cheese)])", + ProduceString(Value{"ham", "cheese"})); + + EXPECT_EQ( + "object({{first, string(cheese)}, {second, number(42)}})", + ProduceString(Value::Object{{"first", "cheese"}, {"second", 42}})); +} + +// NOLINTEND cppcoreguidelines-avoid-magic-numbers