diff --git a/libs/common/include/attributes.hpp b/libs/common/include/attributes.hpp index 28ec70574..01db517e0 100644 --- a/libs/common/include/attributes.hpp +++ b/libs/common/include/attributes.hpp @@ -152,4 +152,5 @@ class Attributes { // Kinds are contained at the context level, not inside attributes. }; + } // namespace launchdarkly diff --git a/libs/common/include/data/evaluation_detail_internal.hpp b/libs/common/include/data/evaluation_detail_internal.hpp new file mode 100644 index 000000000..56ae7cbd3 --- /dev/null +++ b/libs/common/include/data/evaluation_detail_internal.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include +#include +#include + +#include "data/evaluation_reason.hpp" +#include "value.hpp" + +namespace launchdarkly { + +/** + * An object that combines the result of a feature flag evaluation with + * information about how it was calculated. + * + * This is the result of calling [TODO: Evaluate detail]. + * + * For more information, see the [SDK reference guide] + * (https://docs.launchdarkly.com/sdk/features/evaluation-reasons#TODO). + */ +class EvaluationDetailInternal { + public: + /** + * The result of the flag evaluation. This will be either one of the flag's + * variations or the default value that was passed to [TODO: Evaluate + * detail]. + */ + [[nodiscard]] Value const& value() const; + + /** + * The index of the returned value within the flag's list of variations, + * e.g. 0 for the first variation-- or `nullopt` if the default value was + * returned. + */ + [[nodiscard]] std::optional variation_index() const; + + /** + * An object describing the main factor that influenced the flag evaluation + * value. + */ + [[nodiscard]] std::optional> + reason() const; + + EvaluationDetailInternal(Value value, + std::optional variation_index, + std::optional reason); + + private: + Value value_; + std::optional variation_index_; + std::optional reason_; +}; + +} // namespace launchdarkly diff --git a/libs/common/include/data/evaluation_reason.hpp b/libs/common/include/data/evaluation_reason.hpp new file mode 100644 index 000000000..1133f4ddc --- /dev/null +++ b/libs/common/include/data/evaluation_reason.hpp @@ -0,0 +1,98 @@ +#pragma once + +#include +#include +#include + +namespace launchdarkly { + +/** + * Describes the reason that a flag evaluation produced a particular value. + */ +class EvaluationReason { + public: + /** + * The general category of the reason: + * + * - `"OFF"`: The flag was off and therefore returned its configured off + * value. + * - `"FALLTHROUGH"`: The flag was on but the context did not match any + * targets or rules. + * - `"TARGET_MATCH"`: The context key was specifically targeted for this + * flag. + * - `"RULE_MATCH"`: the context matched one of the flag"s rules. + * - `"PREREQUISITE_FAILED"`: The flag was considered off because it had at + * least one prerequisite flag that either was off or did not return the + * desired variation. + * - `"ERROR"`: The flag could not be evaluated, e.g. because it does not + * exist or due to an unexpected error. + */ + [[nodiscard]] std::string const& kind() const; + + /** + * A further description of the error condition, if the kind was `"ERROR"`. + */ + [[nodiscard]] std::optional error_kind() const; + + /** + * The index of the matched rule (0 for the first), if the kind was + * `"RULE_MATCH"`. + */ + [[nodiscard]] std::optional rule_index() const; + + /** + * The unique identifier of the matched rule, if the kind was + * `"RULE_MATCH"`. + */ + [[nodiscard]] std::optional rule_id() const; + + /** + * The key of the failed prerequisite flag, if the kind was + * `"PREREQUISITE_FAILED"`. + */ + [[nodiscard]] std::optional prerequisite_key() const; + + /** + * Whether the evaluation was part of an experiment. + * + * This is true if the evaluation resulted in an experiment rollout and + * served one of the variations in the experiment. Otherwise it is false or + * undefined. + */ + [[nodiscard]] bool in_experiment() const; + + /** + * Describes the validity of Big Segment information, if and only if the + * flag evaluation required querying at least one Big Segment. + * + * - `"HEALTHY"`: The Big Segment query involved in the flag evaluation was + * successful, and the segment state is considered up to date. + * - `"STALE"`: The Big Segment query involved in the flag evaluation was + * successful, but the segment state may not be up to date + * - `"NOT_CONFIGURED"`: Big Segments could not be queried for the flag + * evaluation because the SDK configuration did not include a Big Segment + * store. + * - `"STORE_ERROR"`: The Big Segment query involved in the flag evaluation + * failed, for instance due to a database error. + */ + [[nodiscard]] std::optional big_segment_status() const; + + EvaluationReason(std::string kind, + std::optional error_kind, + std::optional rule_index, + std::optional rule_id, + std::optional prerequisite_key, + bool in_experiment, + std::optional big_segment_status); + + private: + std::string kind_; + std::optional error_kind_; + std::optional rule_index_; + std::optional rule_id_; + std::optional prerequisite_key_; + bool in_experiment_; + std::optional big_segment_status_; +}; + +} // namespace launchdarkly diff --git a/libs/common/include/data/evaluation_result.hpp b/libs/common/include/data/evaluation_result.hpp new file mode 100644 index 000000000..fa0604402 --- /dev/null +++ b/libs/common/include/data/evaluation_result.hpp @@ -0,0 +1,69 @@ +#pragma once + +#include +#include + +#include "evaluation_detail_internal.hpp" + +namespace launchdarkly { + +/** + * FlagMeta represents an evaluated flag either from the LaunchDarkly service, + * or in bootstrap data generated by a server SDK. + */ +class EvaluationResult { + public: + /** + * Incremented by LaunchDarkly each time the flag's state changes. + */ + [[nodiscard]] uint64_t version() const; + + /** + * Incremented by LaunchDarkly each time the flag's configuration changes. + */ + [[nodiscard]] std::optional flag_version() const; + + /** + * True if a client SDK should track events for this flag. + */ + [[nodiscard]] bool track_events() const; + + /** + * True if a client SDK should track reasons for this flag. + */ + [[nodiscard]] bool track_reason() const; + + /** + * A timestamp, which if the current time is before, a client SDK + * should send debug events for the flag. + * @return + */ + [[nodiscard]] std::optional< + std::chrono::time_point> + debug_events_until_date() const; + + /** + * Details of the flags evaluation. + */ + [[nodiscard]] EvaluationDetailInternal const& detail() const; + + EvaluationResult( + uint64_t version, + std::optional flag_version, + bool track_events, + bool track_reason, + std::optional> + debug_events_until_date, + EvaluationDetailInternal detail); + + private: + uint64_t version_; + std::optional flag_version_; + bool track_events_; + bool track_reason_; + std::optional> + debug_events_until_date_; + EvaluationDetailInternal detail_; +}; + +} // namespace launchdarkly diff --git a/libs/common/include/serialization/json_attributes.hpp b/libs/common/include/serialization/json_attributes.hpp new file mode 100644 index 000000000..b9c5476c3 --- /dev/null +++ b/libs/common/include/serialization/json_attributes.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include +#include "attributes.hpp" + +namespace launchdarkly { +/** + * Method used by boost::json for converting launchdarkly::Attributes into a + * boost::json::value. + */ +void tag_invoke(boost::json::value_from_tag const&, + boost::json::value& json_value, + Attributes const& attributes); +} // namespace launchdarkly diff --git a/libs/common/include/serialization/json_context.hpp b/libs/common/include/serialization/json_context.hpp new file mode 100644 index 000000000..c212a697f --- /dev/null +++ b/libs/common/include/serialization/json_context.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include + +#include "context.hpp" + +namespace launchdarkly { +/** + * Method used by boost::json for converting a launchdarkly::Context into a + * boost::json::value. + */ +void tag_invoke(boost::json::value_from_tag const&, + boost::json::value& json_value, + Context const& ld_context); +} // namespace launchdarkly diff --git a/libs/common/include/serialization/json_evaluation_reason.hpp b/libs/common/include/serialization/json_evaluation_reason.hpp new file mode 100644 index 000000000..150ad2854 --- /dev/null +++ b/libs/common/include/serialization/json_evaluation_reason.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include + +#include "data/evaluation_reason.hpp" + +namespace launchdarkly { +/** + * Method used by boost::json for converting a boost::json::value into a + * launchdarkly::EvaluationReason. + * @return A EvaluationReason representation of the boost::json::value. + */ +EvaluationReason tag_invoke( + boost::json::value_to_tag const& unused, + boost::json::value const& json_value); +} // namespace launchdarkly diff --git a/libs/common/include/serialization/json_evaluation_result.hpp b/libs/common/include/serialization/json_evaluation_result.hpp new file mode 100644 index 000000000..8e510cd5f --- /dev/null +++ b/libs/common/include/serialization/json_evaluation_result.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include + +#include "data/evaluation_result.hpp" + +namespace launchdarkly { +/** + * Method used by boost::json for converting a boost::json::value into a + * launchdarkly::EvaluationResult. + * @return A EvaluationResult representation of the boost::json::value. + */ +EvaluationResult tag_invoke( + boost::json::value_to_tag const& unused, + boost::json::value const& json_value); +} // namespace launchdarkly diff --git a/libs/common/include/serialization/json_value.hpp b/libs/common/include/serialization/json_value.hpp new file mode 100644 index 000000000..6e2e2d908 --- /dev/null +++ b/libs/common/include/serialization/json_value.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include + +#include "value.hpp" + +namespace launchdarkly { +/** + * Method used by boost::json for converting a boost::json::value into a + * launchdarkly::Value. + * @return A Value representation of the boost::json::value. + */ +Value tag_invoke(boost::json::value_to_tag const&, + boost::json::value const&); + +/** + * Method used by boost::json for converting a launchdarkly::Value into a + * boost::json::value. + */ +void tag_invoke(boost::json::value_from_tag const&, + boost::json::value& json_value, + Value const& ld_value); +} // namespace launchdarkly diff --git a/libs/common/include/serialization/value_mapping.hpp b/libs/common/include/serialization/value_mapping.hpp new file mode 100644 index 000000000..31a09ebd4 --- /dev/null +++ b/libs/common/include/serialization/value_mapping.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include + +namespace launchdarkly { + +template +std::optional ValueAsOpt(boost::json::object::iterator iterator, + boost::json::object::iterator end) { + boost::ignore_unused(iterator); + boost::ignore_unused(end); + + static_assert(sizeof(Type) == -1, "Must be specialized to use ValueAsOpt"); +} + +template +Type ValueOrDefault(boost::json::object::iterator iterator, + boost::json::object::iterator end, + Type default_value) { + boost::ignore_unused(iterator); + boost::ignore_unused(end); + boost::ignore_unused(default_value); + + static_assert(sizeof(Type) == -1, + "Must be specialized to use ValueOrDefault"); +} + +template +std::optional MapOpt(std::optional opt, + std::function const& mapper) { + if (opt.has_value()) { + return std::make_optional(mapper(opt.value())); + } + return std::nullopt; +} + +template <> +std::optional ValueAsOpt(boost::json::object::iterator iterator, + boost::json::object::iterator end); + +template <> +std::optional ValueAsOpt(boost::json::object::iterator iterator, + boost::json::object::iterator end); + +template <> +bool ValueOrDefault(boost::json::object::iterator iterator, + boost::json::object::iterator end, + bool default_value); + +template <> +uint64_t ValueOrDefault(boost::json::object::iterator iterator, + boost::json::object::iterator end, + uint64_t default_value); + +template <> +std::string ValueOrDefault(boost::json::object::iterator iterator, + boost::json::object::iterator end, + std::string default_value); +} // namespace launchdarkly diff --git a/libs/common/src/CMakeLists.txt b/libs/common/src/CMakeLists.txt index faa91447a..387e0b836 100644 --- a/libs/common/src/CMakeLists.txt +++ b/libs/common/src/CMakeLists.txt @@ -21,9 +21,18 @@ add_library(${LIBNAME} config/endpoints_builder.cpp config/config_builder.cpp config/config.cpp - context_filter.cpp lib.cpp + data/evaluation_reason.cpp + data/evaluation_detail_internal.cpp + data/evaluation_result.cpp + context_filter.cpp config/application_info.cpp - ) + lib.cpp + value_mapping.cpp + serialization/json_attributes.cpp + serialization/json_context.cpp + serialization/json_evaluation_reason.cpp + serialization/json_evaluation_result.cpp + serialization/json_value.cpp) add_library(launchdarkly::common ALIAS ${LIBNAME}) diff --git a/libs/common/src/data/evaluation_detail_internal.cpp b/libs/common/src/data/evaluation_detail_internal.cpp new file mode 100644 index 000000000..2b5a01d75 --- /dev/null +++ b/libs/common/src/data/evaluation_detail_internal.cpp @@ -0,0 +1,25 @@ +#include "data/evaluation_detail_internal.hpp" + +namespace launchdarkly { + +Value const& EvaluationDetailInternal::value() const { + return value_; +} + +std::optional EvaluationDetailInternal::variation_index() const { + return variation_index_; +} + +std::optional> +EvaluationDetailInternal::reason() const { + return reason_; +} +EvaluationDetailInternal::EvaluationDetailInternal( + Value value, + std::optional variation_index, + std::optional reason) + : value_(std::move(value)), + variation_index_(variation_index), + reason_(std::move(reason)) {} + +} // namespace launchdarkly diff --git a/libs/common/src/data/evaluation_reason.cpp b/libs/common/src/data/evaluation_reason.cpp new file mode 100644 index 000000000..d164a76fe --- /dev/null +++ b/libs/common/src/data/evaluation_reason.cpp @@ -0,0 +1,49 @@ +#include "data/evaluation_reason.hpp" + +namespace launchdarkly { + +std::string const& EvaluationReason::kind() const { + return kind_; +} + +std::optional EvaluationReason::error_kind() const { + return error_kind_; +} + +std::optional EvaluationReason::rule_index() const { + return rule_index_; +} + +std::optional EvaluationReason::rule_id() const { + return rule_id_; +} + +std::optional EvaluationReason::prerequisite_key() const { + return prerequisite_key_; +} + +bool EvaluationReason::in_experiment() const { + return in_experiment_; +} + +std::optional EvaluationReason::big_segment_status() const { + return big_segment_status_; +} + +EvaluationReason::EvaluationReason( + std::string kind, + std::optional error_kind, + std::optional rule_index, + std::optional rule_id, + std::optional prerequisite_key, + bool in_experiment, + std::optional big_segment_status) + : kind_(std::move(kind)), + error_kind_(std::move(error_kind)), + rule_index_(rule_index), + rule_id_(std::move(rule_id)), + prerequisite_key_(std::move(prerequisite_key)), + in_experiment_(in_experiment), + big_segment_status_(std::move(big_segment_status)) {} + +} // namespace launchdarkly diff --git a/libs/common/src/data/evaluation_result.cpp b/libs/common/src/data/evaluation_result.cpp new file mode 100644 index 000000000..bc196e4b1 --- /dev/null +++ b/libs/common/src/data/evaluation_result.cpp @@ -0,0 +1,47 @@ +#include "data/evaluation_result.hpp" + +#include + +namespace launchdarkly { + +uint64_t EvaluationResult::version() const { + return version_; +} + +std::optional EvaluationResult::flag_version() const { + return flag_version_; +} + +bool EvaluationResult::track_events() const { + return track_events_; +} + +bool EvaluationResult::track_reason() const { + return track_reason_; +} + +std::optional> +EvaluationResult::debug_events_until_date() const { + return debug_events_until_date_; +} + +EvaluationDetailInternal const& EvaluationResult::detail() const { + return detail_; +} + +EvaluationResult::EvaluationResult( + uint64_t version, + std::optional flag_version, + bool track_events, + bool track_reason, + std::optional> + debug_events_until_date, + EvaluationDetailInternal detail) + : version_(version), + flag_version_(flag_version), + track_events_(track_events), + track_reason_(track_reason), + debug_events_until_date_(debug_events_until_date), + detail_(std::move(detail)) {} + +} // namespace launchdarkly diff --git a/libs/common/src/serialization/json_attributes.cpp b/libs/common/src/serialization/json_attributes.cpp new file mode 100644 index 000000000..4cd124dc8 --- /dev/null +++ b/libs/common/src/serialization/json_attributes.cpp @@ -0,0 +1,36 @@ +#include "serialization/json_attributes.hpp" +#include "serialization/json_value.hpp" + +namespace launchdarkly { +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + Attributes const& attributes) { + boost::ignore_unused(unused); + + auto& obj = json_value.emplace_object(); + + obj.emplace("key", attributes.key()); + if (!attributes.name().empty()) { + obj.emplace("name", attributes.name()); + } + + if (attributes.anonymous()) { + obj.emplace("anonymous", attributes.anonymous()); + } + + for (auto const& attr : attributes.custom_attributes().as_object()) { + obj.emplace(attr.first, boost::json::value_from(attr.second)); + } + + auto private_attributes = attributes.private_attributes(); + if (!private_attributes.empty()) { + obj.emplace("_meta", boost::json::object()); + auto& meta = obj.at("_meta").as_object(); + meta.emplace("privateAttributes", boost::json::array()); + auto& output_array = meta.at("privateAttributes").as_array(); + for (auto const& ref : private_attributes) { + output_array.push_back(boost::json::value(ref.redaction_name())); + } + } +} +} // namespace launchdarkly diff --git a/libs/common/src/serialization/json_context.cpp b/libs/common/src/serialization/json_context.cpp new file mode 100644 index 000000000..6ac5e0945 --- /dev/null +++ b/libs/common/src/serialization/json_context.cpp @@ -0,0 +1,26 @@ +#include "serialization/json_context.hpp" +#include "serialization/json_attributes.hpp" + +namespace launchdarkly { +void tag_invoke(boost::json::value_from_tag const&, + boost::json::value& json_value, + Context const& ld_context) { + if (ld_context.valid()) { + if (ld_context.kinds().size() == 1) { + auto kind = ld_context.kinds()[0].data(); + auto& obj = json_value.emplace_object() = + std::move(boost::json::value_from(ld_context.attributes(kind)) + .as_object()); + obj.emplace("kind", kind); + } else { + auto& obj = json_value.emplace_object(); + obj.emplace("kind", "multi"); + for (auto const& kind : ld_context.kinds()) { + obj.emplace(kind.data(), + boost::json::value_from( + ld_context.attributes(kind.data()))); + } + } + } +} +} // namespace launchdarkly diff --git a/libs/common/src/serialization/json_evaluation_reason.cpp b/libs/common/src/serialization/json_evaluation_reason.cpp new file mode 100644 index 000000000..0a95798a0 --- /dev/null +++ b/libs/common/src/serialization/json_evaluation_reason.cpp @@ -0,0 +1,44 @@ +#include "serialization/json_evaluation_reason.hpp" +#include "serialization/value_mapping.hpp" + +namespace launchdarkly { +EvaluationReason tag_invoke( + boost::json::value_to_tag const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + if (json_value.is_object()) { + auto json_obj = json_value.as_object(); + + auto* kind_iter = json_obj.find("kind"); + auto kind = + ValueOrDefault(kind_iter, json_obj.end(), "ERROR"); + + auto* error_kind_iter = json_obj.find("errorKind"); + auto error_kind = + ValueAsOpt(error_kind_iter, json_obj.end()); + + auto* rule_index_iter = json_obj.find("ruleIndex"); + auto rule_index = ValueAsOpt(rule_index_iter, json_obj.end()); + + auto* rule_id_iter = json_obj.find("ruleId"); + auto rule_id = ValueAsOpt(rule_id_iter, json_obj.end()); + + auto* prerequisite_key_iter = json_obj.find("prerequisiteKey"); + auto prerequisite_key = + ValueAsOpt(prerequisite_key_iter, json_obj.end()); + + auto* in_experiment_iter = json_obj.find("inExperiment"); + auto in_experiment = + ValueOrDefault(in_experiment_iter, json_obj.end(), false); + + auto* big_segment_status_iter = json_obj.find("bigSegmentStatus"); + auto big_segment_status = + ValueAsOpt(big_segment_status_iter, json_obj.end()); + + return {std::string(kind), error_kind, rule_index, rule_id, + prerequisite_key, in_experiment, big_segment_status}; + } + return {"ERROR", std::nullopt, 0, std::nullopt, + std::nullopt, false, std::nullopt}; +} +} // namespace launchdarkly diff --git a/libs/common/src/serialization/json_evaluation_result.cpp b/libs/common/src/serialization/json_evaluation_result.cpp new file mode 100644 index 000000000..9d5291167 --- /dev/null +++ b/libs/common/src/serialization/json_evaluation_result.cpp @@ -0,0 +1,76 @@ +#include "serialization/json_evaluation_result.hpp" +#include "serialization/json_evaluation_reason.hpp" +#include "serialization/json_value.hpp" +#include "serialization/value_mapping.hpp" + +namespace launchdarkly { +EvaluationResult tag_invoke( + boost::json::value_to_tag const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + if (json_value.is_object()) { + auto json_obj = json_value.as_object(); + + auto* version_iter = json_obj.find("version"); + auto version = ValueOrDefault(version_iter, json_obj.end(), 0UL); + + auto* flag_version_iter = json_obj.find("flagVersion"); + auto flag_version = + ValueAsOpt(flag_version_iter, json_obj.end()); + + auto* track_events_iter = json_obj.find("trackEvents"); + auto track_events = + ValueOrDefault(track_events_iter, json_obj.end(), false); + + auto* track_reason_iter = json_obj.find("trackReason"); + auto track_reason = + ValueOrDefault(track_reason_iter, json_obj.end(), false); + + auto* debug_events_until_date_iter = + json_obj.find("debugEventsUntilDate"); + + auto debug_events_until_date = + MapOpt, + uint64_t>(ValueAsOpt(debug_events_until_date_iter, + json_obj.end()), + [](auto value) { + return std::chrono::system_clock::time_point{ + std::chrono::milliseconds{value}}; + }); + + // Evaluation detail is directly de-serialized inline here. + // This is because the shape of the evaluation detail is different + // when deserializing FlagMeta. Primarily `variation` not + // `variationIndex`. + + auto* value_iter = json_obj.find("value"); + auto value = value_iter != json_obj.end() + ? boost::json::value_to(value_iter->value()) + : Value(); + + auto* variation_iter = json_obj.find("variation"); + auto variation = ValueAsOpt(variation_iter, json_obj.end()); + + auto* reason_iter = json_obj.find("reason"); + auto reason = + reason_iter != json_obj.end() + ? std::make_optional(boost::json::value_to( + reason_iter->value())) + : std::nullopt; + + return {version, + flag_version, + track_events, + track_reason, + debug_events_until_date, + EvaluationDetailInternal(value, variation, reason)}; + } + // This would represent malformed JSON. + return {0, + 0, + false, + false, + std::nullopt, + EvaluationDetailInternal(Value(), std::nullopt, std::nullopt)}; +} +} // namespace launchdarkly diff --git a/libs/common/src/serialization/json_value.cpp b/libs/common/src/serialization/json_value.cpp new file mode 100644 index 000000000..6b3a47f6e --- /dev/null +++ b/libs/common/src/serialization/json_value.cpp @@ -0,0 +1,87 @@ +#include "serialization/json_value.hpp" +#include "serialization/value_mapping.hpp" + +namespace launchdarkly { +// NOLINTBEGIN modernize-return-braced-init-list + +// Braced initializer list is not the same for a single item as the +// constructors. Replacing them with braced init lists would result in all types +// being lists. + +Value tag_invoke(boost::json::value_to_tag const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + // The name of the function needs to be tag_invoke for boost::json. + + // The conditions in these switches explicitly use the constructors, because + // otherwise it is an init list, which is an array. + switch (json_value.kind()) { + case boost::json::kind::null: + return Value(); + case boost::json::kind::bool_: + return Value(json_value.as_bool()); + case boost::json::kind::int64: + return Value(json_value.to_number()); + case boost::json::kind::uint64: + return Value(json_value.to_number()); + case boost::json::kind::double_: + return Value(json_value.as_double()); + case boost::json::kind::string: + return Value(std::string(json_value.as_string())); + case boost::json::kind::array: { + auto vec = json_value.as_array(); + std::vector values; + for (auto const& item : vec) { + values.push_back(boost::json::value_to(item)); + } + return Value(values); + } + case boost::json::kind::object: { + auto map = json_value.as_object(); + std::map values; + for (auto const& pair : map) { + auto value = boost::json::value_to(pair.value()); + values.emplace(pair.key().data(), std::move(value)); + } + return Value(std::move(values)); + } + } + // The above switch is exhaustive, so this can only happen if a new + // type is added to boost::json::value. + assert(!"All types need to be handled."); +} + +void tag_invoke(boost::json::value_from_tag const&, + boost::json::value& json_value, + Value const& ld_value) { + switch (ld_value.type()) { + case Value::Type::kNull: + json_value.emplace_null(); + break; + case Value::Type::kBool: + json_value.emplace_bool() = ld_value.as_bool(); + break; + case Value::Type::kNumber: + json_value.emplace_double() = ld_value.as_double(); + break; + case Value::Type::kString: + json_value.emplace_string() = ld_value.as_string(); + break; + case Value::Type::kObject: { + auto& obj = json_value.emplace_object(); + for (auto const& pair : ld_value.as_object()) { + obj.insert_or_assign(pair.first.c_str(), + boost::json::value_from(pair.second)); + } + } break; + case Value::Type::kArray: { + auto& arr = json_value.emplace_array(); + for (auto const& val : ld_value.as_array()) { + arr.push_back(boost::json::value_from(val)); + } + } break; + } +} + +// NOLINTEND modernize-return-braced-init-list +} // namespace launchdarkly diff --git a/libs/common/src/value.cpp b/libs/common/src/value.cpp index b52f2a962..55b1d5013 100644 --- a/libs/common/src/value.cpp +++ b/libs/common/src/value.cpp @@ -205,4 +205,5 @@ Value::Object::Iterator Value::Object::end() const { Value::Object::Iterator Value::Object::find(std::string const& key) const { return {map_.find(key)}; } + } // namespace launchdarkly diff --git a/libs/common/src/value_mapping.cpp b/libs/common/src/value_mapping.cpp new file mode 100644 index 000000000..d08ed53bb --- /dev/null +++ b/libs/common/src/value_mapping.cpp @@ -0,0 +1,53 @@ +#include "serialization/value_mapping.hpp" + +namespace launchdarkly { + +template <> +std::optional ValueAsOpt(boost::json::object::iterator iterator, + boost::json::object::iterator end) { + if (iterator != end && iterator->value().is_number()) { + return iterator->value().to_number(); + } + return std::nullopt; +} + +template <> +std::optional ValueAsOpt(boost::json::object::iterator iterator, + boost::json::object::iterator end) { + if (iterator != end && iterator->value().is_string()) { + return std::string(iterator->value().as_string()); + } + return std::nullopt; +} + +template <> +bool ValueOrDefault(boost::json::object::iterator iterator, + boost::json::object::iterator end, + bool default_value) { + if (iterator != end && iterator->value().is_bool()) { + return iterator->value().as_bool(); + } + return default_value; +} + +template <> +std::string ValueOrDefault(boost::json::object::iterator iterator, + boost::json::object::iterator end, + std::string default_value) { + if (iterator != end && iterator->value().is_string()) { + return std::string(iterator->value().as_string()); + } + return default_value; +} +template <> + +uint64_t ValueOrDefault(boost::json::object::iterator iterator, + boost::json::object::iterator end, + uint64_t default_value) { + if (iterator != end && iterator->value().is_number()) { + return iterator->value().to_number(); + } + return default_value; +} + +} // namespace launchdarkly diff --git a/libs/common/tests/attributes_test.cpp b/libs/common/tests/attributes_test.cpp index c45d25d0e..3e68684da 100644 --- a/libs/common/tests/attributes_test.cpp +++ b/libs/common/tests/attributes_test.cpp @@ -1,11 +1,16 @@ #include +#include + #include "attributes.hpp" +#include "serialization/json_attributes.hpp" using launchdarkly::AttributeReference; using launchdarkly::Attributes; using launchdarkly::Value; +// NOLINTBEGIN cppcoreguidelines-avoid-magic-numbers + TEST(AttributesTests, CanGetBuiltInAttributesByReference) { Attributes attributes("the-key", "the-name", true, Value()); @@ -91,13 +96,62 @@ TEST(AttributesTests, OStreamOperator) { "[] custom: object({{int, number(42)}})}", ProduceString(attributes)); - Attributes attributes2("the-key", "the-name", true, - Value(std::map({{"int", 42}})), - AttributeReference::SetType{"/potato", "/bacon"}); + Attributes attributes_2("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)); + ProduceString(attributes_2)); +} + +TEST(AttributesTests, JsonSerializationtest) { + auto attributes_value = boost::json::value_from( + Attributes("the-key", std::nullopt, true, + Value(std::map({{"double", 4.2}})), + AttributeReference::SetType{"/potato", "/bacon"})); + + auto parsed_value = boost::json::parse( + "{" + "\"key\":\"the-key\"," + "\"anonymous\":true," + "\"double\":4.2," + "\"_meta\":{\"privateAttributes\": [\"/bacon\", \"/potato\"]}" + "}"); + + EXPECT_EQ(parsed_value, attributes_value); } + +TEST(AttributesTests, JsonSerializationOmitsFalseAnonymous) { + auto attributes_value = boost::json::value_from( + Attributes("the-key", std::nullopt, false, + Value(std::map({{"double", 4.2}})), + AttributeReference::SetType{"/potato", "/bacon"})); + + auto parsed_value = boost::json::parse( + "{" + "\"key\":\"the-key\"," + "\"double\":4.2," + "\"_meta\":{\"privateAttributes\": [\"/bacon\", \"/potato\"]}" + "}"); + + EXPECT_EQ(parsed_value, attributes_value); +} + +TEST(AttributesTests, JsonSerializationOmitsMetaIfPrivateAttributesEmpty) { + auto attributes_value = boost::json::value_from( + Attributes("the-key", std::nullopt, false, + Value(std::map({{"double", 4.2}})))); + + auto parsed_value = boost::json::parse( + "{" + "\"key\":\"the-key\"," + "\"double\":4.2" + "}"); + + EXPECT_EQ(parsed_value, attributes_value); +} + +// NOLINTEND cppcoreguidelines-avoid-magic-numbers diff --git a/libs/common/tests/context_tests.cpp b/libs/common/tests/context_tests.cpp index e2acf6ad4..aaa243b10 100644 --- a/libs/common/tests/context_tests.cpp +++ b/libs/common/tests/context_tests.cpp @@ -1,6 +1,9 @@ #include +#include + #include "context_builder.hpp" +#include "serialization/json_context.hpp" using launchdarkly::Context; using launchdarkly::ContextBuilder; @@ -85,3 +88,51 @@ TEST(ContextTests, OstreamOperatorInvalidContext) { "#$#*(: \"The key for a context may not be empty.\"]", ProduceString(context)); } + +// The attributes_test covers most serialization conditions. +// So these tests are more for overall shape of single and multi-contexts. +TEST(ContextTests, JsonSerializeSingleContext) { + auto context_value = + boost::json::value_from(ContextBuilder() + .kind("user", "user-key") + .set("isCat", true) + .set_private("email", "cat@email.email") + .build()); + + auto parsed_value = boost::json::parse( + "{" + "\"kind\":\"user\"," + "\"key\":\"user-key\"," + "\"isCat\":true," + "\"email\":\"cat@email.email\"," + "\"_meta\":{\"privateAttributes\": [\"email\"]}" + "}"); + + EXPECT_EQ(parsed_value, context_value); +} + +TEST(ContextTests, JsonSerializeMultiContext) { + auto context_value = + boost::json::value_from(ContextBuilder() + .kind("user", "user-key") + .set("isCat", true) + .set_private("email", "cat@email.email") + .kind("org", "org-key") + .build()); + + auto parsed_value = boost::json::parse( + "{" + "\"kind\":\"multi\"," + "\"user\":{" + "\"key\":\"user-key\"," + "\"isCat\":true," + "\"email\":\"cat@email.email\"," + "\"_meta\":{\"privateAttributes\": [\"email\"]}" + "}," + "\"org\":{" + "\"key\":\"org-key\"" + "}" + "}"); + + EXPECT_EQ(parsed_value, context_value); +} diff --git a/libs/common/tests/evaluation_reason_test.cpp b/libs/common/tests/evaluation_reason_test.cpp new file mode 100644 index 000000000..dbb79f2fd --- /dev/null +++ b/libs/common/tests/evaluation_reason_test.cpp @@ -0,0 +1,44 @@ +#include + +#include + +#include "data/evaluation_reason.hpp" +#include "serialization/json_evaluation_reason.hpp" + +using launchdarkly::EvaluationReason; + +TEST(EvaluationReasonsTests, FromJsonAllFields) { + auto reason = boost::json::value_to( + boost::json::parse("{" + "\"kind\":\"OFF\"," + "\"errorKind\":\"ERROR_KIND\"," + "\"ruleIndex\":12," + "\"ruleId\":\"RULE_ID\"," + "\"prerequisiteKey\":\"PREREQ_KEY\"," + "\"inExperiment\":true," + "\"bigSegmentStatus\":\"STORE_ERROR\"" + "}")); + + EXPECT_EQ("OFF", reason.kind()); + EXPECT_EQ("ERROR_KIND", reason.error_kind()); + EXPECT_EQ(12, reason.rule_index()); + EXPECT_EQ("RULE_ID", reason.rule_id()); + EXPECT_EQ("PREREQ_KEY", reason.prerequisite_key()); + EXPECT_EQ("STORE_ERROR", reason.big_segment_status()); + EXPECT_TRUE(reason.in_experiment()); +} + +TEST(EvaluationReasonsTests, FromMinimalJson) { + auto reason = boost::json::value_to( + boost::json::parse("{" + "\"kind\":\"RULE_MATCH\"" + "}")); + + EXPECT_EQ("RULE_MATCH", reason.kind()); + EXPECT_EQ(std::nullopt, reason.error_kind()); + EXPECT_EQ(std::nullopt, reason.rule_index()); + EXPECT_EQ(std::nullopt, reason.rule_id()); + EXPECT_EQ(std::nullopt, reason.prerequisite_key()); + EXPECT_EQ(std::nullopt, reason.big_segment_status()); + EXPECT_FALSE(reason.in_experiment()); +} diff --git a/libs/common/tests/evaluation_result_test.cpp b/libs/common/tests/evaluation_result_test.cpp new file mode 100644 index 000000000..65401ba21 --- /dev/null +++ b/libs/common/tests/evaluation_result_test.cpp @@ -0,0 +1,92 @@ +#include + +#include + +#include "data/evaluation_result.hpp" +#include "serialization/json_evaluation_result.hpp" + +using launchdarkly::EvaluationResult; + +// NOLINTBEGIN bugprone-unchecked-optional-access +// In the tests I do not care to check it. + +TEST(EvaluationResultTests, FromJsonAllFields) { + auto evaluation_result = boost::json::value_to( + boost::json::parse("{" + "\"version\": 12," + "\"flagVersion\": 24," + "\"trackEvents\": true," + "\"trackReason\": true," + "\"debugEventsUntilDate\": 1680555761," + "\"value\": {\"item\": 42}," + "\"variation\": 84," + "\"reason\": {" + "\"kind\":\"OFF\"," + "\"errorKind\":\"ERROR_KIND\"," + "\"ruleIndex\":12," + "\"ruleId\":\"RULE_ID\"," + "\"prerequisiteKey\":\"PREREQ_KEY\"," + "\"inExperiment\":true," + "\"bigSegmentStatus\":\"STORE_ERROR\"" + "}" + "}")); + + EXPECT_EQ(12, evaluation_result.version()); + EXPECT_EQ(24, evaluation_result.flag_version()); + EXPECT_TRUE(evaluation_result.track_events()); + EXPECT_TRUE(evaluation_result.track_reason()); + EXPECT_EQ(std::chrono::system_clock::time_point{std::chrono::milliseconds{ + 1680555761}}, + evaluation_result.debug_events_until_date()); + EXPECT_EQ(42, + evaluation_result.detail().value().as_object()["item"].as_int()); + EXPECT_EQ(84, evaluation_result.detail().variation_index()); + EXPECT_EQ("OFF", evaluation_result.detail().reason()->get().kind()); + EXPECT_EQ("ERROR_KIND", + evaluation_result.detail().reason()->get().error_kind()); + EXPECT_EQ(12, evaluation_result.detail().reason()->get().rule_index()); + EXPECT_EQ("RULE_ID", evaluation_result.detail().reason()->get().rule_id()); + EXPECT_EQ("PREREQ_KEY", + evaluation_result.detail().reason()->get().prerequisite_key()); + EXPECT_EQ("STORE_ERROR", + evaluation_result.detail().reason()->get().big_segment_status()); + EXPECT_TRUE(evaluation_result.detail().reason()->get().in_experiment()); +} + +TEST(EvaluationResultTests, FromJsonMinimalFields) { + auto evaluation_result = boost::json::value_to( + boost::json::parse("{" + "\"version\": 12," + "\"value\": {\"item\": 42}" + "}")); + + EXPECT_EQ(12, evaluation_result.version()); + EXPECT_EQ(std::nullopt, evaluation_result.flag_version()); + EXPECT_FALSE(evaluation_result.track_events()); + EXPECT_FALSE(evaluation_result.track_reason()); + EXPECT_EQ(std::nullopt, evaluation_result.debug_events_until_date()); + EXPECT_EQ(42, + evaluation_result.detail().value().as_object()["item"].as_int()); + EXPECT_EQ(std::nullopt, evaluation_result.detail().variation_index()); + EXPECT_EQ(std::nullopt, evaluation_result.detail().reason()); +} + +TEST(EvaluationResultTests, FromMapOfResults) { + auto results = + boost::json::value_to>( + boost::json::parse("{" + "\"flagA\":{" + "\"version\": 12," + "\"value\": true" + "}," + "\"flagB\":{" + "\"version\": 12," + "\"value\": false" + "}" + "}")); + + EXPECT_TRUE(results.at("flagA").detail().value().as_bool()); + EXPECT_FALSE(results.at("flagB").detail().value().as_bool()); +} + +// NOLINTEND bugprone-unchecked-optional-access diff --git a/libs/common/tests/value_test.cpp b/libs/common/tests/value_test.cpp index 5dbef0e27..4729b5035 100644 --- a/libs/common/tests/value_test.cpp +++ b/libs/common/tests/value_test.cpp @@ -4,10 +4,17 @@ #include #include +#include "serialization/json_value.hpp" #include "value.hpp" +#include + // NOLINTBEGIN cppcoreguidelines-avoid-magic-numbers +using BoostValue = boost::json::value; +using BoostObject = boost::json::object; +using BoostArray = boost::json::array; + using launchdarkly::Value; TEST(ValueTests, CanMakeNullValue) { Value null_val; @@ -213,4 +220,53 @@ TEST(ValueTests, OstreamOperator) { ProduceString(Value::Object{{"first", "cheese"}, {"second", 42}})); } +TEST(ValueTests, FromBoostJson) { + BoostValue bool_val(true); + auto ld_bool = boost::json::value_to(bool_val); + EXPECT_TRUE(ld_bool.as_bool()); + + BoostValue string_val("potato"); + auto ld_string = boost::json::value_to(string_val); + EXPECT_EQ("potato", ld_string.as_string()); + + BoostValue number_val(3.14); + auto ld_number = boost::json::value_to(number_val); + EXPECT_EQ(3.14, ld_number.as_double()); + + BoostValue arr_val = BoostArray{true, false, BoostObject{{"name", "Bob"}}}; + auto ld_array = boost::json::value_to(arr_val); + EXPECT_TRUE(ld_array.as_array()[0].as_bool()); + EXPECT_FALSE(ld_array.as_array()[1].as_bool()); + EXPECT_EQ("Bob", ld_array.as_array()[2].as_object()["name"].as_string()); + + BoostValue obj_val = + BoostObject{{"name", "Bob"}, {"array", BoostArray{true, false}}}; + auto ld_obj = boost::json::value_to(obj_val); + EXPECT_EQ("Bob", ld_obj.as_object()["name"].as_string()); + EXPECT_TRUE(ld_obj.as_object()["array"].as_array()[0].as_bool()); + EXPECT_FALSE(ld_obj.as_object()["array"].as_array()[1].as_bool()); +} + +TEST(ValueTests, ToBoostJson) { + Value bool_val(true); + auto boost_bool = boost::json::value_from(bool_val); + EXPECT_TRUE(boost_bool.as_bool()); + + Value string_val("potato"); + auto boost_string = boost::json::value_from(string_val); + EXPECT_EQ("potato", boost_string.as_string()); + + Value number_val(3.14); + auto boost_number = boost::json::value_from(number_val); + EXPECT_EQ(3.14, boost_number.as_double()); + + Value arr_val{true, false, {"a", "b"}, Value::Object{{"string", "ham"}}}; + auto boost_arr = boost::json::value_from(arr_val); + EXPECT_TRUE(boost_arr.as_array().at(0).as_bool()); + EXPECT_FALSE(boost_arr.as_array().at(1).as_bool()); + EXPECT_EQ("a", boost_arr.as_array().at(2).as_array().at(0)); + EXPECT_EQ("b", boost_arr.as_array().at(2).as_array().at(1)); + EXPECT_EQ("ham", boost_arr.as_array().at(3).as_object().at("string")); +} + // NOLINTEND cppcoreguidelines-avoid-magic-numbers