1 package ldtestdata 2 3 import ( 4 "fmt" 5 "sort" 6 7 "github.com/launchdarkly/go-sdk-common/v3/ldcontext" 8 "github.com/launchdarkly/go-sdk-common/v3/ldvalue" 9 "github.com/launchdarkly/go-server-sdk-evaluation/v2/ldbuilders" 10 "github.com/launchdarkly/go-server-sdk-evaluation/v2/ldmodel" 11 12 "golang.org/x/exp/maps" 13 "golang.org/x/exp/slices" 14 ) 15 16 const ( 17 trueVariationForBool = 0 18 falseVariationForBool = 1 19 ) 20 21 // FlagBuilder is a builder for feature flag configurations to be used with [TestDataSource]. 22 type FlagBuilder struct { 23 key string 24 on bool 25 offVariation ldvalue.OptionalInt 26 fallthroughVariation ldvalue.OptionalInt 27 variations []ldvalue.Value 28 targets map[ldcontext.Kind]map[int]map[string]bool 29 rules []*RuleBuilder 30 } 31 32 // RuleBuilder is a builder for feature flag rules to be used with [TestDataSource.] 33 // 34 // In the LaunchDarkly model, a flag can have any number of rules, and a rule can have any number of 35 // clauses. A clause is an individual test such as "name is 'X'". A rule matches a user if all of the 36 // rule's clauses match the user. 37 // 38 // To start defining a rule, use one of the flag builder's matching methods such as [RuleBuilder.IfMatch]. 39 // This defines the first clause for the rule. Optionally, you may add more clauses with the rule builder's 40 // methods such as [RuleBuilder.AndMatch]. Finally, call [RuleBuilder.ThenReturn] or 41 // [RuleBuilder.ThenReturnIndex] to finish defining the rule. 42 type RuleBuilder struct { 43 owner *FlagBuilder 44 variation int 45 clauses []ldmodel.Clause 46 } 47 48 func newFlagBuilder(key string) *FlagBuilder { 49 return &FlagBuilder{ 50 key: key, 51 on: true, 52 } 53 } 54 55 func copyFlagBuilder(from *FlagBuilder) *FlagBuilder { 56 f := new(FlagBuilder) 57 *f = *from 58 f.variations = slices.Clone(from.variations) 59 if f.rules != nil { 60 f.rules = make([]*RuleBuilder, 0, len(from.rules)) 61 for _, r := range from.rules { 62 f.rules = append(f.rules, copyTestFlagRuleBuilder(r, f)) 63 } 64 } 65 if f.targets != nil { 66 f.targets = make(map[ldcontext.Kind]map[int]map[string]bool) 67 for k, v := range from.targets { 68 map1 := make(map[int]map[string]bool) 69 for k1, v1 := range v { 70 map1[k1] = maps.Clone(v1) 71 } 72 f.targets[k] = map1 73 } 74 } 75 return f 76 } 77 78 // BooleanFlag is a shortcut for setting the flag to use the standard boolean configuration. 79 // 80 // This is the default for all new flags created with [TestDataSource.Flag]. The flag will have two 81 // variations, true and false (in that order); it will return false whenever targeting is off, and 82 // true when targeting is on if no other settings specify otherwise. 83 func (f *FlagBuilder) BooleanFlag() *FlagBuilder { 84 if f.isBooleanFlag() { 85 return f 86 } 87 return f.Variations(ldvalue.Bool(true), ldvalue.Bool(false)). 88 FallthroughVariationIndex(trueVariationForBool). 89 OffVariationIndex(falseVariationForBool) 90 } 91 92 // On sets targeting to be on or off for this flag. 93 // 94 // The effect of this depends on the rest of the flag configuration, just as it does on the 95 // real LaunchDarkly dashboard. In the default configuration that you get from calling 96 // [TestDataSource.Flag] with a new flag key, the flag will return false whenever targeting is 97 // off, and true when targeting is on. 98 func (f *FlagBuilder) On(on bool) *FlagBuilder { 99 f.on = on 100 return f 101 } 102 103 // FallthroughVariation specifies the fallthrough variation for a boolean flag. The fallthrough is 104 // the value that is returned if targeting is on and the user was not matched by a more specific 105 // target or rule. 106 // 107 // If the flag was previously configured with other variations, this also changes it to a boolean 108 // boolean flag. 109 // 110 // To specify the variation by variation index instead (such as for a non-boolean flag), use 111 // [FlagBuilder.FallthroughVariationIndex]. 112 func (f *FlagBuilder) FallthroughVariation(variation bool) *FlagBuilder { 113 return f.BooleanFlag().FallthroughVariationIndex(variationForBool(variation)) 114 } 115 116 // FallthroughVariationIndex specifies the index of the fallthrough variation. The fallthrough is 117 // the value that is returned if targeting is on and the user was not matched by a more specific 118 // target or rule. The index is 0 for the first variation, 1 for the second, etc. 119 // 120 // To specify the variation as true or false instead, for a boolean flag, use 121 // [FlagBuilder.FallthroughVariation]. 122 func (f *FlagBuilder) FallthroughVariationIndex(variationIndex int) *FlagBuilder { 123 f.fallthroughVariation = ldvalue.NewOptionalInt(variationIndex) 124 return f 125 } 126 127 // OffVariation specifies the off variation for a boolean flag. This is the variation that is 128 // returned whenever targeting is off. 129 // 130 // If the flag was previously configured with other variations, this also changes it to a boolean 131 // boolean flag. 132 // 133 // To specify the variation by variation index instead (such as for a non-boolean flag), use 134 // [FlagBuilder.OffVariationIndex]. 135 func (f *FlagBuilder) OffVariation(variation bool) *FlagBuilder { 136 return f.BooleanFlag().OffVariationIndex(variationForBool(variation)) 137 } 138 139 // OffVariationIndex specifies the index of the off variation. This is the variation that is 140 // returned whenever targeting is off. The index is 0 for the first variation, 1 for the second, etc. 141 // 142 // To specify the variation as true or false instead, for a boolean flag, use 143 // [FlagBuilder.OffVariation]. 144 func (f *FlagBuilder) OffVariationIndex(variationIndex int) *FlagBuilder { 145 f.offVariation = ldvalue.NewOptionalInt(variationIndex) 146 return f 147 } 148 149 // VariationForAll sets the flag to return the specified boolean variation by default for all contexts. 150 // 151 // Targeting is switched on, any existing targets or rules are removed, and the flag's variations are 152 // set to true and false. The fallthrough variation is set to the specified value. The off variation is 153 // left unchanged. 154 // 155 // To specify the variation by variation index instead (such as for a non-boolean flag), use 156 // [FlagBuilder.VariationForAllIndex]. 157 func (f *FlagBuilder) VariationForAll(variation bool) *FlagBuilder { 158 return f.BooleanFlag().VariationForAllIndex(variationForBool(variation)) 159 } 160 161 // VariationForAllIndex sets the flag to always return the specified variation for all contexts. 162 // The index is 0 for the first variation, 1 for the second, etc. 163 // 164 // Targeting is switched on, and any existing targets or rules are removed. The fallthrough variation 165 // is set to the specified value. The off variation is left unchanged. 166 // 167 // To specify the variation as true or false instead, for a boolean flag, use 168 // [FlagBuilder.VariationForAll]. 169 func (f *FlagBuilder) VariationForAllIndex(variationIndex int) *FlagBuilder { 170 return f.On(true).ClearRules().ClearTargets().FallthroughVariationIndex(variationIndex) 171 } 172 173 // ValueForAll sets the flag to always return the specified variation value for all contexts. 174 // 175 // The value may be of any JSON type, as defined by [ldvalue.Value]. This method changes the flag to 176 // only a single variation, which is this value, and to return the same variation regardless of 177 // whether targeting is on or off. Any existing targets or rules are removed. 178 func (f *FlagBuilder) ValueForAll(value ldvalue.Value) *FlagBuilder { 179 f.variations = []ldvalue.Value{value} 180 return f.VariationForAllIndex(0) 181 } 182 183 // VariationForUser sets the flag to return the specified boolean variation for a specific user key 184 // (that is, for a context with that key whose context kind is "user") when targeting is on. 185 // 186 // This has no effect when targeting is turned off for the flag. 187 // 188 // If the flag was not already a boolean flag, this also changes it to a boolean flag. 189 // 190 // To specify the variation by variation index instead (such as for a non-boolean flag), use 191 // [FlagBuilder.VariationIndexForUser]. 192 func (f *FlagBuilder) VariationForUser(userKey string, variation bool) *FlagBuilder { 193 return f.BooleanFlag().VariationIndexForUser(userKey, variationForBool(variation)) 194 } 195 196 // VariationForKey sets the flag to return the specified boolean variation for a specific context, 197 // identified by context kind and key, when targeting is on. 198 // 199 // This has no effect when targeting is turned off for the flag. 200 // 201 // If the flag was not already a boolean flag, this also changes it to a boolean flag. 202 // 203 // To specify the variation by variation index instead (such as for a non-boolean flag), use 204 // [FlagBuilder.VariationIndexForKey]. 205 func (f *FlagBuilder) VariationForKey(contextKind ldcontext.Kind, key string, variation bool) *FlagBuilder { 206 return f.BooleanFlag().VariationIndexForKey(contextKind, key, variationForBool(variation)) 207 } 208 209 // VariationIndexForUser sets the flag to return the specified variation for a specific user key 210 // (that is, for a context with that key whose context kind is "user") when targeting is on. 211 // The index is 0 for the first variation, 1 for the second, etc. 212 // 213 // This has no effect when targeting is turned off for the flag. 214 // 215 // To specify the variation as a true or false value if it is a boolean flag, you can use 216 // [FlagBuilder.VariationForUser] instead. 217 func (f *FlagBuilder) VariationIndexForUser(userKey string, variationIndex int) *FlagBuilder { 218 return f.VariationIndexForKey(ldcontext.DefaultKind, userKey, variationIndex) 219 } 220 221 // VariationIndexForKey sets the flag to return the specified variation for a specific context, 222 // identified by context kind and key, when targeting is on. The index is 0 for the first variation, 223 // 1 for the second, etc. 224 // 225 // This has no effect when targeting is turned off for the flag. 226 // 227 // To specify the variation as a true or false value if it is a boolean flag, you can use 228 // [FlagBuilder.VariationForKey] instead. 229 func (f *FlagBuilder) VariationIndexForKey(contextKind ldcontext.Kind, key string, variationIndex int) *FlagBuilder { 230 if f.targets == nil { 231 f.targets = make(map[ldcontext.Kind]map[int]map[string]bool) 232 } 233 if contextKind == "" { 234 contextKind = ldcontext.DefaultKind 235 } 236 keysByVar := f.targets[contextKind] 237 if keysByVar == nil { 238 keysByVar = make(map[int]map[string]bool) 239 f.targets[contextKind] = keysByVar 240 } 241 for i := range f.variations { 242 keys := keysByVar[i] 243 if i == variationIndex { 244 if keys == nil { 245 keys = make(map[string]bool) 246 keysByVar[i] = keys 247 } 248 keys[key] = true 249 } else { 250 delete(keys, key) 251 } 252 } 253 return f 254 } 255 256 // Variations changes the allowable variation values for the flag. 257 // 258 // The values may be of any JSON type, as defined by [ldvalue.Value]. For instance, a boolean flag 259 // normally has ldvalue.Bool(true), ldvalue.Bool(false); a string-valued flag might have 260 // ldvalue.String("red"), ldvalue.String("green")}; etc. 261 func (f *FlagBuilder) Variations(values ...ldvalue.Value) *FlagBuilder { 262 f.variations = slices.Clone(values) 263 return f 264 } 265 266 // IfMatch starts defining a flag rule, using the "is one of" operator. This is a shortcut for 267 // calling [FlagBuilder.IfMatchContext] with "user" as the context kind. 268 // 269 // The method returns a [RuleBuilder]. Call its [RuleBuilder.ThenReturn] or [RuleBuilder.ThenReturnIndex] 270 // method to finish the rule, or add more tests with another method like [RuleBuilder.AndMatch]. 271 // 272 // For example, this creates a rule that returns true if the user name attribute is "Patsy" or "Edina": 273 // 274 // testData.Flag("flag"). 275 // IfMatch("name", ldvalue.String("Patsy"), ldvalue.String("Edina")). 276 // ThenReturn(true) 277 func (f *FlagBuilder) IfMatch(attribute string, values ...ldvalue.Value) *RuleBuilder { 278 return newTestFlagRuleBuilder(f).AndMatch(attribute, values...) 279 } 280 281 // IfMatchContext starts defining a flag rule, using the "is one of" operator. This matching 282 // expression only applies to contexts of a specific kind, identified by the contextKind parameter. 283 // 284 // The method returns a [RuleBuilder]. Call its [RuleBuilder.ThenReturn] or [RuleBuilder.ThenReturnIndex] 285 // method to finish the rule, or add more tests with another method like [RuleBuilder.AndMatch]. 286 // 287 // For example, this creates a rule that returns true if the name attribute for the "company" context 288 // is "Ella" or "Monsoon": 289 // 290 // testData.Flag("flag"). 291 // IfMatchContext("company", "name", ldvalue.String("Ella"), ldvalue.String("Monsoon")). 292 // ThenReturn(true) 293 func (f *FlagBuilder) IfMatchContext( 294 contextKind ldcontext.Kind, 295 attribute string, 296 values ...ldvalue.Value, 297 ) *RuleBuilder { 298 return newTestFlagRuleBuilder(f).AndMatchContext(contextKind, attribute, values...) 299 } 300 301 // IfNotMatch starts defining a flag rule, using the "is not one of" operator. This is a shortcut for 302 // calling [FlagBuilder.IfNotMatchContext] with "user" as the context kind. 303 // 304 // The method returns a [RuleBuilder]. Call its [RuleBuilder.ThenReturn] or [RuleBuilder.ThenReturnIndex] 305 // method to finish the rule, or add more tests with another method like [RuleBuilder.AndMatch]. 306 // 307 // For example, this creates a rule that returns true if the user name attribute is neither "Saffron" 308 // nor "Bubble": 309 // 310 // testData.Flag("flag"). 311 // IfNotMatch("name", ldvalue.String("Saffron"), ldvalue.String("Bubble")). 312 // ThenReturn(true) 313 func (f *FlagBuilder) IfNotMatch(attribute string, values ...ldvalue.Value) *RuleBuilder { 314 return newTestFlagRuleBuilder(f).AndNotMatch(attribute, values...) 315 } 316 317 // IfNotMatchContext starts defining a flag rule, using the "is not one of" operator. This matching 318 // expression only applies to contexts of a specific kind, identified by the contextKind parameter. 319 // 320 // The method returns a [RuleBuilder]. Call its [RuleBuilder.ThenReturn] or [RuleBuilder.ThenReturnIndex] 321 // method to finish the rule, or add more tests with another method like [RuleBuilder.AndMatch]. 322 // 323 // For example, this creates a rule that returns true if the name attribute for the "company" context 324 // is neither "Pendant" nor "Sterling Cooper": 325 // 326 // testData.Flag("flag"). 327 // IfNotMatch("company", "name", ldvalue.String("Pendant"), ldvalue.String("Sterling Cooper")). 328 // ThenReturn(true) 329 func (f *FlagBuilder) IfNotMatchContext( 330 contextKind ldcontext.Kind, 331 attribute string, 332 values ...ldvalue.Value, 333 ) *RuleBuilder { 334 return newTestFlagRuleBuilder(f).AndNotMatchContext(contextKind, attribute, values...) 335 } 336 337 // ClearRules removes any existing rules from the flag. This undoes the effect of methods like 338 // [FlagBuilder.IfMatch]. 339 func (f *FlagBuilder) ClearRules() *FlagBuilder { 340 f.rules = nil 341 return f 342 } 343 344 // ClearTargets removes any existing user targets from the flag. This undoes the effect of methods 345 // like [FlagBuilder.VariationForUser]. 346 func (f *FlagBuilder) ClearTargets() *FlagBuilder { 347 f.targets = nil 348 return f 349 } 350 351 func (f *FlagBuilder) isBooleanFlag() bool { 352 return len(f.variations) == 2 && 353 f.variations[trueVariationForBool].Equal(ldvalue.Bool(true)) && 354 f.variations[falseVariationForBool].Equal(ldvalue.Bool(false)) 355 } 356 357 func (f *FlagBuilder) createFlag(version int) ldmodel.FeatureFlag { 358 fb := ldbuilders.NewFlagBuilder(f.key). 359 Version(version). 360 On(f.on). 361 Variations(f.variations...) 362 if f.offVariation.IsDefined() { 363 fb.OffVariation(f.offVariation.IntValue()) 364 } 365 if f.fallthroughVariation.IsDefined() { 366 fb.FallthroughVariation(f.fallthroughVariation.IntValue()) 367 } 368 369 // Iterate through any context kinds that there are targets for. A quirk of the data model, for 370 // backward-compatibility reasons, is that each entry in the old-style targets list (for users) 371 // must be matched by a placeholder entry in ContextTargets. 372 // Also, for the sake of test determinaciy, we sort the context kinds and the context keys. 373 targetKinds := make([]ldcontext.Kind, 0, len(f.targets)) 374 for kind := range f.targets { 375 targetKinds = append(targetKinds, kind) 376 } 377 slices.Sort(targetKinds) 378 for _, kind := range targetKinds { 379 keysByVar := f.targets[kind] 380 for varIndex := range f.variations { 381 if keysMap, ok := keysByVar[varIndex]; ok { 382 keys := make([]string, 0, len(keysMap)) 383 for key := range keysMap { 384 keys = append(keys, key) 385 } 386 sort.Strings(keys) 387 if kind == ldcontext.DefaultKind { 388 fb.AddTarget(varIndex, keys...) 389 // A quirk of the data model, for backward-compatibility reasons, is that each entry in the 390 // old-style targets list (for users) must be matched by a placeholder entry in ContextTargets. 391 fb.AddContextTarget(ldcontext.DefaultKind, varIndex) 392 } else { 393 fb.AddContextTarget(kind, varIndex, keys...) 394 } 395 } 396 } 397 } 398 for i, r := range f.rules { 399 fb.AddRule(ldbuilders.NewRuleBuilder(). 400 ID(fmt.Sprintf("rule%d", i)). 401 Variation(r.variation). 402 Clauses(r.clauses...), 403 ) 404 } 405 return fb.Build() 406 } 407 408 func newTestFlagRuleBuilder(owner *FlagBuilder) *RuleBuilder { 409 return &RuleBuilder{owner: owner} 410 } 411 412 func copyTestFlagRuleBuilder(from *RuleBuilder, owner *FlagBuilder) *RuleBuilder { 413 r := RuleBuilder{owner: owner, variation: from.variation} 414 r.clauses = slices.Clone(from.clauses) 415 return &r 416 } 417 418 // AndMatch adds another clause, using the "is one of" operator. This is a shortcut for calling 419 // [RuleBuilder.AndMatchContext] with "user" as the context kind. 420 // 421 // For example, this creates a rule that returns true if the user name attribute is "Patsy" and the 422 // country is "gb": 423 // 424 // testData.Flag("flag"). 425 // IfMatch("name", ldvalue.String("Patsy")). 426 // AndMatch("country", ldvalue.String("gb")). 427 // ThenReturn(true) 428 func (r *RuleBuilder) AndMatch(attribute string, values ...ldvalue.Value) *RuleBuilder { 429 return r.AndMatchContext(ldcontext.DefaultKind, attribute, values...) 430 } 431 432 // AndMatchContext adds another clause, using the "is one of" operator. This matching expression 433 // only applies to contexts of a specific kind, identified by the contextKind parameter. 434 // 435 // For example, this creates a rule that returns true if the name attribute for the "company" context 436 // is "Ella" and the country is "gb": 437 // 438 // testData.Flag("flag"). 439 // IfMatchContext("company", "name", ldvalue.String("Ella")). 440 // AndMatchContext("company", "country", ldvalue.String("gb")). 441 // ThenReturn(true) 442 func (r *RuleBuilder) AndMatchContext( 443 contextKind ldcontext.Kind, 444 attribute string, 445 values ...ldvalue.Value, 446 ) *RuleBuilder { 447 r.clauses = append(r.clauses, ldbuilders.ClauseWithKind(contextKind, attribute, ldmodel.OperatorIn, values...)) 448 return r 449 } 450 451 // AndNotMatch adds another clause, using the "is not one of" operator. This is a shortcut for calling 452 // [RuleBuilder.AndNotMatchContext] with "user" as the context kind. 453 // 454 // For example, this creates a rule that returns true if the user name attribute is "Patsy" and the 455 // country is not "gb": 456 // 457 // testData.Flag("flag"). 458 // IfMatch("name", ldvalue.String("Patsy")). 459 // AndNotMatch("country", ldvalue.String("gb")). 460 // ThenReturn(true) 461 func (r *RuleBuilder) AndNotMatch(attribute string, values ...ldvalue.Value) *RuleBuilder { 462 return r.AndNotMatchContext(ldcontext.DefaultKind, attribute, values...) 463 } 464 465 // AndNotMatchContext adds another clause, using the "is not one of" operator. This matching expression 466 // only applies to contexts of a specific kind, identified by the contextKind parameter. 467 // 468 // For example, this creates a rule that returns true if the name attribute for the "company" context 469 // is "Ella" and the country is not "gb": 470 // 471 // testData.Flag("flag"). 472 // IfMatchContext("company", "name", ldvalue.String("Ella")). 473 // AndNotMatchContext("company", "country", ldvalue.String("gb")). 474 // ThenReturn(true) 475 func (r *RuleBuilder) AndNotMatchContext( 476 contextKind ldcontext.Kind, 477 attribute string, 478 values ...ldvalue.Value, 479 ) *RuleBuilder { 480 r.clauses = append(r.clauses, ldbuilders.Negate(ldbuilders.ClauseWithKind(contextKind, 481 attribute, ldmodel.OperatorIn, values...))) 482 return r 483 } 484 485 // ThenReturn finishes defining the rule, specifying the result value as a boolean. 486 func (r *RuleBuilder) ThenReturn(variation bool) *FlagBuilder { 487 r.owner.BooleanFlag() 488 return r.ThenReturnIndex(variationForBool(variation)) 489 } 490 491 // ThenReturnIndex finishes defining the rule, specifying the result as a variation index. The index 492 // is 0 for the first variation, 1 for the second, etc. 493 func (r *RuleBuilder) ThenReturnIndex(variation int) *FlagBuilder { 494 r.variation = variation 495 r.owner.rules = append(r.owner.rules, r) 496 return r.owner 497 } 498 499 func variationForBool(value bool) int { 500 if value { 501 return trueVariationForBool 502 } 503 return falseVariationForBool 504 } 505