@@ -12,17 +12,55 @@ import (
1212
1313// Ref is an attribute name or path expression identifying a value within a Context.
1414//
15- // This can be used to retrieve a value with Context.GetValueForRef(), or to identify an attribute or
15+ // This type is mainly intended to be used internally by LaunchDarkly SDK and service code, where
16+ // efficiency is a major concern so it's desirable to do any parsing or preprocessing just once.
17+ // Applications are unlikely to need to use the Ref type directly.
18+ //
19+ // It can be used to retrieve a value with Context.GetValueForRef(), or to identify an attribute or
1620// nested value that should be considered private with Builder.Private() (the SDK configuration can also
1721// have a list of private attribute references).
1822//
19- // This is represented as a separate type, rather than just a string, so that validation and parsing can
20- // be done ahead of time if an attribute reference will be used repeatedly later (such as in flag
21- // evaluations).
23+ // Parsing and validation are done at the time that the NewRef or NewLiteralRef constructor is called.
24+ // If a Ref instance was created from an invalid string, or if it is an uninitialized Ref{}, it is
25+ // considered invalid and its Err() method will return a non-nil error.
26+ //
27+ // Syntax
28+ //
29+ // The string representation of an attribute reference in LaunchDarkly JSON data uses the following
30+ // syntax:
31+ //
32+ // If the first character is not a slash, the string is interpreted literally as an attribute name.
33+ // An attribute name can contain any characters, but must not be empty.
34+ //
35+ // If the first character is a slash, the string is interpreted as a slash-delimited path where the
36+ // first path component is an attribute name, and each subsequent path component is either the name of
37+ // a property in a JSON object, or a decimal numeric string that is the index of an element in a JSON
38+ // array. Any instances of the characters "/" or "~" in a path component are escaped as "~1" or "~0"
39+ // respectively. This syntax deliberately resembles JSON Pointer, but no JSON Pointer behaviors other
40+ // than those mentioned here are supported.
2241//
23- // Call NewRef() to create an Ref. An uninitialized ldattr.Ref{} struct is not valid for use in any
24- // SDK operations. Also, an Ref can be in an error state if it was built from an invalid string. See
25- // Ref.Err().
42+ // Examples
43+ //
44+ // Suppose there is a context whose JSON implementation looks like this:
45+ //
46+ // {
47+ // "kind": "user",
48+ // "key": "value1",
49+ // "address": {
50+ // "street": "value2",
51+ // "city": "value3"
52+ // },
53+ // "groups": [ "value4", "value5" ],
54+ // "good/bad": "value6"
55+ // }
56+ //
57+ // The attribute references "key" and "/key" would both point to "value1".
58+ //
59+ // The attribute reference "/address/street" would point to "value2".
60+ //
61+ // The attribute reference "/groups/0" would point to "value4".
62+ //
63+ // The attribute references "good/bad" and "/good~1bad" would both point to "value6".
2664type Ref struct {
2765 err error
2866 rawPath string
@@ -35,38 +73,12 @@ type attrRefComponent struct {
3573 intValue ldvalue.OptionalInt
3674}
3775
38- // NewRef creates a Ref from a string.
39- //
40- // If the string starts with '/', then this is treated as a slash-delimited path reference where the
41- // first component is the name of an attribute, and subsequent components are the names of nested JSON
42- // object properties (or, if they are numeric, the indices of JSON array elements). In this syntax, the
43- // escape sequences "~0" and "~1" represent '~' and '/' respectively within a path component.
44- //
45- // If the string does not start with '/', then it is treated as the literal name of an attribute (the
46- // same as NewNameRef).
47- //
48- // For instance, if the JSON representation of a context is as follows--
49- //
50- // {
51- // "kind": "user",
52- // "key": "123",
53- // "name": "xyz",
54- // "address": {
55- // "street": "99 Main St.",
56- // "city": "Westview"
57- // },
58- // "groups": [ "p", "q" ],
59- // "a/b": "ok"
60- // }
61- //
62- // --then NewRef("name") or NewRef("/name") would refer to the value "xyz"; NewRef("/address/street")
63- // would refer to the value "99 Main St."; NewRef("/groups/0") would refer to the value "p"; and
64- // NewRef("a/b") or NewRef("/a~1b") would refer to the value "ok".
76+ // NewRef creates a Ref from a string. For the supported syntax and examples, see comments on the Ref type.
6577//
6678// This constructor always returns a Ref that preserves the original string, even if validation fails,
6779// so that calling String() (or serializing the Ref to JSON) will produce the original string. If
68- // validation fails, Err() will return nil and any SDK method that takes this Ref as a parameter will
69- // consider it invalid.
80+ // validation fails, Err() will return a non- nil error and any SDK method that takes this Ref as a
81+ // parameter will consider it invalid.
7082func NewRef (referenceString string ) Ref {
7183 if referenceString == "" || referenceString == "/" {
7284 return Ref {err : errAttributeEmpty , rawPath : referenceString }
@@ -79,7 +91,10 @@ func NewRef(referenceString string) Ref {
7991 if ! strings .Contains (path , "/" ) {
8092 // There's only one segment, so this is still a simple attribute reference. However, we still may
8193 // need to unescape special characters.
82- return Ref {singlePathComponent : unescapePath (path ), rawPath : referenceString }
94+ if unescaped , ok := unescapePath (path ); ok {
95+ return Ref {singlePathComponent : unescaped , rawPath : referenceString }
96+ }
97+ return Ref {err : errAttributeInvalidEscape , rawPath : referenceString }
8398 }
8499 parts := strings .Split (path , "/" )
85100 ret := Ref {rawPath : referenceString , components : make ([]attrRefComponent , 0 , len (parts ))}
@@ -88,7 +103,11 @@ func NewRef(referenceString string) Ref {
88103 ret .err = errAttributeExtraSlash
89104 return ret
90105 }
91- component := attrRefComponent {name : unescapePath (p )}
106+ unescaped , ok := unescapePath (p )
107+ if ! ok {
108+ return Ref {err : errAttributeInvalidEscape , rawPath : referenceString }
109+ }
110+ component := attrRefComponent {name : unescaped }
92111 if p [0 ] >= '0' && p [0 ] <= '9' {
93112 if n , err := strconv .Atoi (p ); err == nil {
94113 component .intValue = ldvalue .NewOptionalInt (n )
@@ -99,11 +118,16 @@ func NewRef(referenceString string) Ref {
99118 return ret
100119}
101120
102- // NewNameRef is similar to NewRef except that it always interprets the string as a single
121+ // NewLiteralRef is similar to NewRef except that it always interprets the string as a literal
103122// attribute name, never as a slash-delimited path expression. There is no escaping or unescaping,
104- // even if the name contains literal '/' or '~' characters. This method always returns a valid Ref
105- // unless the name is empty.
106- func NewNameRef (attrName string ) Ref {
123+ // even if the name contains literal '/' or '~' characters. Since an attribute name can contain
124+ // any characters, this method always returns a valid Ref unless the name is empty.
125+ //
126+ // For example: ldattr.NewLiteralRef("name") is exactly equivalent to ldattr.NewRef("name").
127+ // ldattr.NewLiteralRef("a/b") is exactly equivalent to ldattr.NewRef("a/b") (since the syntax
128+ // used by NewRef treats the whole string as a literal as long as it does not start with a slash),
129+ // or to ldattr.NewRef("/a~1b").
130+ func NewLiteralRef (attrName string ) Ref {
107131 if attrName == "" {
108132 return Ref {err : errAttributeEmpty , rawPath : attrName }
109133 }
@@ -140,13 +164,17 @@ func (a Ref) Equal(other Ref) bool {
140164//
141165// A Ref can only be invalid for the following reasons:
142166//
143- // - The input string was empty, or consisted only of "/".
167+ // 1. The input string was empty, or consisted only of "/".
168+ //
169+ // 2. A slash-delimited string had a double slash causing one component to be empty, such as "/a//b".
144170//
145- // - A slash-delimited string had a double slash causing one component to be empty, such as "/a//b ".
171+ // 3. A slash-delimited string contained a "~" character that was not followed by "0" or "1 ".
146172//
147173// Otherwise, the Ref is valid, but that does not guarantee that such an attribute exists in any
148174// given Context. For instance, NewRef("name") is a valid Ref, but a specific Context might or might
149175// not have a name.
176+ //
177+ // See comments on the Ref type for more details of the attribute reference syntax.
150178func (a Ref ) Err () error {
151179 if a .err == nil && a .rawPath == "" {
152180 return errAttributeEmpty
@@ -237,10 +265,39 @@ func (a *Ref) UnmarshalJSON(data []byte) error {
237265 return nil
238266}
239267
240- func unescapePath (path string ) string {
268+ // Performs unescaping of attribute reference path components:
269+ //
270+ // "~1" becomes "/"
271+ // "~0" becomes "~"
272+ // "~" followed by any character other than "0" or "1" is invalid
273+ //
274+ // The second return value is true if successful, or false if there was an invalid escape sequence.
275+ func unescapePath (path string ) (string , bool ) {
241276 // If there are no tildes then there's definitely nothing to do
242277 if ! strings .Contains (path , "~" ) {
243- return path
278+ return path , true
279+ }
280+ out := make ([]byte , 0 , 100 ) // arbitrary preallocated size - path components will almost always be shorter than this
281+ for i := 0 ; i < len (path ); i ++ {
282+ ch := path [i ]
283+ if ch != '~' {
284+ out = append (out , ch )
285+ continue
286+ }
287+ i ++
288+ if i >= len (path ) {
289+ return "" , false
290+ }
291+ var unescaped byte
292+ switch path [i ] {
293+ case '0' :
294+ unescaped = '~'
295+ case '1' :
296+ unescaped = '/'
297+ default :
298+ return "" , false
299+ }
300+ out = append (out , unescaped )
244301 }
245- return strings . ReplaceAll ( strings . ReplaceAll ( path , "~1" , "/" ), "~0" , "~" )
302+ return string ( out ), true
246303}
0 commit comments