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.hpp b/libs/common/include/data/evaluation_detail_internal.hpp similarity index 95% rename from libs/common/include/data/evaluation_detail.hpp rename to libs/common/include/data/evaluation_detail_internal.hpp index d1a96593c..56ae7cbd3 100644 --- a/libs/common/include/data/evaluation_detail.hpp +++ b/libs/common/include/data/evaluation_detail_internal.hpp @@ -18,7 +18,7 @@ namespace launchdarkly { * For more information, see the [SDK reference guide] * (https://docs.launchdarkly.com/sdk/features/evaluation-reasons#TODO). */ -class EvaluationDetail { +class EvaluationDetailInternal { public: /** * The result of the flag evaluation. This will be either one of the flag's @@ -41,7 +41,7 @@ class EvaluationDetail { [[nodiscard]] std::optional> reason() const; - EvaluationDetail(Value value, + EvaluationDetailInternal(Value value, std::optional variation_index, std::optional reason); diff --git a/libs/common/include/data/evaluation_reason.hpp b/libs/common/include/data/evaluation_reason.hpp index cb148e0d3..1133f4ddc 100644 --- a/libs/common/include/data/evaluation_reason.hpp +++ b/libs/common/include/data/evaluation_reason.hpp @@ -4,8 +4,6 @@ #include #include -#include - namespace launchdarkly { /** @@ -97,13 +95,4 @@ class EvaluationReason { std::optional big_segment_status_; }; -/** - * 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/data/evaluation_result.hpp b/libs/common/include/data/evaluation_result.hpp index fce0c7260..fa0604402 100644 --- a/libs/common/include/data/evaluation_result.hpp +++ b/libs/common/include/data/evaluation_result.hpp @@ -3,9 +3,7 @@ #include #include -#include - -#include "evaluation_detail.hpp" +#include "evaluation_detail_internal.hpp" namespace launchdarkly { @@ -47,7 +45,7 @@ class EvaluationResult { /** * Details of the flags evaluation. */ - [[nodiscard]] EvaluationDetail const& detail() const; + [[nodiscard]] EvaluationDetailInternal const& detail() const; EvaluationResult( uint64_t version, @@ -56,7 +54,7 @@ class EvaluationResult { bool track_reason, std::optional> debug_events_until_date, - EvaluationDetail detail); + EvaluationDetailInternal detail); private: uint64_t version_; @@ -65,16 +63,7 @@ class EvaluationResult { bool track_reason_; std::optional> debug_events_until_date_; - EvaluationDetail detail_; + EvaluationDetailInternal detail_; }; -/** - * 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_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/value_mapping.hpp b/libs/common/include/serialization/value_mapping.hpp similarity index 100% rename from libs/common/include/value_mapping.hpp rename to libs/common/include/serialization/value_mapping.hpp diff --git a/libs/common/include/value.hpp b/libs/common/include/value.hpp index 006697278..02ff8841c 100644 --- a/libs/common/include/value.hpp +++ b/libs/common/include/value.hpp @@ -7,7 +7,6 @@ #include #include -#include #include namespace launchdarkly { @@ -425,12 +424,4 @@ class Value { static const Value null_value_; }; -/** - * 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&); - } // namespace launchdarkly diff --git a/libs/common/src/CMakeLists.txt b/libs/common/src/CMakeLists.txt index 51c01f8f8..387e0b836 100644 --- a/libs/common/src/CMakeLists.txt +++ b/libs/common/src/CMakeLists.txt @@ -22,12 +22,17 @@ add_library(${LIBNAME} config/config_builder.cpp config/config.cpp data/evaluation_reason.cpp - data/evaluation_detail.cpp + data/evaluation_detail_internal.cpp data/evaluation_result.cpp context_filter.cpp config/application_info.cpp lib.cpp - value_mapping.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.cpp b/libs/common/src/data/evaluation_detail.cpp deleted file mode 100644 index ee4c23200..000000000 --- a/libs/common/src/data/evaluation_detail.cpp +++ /dev/null @@ -1,24 +0,0 @@ -#include "data/evaluation_detail.hpp" - -namespace launchdarkly { - -Value const& EvaluationDetail::value() const { - return value_; -} - -std::optional EvaluationDetail::variation_index() const { - return variation_index_; -} - -std::optional> -EvaluationDetail::reason() const { - return reason_; -} -EvaluationDetail::EvaluationDetail(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_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 index 2cdf0f849..d164a76fe 100644 --- a/libs/common/src/data/evaluation_reason.cpp +++ b/libs/common/src/data/evaluation_reason.cpp @@ -1,5 +1,4 @@ #include "data/evaluation_reason.hpp" -#include "value_mapping.hpp" namespace launchdarkly { @@ -47,43 +46,4 @@ EvaluationReason::EvaluationReason( in_experiment_(in_experiment), big_segment_status_(std::move(big_segment_status)) {} -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/data/evaluation_result.cpp b/libs/common/src/data/evaluation_result.cpp index aded6caad..bc196e4b1 100644 --- a/libs/common/src/data/evaluation_result.cpp +++ b/libs/common/src/data/evaluation_result.cpp @@ -1,5 +1,4 @@ #include "data/evaluation_result.hpp" -#include "value_mapping.hpp" #include @@ -26,7 +25,7 @@ EvaluationResult::debug_events_until_date() const { return debug_events_until_date_; } -EvaluationDetail const& EvaluationResult::detail() const { +EvaluationDetailInternal const& EvaluationResult::detail() const { return detail_; } @@ -37,7 +36,7 @@ EvaluationResult::EvaluationResult( bool track_reason, std::optional> debug_events_until_date, - EvaluationDetail detail) + EvaluationDetailInternal detail) : version_(version), flag_version_(flag_version), track_events_(track_events), @@ -45,73 +44,4 @@ EvaluationResult::EvaluationResult( debug_events_until_date_(debug_events_until_date), detail_(std::move(detail)) {} -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, - EvaluationDetail(value, variation, reason)}; - } - // This would represent malformed JSON. - return {0, - 0, - false, - false, - std::nullopt, - EvaluationDetail(Value(), std::nullopt, std::nullopt)}; -} } // 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 6042f12d5..55b1d5013 100644 --- a/libs/common/src/value.cpp +++ b/libs/common/src/value.cpp @@ -206,55 +206,4 @@ Value::Object::Iterator Value::Object::find(std::string const& key) const { return {map_.find(key)}; } -// 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."); -} - -// NOLINTEND modernize-return-braced-init-list - } // namespace launchdarkly diff --git a/libs/common/src/value_mapping.cpp b/libs/common/src/value_mapping.cpp index 6282b8db6..d08ed53bb 100644 --- a/libs/common/src/value_mapping.cpp +++ b/libs/common/src/value_mapping.cpp @@ -1,4 +1,4 @@ -#include "value_mapping.hpp" +#include "serialization/value_mapping.hpp" 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 index 2fc5c3129..dbb79f2fd 100644 --- a/libs/common/tests/evaluation_reason_test.cpp +++ b/libs/common/tests/evaluation_reason_test.cpp @@ -3,6 +3,7 @@ #include #include "data/evaluation_reason.hpp" +#include "serialization/json_evaluation_reason.hpp" using launchdarkly::EvaluationReason; diff --git a/libs/common/tests/evaluation_result_test.cpp b/libs/common/tests/evaluation_result_test.cpp index 2b99b3f52..65401ba21 100644 --- a/libs/common/tests/evaluation_result_test.cpp +++ b/libs/common/tests/evaluation_result_test.cpp @@ -3,6 +3,7 @@ #include #include "data/evaluation_result.hpp" +#include "serialization/json_evaluation_result.hpp" using launchdarkly::EvaluationResult; diff --git a/libs/common/tests/value_test.cpp b/libs/common/tests/value_test.cpp index 624b5c0ac..4729b5035 100644 --- a/libs/common/tests/value_test.cpp +++ b/libs/common/tests/value_test.cpp @@ -4,6 +4,7 @@ #include #include +#include "serialization/json_value.hpp" #include "value.hpp" #include @@ -246,4 +247,26 @@ TEST(ValueTests, FromBoostJson) { 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