1 package ldattr 2 3 import ( 4 "encoding/json" 5 "strings" 6 7 "github.com/launchdarkly/go-sdk-common/v3/lderrors" 8 9 "github.com/launchdarkly/go-jsonstream/v3/jreader" 10 ) 11 12 // Ref is an attribute name or path expression identifying a value within a Context. 13 // 14 // This type is mainly intended to be used internally by LaunchDarkly SDK and service code, where 15 // efficiency is a major concern so it's desirable to do any parsing or preprocessing just once. 16 // Applications are unlikely to need to use the Ref type directly. 17 // 18 // It can be used to retrieve a value with Context.GetValueForRef, or to identify an attribute or 19 // nested value that should be considered private with Builder.PrivateRef (the SDK configuration 20 // can also have a list of private attribute references). 21 // 22 // Parsing and validation are done at the time that the [NewRef] or [NewLiteralRef] constructor is called. 23 // If a Ref instance was created from an invalid string, or if it is an uninitialized Ref{}, it is 24 // considered invalid and its [Ref.Err] method will return a non-nil error. 25 // 26 // # Syntax 27 // 28 // The string representation of an attribute reference in LaunchDarkly JSON data uses the following 29 // syntax: 30 // 31 // If the first character is not a slash, the string is interpreted literally as an attribute name. 32 // An attribute name can contain any characters, but must not be empty. 33 // 34 // If the first character is a slash, the string is interpreted as a slash-delimited path where the 35 // first path component is an attribute name, and each subsequent path component is the name of a 36 // property in a JSON object. Any instances of the characters "/" or "~" in a path component are 37 // escaped as "~1" or "~0" respectively. This syntax deliberately resembles JSON Pointer, but no JSON 38 // Pointer behaviors other than those mentioned here are supported. 39 // 40 // # Examples 41 // 42 // Suppose there is a context whose JSON implementation looks like this: 43 // 44 // { 45 // "kind": "user", 46 // "key": "value1", 47 // "address": { 48 // "street": { 49 // "line1": "value2", 50 // "line2": "value3" 51 // }, 52 // "city": "value4" 53 // }, 54 // "good/bad": "value5" 55 // } 56 // 57 // The attribute references "key" and "/key" would both point to "value1". 58 // 59 // The attribute reference "/address/street/line1" would point to "value2". 60 // 61 // The attribute references "good/bad" and "/good~1bad" would both point to "value5". 62 type Ref struct { 63 err error 64 rawPath string 65 singlePathComponent string 66 components []string 67 } 68 69 // NewRef creates a Ref from a string. For the supported syntax and examples, see [Ref]. 70 // 71 // This constructor always returns a Ref that preserves the original string, even if validation fails, 72 // so that calling [Ref.String] (or serializing the Ref to JSON) will produce the original string. If 73 // validation fails, [Ref.Err] will return a non-nil error and any SDK method that takes this Ref as a 74 // parameter will consider it invalid. 75 func NewRef(referenceString string) Ref { 76 if referenceString == "" || referenceString == "/" { 77 return Ref{err: lderrors.ErrAttributeEmpty{}, rawPath: referenceString} 78 } 79 if referenceString[0] != '/' { 80 // When there is no leading slash, this is a simple attribute reference with no character escaping. 81 return Ref{singlePathComponent: referenceString, rawPath: referenceString} 82 } 83 path := referenceString[1:] 84 if !strings.Contains(path, "/") { 85 // There's only one segment, so this is still a simple attribute reference. However, we still may 86 // need to unescape special characters. 87 if unescaped, ok := unescapePath(path); ok { 88 return Ref{singlePathComponent: unescaped, rawPath: referenceString} 89 } 90 return Ref{err: lderrors.ErrAttributeInvalidEscape{}, rawPath: referenceString} 91 } 92 parts := strings.Split(path, "/") 93 ret := Ref{rawPath: referenceString, components: make([]string, 0, len(parts))} 94 for _, p := range parts { 95 if p == "" { 96 ret.err = lderrors.ErrAttributeExtraSlash{} 97 return ret 98 } 99 unescaped, ok := unescapePath(p) 100 if !ok { 101 return Ref{err: lderrors.ErrAttributeInvalidEscape{}, rawPath: referenceString} 102 } 103 ret.components = append(ret.components, unescaped) 104 } 105 return ret 106 } 107 108 // NewLiteralRef is similar to [NewRef] except that it always interprets the string as a literal 109 // attribute name, never as a slash-delimited path expression. There is no escaping or unescaping, 110 // even if the name contains literal '/' or '~' characters. Since an attribute name can contain 111 // any characters, this method always returns a valid Ref unless the name is empty. 112 // 113 // For example: ldattr.NewLiteralRef("name") is exactly equivalent to ldattr.NewRef("name"). 114 // ldattr.NewLiteralRef("a/b") is exactly equivalent to ldattr.NewRef("a/b") (since the syntax 115 // used by NewRef treats the whole string as a literal as long as it does not start with a slash), 116 // or to ldattr.NewRef("/a~1b"). 117 func NewLiteralRef(attrName string) Ref { 118 if attrName == "" { 119 return Ref{err: lderrors.ErrAttributeEmpty{}, rawPath: attrName} 120 } 121 if attrName[0] != '/' { 122 // When there is no leading slash, this is a simple attribute reference with no character escaping. 123 return Ref{singlePathComponent: attrName, rawPath: attrName} 124 } 125 // If there is a leading slash, then the attribute name actually starts with a slash. To represent it 126 // as an Ref, it'll need to be escaped. 127 escapedPath := "/" + strings.ReplaceAll(strings.ReplaceAll(attrName, "~", "~0"), "/", "~1") 128 return Ref{singlePathComponent: attrName, rawPath: escapedPath} 129 } 130 131 // IsDefined returns true if the Ref has a value, meaning that it is not an uninitialized Ref{}. 132 // That does not guarantee that the value is valid; use [Ref.Err] to test that. 133 func (a Ref) IsDefined() bool { 134 return a.rawPath != "" || a.err != nil 135 } 136 137 // Equal returns true if the two Ref instances have the same value. 138 // 139 // You cannot compare Ref instances with the == operator, because the struct may contain a slice; 140 // [reflect.DeepEqual] will work, but is less efficient. 141 func (a Ref) Equal(other Ref) bool { 142 if a.err != other.err || a.rawPath != other.rawPath || a.singlePathComponent != other.singlePathComponent { 143 return false 144 } 145 return true 146 // We don't need to check the components slice, because it's impossible for the components to be different 147 // if rawPath is the same. 148 } 149 150 // Err returns nil for a valid Ref, or a non-nil error value for an invalid Ref. 151 // 152 // A Ref is invalid if the input string is empty, or starts with a slash but is not a valid 153 // slash-delimited path, or starts with a slash and contains an invalid escape sequence. For a list of 154 // the possible validation errors, see the [lderrors] package. 155 // 156 // Otherwise, the Ref is valid, but that does not guarantee that such an attribute exists in any 157 // given Context. For instance, NewRef("name") is a valid Ref, but a specific Context might or might 158 // not have a name. 159 // 160 // See comments on the Ref type for more details of the attribute reference syntax. 161 func (a Ref) Err() error { 162 if a.err == nil && a.rawPath == "" { 163 return lderrors.ErrAttributeEmpty{} 164 } 165 return a.err 166 } 167 168 // Depth returns the number of path components in the Ref. 169 // 170 // For a simple attribute reference such as "name" with no leading slash, this returns 1. 171 // 172 // For an attribute reference with a leading slash, it is the number of slash-delimited path 173 // components after the initial slash. For instance, NewRef("/a/b").Depth() returns 2. 174 func (a Ref) Depth() int { 175 if a.err != nil || (a.singlePathComponent == "" && a.components == nil) { 176 return 0 177 } 178 if a.components == nil { 179 return 1 180 } 181 return len(a.components) 182 } 183 184 // Component retrieves a single path component from the attribute reference. 185 // 186 // For a simple attribute reference such as "name" with no leading slash, if index is zero, 187 // Component returns the attribute name. 188 // 189 // For an attribute reference with a leading slash, if index is non-negative and less than 190 // a.Depth(), Component returns the path component. 191 // 192 // If index is out of range, it returns "". 193 // 194 // NewRef("a").Component(0) // returns "a" 195 // NewRef("/a/b").Component(1) // returns "b" 196 func (a Ref) Component(index int) string { 197 if index == 0 && len(a.components) == 0 { 198 return a.singlePathComponent 199 } 200 if index < 0 || index >= len(a.components) { 201 return "" 202 } 203 return a.components[index] 204 } 205 206 // String returns the attribute reference as a string, in the same format used by NewRef(). 207 // If the Ref was created with [NewRef], this value is identical to the original string. If it 208 // was created with [NewLiteralRef], the value may be different due to unescaping (for instance, 209 // an attribute whose name is "/a" would be represented as "~1a". 210 func (a Ref) String() string { 211 return a.rawPath 212 } 213 214 // MarshalJSON produces a JSON representation of the Ref. If it is an uninitialized Ref{}, this 215 // is a JSON null token. Otherwise, it is a JSON string using the same value returned by [Ref.String]. 216 func (a Ref) MarshalJSON() ([]byte, error) { 217 if !a.IsDefined() { 218 return []byte(`null`), nil 219 } 220 return json.Marshal(a.String()) 221 } 222 223 // UnmarshalJSON parses a Ref from a JSON value. If the value is null, the result is an 224 // uninitialized Ref(). If the value is a string, it is passed to [NewRef]. Any other type 225 // causes an error. 226 // 227 // A valid JSON string that is not valid as a Ref path (such as "" or "///") does not cause 228 // UnmarshalJSON to return an error; instead, it stores the string in the Ref and the error 229 // can be obtained from [Ref.Err]. This is deliberate, so that the LaunchDarkly SDK will be 230 // able to parse a set of feature flag data even if one of the flags contains an invalid Ref. 231 func (a *Ref) UnmarshalJSON(data []byte) error { 232 r := jreader.NewReader(data) 233 s, nonNull := r.StringOrNull() 234 if err := r.Error(); err != nil { 235 return err 236 } 237 if nonNull { 238 *a = NewRef(s) 239 } else { 240 *a = Ref{} 241 } 242 return nil 243 } 244 245 // Performs unescaping of attribute reference path components: 246 // 247 // - "~1" becomes "/" 248 // - "~0" becomes "~" 249 // - "~" followed by any character other than "0" or "1" is invalid 250 // 251 // The second return value is true if successful, or false if there was an invalid escape sequence. 252 func unescapePath(path string) (string, bool) { 253 // If there are no tildes then there's definitely nothing to do 254 if !strings.Contains(path, "~") { 255 return path, true 256 } 257 out := make([]byte, 0, 100) // arbitrary preallocated size - path components will almost always be shorter than this 258 for i := 0; i < len(path); i++ { 259 ch := path[i] 260 if ch != '~' { 261 out = append(out, ch) 262 continue 263 } 264 i++ 265 if i >= len(path) { 266 return "", false 267 } 268 var unescaped byte 269 switch path[i] { 270 case '0': 271 unescaped = '~' 272 case '1': 273 unescaped = '/' 274 default: 275 return "", false 276 } 277 out = append(out, unescaped) 278 } 279 return string(out), true 280 } 281