...

Source file src/github.com/launchdarkly/go-sdk-common/v3/ldcontext/builder_simple.go

Documentation: github.com/launchdarkly/go-sdk-common/v3/ldcontext

     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  

View as plain text