...

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

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

     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  

View as plain text