1 package ldcontext 2 3 import ( 4 "encoding/json" 5 "sort" 6 7 "github.com/launchdarkly/go-sdk-common/v3/ldattr" 8 "github.com/launchdarkly/go-sdk-common/v3/lderrors" 9 "github.com/launchdarkly/go-sdk-common/v3/ldvalue" 10 11 "golang.org/x/exp/slices" 12 ) 13 14 // Context is a collection of attributes that can be referenced in flag evaluations and analytics events. 15 // 16 // To create a Context of a single kind, such as a user, you may use the [New] or [NewWithKind] 17 // constructors. Or, to specify other attributes, use [NewBuilder]. See the [Builder] type for more 18 // information about how to set attributes. 19 // 20 // To create a multi-context, use [NewMultiBuilder]. 21 // 22 // An uninitialized Context struct is not valid for use in any SDK operations. Also, a Context can 23 // be in an error state if it was built with invalid attributes. See [Context.Err]. 24 // 25 // To learn more, read: https://docs.launchdarkly.com/home/contexts 26 type Context struct { 27 defined bool 28 err error 29 kind Kind 30 multiContexts []Context 31 key string 32 fullyQualifiedKey string 33 name ldvalue.OptionalString 34 attributes ldvalue.ValueMap 35 secondary ldvalue.OptionalString 36 anonymous bool 37 privateAttrs []ldattr.Ref 38 39 // Note that the secondary field cannot be set by any builder method. We support this 40 // meta-attribute internally in order to be able to evaluate flags for old-style users, 41 // and the only way it can be set is from the user JSON unmarshaling logic. 42 } 43 44 // IsDefined returns true if this is a Context that was created with a constructor or builder 45 // (regardless of whether its properties are valid), or false if it is an empty uninitialized 46 // Context{}. 47 func (c Context) IsDefined() bool { 48 return c.defined 49 } 50 51 // Err returns nil for a valid Context, or a non-nil error value for an invalid Context. 52 // 53 // A valid [Context] is one that can be used in SDK operations. An invalid Context is one that is 54 // missing necessary attributes or has invalid attributes, indicating an incorrect usage of the 55 // SDK API. For a complete list of the ways a Context can be invalid, see the [lderrors] package. 56 // 57 // Since in normal usage it is easy for applications to be sure they are using context kinds 58 // correctly (so that having to constantly check error return values would be needlessly 59 // inconvenient), and because some states such as the empty value are impossible to prevent in the 60 // Go language, the SDK stores the error state in the Context itself and checks for such errors 61 // at the time the Context is used, such as in a flag evaluation. At that point, if the Context is 62 // invalid, the operation will fail in some well-defined way as described in the documentation for 63 // that method, and the SDK will generally log a warning as well. But in any situation where you 64 // are not sure if you have a valid Context, you can call Err() to check. 65 func (c Context) Err() error { 66 if !c.defined && c.err == nil { 67 return lderrors.ErrContextUninitialized{} 68 } 69 return c.err 70 } 71 72 // Kind returns the Context's kind attribute. 73 // 74 // Every valid Context has a non-empty kind. For multi-contexts, this value is [MultiKind] and the 75 // kinds within the Context can be inspected with [Context.IndividualContextCount], 76 // [Context.IndividualContextByIndex], [Context.IndividualContextByKind], or 77 // [Context.GetAllIndividualContexts]. 78 // 79 // For rules regarding the kind value, see [Builder.Kind]. 80 func (c Context) Kind() Kind { 81 return c.kind 82 } 83 84 // Multiple returns true for a multi-context, or false for a single context. 85 // 86 // If this value is true, then [Context.Kind] is guaranteed to return [MultiKind], and you can 87 // inspect the individual Contexts for each kind by calling [Context.IndividualContextCount], 88 // [Context.IndividualContextByIndex], [Context.IndividualContextByKind], or 89 // [Context.GetAllIndividualContexts]. 90 // 91 // If this value is false, then [Context.Kind] is guaranteed to return a value that is not [MultiKind], 92 // and [Context.IndividualContextCount] is guaranteed to return 1. 93 func (c Context) Multiple() bool { 94 return len(c.multiContexts) != 0 95 } 96 97 // Key returns the Context's key attribute. 98 // 99 // For a single context, this value is set by the [Context] constructors or the [Builder] methods. 100 // 101 // For a multi-context, there is no single value, so Key() returns an empty name; use 102 // [Context.IndividualContextByIndex], [Context.IndividualContextByKind], or 103 // [Context.GetAllIndividualContexts] to get the Context for a particular kind and then call 104 // Key() on it. 105 func (c Context) Key() string { 106 return c.key 107 } 108 109 // FullyQualifiedKey returns a string that describes the entire Context based on Kind and Key values. 110 // 111 // This value is used whenever LaunchDarkly needs a string identifier based on all of the Kind and 112 // Key values in the context; the SDK may use this for caching previously seen contexts, for instance. 113 func (c Context) FullyQualifiedKey() string { 114 return c.fullyQualifiedKey 115 } 116 117 // Name returns the Context's optional name attribute. 118 // 119 // For a single context, this value is set by [Builder.Name] or [Builder.OptName]. If no value was 120 // specified, it returns the empty value [ldvalue.OptionalString]{}. The name attribute is treated 121 // differently from other user attributes in that its value, if specified, can only be a string, and 122 // it is used as the display name for the Context on the LaunchDarkly dashboard. 123 // 124 // For a multi-context, there is no single value, so Name() returns an empty string; use 125 // [Context.IndividualContextByIndex], [Context.IndividualContextByKind], or 126 // [Context.GetAllIndividualContexts] to get the Context for a particular kind and then call 127 // Name() on it. 128 func (c Context) Name() ldvalue.OptionalString { 129 return c.name 130 } 131 132 // GetOptionalAttributeNames returns a slice containing the names of all regular optional attributes defined 133 // on this Context. These do not include the mandatory Kind and Key, or the metadata attributes Secondary, 134 // Anonymous, and Private. 135 // 136 // If a non-nil slice is passed in, it will be reused to hold the return values if it has enough capacity. 137 // For instance, in the following example, no heap allocations will happen unless there are more than 10 138 // optional attribute names; if there are more than 10, the slice will be allocated on the stack: 139 // 140 // preallocNames := make([]string, 0, 10) 141 // names := c.GetOptionalAttributeNames(preallocNames) 142 func (c Context) GetOptionalAttributeNames(sliceIn []string) []string { 143 if c.Multiple() { 144 return nil 145 } 146 ret := c.attributes.Keys(sliceIn) 147 if c.name.IsDefined() { 148 ret = append(ret, ldattr.NameAttr) 149 } 150 return ret 151 } 152 153 // GetValue looks up the value of any attribute of the Context by name. 154 // 155 // This includes only attributes that are addressable in evaluations, not metadata such as 156 // [Context.PrivateAttributeByIndex]. 157 // 158 // For a single context, the attribute name can be any custom attribute that was set by methods 159 // like [Builder.SetString]. It can also be one of the built-in ones like "kind", "key", or "name"; in 160 // such cases, it is equivalent to calling [Context.Kind], [Context.Key], or [Context.Name], except that 161 // the value is returned using the general-purpose [ldvalue.Value] type. 162 // 163 // For a multi-context, the only supported attribute name is "kind". Use 164 // [Context.IndividualContextByIndex], [Context.IndividualContextByKind], or 165 // [Context.GetAllIndividualContexts] to get the Context for a particular kind and then get its attributes. 166 // 167 // This method does not support complex expressions for getting individual values out of JSON objects 168 // or arrays, such as "/address/street". Use [Context.GetValueForRef] for that purpose. 169 // 170 // If the value is found, the return value is the attribute value, using the type [ldvalue.Value] to 171 // represent a value of any JSON type. 172 // 173 // If there is no such attribute, the return value is [ldvalue.Null](). An attribute that actually 174 // exists cannot have a null value. 175 func (c Context) GetValue(attrName string) ldvalue.Value { 176 return c.GetValueForRef(ldattr.NewLiteralRef(attrName)) 177 } 178 179 // GetValueForRef looks up the value of any attribute of the Context, or a value contained within an 180 // attribute, based on an [ldattr.Ref]. 181 // 182 // This includes only attributes that are addressable in evaluations, not metadata such as 183 // [Context.PrivateAttributeByIndex]. 184 // 185 // This implements the same behavior that the SDK uses to resolve attribute references during a flag 186 // evaluation. In a single context, the [ldattr.Ref] can represent a simple attribute name-- either a 187 // built-in one like "name" or "key", or a custom attribute that was set by methods like 188 // [Builder.SetString]-- or, it can be a slash-delimited path using a JSON-Pointer-like syntax. See 189 // [ldattr.Ref] for more details. 190 // 191 // For a multi-context, the only supported attribute name is "kind". Use 192 // [Context.IndividualContextByIndex], [Context.IndividualContextByKind], or 193 // [Context.GetAllIndividualContexts] to get the Context for a particular kind and then get its attributes. 194 // 195 // If the value is found, the return value is the attribute value, using the type [ldvalue.Value] to 196 // represent a value of any JSON type). 197 // 198 // If there is no such attribute, or if the [ldattr.Ref] is invalid, the return value is [ldvalue.Null](). 199 // An attribute that actually exists cannot have a null value. 200 func (c Context) GetValueForRef(ref ldattr.Ref) ldvalue.Value { 201 if ref.Err() != nil { 202 return ldvalue.Null() 203 } 204 205 firstPathComponent := ref.Component(0) 206 207 if c.Multiple() { 208 if ref.Depth() == 1 && firstPathComponent == ldattr.KindAttr { 209 return ldvalue.String(string(c.kind)) 210 } 211 return ldvalue.Null() // multi-context has no other addressable attributes 212 } 213 214 // Look up attribute in single context 215 value, ok := c.getTopLevelAddressableAttributeSingleKind(firstPathComponent) 216 if !ok { 217 return ldvalue.Null() 218 } 219 for i := 1; i < ref.Depth(); i++ { 220 name := ref.Component(i) 221 if value.Type() == ldvalue.RawType { 222 // The "raw" type in ldvalue.Value is for unparsed JSON data, but we do need to parse it if 223 // we're going to look for a property within it. 224 value = ldvalue.Parse(value.AsRaw()) 225 } 226 value = value.GetByKey(name) 227 // The defined behavior of GetByKey is that it sets value to ldvalue.Null() if the key was not 228 // found, or if the value was not an object. 229 } 230 return value 231 } 232 233 // Anonymous returns true if this Context is only intended for flag evaluations and will not be indexed by 234 // LaunchDarkly. 235 // 236 // For a single context, this value can be set by [Builder.Anonymous], and is false if not specified. 237 // 238 // For a multi-context, there is no single value, so Anonymous() always returns false; use 239 // [Context.IndividualContextByIndex], [Context.IndividualContextByKind], or 240 // [Context.GetAllIndividualContexts] to get the Context for a particular kind and then call 241 // Anonymous() on it. 242 func (c Context) Anonymous() bool { 243 return c.anonymous 244 } 245 246 // Secondary returns the deprecated secondary key meta-attribute for the Context, if any. 247 // 248 // This corresponds to the "secondary" attribute in the older LaunchDarkly user schema. This attribute 249 // is no longer supported for flag evaluations with the LaunchDarkly Go SDK, and cannot be set via the 250 // context builder. This method only exists to allow other LaunchDarkly code to detect the presence of 251 // the attribute in JSON data produced by older LaunchDarkly SDKs. 252 // 253 // Deprecated: this method will be removed in the future and application code should not rely on it. 254 func (c Context) Secondary() ldvalue.OptionalString { 255 return c.secondary 256 } 257 258 // PrivateAttributeCount returns the number of attributes that were marked as private for this Context 259 // with [Builder.Private] or [Builder.PrivateRef]. 260 func (c Context) PrivateAttributeCount() int { 261 return len(c.privateAttrs) 262 } 263 264 // PrivateAttributeByIndex returns one of the attributes that were marked as private for thie Context 265 // with [Builder.Private] or [Builder.PrivateRef]. 266 func (c Context) PrivateAttributeByIndex(index int) (ldattr.Ref, bool) { 267 if index < 0 || index >= len(c.privateAttrs) { 268 return ldattr.Ref{}, false 269 } 270 return c.privateAttrs[index], true 271 } 272 273 // IndividualContextCount returns the number of Kinds in the context. 274 // 275 // For a single context, the return value is always 1. For a multi-context, it is the number of 276 // individual contexts within. For an invalid context, it is zero. 277 func (c Context) IndividualContextCount() int { 278 if n := len(c.multiContexts); n != 0 { 279 return n 280 } 281 return 1 282 } 283 284 // IndividualContextByIndex returns the single context corresponding to one of the Kinds in 285 // this context. If the method is called on a single context, then the only allowable value 286 // for index is zero, and the return value on success is the same context. If the method is called 287 // on a multi-context, then index must be >= zero and < the number of kinds, and the return 288 // value on success is one of the individual contexts within. 289 // 290 // If the index is out of range, then the return value is an uninitialized Context{}. You can 291 // detect this condition because [Context.IsDefined] will return false. 292 // 293 // In a multi-context, the ordering of the individual contexts is not guaranteed to be the 294 // same order that was passed into the builder or constructor. 295 func (c Context) IndividualContextByIndex(index int) Context { 296 if n := len(c.multiContexts); n != 0 { 297 if index < 0 || index >= n { 298 return Context{} 299 } 300 return c.multiContexts[index] 301 } 302 if index != 0 { 303 return Context{} 304 } 305 return c 306 } 307 308 // IndividualContextByKind returns the single context, if any, whose Kind matches the 309 // specified value exactly. If the method is called on a single context, then the specified 310 // Kind must match the kind of that context. If the method is called on a multi-context, 311 // then the Kind can match any of the individual contexts within. 312 // 313 // If the kind parameter is an empty string, [DefaultKind] is used instead. 314 // 315 // If no matching Kind is found, then the return value is an uninitialized Context{}. You can 316 // detect this condition because [Context.IsDefined] will return false. 317 func (c Context) IndividualContextByKind(kind Kind) Context { 318 if kind == "" { 319 kind = DefaultKind 320 } 321 if len(c.multiContexts) == 0 { 322 if c.kind == kind { 323 return c 324 } 325 } else { 326 for _, mc := range c.multiContexts { 327 if mc.kind == kind { 328 return mc 329 } 330 } 331 } 332 return Context{} 333 } 334 335 // IndividualContextKeyByKind returns the Key of the single context, if any, whose Kind 336 // matches the specified value exactly. If the method is called on a single context, then 337 // the specified Kind must match the Kind of that context. If the method is called on a 338 // multi-context, then the Kind can match any of the individual contexts within. 339 // 340 // If the kind parameter is an empty string, [DefaultKind] is used instead. 341 // 342 // If no matching Kind is found, the return value is an empty string. 343 // 344 // This method is equivalent to calling [Context.IndividualContextByKind] and then Key, but 345 // is slightly more efficient (since it does not require copying an entire Context struct by 346 // value). 347 func (c Context) IndividualContextKeyByKind(kind Kind) string { 348 if kind == "" { 349 kind = DefaultKind 350 } 351 if len(c.multiContexts) == 0 { 352 if c.kind == kind { 353 return c.key 354 } 355 } else { 356 for _, mc := range c.multiContexts { 357 if mc.kind == kind { 358 return mc.key 359 } 360 } 361 } 362 return "" 363 } 364 365 // GetAllIndividualContexts converts this context to a slice of individual contexts. If the method 366 // is called on a single context, then the resulting slice has exactly one element, which 367 // is the same context. If the method is called on a multi-context, then the resulting slice 368 // contains each individual context within. 369 // 370 // If a non-nil slice is passed in, it will be reused to hold the return values if it has enough 371 // capacity. For instance, in the following example, no heap allocations will happen unless there 372 // are more than 10 individual contexts; if there are more than 10, the slice will be allocated on 373 // the stack: 374 // 375 // preallocContexts := make([]ldcontext.Context, 0, 10) 376 // contexts := c.GetAllIndividualContexts(preallocContexts) 377 func (c Context) GetAllIndividualContexts(sliceIn []Context) []Context { 378 ret := sliceIn[0:0] 379 if len(c.multiContexts) == 0 { 380 return append(ret, c) 381 } 382 return append(ret, c.multiContexts...) 383 } 384 385 // String returns a string representation of the Context. 386 // 387 // This is currently defined as being the same as the JSON representation, since that is the simplest 388 // way to represent all of the Context properties. However, Go's [fmt.Stringer] interface is deliberately 389 // nonspecific about what format a type may use for its string representation, and application code 390 // should not rely on String() always being the same as the JSON representation. If you specifically 391 // want the latter, use [Context.JSONString] or [json.Marshal]. However, if you do use String() for 392 // convenience in debugging or logging, you should assume that the output may contain any and all 393 // properties of the Context, so if there is anything you do not want to be visible, you should write 394 // your own formatting logic. 395 func (c Context) String() string { 396 data, _ := json.Marshal(c) 397 return string(data) 398 } 399 400 func (c Context) getTopLevelAddressableAttributeSingleKind(name string) (ldvalue.Value, bool) { 401 switch name { 402 case ldattr.KindAttr: 403 return ldvalue.String(string(c.kind)), true 404 case ldattr.KeyAttr: 405 return ldvalue.String(c.key), true 406 case ldattr.NameAttr: 407 return c.name.AsValue(), c.name.IsDefined() 408 case ldattr.AnonymousAttr: 409 return ldvalue.Bool(c.anonymous), true 410 default: 411 return c.attributes.TryGet(name) 412 } 413 } 414 415 // Equal tests whether two contexts are logically equal. 416 // 417 // Two single contexts are logically equal if they have the same attribute names and values. 418 // Two multi-contexts are logically equal if they contain the same kinds (in any order) and 419 // the individual contexts are equal. A single context is never equal to a multi-context. 420 func (c Context) Equal(other Context) bool { 421 if !c.defined || !other.defined { 422 return c.defined == other.defined 423 } 424 425 if c.kind != other.kind { 426 return false 427 } 428 429 if c.Multiple() { 430 if len(c.multiContexts) != len(other.multiContexts) { 431 return false 432 } 433 for _, mc1 := range c.multiContexts { 434 if mc2 := other.IndividualContextByKind(mc1.kind); !mc1.Equal(mc2) { 435 return false 436 } 437 } 438 return true 439 } 440 441 if c.key != other.key || 442 c.name != other.name || 443 c.anonymous != other.anonymous || 444 c.secondary != other.secondary { 445 return false 446 } 447 if !c.attributes.Equal(other.attributes) { 448 return false 449 } 450 if len(c.privateAttrs) != len(other.privateAttrs) { 451 return false 452 } 453 sortedPrivateAttrs := func(attrs []ldattr.Ref) []string { 454 ret := make([]string, 0, len(attrs)) 455 for _, a := range attrs { 456 ret = append(ret, a.String()) 457 } 458 sort.Strings(ret) 459 return ret 460 } 461 attrs1, attrs2 := sortedPrivateAttrs(c.privateAttrs), sortedPrivateAttrs(other.privateAttrs) 462 return slices.Equal(attrs1, attrs2) 463 } 464