...

Source file src/github.com/launchdarkly/go-server-sdk/v6/testhelpers/ldtestdata/test_data_source_flag.go

Documentation: github.com/launchdarkly/go-server-sdk/v6/testhelpers/ldtestdata

     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  

View as plain text