...

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

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

     1  package ldcontext
     2  
     3  import (
     4  	"sort"
     5  
     6  	"github.com/launchdarkly/go-sdk-common/v3/lderrors"
     7  
     8  	"golang.org/x/exp/slices"
     9  )
    10  
    11  const defaultMultiBuilderCapacity = 3 // arbitrary value based on presumed likely use cases
    12  
    13  // MultiBuilder is a mutable object that uses the builder pattern to create a multi-context,
    14  // as an alternative to [NewMulti].
    15  //
    16  // Use this type if you need to construct a Context that has multiple Kind values, each with its
    17  // own nested [Context]. To define a single context, use [Builder] instead.
    18  //
    19  // Obtain an instance of MultiBuilder by calling [NewMultiBuilder]; then, call [MultiBuilder.Add] to
    20  // specify the nested [Context] for each Kind. Finally, call [MultiBuilder.Build]. MultiBuilder
    21  // setters return a reference to the same builder, so they can be chained together:
    22  //
    23  //	context := ldcontext.NewMultiBuilder().
    24  //		Add(ldcontext.New("my-user-key")).
    25  //		Add(ldcontext.NewBuilder("my-org-key").Kind("organization").Name("Org1").Build()).
    26  //		Build()
    27  //
    28  // A MultiBuilder should not be accessed by multiple goroutines at once. Once you have called
    29  // [MultiBuilder.Build], the resulting Context is immutable and safe to use from multiple
    30  // goroutines.
    31  type MultiBuilder struct {
    32  	contexts            []Context
    33  	contextsCopyOnWrite bool
    34  }
    35  
    36  // NewMultiBuilder creates a MultiBuilder for building a multi-context.
    37  //
    38  // This method is for building a [Context] that has multiple [Context.Kind] values, each with its
    39  // own nested Context. To define a single context, use [NewBuilder] instead.
    40  func NewMultiBuilder() *MultiBuilder {
    41  	return &MultiBuilder{contexts: make([]Context, 0, defaultMultiBuilderCapacity)}
    42  }
    43  
    44  // Build creates a Context from the current MultiBuilder properties.
    45  //
    46  // The [Context] is immutable and will not be affected by any subsequent actions on the MultiBuilder.
    47  //
    48  // It is possible for a MultiBuilder to represent an invalid state. Instead of returning two
    49  // values (Context, error), the Builder always returns a Context and you can call Context.Err()
    50  // to see if it has an error. See [Context.Err] for more information about invalid Context
    51  // conditions. Using a single-return-value syntax is more convenient for application code, since
    52  // in normal usage an application will never build an invalid Context.
    53  //
    54  // If only one context was added to the builder, Build returns that same context, rather than a
    55  // multi-context-- since there is no logical difference in LaunchDarkly between a single context and
    56  // a multi-context that only contains one context.
    57  func (m *MultiBuilder) Build() Context {
    58  	if len(m.contexts) == 0 {
    59  		return Context{defined: true, err: lderrors.ErrContextKindMultiWithNoKinds{}}
    60  	}
    61  
    62  	if len(m.contexts) == 1 {
    63  		// If only one context was added, the result is just the same as that one
    64  		return m.contexts[0]
    65  	}
    66  
    67  	m.contextsCopyOnWrite = true // see note on ___CopyOnWrite in Builder.Build()
    68  
    69  	// Sort the list by kind - this makes our output deterministic and will also be important when we
    70  	// compute a fully qualified key.
    71  	sort.Slice(m.contexts, func(i, j int) bool { return m.contexts[i].Kind() < m.contexts[j].Kind() })
    72  
    73  	// Check for conditions that could make a multi-context invalid
    74  	var individualErrors map[string]error
    75  	duplicates := false
    76  	for i, c := range m.contexts {
    77  		err := c.Err()
    78  		switch {
    79  		case err != nil: // one of the individual contexts already had an error
    80  			if individualErrors == nil {
    81  				individualErrors = make(map[string]error)
    82  			}
    83  			individualErrors[string(c.Kind())] = err
    84  		default:
    85  			// duplicate check's correctness relies on m.contexts being sorted by kind.
    86  			if i > 0 && m.contexts[i-1].Kind() == c.Kind() {
    87  				duplicates = true
    88  			}
    89  		}
    90  	}
    91  	var err error
    92  	switch {
    93  	case duplicates:
    94  		err = lderrors.ErrContextKindMultiDuplicates{}
    95  	case len(individualErrors) != 0:
    96  		err = lderrors.ErrContextPerKindErrors{Errors: individualErrors}
    97  	}
    98  	if err != nil {
    99  		return Context{
   100  			defined: true,
   101  			err:     err,
   102  		}
   103  	}
   104  
   105  	ret := Context{
   106  		defined:       true,
   107  		kind:          MultiKind,
   108  		multiContexts: m.contexts,
   109  	}
   110  
   111  	// Fully-qualified key for multi-context is defined as "kind1:key1:kind2:key2" etc., where kinds are in
   112  	// alphabetical order (we have already sorted them above) and keys are URL-encoded. In this case we
   113  	// do _not_ omit a default kind of "user".
   114  	for _, c := range m.contexts {
   115  		if ret.fullyQualifiedKey != "" {
   116  			ret.fullyQualifiedKey += ":"
   117  		}
   118  		ret.fullyQualifiedKey += makeFullyQualifiedKeySingleKind(c.kind, c.key, false)
   119  	}
   120  
   121  	return ret
   122  }
   123  
   124  // TryBuild is an alternative to Build that returns any validation errors as a second value.
   125  //
   126  // As described in [MultiBuilder.Build], there are several ways the state of a [Context] could
   127  // be invalid. Since in normal usage it is possible to be confident that these will not occur,
   128  // the Build method is designed for convenient use within expressions by returning a single
   129  // Context value, and any validation problems are contained within that value where they can be
   130  // detected by calling the context's [Context.Err] method. But, if you prefer to use the
   131  // two-value pattern that is common in Go, you can call TryBuild instead:
   132  //
   133  //	c, err := ldcontext.NewMultiBuilder().
   134  //		Add(context1).Add(context2).
   135  //		TryBuild()
   136  //	if err != nil {
   137  //		// do whatever is appropriate if building the context failed
   138  //	}
   139  //
   140  // The two return values are the same as to 1. the Context that would be returned by Build(),
   141  // and 2. the result of calling [Context.Err] on that Context. So, the above example is exactly
   142  // equivalent to:
   143  //
   144  //	c := ldcontext.NewMultiBuilder().
   145  //		Add(context1).Add(context2).
   146  //		Build()
   147  //	if c.Err() != nil {
   148  //		// do whatever is appropriate if building the context failed
   149  //	}
   150  //
   151  // Note that unlike some Go methods where the first return value is normally an
   152  // uninitialized zero value if the error is non-nil, the Context returned by TryBuild in case
   153  // of an error is not completely uninitialized: it does contain the error information as well,
   154  // so that if it is mistakenly passed to an SDK method, the SDK can tell what the error was.
   155  func (m *MultiBuilder) TryBuild() (Context, error) {
   156  	c := m.Build()
   157  	return c, c.Err()
   158  }
   159  
   160  // Add adds a nested context for a specific Kind to a MultiBuilder.
   161  //
   162  // It is invalid to add more than one context with the same Kind. This error is detected
   163  // when you call [MultiBuilder.Build] or [MultiBuilder.TryBuild].
   164  //
   165  // If the parameter is a multi-context, this is exactly equivalent to adding each of the
   166  // individual kinds from it separately. For instance, in the following example, "multi1" and
   167  // "multi2" end up being exactly the same:
   168  //
   169  //	c1 := ldcontext.NewWithKind("kind1", "key1")
   170  //	c2 := ldcontext.NewWithKind("kind2", "key2")
   171  //	c3 := ldcontext.NewWithKind("kind3", "key3")
   172  //
   173  //	multi1 := ldcontext.NewMultiBuilder().Add(c1).Add(c2).Add(c3).Build()
   174  //
   175  //	c1plus2 := ldcontext.NewMultiBuilder().Add(c1).Add(c2).Build()
   176  //	multi2 := ldcontext.NewMultiBuilder().Add(c1plus2).Add(c3).Build()
   177  func (m *MultiBuilder) Add(context Context) *MultiBuilder {
   178  	if m.contextsCopyOnWrite {
   179  		m.contexts = slices.Clone(m.contexts)
   180  		m.contextsCopyOnWrite = true
   181  	}
   182  	if context.Multiple() {
   183  		m.contexts = append(m.contexts, context.multiContexts...)
   184  	} else {
   185  		m.contexts = append(m.contexts, context)
   186  	}
   187  	return m
   188  }
   189  

View as plain text