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