Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions libs/common/include/attribute_reference.hpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#pragma once

#include <algorithm>
#include <cstddef>
#include <ostream>
#include <string>
Expand Down Expand Up @@ -104,6 +105,16 @@ class AttributeReference {
*/
static AttributeReference from_literal_str(std::string lit_str);

/**
* For a path, a series of names to address an attribute, create a name
* suitable for including in event meta data.
*
* @param path The path to get a name for.
* @return The path as a reference string.
*/
static std::string path_to_string_reference(
std::vector<std::string_view> path);

friend std::ostream& operator<<(std::ostream& os,
AttributeReference const& ref) {
os << (ref.valid() ? "valid" : "invalid") << "(" << ref.redaction_name()
Expand All @@ -127,10 +138,19 @@ class AttributeReference {
return components_ == other.components_;
}

bool operator==(std::vector<std::string_view> const& path) const {
return components_.size() == path.size() &&
std::equal(components_.begin(), components_.end(), path.begin());
}

bool operator!=(AttributeReference const& other) const {
return !(*this == other);
}

bool operator!=(std::vector<std::string_view> const& path) const {
return !(*this == path);
}

bool operator<(AttributeReference const& rhs) const {
return components_ < rhs.components_;
}
Expand Down
10 changes: 7 additions & 3 deletions libs/common/include/attributes.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,17 @@ class Attributes {
*/
bool anonymous() const;

/**
* Get the custom attributes as a Value. This value is an kObject type.
* @return The custom attributes.
*/
Value const& custom_attributes() 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_;
}
AttributeReference::SetType const& private_attributes() const;

/**
* Gets the item by the specified attribute reference, or returns a null
Expand Down
2 changes: 1 addition & 1 deletion libs/common/include/context.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class Context {
*
* @return A vector of kinds.
*/
std::vector<std::string_view> const& kinds();
[[nodiscard]] std::vector<std::string_view> const& kinds() const;

/**
* Get a set of attributes associated with a kind.
Expand Down
95 changes: 95 additions & 0 deletions libs/common/include/context_filter.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#pragma once

#include <string>
#include <unordered_set>
#include <vector>

#include <boost/json.hpp>

#include "attribute_reference.hpp"
#include "context.hpp"

namespace launchdarkly {

/**
* Class used by the SDK for filtering contexts to produce redacted JSON
* for analytics events.
*/
class ContextFilter {
public:
using JsonValue = boost::json::value;
using JsonObject = boost::json::object;
using JsonArray = boost::json::array;

ContextFilter(bool all_attributes_private,
AttributeReference::SetType const& global_private_attributes);

/**
* Filter the given context and produce a JSON value.
*
* Only call this method for valid contexts.
*
* @param context The context to redact.
* @return JSON suitable for an analytics event.
*/
JsonValue filter(Context const& context);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wonder if it'd be highly inconvenient to guarantee that all Contexts are valid.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In what sense?
That they are valid before we call this, or you cannot even make an invalid one?

It should be easy to always be valid before this call, because we don't put invalid contexts into events, and many events we don't send at all if the context was not valid.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In regards to all contexts being valid, I think that would preclude the ability to dynamically populate contexts.


private:
/**
* The filtering and JSON conversion algorithm is stack based
* instead of recursive. Each node is visited, and if that node is a basic
* type bool, number, string, then it is processed immediately, being
* added to the output object. If the node is complex, then each of its
* immediately children is added to the stack to be processed.
*/
struct StackItem {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I generally try to avoid recursive implementations outside functional languages. I have done that here, but I have not specifically benchmarked and compared the stack size and performance between the two implementations.

Value const& value;
std::vector<std::string_view> path;
JsonValue& parent;
};

/**
* Put an item into its parent. Either as a pair in a map,
* or pushed onto an array.
* @param item The stack item denoting placement information.
* @param addition The item to add.
*/
static void emplace(StackItem& item, JsonValue&& addition);

/**
* If the path needs to be redacted, then redact it and add it to the redactions.
* @param redactions The list of redacted items.
* @param path The path to check.
* @param attributes Attributes which may contain additional private
* attributes.
* @return True if the item was redacted.
*/
bool redact(std::vector<std::string>& redactions,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is redact being called multiple times to build up the redactions list? or is this a candidate for a return value (once the tl::expected is in)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same list is used throughout the process.

std::vector<std::string_view> path,
Attributes const& attributes);

/**
* Append a container to the parent.
* @param item The stack item containing the parent.
* @param value The container to append.
* @return The appended container.
*/
static JsonValue* append_container(StackItem& item, JsonValue&& value);

/**
* Put a simple value into the parent specified by its stack item.
* @param item The stack item with value information and the parent.
*/
static void append_simple_type(StackItem& item);

JsonValue filter_single_context(std::string_view kind,
bool include_kind,
Attributes const& attributes);

JsonValue filter_multi_context(Context const& context);

bool all_attributes_private_;
AttributeReference::SetType const& global_private_attributes_;
};

} // namespace launchdarkly
5 changes: 4 additions & 1 deletion libs/common/include/value.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class Value {
class Array {
public:
struct Iterator {
using iterator_category = std::forward_iterator_tag;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we can iterate the array either direction.

using iterator_category = std::bidirectional_iterator_tag;
using difference_type = std::ptrdiff_t;
using value_type = Value;
using pointer = value_type const*;
Expand All @@ -66,6 +66,9 @@ class Value {
Iterator& operator++();
Iterator operator++(int);

Iterator& operator--();
Iterator operator--(int);

friend bool operator==(Iterator const& lhs, Iterator const& rhs) {
return lhs.iterator_ == rhs.iterator_;
};
Expand Down
1 change: 1 addition & 0 deletions libs/common/src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ add_library(${LIBNAME}
config/endpoints_builder.cpp
config/config_builder.cpp
config/config.cpp
context_filter.cpp lib.cpp
config/application_info.cpp
)

Expand Down
40 changes: 37 additions & 3 deletions libs/common/src/attribute_reference.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "attribute_reference.hpp"
#include <numeric>
#include <utility>

namespace launchdarkly {
Expand Down Expand Up @@ -149,10 +150,13 @@ bool ParseRef(std::string str, std::vector<std::string>& components) {

/**
* Literal starting with a '/' needs to be converted to an attribute
* reference string.
* reference string. Additionally when making redaction names fields
* which contain special characters may also need to be escaped even
* when they do not start with a '/'.
*/
std::string EscapeLiteral(std::string const& literal) {
std::string escaped = "/";
std::string EscapeLiteral(std::string const& literal,
bool prepend_slash = true) {
std::string escaped = prepend_slash ? "/" : "";
for (auto const& character : literal) {
if (character == '~') {
escaped.append("~0");
Expand Down Expand Up @@ -222,4 +226,34 @@ AttributeReference::AttributeReference(std::string ref_str)
AttributeReference::AttributeReference(char const* ref_str)
: AttributeReference(std::string(ref_str)) {}

std::string AttributeReference::path_to_string_reference(
std::vector<std::string_view> path) {
// Approximate size to reduce resizes.
auto size = std::accumulate(path.begin(), path.end(), 0,
[](auto sum, auto const& component) {
return sum + component.size() + 1;
});

std::string redaction_name;
redaction_name.reserve(size);

redaction_name.push_back('/');
bool first = true;
for (auto const& component : path) {
if (first) {
first = false;
} else {
redaction_name.push_back('/');
}
// Unlike legacy literals we need to escape each part of the
// path.
if (component.find_first_of("/~") != std::string_view::npos) {
redaction_name.append(EscapeLiteral(component.data(), false));
} else {
redaction_name.append(component);
}
}
return redaction_name;
}

} // namespace launchdarkly
9 changes: 9 additions & 0 deletions libs/common/src/attributes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,13 @@ std::string const& Attributes::name() const {
bool Attributes::anonymous() const {
return anonymous_.as_bool();
}

Value const& Attributes::custom_attributes() const {
return custom_attributes_;
}

AttributeReference::SetType const& Attributes::private_attributes() const {
return private_attributes_;
}

} // namespace launchdarkly
2 changes: 1 addition & 1 deletion libs/common/src/context.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ static std::string EscapeKey(std::string_view const& to_escape) {
return escaped;
}

std::vector<std::string_view> const& Context::kinds() {
std::vector<std::string_view> const& Context::kinds() const {
return kinds_;
}

Expand Down
Loading