1 package ldcontext 2 3 import ( 4 "fmt" 5 "strings" 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 // Builder is a mutable object that uses the builder pattern to specify properties for a Context. 15 // 16 // Use this type if you need to construct a [Context] that has only a single [Context.Kind]. To 17 // define a multi-context, use [MultiBuilder] instead. 18 // 19 // Obtain an instance of Builder by calling [NewBuilder]. Then, call setter methods such as 20 // [Builder.Kind] or [Builder.Name] to specify any additional attributes; all of the Builder 21 // setters return a reference to the same builder, so they can be chained together. Then, call 22 // [Builder.Build] to produce the immutable [Context]. 23 // 24 // context := ldcontext.NewBuilder("user-key"). 25 // Name("my-name"). 26 // SetString("country", "us"). 27 // Build() 28 // 29 // A Builder should not be accessed by multiple goroutines at once. Once you have called 30 // [Builder.Build], the resulting Context is immutable and is safe to use from multiple 31 // goroutines. 32 // 33 // # Context attributes 34 // 35 // There are several built-in attribute names with special meaning in LaunchDarkly, and 36 // restrictions on the type of their value. These have their own builder methods: see 37 // [Builder.Key], [Builder.Kind], [Builder.Name], and [Builder.Anonymous]. 38 // 39 // You may also set any number of other attributes with whatever names are useful for your 40 // application (subject to validation constraints; see [Builder.SetValue] for rules regarding 41 // attribute names). These attributes can have any data type that is supported in JSON: 42 // boolean, number, string, array, or object. 43 // 44 // # Setting attributes with simple value types 45 // 46 // For convenience, there are setter methods for simple types: 47 // 48 // context := ldcontext.NewBuilder("user-key"). 49 // SetBool("a", true). // this attribute has a boolean value 50 // SetString("b", "xyz"). // this attribute has a string value 51 // SetInt("c", 3). // this attribute has an integer numeric value 52 // SetFloat64("d", 4.5). // this attribute has a floating-point numeric value 53 // Build() 54 // 55 // # Setting attributes with complex value types 56 // 57 // JSON arrays and objects are represented by the [ldvalue.Value] type. The [Builder.SetValue] 58 // method takes a value of this type. 59 // 60 // The [ldvalue] package provides several ways to construct such values. Here are some examples; 61 // for more information, see [ldvalue.Value]. 62 // 63 // context := ldcontext.NewBuilder("user-key"). 64 // SetValue("arrayAttr1", 65 // ldvalue.ArrayOf(ldvalue.String("a"), ldvalue.String("b"))). 66 // SetValue("arrayAttr2", 67 // ldvalue.CopyArbitraryValue([]string{"a", "b"})). 68 // SetValue("objectAttr1", 69 // ldvalue.ObjectBuild().SetString("color", "green").Build()). 70 // SetValue("objectAttr2", 71 // ldvalue.FromJSONMarshal(MyStructType{Color: "green"})). 72 // Build() 73 // 74 // Arrays and objects have special meanings in LaunchDarkly flag evaluation: 75 // - An array of values means "try to match any of these values to the targeting rule." 76 // - An object allows you to match a property within the object to the targeting rule. For instance, 77 // in the example above, a targeting rule could reference /objectAttr1/color to match the value 78 // "green". Nested property references like /objectAttr1/address/street are allowed if a property 79 // contains another JSON object. 80 // 81 // # Private attributes 82 // 83 // You may designate certain attributes, or values within them, as "private", meaning that their 84 // values are not included in analytics data sent to LaunchDarkly. See [Builder.Private]. 85 // 86 // context := ldcontext.NewBuilder("user-key"). 87 // SetString("email", "test@example.com"). 88 // Private("email"). 89 // Build() 90 type Builder struct { 91 kind Kind 92 key string 93 allowEmptyKey bool 94 name ldvalue.OptionalString 95 attributes ldvalue.ValueMapBuilder 96 anonymous bool 97 privateAttrs []ldattr.Ref 98 privateCopyOnWrite bool 99 } 100 101 // NewBuilder creates a Builder for building a Context, initializing its Key property and 102 // setting Kind to DefaultKind. 103 // 104 // You may use [Builder] methods to set additional attributes and/or change the [Builder.Kind] 105 // before calling [Builder.Build]. If you do not change any values, the defaults for the 106 // [Context] are that its [Builder.Kind] is [DefaultKind] ("user"), its [Builder.Key] is set 107 // to whatever value you passed to [NewBuilder], its [Builder.Anonymous] attribute is false, 108 // and it has no values for any other attributes. 109 // 110 // This method is for building a Context that has only a single Kind. To define a 111 // multi-Context, use [NewMultiBuilder] instead. 112 // 113 // If the key parameter is an empty string, there is no default. A Context must have a 114 // non-empty key, so if you call [Builder.Build] in this state without using [Builder.Key] to 115 // set the key, you will get an invalid Context. 116 // 117 // An empty Builder{} is valid as long as you call [Builder.Key] to set a non-empty key. This 118 // means that in in performance-critical code paths where you want to minimize heap allocations, 119 // if you do not want to allocate a Builder on the heap with NewBuilder, you can declare one 120 // locally instead: 121 // 122 // var b ldcontext.Builder 123 // c := b.Kind("org").Key("my-key").Name("my-name").Build() 124 func NewBuilder(key string) *Builder { 125 b := &Builder{} 126 return b.Key(key) 127 } 128 129 // NewBuilderFromContext creates a Builder whose properties are the same as an existing 130 // single context. 131 // 132 // You may then change the Builder's state in any way and call [Builder.Build] to create 133 // a new independent [Context]. 134 // 135 // If fromContext is a multi-context created with [NewMulti] or [MultiBuilder], this method is 136 // not applicable and returns an uninitialized [Builder]. 137 func NewBuilderFromContext(fromContext Context) *Builder { 138 b := &Builder{} 139 b.copyFrom(fromContext) 140 return b 141 } 142 143 // Build creates a Context from the current Builder properties. 144 // 145 // The [Context] is immutable and will not be affected by any subsequent actions on the [Builder]. 146 // 147 // It is possible to specify invalid attributes for a Builder, such as an empty [Builder.Key]. 148 // Instead of returning two values (Context, error), the Builder always returns a Context and you 149 // can call [Context.Err] to see if it has an error. Using a single-return-value syntax is more 150 // convenient for application code, since in normal usage an application will never build an 151 // invalid Context. If you pass an invalid Context to an SDK method, the SDK will detect this and 152 // will generally log a description of the error. 153 // 154 // You may call [Builder.TryBuild] instead of Build if you prefer to use two-value return semantics, 155 // but the validation behavior is the same for both. 156 func (b *Builder) Build() Context { 157 if b == nil { 158 return Context{} 159 } 160 actualKind, err := validateSingleKind(b.kind) 161 if err != nil { 162 return Context{defined: true, err: err, kind: b.kind} 163 } 164 if b.key == "" && !b.allowEmptyKey { 165 return Context{defined: true, err: lderrors.ErrContextKeyEmpty{}, kind: b.kind} 166 } 167 // We set the kind in the error cases above because that improves error reporting if this 168 // context is used within a multi-context. 169 170 ret := Context{ 171 defined: true, 172 kind: actualKind, 173 key: b.key, 174 name: b.name, 175 anonymous: b.anonymous, 176 } 177 178 ret.fullyQualifiedKey = makeFullyQualifiedKeySingleKind(actualKind, ret.key, true) 179 ret.attributes = b.attributes.Build() 180 if b.privateAttrs != nil { 181 ret.privateAttrs = b.privateAttrs 182 b.privateCopyOnWrite = true 183 // The ___CopyOnWrite fields allow us to avoid the overhead of cloning maps/slices in 184 // the typical case where Builder properties do not get modified after calling Build(). 185 // To guard against concurrent modification if someone does continue to modify the 186 // Builder after calling Build(), we will clone the data later if and only if someone 187 // tries to modify it when ___CopyOnWrite is true. That is safe as long as no one is 188 // trying to modify Builder from two goroutines at once, which (per our documentation) 189 // is not supported anyway. 190 } 191 192 return ret 193 } 194 195 // TryBuild is an alternative to Build that returns any validation errors as a second value. 196 // 197 // As described in [Builder.Build], there are several ways the state of a [Context] could be 198 // invalid. Since in normal usage it is possible to be confident that these will not occur, 199 // the Build method is designed for convenient use within expressions by returning a single 200 // Context value, and any validation problems are contained within that value where they can 201 // be detected by calling [Context.Err]. But, if you prefer to use the two-value pattern 202 // that is common in Go, you can call TryBuild instead: 203 // 204 // c, err := ldcontext.NewBuilder("my-key"). 205 // Name("my-name"). 206 // TryBuild() 207 // if err != nil { 208 // // do whatever is appropriate if building the context failed 209 // } 210 // 211 // The two return values are the same as to 1. the Context that would be returned by Build(), 212 // and 2. the result of calling Err() on that Context. So, the above example is exactly 213 // equivalent to: 214 // 215 // c := ldcontext.NewBuilder("my-key"). 216 // Name("my-name"). 217 // Build() 218 // if c.Err() != nil { 219 // // do whatever is appropriate if building the context failed 220 // } 221 // 222 // Note that unlike some Go methods where the first return value is normally an 223 // uninitialized zero value if the error is non-nil, the Context returned by TryBuild in case 224 // of an error is not completely uninitialized: it does contain the error information as well, 225 // so that if it is mistakenly passed to an SDK method, the SDK can tell what the error was. 226 func (b *Builder) TryBuild() (Context, error) { 227 c := b.Build() 228 return c, c.Err() 229 } 230 231 // Kind sets the Context's kind attribute. 232 // 233 // Every [Context] has a kind. Setting it to an empty string is equivalent to the default kind of 234 // "user". This value is case-sensitive. Validation rules are as follows: 235 // 236 // - It may only contain letters, numbers, and the characters ".", "_", and "-". 237 // - It cannot equal the literal string "kind". 238 // - It cannot equal the literal string "multi" ([MultiKind]). 239 // 240 // If the value is invalid at the time [Builder.Build] is called, you will receive an invalid Context 241 // whose [Context.Err] value will describe the problem. 242 func (b *Builder) Kind(kind Kind) *Builder { 243 if b != nil { 244 if kind == "" { 245 b.kind = DefaultKind 246 } else { 247 b.kind = kind 248 } 249 } 250 return b 251 } 252 253 // Key sets the Context's key attribute. 254 // 255 // Every [Context] has a key, which is always a string. There are no restrictions on its value except 256 // that it cannot be empty. 257 // 258 // The key attribute can be referenced by flag rules, flag target lists, and segments. 259 // 260 // If the key is empty at the time [Builder.Build] is called, you will receive an invalid Context 261 // whose [Context.Err] value will describe the problem. 262 func (b *Builder) Key(key string) *Builder { 263 if b != nil { 264 b.key = key 265 } 266 return b 267 } 268 269 // Used internally when we are deserializing an old-style user from JSON; otherwise an empty key is 270 // never allowed. 271 func (b *Builder) setAllowEmptyKey(value bool) *Builder { 272 if b != nil { 273 b.allowEmptyKey = value 274 } 275 return b 276 } 277 278 // Name sets the Context's name attribute. 279 // 280 // This attribute is optional. It has the following special rules: 281 // - Unlike most other attributes, it is always a string if it is specified. 282 // - The LaunchDarkly dashboard treats this attribute as the preferred display name for contexts. 283 func (b *Builder) Name(name string) *Builder { 284 if b == nil { 285 return b 286 } 287 return b.OptName(ldvalue.NewOptionalString(name)) 288 } 289 290 // OptName sets or clears the Context's name attribute. 291 // 292 // Calling b.OptName(ldvalue.NewOptionalString("x")) is equivalent to b.Name("x"), but since it uses 293 // the OptionalString type, it also allows clearing a previously set name with 294 // b.OptName(ldvalue.OptionalString{}). 295 func (b *Builder) OptName(name ldvalue.OptionalString) *Builder { 296 if b != nil { 297 b.name = name 298 } 299 return b 300 } 301 302 // SetBool sets an attribute to a boolean value. 303 // 304 // For rules regarding attribute names and values, see [Builder.SetValue]. This method is exactly 305 // equivalent to calling b.SetValue(attributeName, ldvalue.Bool(value)). 306 func (b *Builder) SetBool(attributeName string, value bool) *Builder { 307 return b.SetValue(attributeName, ldvalue.Bool(value)) 308 } 309 310 // SetFloat64 sets an attribute to a float64 numeric value. 311 // 312 // For rules regarding attribute names and values, see [Builder.SetValue]. This method is exactly 313 // equivalent to calling b.SetValue(attributeName, ldvalue.Float64(value)). 314 // 315 // Note: the LaunchDarkly model for feature flags and user attributes is based on JSON types, 316 // and JSON does not distinguish between integer and floating-point types. Therefore, 317 // b.SetFloat64(name, float64(1.0)) is exactly equivalent to b.SetInt(name, 1). 318 func (b *Builder) SetFloat64(attributeName string, value float64) *Builder { 319 return b.SetValue(attributeName, ldvalue.Float64(value)) 320 } 321 322 // SetInt sets an attribute to an int numeric value. 323 // 324 // For rules regarding attribute names and values, see [Builder.SetValue]. This method is exactly 325 // equivalent to calling b.SetValue(attributeName, ldvalue.Int(value)). 326 // 327 // Note: the LaunchDarkly model for feature flags and user attributes is based on JSON types, 328 // and JSON does not distinguish between integer and floating-point types. Therefore, 329 // b.SetFloat64(name, float64(1.0)) is exactly equivalent to b.SetInt(name, 1). 330 func (b *Builder) SetInt(attributeName string, value int) *Builder { 331 return b.SetValue(attributeName, ldvalue.Int(value)) 332 } 333 334 // SetString sets an attribute to a string value. 335 // 336 // For rules regarding attribute names and values, see [Builder.SetValue]. This method is exactly 337 // equivalent to calling b.SetValue(attributeName, ldvalue.String(value)). 338 func (b *Builder) SetString(attributeName string, value string) *Builder { 339 return b.SetValue(attributeName, ldvalue.String(value)) 340 } 341 342 // SetValue sets the value of any attribute for the Context. 343 // 344 // This method uses the [ldvalue.Value] type to represent a value of any JSON type: boolean, 345 // number, string, array, or object. The [ldvalue] package provides several ways to construct 346 // values of each type. 347 // 348 // The return value is always the same [Builder], for convenience (to allow method chaining). 349 // 350 // # Allowable attribute names 351 // 352 // The attribute names "kind", "key", "name", and "anonymous" have special meaning in 353 // LaunchDarkly. You may use these names with SetValue, as an alternative to using the 354 // methods [Builder.Kind], [Builder.Key], [Builder.Name], and [Builder.Anonymous]. However, 355 // there are restrictions on the value type: "kind" and "key" must be a string, "name" must 356 // be a string or null, and "anonymous" must be a boolean. Any value of an unsupported type 357 // is ignored (leaving the attribute unchanged). 358 // 359 // The string "_meta" cannot be used as an attribute name. 360 // 361 // All other non-empty strings are valid as an attribute name, and have no special meaning 362 // in LaunchDarkly; their definition is up to you. 363 // 364 // Context metadata such as [Builder.Private], which is not addressable in evaluations, is not 365 // considered an attribute; if you define an attribute of your own with the name "private", 366 // it is simply an attribute like any other, unrelated to the context metadata. 367 // 368 // # Simple value types 369 // 370 // Passing a simple value constructed with [ldvalue.Bool], [ldvalue.Int], [ldvalue.Float64], 371 // or [ldvalue.String], is exactly equivalent to calling one of the typed setter methods 372 // [Builder.SetBool], [Builder.SetInt], [Builder.SetFloat64], or [Builder.SetString]. 373 // 374 // Values of different JSON types are always treated as different values. For instance, the 375 // number 1 is not the same as the string "1". 376 // 377 // The null value, [ldvalue.Null](), is a special case: it is a valid value in JSON, but 378 // LaunchDarkly considers null to be the same as "no such attribute", so setting an 379 // attribute's value to null is the same as removing it. 380 // 381 // # Complex value types 382 // 383 // The ldvalue package provides several ways to construct JSON array or object values. Here 384 // are some examples; for more information, see [ldvalue.Value]. 385 // 386 // context := ldcontext.NewBuilder("user-key"). 387 // SetValue("arrayAttr1", 388 // ldvalue.ArrayOf(ldvalue.String("a"), ldvalue.String("b"))). 389 // SetValue("arrayAttr2", 390 // ldvalue.CopyArbitraryValue([]string{"a", "b"})). 391 // SetValue("objectAttr1", 392 // ldvalue.ObjectBuild().SetString("color", "green").Build()). 393 // SetValue("objectAttr2", 394 // ldvalue.FromJSONMarshal(MyStructType{Color: "green"})). 395 // Build() 396 // 397 // Arrays and objects have special meanings in LaunchDarkly flag evaluation: 398 // - An array of values means "try to match any of these values to the targeting rule." 399 // - An object allows you to match a property within the object to the targeting rule. For instance, 400 // in the example above, a targeting rule could reference /objectAttr1/color to match the value 401 // "green". Nested property references like /objectAttr1/address/street are allowed if a property 402 // contains another JSON object. 403 func (b *Builder) SetValue(attributeName string, value ldvalue.Value) *Builder { 404 _ = b.TrySetValue(attributeName, value) 405 return b 406 } 407 408 // TrySetValue sets the value of any attribute for the Context. 409 // 410 // This is the same as [Builder.SetValue], except that it returns true for success, or false if the 411 // parameters violated one of the restrictions described for SetValue (for instance, 412 // attempting to set "key" to a value that was not a string). 413 func (b *Builder) TrySetValue(attributeName string, value ldvalue.Value) bool { 414 if b == nil || attributeName == "" { 415 return false 416 } 417 switch attributeName { 418 case ldattr.KindAttr: 419 if !value.IsString() { 420 return false 421 } 422 b.Kind(Kind(value.StringValue())) 423 case ldattr.KeyAttr: 424 if !value.IsString() { 425 return false 426 } 427 b.Key(value.StringValue()) 428 case ldattr.NameAttr: 429 if !value.IsString() && !value.IsNull() { 430 return false 431 } 432 b.OptName(value.AsOptionalString()) 433 case ldattr.AnonymousAttr: 434 if !value.IsBool() { 435 return false 436 } 437 b.Anonymous(value.BoolValue()) 438 case jsonPropMeta: 439 return false 440 default: 441 if value.IsNull() { 442 b.attributes.Remove(attributeName) 443 } else { 444 b.attributes.Set(attributeName, value) 445 } 446 return true 447 } 448 return true 449 } 450 451 // Anonymous sets whether the Context is only intended for flag evaluations and should not be indexed by 452 // LaunchDarkly. 453 // 454 // The default value is false. False means that this [Context] represents an entity such as a user that you 455 // want to be able to see on the LaunchDarkly dashboard. 456 // 457 // Setting Anonymous to true excludes this Context from the database that is used by the dashboard. It does 458 // not exclude it from analytics event data, so it is not the same as making attributes private; all 459 // non-private attributes will still be included in events and data export. There is no limitation on what 460 // other attributes may be included (so, for instance, Anonymous does not mean there is no [Builder.Name]). 461 // 462 // This value is also addressable in evaluations as the attribute name "anonymous". It is always treated as 463 // a boolean true or false in evaluations; it cannot be null/undefined. 464 func (b *Builder) Anonymous(value bool) *Builder { 465 if b != nil { 466 b.anonymous = value 467 } 468 return b 469 } 470 471 // Private designates any number of Context attributes, or properties within them, as private: that is, 472 // their values will not be sent to LaunchDarkly in analytics data. 473 // 474 // This action only affects analytics events that involve this particular [Context]. To mark some (or all) 475 // Context attributes as private for all context, use the overall event configuration for the SDK. 476 // 477 // In this example, firstName is marked as private, but lastName is not: 478 // 479 // c := ldcontext.NewBuilder("org", "my-key"). 480 // SetString("firstName", "Pierre"). 481 // SetString("lastName", "Menard"). 482 // Private("firstName"). 483 // Build() 484 // 485 // The attributes "kind", "key", and "anonymous" cannot be made private. 486 // 487 // This is a metadata property, rather than an attribute that can be addressed in evaluations: that is, 488 // a rule clause that references the attribute name "private" will not use this value, but instead will 489 // use whatever value (if any) you have set for that name with a method such as [Builder.SetString]. 490 // 491 // # Designating an entire attribute as private 492 // 493 // If the parameter is an attribute name such as "email" that does not start with a '/' character, the 494 // entire attribute is private. 495 // 496 // # Designating a property within a JSON object as private 497 // 498 // If the parameter starts with a '/' character, it is interpreted as a slash-delimited path to a 499 // property within a JSON object. The first path component is an attribute name, and each following 500 // component is a property name. 501 // 502 // For instance, suppose that the attribute "address" had the following JSON object value: 503 // {"street": {"line1": "abc", "line2": "def"}, "city": "ghi"} 504 // 505 // - Calling either Private("address") or Private("/address") would cause the entire "address" 506 // attribute to be private. 507 // - Calling Private("/address/street") would cause the "street" property to be private, so that 508 // only {"city": "ghi"} is included in analytics. 509 // - Calling Private("/address/street/line2") would cause only "line2" within "street" to be private, 510 // so that {"street": {"line1": "abc"}, "city": "ghi"} is included in analytics. 511 // 512 // This syntax deliberately resembles JSON Pointer, but other JSON Pointer features such as array 513 // indexing are not supported for Private. 514 // 515 // If an attribute's actual name starts with a '/' character, you must use the same escaping syntax as 516 // JSON Pointer: replace "~" with "~0", and "/" with "~1". 517 func (b *Builder) Private(attrRefStrings ...string) *Builder { 518 refs := make([]ldattr.Ref, 0, 20) // arbitrary capacity that's likely greater than needed, to preallocate on stack 519 for _, s := range attrRefStrings { 520 refs = append(refs, ldattr.NewRef(s)) 521 } 522 return b.PrivateRef(refs...) 523 } 524 525 // PrivateRef is equivalent to Private, but uses the ldattr.Ref type. It designates any number of 526 // Context attributes, or properties within them, as private: that is, their values will not be 527 // sent to LaunchDarkly. 528 // 529 // Application code is unlikely to need to use the ldattr.Ref type directly; however, in cases where 530 // you are constructing Contexts constructed repeatedly with the same set of private attributes, if 531 // you are also using complex private attribute path references such as "/address/street", converting 532 // this to an [ldattr.Ref] once and reusing it in many PrivateRef calls is slightly more efficient than 533 // calling [Builder.Private] (since it does not need to parse the path repeatedly). 534 func (b *Builder) PrivateRef(attrRefs ...ldattr.Ref) *Builder { 535 if b == nil { 536 return b 537 } 538 if b.privateAttrs == nil { 539 b.privateAttrs = make([]ldattr.Ref, 0, len(attrRefs)) 540 } else if b.privateCopyOnWrite { 541 // See note in Build() on ___CopyOnWrite 542 b.privateAttrs = slices.Clone(b.privateAttrs) 543 b.privateCopyOnWrite = false 544 } 545 b.privateAttrs = append(b.privateAttrs, attrRefs...) 546 return b 547 } 548 549 // RemovePrivate removes any private attribute references previously added with [Builder.Private] 550 // or [Builder.PrivateRef] that exactly match any of the specified attribute references. 551 func (b *Builder) RemovePrivate(attrRefStrings ...string) *Builder { 552 refs := make([]ldattr.Ref, 0, 20) // arbitrary capacity that's likely greater than needed, to preallocate on stack 553 for _, s := range attrRefStrings { 554 refs = append(refs, ldattr.NewRef(s)) 555 } 556 return b.RemovePrivateRef(refs...) 557 } 558 559 // RemovePrivateRef removes any private attribute references previously added with [Builder.Private] 560 // or [Builder.PrivateRef] that exactly match that of any of the specified attribute references. 561 // 562 // Application code is unlikely to need to use the [ldattr.Ref] type directly, and can use 563 // RemovePrivate with a string parameter to accomplish the same thing. This method is mainly for 564 // use by internal LaunchDarkly SDK and service code which uses ldattr.Ref. 565 func (b *Builder) RemovePrivateRef(attrRefs ...ldattr.Ref) *Builder { 566 if b == nil { 567 return b 568 } 569 if b.privateCopyOnWrite { 570 // See note in Build() on ___CopyOnWrite 571 b.privateAttrs = slices.Clone(b.privateAttrs) 572 b.privateCopyOnWrite = false 573 } 574 for _, attrRefToRemove := range attrRefs { 575 for i := 0; i < len(b.privateAttrs); i++ { 576 if b.privateAttrs[i].String() == attrRefToRemove.String() { 577 b.privateAttrs = append(b.privateAttrs[0:i], b.privateAttrs[i+1:]...) 578 i-- 579 } 580 } 581 } 582 return b 583 } 584 585 func (b *Builder) copyFrom(fromContext Context) { 586 if fromContext.Multiple() || b == nil { 587 return 588 } 589 b.kind = fromContext.kind 590 b.key = fromContext.key 591 b.name = fromContext.name 592 b.anonymous = fromContext.anonymous 593 b.attributes = ldvalue.ValueMapBuilder{} 594 b.attributes.SetAllFromValueMap(fromContext.attributes) 595 b.privateAttrs = fromContext.privateAttrs 596 b.privateCopyOnWrite = true 597 } 598 599 func makeFullyQualifiedKeySingleKind(kind Kind, key string, omitDefaultKind bool) string { 600 // Per the users-to-contexts specification, the fully-qualified key for a single context is: 601 // - equal to the regular "key" property, if the kind is "user" (a.k.a. DefaultKind) 602 // - or, for any other kind, it's the kind plus ":" plus the result of partially URL-encoding the 603 // "key" property ("partially URL-encoding" here means that ':' and '%' are percent-escaped; other 604 // URL-encoding behaviors are inconsistent across platforms, so we do not use a library function). 605 if omitDefaultKind && kind == DefaultKind { 606 return key 607 } 608 escapedKey := strings.ReplaceAll(strings.ReplaceAll(key, "%", "%25"), ":", "%3A") 609 return fmt.Sprintf("%s:%s", kind, escapedKey) 610 } 611