Skip to content

Commit 2203788

Browse files
authored
feat: Add attribute reference support. (#7)
1 parent 14f4161 commit 2203788

File tree

4 files changed

+411
-1
lines changed

4 files changed

+411
-1
lines changed
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
#pragma once
2+
3+
#include <ostream>
4+
#include <string>
5+
#include <vector>
6+
7+
namespace launchdarkly {
8+
9+
/**
10+
* Represents an attribute name or path expression identifying a value within a
11+
* [TODO: Context]. This can be used to retrieve a value with [TODO: Get Value],
12+
* or to identify an attribute or nested value that should be considered private
13+
* with [TODO: private attribute] (the SDK configuration can also have a list of
14+
* private attribute references).
15+
*
16+
* This is represented as a separate type, rather than just a string, so that
17+
* validation and parsing can be done ahead of time if an attribute reference
18+
* will be used repeatedly later (such as in flag evaluations).
19+
*
20+
* If the string starts with '/', then this is treated as a slash-delimited
21+
* path reference where the first component is the name of an attribute, and
22+
* subsequent components are the names of nested JSON object properties. In this
23+
* syntax, the escape sequences "~0" and "~1" represent '~' and '/' respectively
24+
* within a path component.
25+
*
26+
* If the string does not start with '/', then it is treated as the literal
27+
* name of an attribute.
28+
*/
29+
class AttributeReference {
30+
public:
31+
/**
32+
* Get the component of the attribute reference at the specified depth.
33+
*
34+
* For example, component(1) on the reference `/a/b/c` would return
35+
* `b`.
36+
*
37+
* @param depth The depth to get a component for.
38+
* @return The component at the specified depth or an empty string if the
39+
* depth is out of bounds.
40+
*/
41+
std::string const& component(size_t depth) const;
42+
43+
/**
44+
* Get the total depth of the reference.
45+
*
46+
* For example, depth() on the reference `/a/b/c` would return 3.
47+
* @return
48+
*/
49+
size_t depth() const;
50+
51+
/**
52+
* Check if the reference is a "kind" reference. Either `/kind` or `kind`.
53+
*
54+
* @return True if it is a kind reference.
55+
*/
56+
bool is_kind() const;
57+
58+
/** Check if the reference is valid.
59+
*
60+
* @return True if the reference is valid.
61+
*/
62+
bool valid() const;
63+
64+
/**
65+
* The redaction name will always be an attribute reference compatible
66+
* string. So, for instance, a literal that contained `/attr` would be
67+
* converted to `/~1attr`.
68+
* @return String to use in redacted attributes.
69+
*/
70+
std::string const& redaction_name() const;
71+
72+
/**
73+
* Create an attribute from a string that is known to be an attribute
74+
* reference string.
75+
* @param ref_str The reference string.
76+
* @return A new AttributeReference based on the reference string.
77+
*/
78+
static AttributeReference from_reference_str(std::string ref_str);
79+
80+
/**
81+
* Create a string from an attribute that is known to be a literal.
82+
*
83+
* This allows escaping literals that contained special characters.
84+
*
85+
* @param lit_str The literal attribute name.
86+
* @return A new AttributeReference based on the literal name.
87+
*/
88+
static AttributeReference from_literal_str(std::string lit_str);
89+
90+
friend std::ostream& operator<<(std::ostream& os,
91+
AttributeReference const& ref) {
92+
os << (ref.valid() ? "valid" : "invalid") << "(" << ref.redaction_name()
93+
<< ")";
94+
return os;
95+
}
96+
97+
private:
98+
AttributeReference(std::string str, bool is_literal);
99+
100+
bool valid_;
101+
102+
std::string redaction_name_;
103+
std::vector<std::string> components_;
104+
inline static const std::string empty_;
105+
};
106+
107+
} // namespace launchdarkly

libs/common/src/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
file(GLOB HEADER_LIST CONFIGURE_DEPENDS "${LaunchDarklyCPPCommon_SOURCE_DIR}/include/*.hpp")
33

44
# Automatic library: static or dynamic based on user config.
5-
add_library(${LIBNAME} logger.cpp ${HEADER_LIST} console_backend.cpp log_level.cpp ../include/console_backend.hpp)
5+
add_library(${LIBNAME} logger.cpp ${HEADER_LIST} console_backend.cpp log_level.cpp attribute_reference.cpp)
66

77
add_library(launchdarkly::common ALIAS ${LIBNAME})
88

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
#include "attribute_reference.hpp"
2+
#include <utility>
3+
4+
namespace launchdarkly {
5+
6+
enum class ParseState {
7+
kBegin, /* start state */
8+
kPlain, /* plain, top-level attribute name detected */
9+
kTokenBegin, /* found the beginning of a token */
10+
kSearchingEnd, /* searching for the end of a token */
11+
kEscapeBegin, /* found start of an escape sequence */
12+
kRefEnd /* end state */
13+
};
14+
15+
enum class ParseEvent {
16+
kNoop, /* no event */
17+
kChar, /* write the input character */
18+
kTilde, /* write a '~' */
19+
kForwardSlash, /* write a '/' */
20+
kTokenEnd, /* end of token */
21+
kInputEnd, /* end of input */
22+
kError /* error */
23+
};
24+
25+
/**
26+
* This function is responsible for decoding an input string,
27+
* representing a reference, into a list of components.
28+
*
29+
* Each component is the bits between the '/' separators. For example,
30+
* the reference "/foo/bar" will be represented by the components "foo" and
31+
* "bar".
32+
*
33+
* The algorithm proceeds as a single-pass over the input string, performed by
34+
* a state machine.
35+
*/
36+
std::pair<ParseState, ParseEvent> ParseChar(ParseState state, char input) {
37+
switch (state) {
38+
case ParseState::kBegin: {
39+
switch (input) {
40+
case '/': {
41+
return {ParseState::kTokenBegin, ParseEvent::kNoop};
42+
}
43+
case '\0': {
44+
return {ParseState::kRefEnd, ParseEvent::kError};
45+
}
46+
default: {
47+
return {ParseState::kPlain, ParseEvent::kChar};
48+
}
49+
}
50+
}
51+
case ParseState::kPlain: {
52+
if (input == '\0') {
53+
return {ParseState::kRefEnd, ParseEvent::kInputEnd};
54+
}
55+
56+
return {ParseState::kPlain, ParseEvent::kChar};
57+
}
58+
case ParseState::kTokenBegin: {
59+
switch (input) {
60+
case '\0': // Falling through to the error case intentionally.
61+
case '/': {
62+
return {ParseState::kRefEnd, ParseEvent::kError};
63+
}
64+
case '~': {
65+
return {ParseState::kEscapeBegin, ParseEvent::kNoop};
66+
}
67+
default: {
68+
return {ParseState::kSearchingEnd, ParseEvent::kChar};
69+
}
70+
}
71+
}
72+
case ParseState::kSearchingEnd: {
73+
switch (input) {
74+
case '\0': {
75+
return {ParseState::kRefEnd, ParseEvent::kInputEnd};
76+
}
77+
case '~': {
78+
return {ParseState::kEscapeBegin, ParseEvent::kNoop};
79+
}
80+
case '/': {
81+
return {ParseState::kTokenBegin, ParseEvent::kTokenEnd};
82+
}
83+
default: {
84+
return {ParseState::kSearchingEnd, ParseEvent::kChar};
85+
}
86+
}
87+
}
88+
case ParseState::kEscapeBegin: {
89+
switch (input) {
90+
case '0': {
91+
return {ParseState::kSearchingEnd, ParseEvent::kTilde};
92+
}
93+
case '1': {
94+
return {ParseState::kSearchingEnd,
95+
ParseEvent::kForwardSlash};
96+
}
97+
default: {
98+
return {ParseState::kRefEnd, ParseEvent::kError};
99+
}
100+
}
101+
}
102+
case ParseState::kRefEnd: {
103+
return {ParseState::kRefEnd, ParseEvent::kNoop};
104+
}
105+
}
106+
// Should only happen if additional states are added but not handled.
107+
return {ParseState::kRefEnd, ParseEvent::kError};
108+
}
109+
110+
bool ParseRef(std::string str, std::vector<std::string>& components) {
111+
auto p_state = ParseState::kBegin;
112+
113+
std::string tmp_token;
114+
// The loop here extends to the size of the string, so we can send a null
115+
// into the parsing logic to terminate the parsing.
116+
for (auto index = 0; index <= str.size(); index++) {
117+
// The character in the string, or null if we go out of bounds of the
118+
// string.
119+
char character = index < str.size() ? str[index] : '\0';
120+
auto [new_p_state, event] = ParseChar(p_state, character);
121+
p_state = new_p_state;
122+
123+
switch (event) {
124+
case ParseEvent::kNoop:
125+
continue;
126+
case ParseEvent::kChar: {
127+
tmp_token.push_back(character);
128+
} break;
129+
case ParseEvent::kForwardSlash: {
130+
tmp_token.push_back('/');
131+
} break;
132+
case ParseEvent::kTilde: {
133+
tmp_token.push_back('~');
134+
} break;
135+
case ParseEvent::kTokenEnd: {
136+
components.push_back(std::move(tmp_token));
137+
// Could .clear here, but this seems more appropriate.
138+
tmp_token = std::string();
139+
} break;
140+
case ParseEvent::kInputEnd:
141+
components.push_back(std::move(tmp_token));
142+
return true;
143+
case ParseEvent::kError:
144+
return false;
145+
}
146+
}
147+
return false;
148+
}
149+
150+
/**
151+
* Literal starting with a '/' needs to be converted to an attribute
152+
* reference string.
153+
*/
154+
std::string EscapeLiteral(std::string const& literal) {
155+
std::string escaped = "/";
156+
for (auto const& character : literal) {
157+
if (character == '~') {
158+
escaped.append("~0");
159+
} else if (character == '/') {
160+
escaped.append("~1");
161+
} else {
162+
escaped.push_back(character);
163+
}
164+
}
165+
return escaped;
166+
}
167+
168+
AttributeReference::AttributeReference(std::string str, bool literal) {
169+
if (literal) {
170+
components_.push_back(str);
171+
// Literal starting with a '/' needs to be converted to an attribute
172+
// reference string.
173+
if (str[0] == '/') {
174+
redaction_name_ = EscapeLiteral(str);
175+
} else {
176+
redaction_name_ = str;
177+
}
178+
} else {
179+
valid_ = ParseRef(str, components_);
180+
redaction_name_ = std::move(str);
181+
if (!valid_) {
182+
components_.clear();
183+
}
184+
}
185+
}
186+
187+
AttributeReference AttributeReference::from_literal_str(std::string lit_str) {
188+
return {std::move(lit_str), true};
189+
}
190+
191+
AttributeReference AttributeReference::from_reference_str(std::string ref_str) {
192+
return {std::move(ref_str), false};
193+
}
194+
195+
std::string const& AttributeReference::component(size_t depth) const {
196+
if (depth < components_.size()) {
197+
return components_[depth];
198+
}
199+
return empty_;
200+
}
201+
202+
size_t AttributeReference::depth() const {
203+
return components_.size();
204+
}
205+
206+
bool AttributeReference::is_kind() const {
207+
return depth() == 1 && component(0) == "kind";
208+
}
209+
210+
bool AttributeReference::valid() const {
211+
return valid_;
212+
}
213+
214+
std::string const& AttributeReference::redaction_name() const {
215+
return redaction_name_;
216+
}
217+
218+
} // namespace launchdarkly

0 commit comments

Comments
 (0)