...

Source file src/github.com/launchdarkly/go-server-sdk-evaluation/v2/ldmodel/model_serialization_testdata_test.go

Documentation: github.com/launchdarkly/go-server-sdk-evaluation/v2/ldmodel

     1  package ldmodel
     2  
     3  import (
     4  	"encoding/json"
     5  
     6  	"github.com/launchdarkly/go-sdk-common/v3/ldattr"
     7  	"github.com/launchdarkly/go-sdk-common/v3/ldcontext"
     8  	"github.com/launchdarkly/go-sdk-common/v3/ldtime"
     9  	"github.com/launchdarkly/go-sdk-common/v3/ldvalue"
    10  )
    11  
    12  type flagSerializationTestParams struct {
    13  	name          string
    14  	flag          FeatureFlag
    15  	jsonString    string   // for marshaling tests, jsonString doesn't need to include any of flagTopDefaultLevelProperties
    16  	jsonAltInputs []string // if specified, unmarshaling test will verify that these parse to the same result
    17  
    18  	isCustomClientSideAvailability bool
    19  }
    20  
    21  type segmentSerializationTestParams struct {
    22  	name          string
    23  	segment       Segment
    24  	jsonString    string   // for marshaling tests, jsonString doesn't need to include any of segmentTopDefaultLevelProperties
    25  	jsonAltInputs []string // if specified, unmarshaling test will verify that these parse to the same result
    26  }
    27  
    28  type clauseSerializationTestParams struct {
    29  	name          string
    30  	clause        Clause
    31  	jsonString    string
    32  	jsonAltInputs []string
    33  }
    34  
    35  type rolloutSerializationTestParams struct {
    36  	name          string
    37  	rollout       Rollout
    38  	jsonString    string
    39  	jsonAltInputs []string
    40  }
    41  
    42  var flagTopLevelDefaultProperties = map[string]interface{}{
    43  	"key":                    "",
    44  	"deleted":                false,
    45  	"variations":             []interface{}{},
    46  	"on":                     false,
    47  	"offVariation":           nil,
    48  	"fallthrough":            map[string]interface{}{},
    49  	"targets":                []interface{}{},
    50  	"contextTargets":         []interface{}{},
    51  	"prerequisites":          []interface{}{},
    52  	"rules":                  []interface{}{},
    53  	"clientSide":             false,
    54  	"trackEvents":            false,
    55  	"trackEventsFallthrough": false,
    56  	"debugEventsUntilDate":   nil,
    57  	"salt":                   "",
    58  	"version":                0,
    59  }
    60  
    61  var segmentTopLevelDefaultProperties = map[string]interface{}{
    62  	"key":              "",
    63  	"deleted":          false,
    64  	"version":          0,
    65  	"generation":       nil,
    66  	"included":         []string{},
    67  	"excluded":         []string{},
    68  	"includedContexts": []interface{}{},
    69  	"excludedContexts": []interface{}{},
    70  	"rules":            []interface{}{},
    71  	"salt":             "",
    72  }
    73  
    74  func makeFlagSerializationTestParams() []flagSerializationTestParams {
    75  	ret := []flagSerializationTestParams{
    76  		{
    77  			name:       "defaults",
    78  			flag:       FeatureFlag{},
    79  			jsonString: `{}`,
    80  			jsonAltInputs: []string{
    81  				`{"deleted": false}`,
    82  				`{"on": false}`,
    83  				`{"offVariation": null}`,
    84  				`{"fallthrough": {"variation": null}}`,
    85  				`{"prerequisites": []}`,
    86  				`{"targets": []}`,
    87  				`{"rules": []}`,
    88  			},
    89  		},
    90  		{
    91  			name:       "key",
    92  			flag:       FeatureFlag{Key: "flag-key"},
    93  			jsonString: `{"key": "flag-key"}`,
    94  		},
    95  		{
    96  			name:       "version",
    97  			flag:       FeatureFlag{Version: 99},
    98  			jsonString: `{"version": 99}`,
    99  		},
   100  		{
   101  			name:       "deleted",
   102  			flag:       FeatureFlag{Deleted: true},
   103  			jsonString: `{"deleted": true}`,
   104  		},
   105  		{
   106  			name: "variations",
   107  			flag: FeatureFlag{Variations: []ldvalue.Value{
   108  				ldvalue.Bool(true),
   109  				ldvalue.Int(1),
   110  				ldvalue.Float64(1.5),
   111  				ldvalue.String("x"),
   112  				ldvalue.ArrayOf(),
   113  				ldvalue.ObjectBuild().Build(),
   114  			},
   115  			},
   116  			jsonString: `{"variations": [true, 1, 1.5, "x", [], {}]}`,
   117  		},
   118  		{
   119  			name:       "on",
   120  			flag:       FeatureFlag{On: true},
   121  			jsonString: `{"on": true}`,
   122  		},
   123  		{
   124  			name:       "offVariation",
   125  			flag:       FeatureFlag{OffVariation: ldvalue.NewOptionalInt(1)},
   126  			jsonString: `{"offVariation": 1}`,
   127  		},
   128  		{
   129  			name:          "fallthrough variation",
   130  			flag:          FeatureFlag{Fallthrough: VariationOrRollout{Variation: ldvalue.NewOptionalInt(1)}},
   131  			jsonString:    `{"fallthrough": {"variation": 1}}`,
   132  			jsonAltInputs: []string{`{"fallthrough": {"variation": 1, "rollout": null}}`},
   133  		},
   134  		{
   135  			name: "prerequisites",
   136  			flag: FeatureFlag{
   137  				Prerequisites: []Prerequisite{
   138  					{Variation: 1, Key: "pre-key"},
   139  				},
   140  			},
   141  			jsonString: `{"prerequisites": [ {"variation": 1, "key": "pre-key"} ]}`,
   142  		},
   143  		{
   144  			name: "targets",
   145  			flag: FeatureFlag{
   146  				Targets: []Target{
   147  					{Variation: 1, Values: []string{"a", "b"}},
   148  				},
   149  			},
   150  			jsonString: `{"targets": [ {"variation": 1, "values": ["a", "b"]} ]}`,
   151  		},
   152  		{
   153  			name: "contextTargets",
   154  			flag: FeatureFlag{
   155  				ContextTargets: []Target{
   156  					{ContextKind: "org", Variation: 1, Values: []string{"a", "b"}},
   157  				},
   158  			},
   159  			jsonString: `{"contextTargets": [ {"contextKind": "org", "variation": 1, "values": ["a", "b"]} ]}`,
   160  		},
   161  		{
   162  			name: "minimal rule with variation",
   163  			flag: FeatureFlag{
   164  				Rules: []FlagRule{
   165  					{VariationOrRollout: VariationOrRollout{Variation: ldvalue.NewOptionalInt(1)}},
   166  				},
   167  			},
   168  			jsonString:    `{"rules": [ {"variation": 1, "clauses": [], "trackEvents": false} ]}`,
   169  			jsonAltInputs: []string{`{"rules": [ {"variation": 1} ]}`},
   170  		},
   171  		{
   172  			name: "rule ID",
   173  			flag: FeatureFlag{
   174  				Rules: []FlagRule{
   175  					{ID: "a", VariationOrRollout: VariationOrRollout{Variation: ldvalue.NewOptionalInt(1)}},
   176  				},
   177  			},
   178  			jsonString: `{"rules": [ {"id": "a", "variation": 1, "clauses": [], "trackEvents": false} ]}`,
   179  		},
   180  		{
   181  			name: "rule trackEvents",
   182  			flag: FeatureFlag{
   183  				Rules: []FlagRule{
   184  					{VariationOrRollout: VariationOrRollout{Variation: ldvalue.NewOptionalInt(1)}, TrackEvents: true},
   185  				},
   186  			},
   187  			jsonString: `{"rules": [ {"variation": 1, "clauses": [], "trackEvents": true} ]}`,
   188  		},
   189  		{
   190  			name: "clientSide",
   191  			flag: FeatureFlag{
   192  				ClientSideAvailability: ClientSideAvailability{
   193  					UsingMobileKey:     true,
   194  					UsingEnvironmentID: true,
   195  					Explicit:           false,
   196  				},
   197  			},
   198  			jsonString:                     `{"clientSide": true}`,
   199  			isCustomClientSideAvailability: true,
   200  		},
   201  		{
   202  			name: "clientSide explicitly false",
   203  			flag: FeatureFlag{
   204  				ClientSideAvailability: ClientSideAvailability{
   205  					UsingMobileKey:     true,
   206  					UsingEnvironmentID: false,
   207  					Explicit:           false,
   208  				},
   209  			},
   210  			jsonString:                     `{"clientSide": false}`,
   211  			isCustomClientSideAvailability: true,
   212  		},
   213  		{
   214  			name: "clientSideAvailability both false",
   215  			flag: FeatureFlag{
   216  				ClientSideAvailability: ClientSideAvailability{
   217  					Explicit: true,
   218  				},
   219  			},
   220  			jsonString:                     `{"clientSideAvailability": {"usingMobileKey": false, "usingEnvironmentId": false}}`,
   221  			isCustomClientSideAvailability: true,
   222  		},
   223  		{
   224  			name: "clientSideAvailability both true",
   225  			flag: FeatureFlag{
   226  				ClientSideAvailability: ClientSideAvailability{
   227  					UsingMobileKey:     true,
   228  					UsingEnvironmentID: true,
   229  					Explicit:           true,
   230  				},
   231  			},
   232  			jsonString:                     `{"clientSide": true, "clientSideAvailability": {"usingMobileKey": true, "usingEnvironmentId": true}}`,
   233  			isCustomClientSideAvailability: true,
   234  		},
   235  		{
   236  			name: "clientSideAvailability usingMobileKey only",
   237  			flag: FeatureFlag{
   238  				ClientSideAvailability: ClientSideAvailability{
   239  					UsingMobileKey: true,
   240  					Explicit:       true,
   241  				},
   242  			},
   243  			jsonString:                     `{"clientSideAvailability": {"usingMobileKey": true, "usingEnvironmentId": false}}`,
   244  			isCustomClientSideAvailability: true,
   245  		},
   246  		{
   247  			name: "clientSideAvailability usingEnvironmentId only",
   248  			flag: FeatureFlag{
   249  				ClientSideAvailability: ClientSideAvailability{
   250  					UsingEnvironmentID: true,
   251  					Explicit:           true,
   252  				},
   253  			},
   254  			jsonString:                     `{"clientSide": true, "clientSideAvailability": {"usingMobileKey": false, "usingEnvironmentId": true}}`,
   255  			isCustomClientSideAvailability: true,
   256  		},
   257  		{
   258  			name:       "salt",
   259  			flag:       FeatureFlag{Salt: "flag-salt"},
   260  			jsonString: `{"salt": "flag-salt"}`,
   261  		},
   262  		{
   263  			name:       "trackEvents",
   264  			flag:       FeatureFlag{TrackEvents: true},
   265  			jsonString: `{"trackEvents": true}`,
   266  		},
   267  		{
   268  			name:       "trackEventsFallthrough",
   269  			flag:       FeatureFlag{TrackEventsFallthrough: true},
   270  			jsonString: `{"trackEventsFallthrough": true}`,
   271  		},
   272  		{
   273  			name:       "debugEventsUntilDate",
   274  			flag:       FeatureFlag{DebugEventsUntilDate: ldtime.UnixMillisecondTime(1000)},
   275  			jsonString: `{"debugEventsUntilDate": 1000}`,
   276  		},
   277  	}
   278  
   279  	makeFlagJSONForClause := func(clauseJSON string) string {
   280  		return `{"rules": [ {"variation": 1, "clauses": [` + clauseJSON + `], "trackEvents": false }]}`
   281  	}
   282  	for _, cp := range makeClauseSerializationTestParams() {
   283  		fp := flagSerializationTestParams{
   284  			name: "rule clause " + cp.name,
   285  			flag: FeatureFlag{
   286  				Rules: []FlagRule{
   287  					{
   288  						VariationOrRollout: VariationOrRollout{Variation: ldvalue.NewOptionalInt(1)},
   289  						Clauses:            []Clause{cp.clause},
   290  					},
   291  				},
   292  			},
   293  			jsonString: makeFlagJSONForClause(cp.jsonString),
   294  		}
   295  		for _, alt := range cp.jsonAltInputs {
   296  			fp.jsonAltInputs = append(fp.jsonAltInputs, makeFlagJSONForClause(alt))
   297  		}
   298  		ret = append(ret, fp)
   299  	}
   300  
   301  	makeFlagJSONForFallthroughRollout := func(rolloutJSON string) string {
   302  		return `{"fallthrough": {"rollout": ` + rolloutJSON + `}}`
   303  	}
   304  	makeFlagJSONForRuleRollout := func(rolloutJSON string) string {
   305  		return `{"rules": [ {"rollout": ` + rolloutJSON + `, "clauses": [], "trackEvents": false} ]}`
   306  	}
   307  	for _, rp := range makeRolloutSerializationTestParams() {
   308  		fp1 := flagSerializationTestParams{
   309  			name: "fallthrough rollout " + rp.name,
   310  			flag: FeatureFlag{
   311  				Fallthrough: VariationOrRollout{Rollout: rp.rollout},
   312  			},
   313  			jsonString: makeFlagJSONForFallthroughRollout(rp.jsonString),
   314  		}
   315  		for _, alt := range rp.jsonAltInputs {
   316  			fp1.jsonAltInputs = append(fp1.jsonAltInputs, makeFlagJSONForFallthroughRollout(alt))
   317  		}
   318  		fp2 := flagSerializationTestParams{
   319  			name: "rule rollout " + rp.name,
   320  			flag: FeatureFlag{
   321  				Rules: []FlagRule{{VariationOrRollout: VariationOrRollout{Rollout: rp.rollout}}},
   322  			},
   323  			jsonString: makeFlagJSONForRuleRollout(rp.jsonString),
   324  		}
   325  		for _, alt := range rp.jsonAltInputs {
   326  			fp2.jsonAltInputs = append(fp2.jsonAltInputs, makeFlagJSONForRuleRollout(alt))
   327  		}
   328  		ret = append(ret, fp1, fp2)
   329  	}
   330  
   331  	return ret
   332  }
   333  
   334  func makeSegmentSerializationTestParams() []segmentSerializationTestParams {
   335  	ret := []segmentSerializationTestParams{
   336  		{
   337  			name:       "defaults",
   338  			segment:    Segment{},
   339  			jsonString: `{}`,
   340  			jsonAltInputs: []string{
   341  				`{"deleted": false}`,
   342  				`{"included": []}`,
   343  				`{"excluded": []}`,
   344  				`{"rules": []}`,
   345  				`{"unbounded": false}`,
   346  				`{"generation": null}`,
   347  			},
   348  		},
   349  		{
   350  			name:       "key",
   351  			segment:    Segment{Key: "segment-key"},
   352  			jsonString: `{"key": "segment-key"}`,
   353  		},
   354  		{
   355  			name:       "version",
   356  			segment:    Segment{Version: 99},
   357  			jsonString: `{"version": 99}`,
   358  		},
   359  		{
   360  			name:       "deleted",
   361  			segment:    Segment{Deleted: true},
   362  			jsonString: `{"deleted": true}`,
   363  		},
   364  		{
   365  			name:       "included",
   366  			segment:    Segment{Included: []string{"a", "b"}},
   367  			jsonString: `{"included": ["a", "b"]}`,
   368  		},
   369  		{
   370  			name:       "excluded",
   371  			segment:    Segment{Excluded: []string{"a", "b"}},
   372  			jsonString: `{"excluded": ["a", "b"]}`,
   373  		},
   374  		{
   375  			name: "includedContexts",
   376  			segment: Segment{
   377  				IncludedContexts: []SegmentTarget{
   378  					{ContextKind: "org", Values: []string{"a", "b"}},
   379  				}},
   380  			jsonString: `{"includedContexts": [ {"contextKind": "org", "values": ["a", "b"]} ]}`,
   381  		},
   382  		{
   383  			name: "excludedContexts",
   384  			segment: Segment{
   385  				ExcludedContexts: []SegmentTarget{
   386  					{ContextKind: "org", Values: []string{"a", "b"}},
   387  				}},
   388  			jsonString: `{"excludedContexts": [ {"contextKind": "org", "values": ["a", "b"]} ]}`,
   389  		},
   390  		{
   391  			name: "minimal rule",
   392  			segment: Segment{
   393  				Rules: []SegmentRule{
   394  					{},
   395  				},
   396  			},
   397  			jsonString:    `{"rules": [ {"id": "", "clauses": []} ]}`,
   398  			jsonAltInputs: []string{`{"rules": [ {} ]}`},
   399  		},
   400  		{
   401  			name: "minimal rule with weight",
   402  			segment: Segment{
   403  				Rules: []SegmentRule{
   404  					{Weight: ldvalue.NewOptionalInt(100000)},
   405  				},
   406  			},
   407  			jsonString: `{"rules": [ {"id": "", "weight": 100000, "clauses": []} ]}`,
   408  		},
   409  		{
   410  			name: "rule bucketBy",
   411  			segment: Segment{
   412  				Rules: []SegmentRule{
   413  					{Weight: ldvalue.NewOptionalInt(100000), BucketBy: ldattr.NewLiteralRef("name")},
   414  				},
   415  			},
   416  			jsonString: `{"rules": [ {"id": "", "weight": 100000, "bucketBy": "name", "clauses": []} ]}`,
   417  		},
   418  		{
   419  			name: "rule bucketBy invalid ref",
   420  			// Here we verify that an invalid attribute ref doesn't make parsing fail and is preserved in reserialization
   421  			segment: Segment{
   422  				Rules: []SegmentRule{
   423  					{
   424  						RolloutContextKind: ldcontext.Kind("user"),
   425  						Weight:             ldvalue.NewOptionalInt(100000),
   426  						BucketBy:           ldattr.NewRef("///"),
   427  					},
   428  				},
   429  			},
   430  			jsonString: `{"rules": [ {"id": "", "weight": 100000, "rolloutContextKind": "user", "bucketBy": "///", "clauses": []} ]}`,
   431  		},
   432  		{
   433  			name: "rule rolloutContextKind",
   434  			segment: Segment{
   435  				Rules: []SegmentRule{
   436  					{Weight: ldvalue.NewOptionalInt(100000), RolloutContextKind: ldcontext.Kind("org")},
   437  				},
   438  			},
   439  			jsonString: `{"rules": [ {"id": "", "weight": 100000, "rolloutContextKind": "org", "clauses": []} ]}`,
   440  		},
   441  		{
   442  			name: "rule ID",
   443  			segment: Segment{
   444  				Rules: []SegmentRule{
   445  					{ID: "a"},
   446  				},
   447  			},
   448  			jsonString: `{"rules": [ {"id": "a", "clauses": []} ]}`,
   449  		},
   450  		{
   451  			name:       "unbounded and generation",
   452  			segment:    Segment{Unbounded: true, Generation: ldvalue.NewOptionalInt(1)},
   453  			jsonString: `{"unbounded": true, "generation": 1}`,
   454  		},
   455  		{
   456  			name:       "unbounded and generation and unboundedContextKind",
   457  			segment:    Segment{Unbounded: true, UnboundedContextKind: "org", Generation: ldvalue.NewOptionalInt(1)},
   458  			jsonString: `{"unbounded": true, "unboundedContextKind": "org", "generation": 1}`,
   459  		},
   460  		{
   461  			name:       "salt",
   462  			segment:    Segment{Salt: "segment-salt"},
   463  			jsonString: `{"salt": "segment-salt"}`,
   464  		},
   465  	}
   466  
   467  	makeSegmentJSONForClause := func(clauseJSON string) string {
   468  		return `{"rules": [ {"id": "", "clauses": [` + clauseJSON + `]} ]}`
   469  	}
   470  	for _, cp := range makeClauseSerializationTestParams() {
   471  		sp := segmentSerializationTestParams{
   472  			name: "segment rule clause " + cp.name,
   473  			segment: Segment{
   474  				Rules: []SegmentRule{
   475  					{
   476  						Clauses: []Clause{cp.clause},
   477  					},
   478  				},
   479  			},
   480  			jsonString: makeSegmentJSONForClause(cp.jsonString),
   481  		}
   482  		for _, alt := range cp.jsonAltInputs {
   483  			sp.jsonAltInputs = append(sp.jsonAltInputs, makeSegmentJSONForClause(alt))
   484  		}
   485  		ret = append(ret, sp)
   486  	}
   487  
   488  	return ret
   489  }
   490  
   491  func makeClauseSerializationTestParams() []clauseSerializationTestParams {
   492  	return []clauseSerializationTestParams{
   493  		{
   494  			name: "simple",
   495  			clause: Clause{
   496  				Attribute: ldattr.NewLiteralRef("key"),
   497  				Op:        OperatorIn,
   498  				Values:    []ldvalue.Value{ldvalue.String("a")},
   499  			},
   500  			jsonString:    `{"attribute": "key", "op": "in", "values": ["a"], "negate": false}`,
   501  			jsonAltInputs: []string{`{"attribute": "key", "op": "in", "values": ["a"]}`},
   502  		},
   503  		{
   504  			name: "with kind",
   505  			clause: Clause{
   506  				ContextKind: ldcontext.Kind("org"),
   507  				Attribute:   ldattr.NewLiteralRef("key"),
   508  				Op:          OperatorIn,
   509  				Values:      []ldvalue.Value{ldvalue.String("a")},
   510  			},
   511  			jsonString: `{"contextKind": "org", "attribute": "key", "op": "in", "values": ["a"], "negate": false}`,
   512  		},
   513  		{
   514  			name: "with kind and complex attribute ref",
   515  			clause: Clause{
   516  				ContextKind: ldcontext.Kind("user"),
   517  				Attribute:   ldattr.NewRef("/attr1/subprop"),
   518  				Op:          OperatorIn,
   519  				Values:      []ldvalue.Value{ldvalue.String("a")},
   520  			},
   521  			jsonString: `{"contextKind": "user", "attribute": "/attr1/subprop", "op": "in", "values": ["a"], "negate": false}`,
   522  		},
   523  		{
   524  			name: "attribute is treated as plain name and not path when kind is omitted",
   525  			clause: Clause{
   526  				Attribute: ldattr.NewLiteralRef("/attr1/subprop"), // note NewLiteralRef, not NewRef
   527  				Op:        OperatorIn,
   528  				Values:    []ldvalue.Value{ldvalue.String("a")},
   529  			},
   530  			jsonString: `{"attribute": "/attr1/subprop", "op": "in", "values": ["a"], "negate": false}`,
   531  		},
   532  		{
   533  			name: "invalid attribute ref",
   534  			clause: Clause{
   535  				ContextKind: ldcontext.Kind("user"),
   536  				Attribute:   ldattr.NewRef("///"),
   537  				Op:          OperatorIn,
   538  				Values:      []ldvalue.Value{ldvalue.String("a")},
   539  			},
   540  			jsonString: `{"contextKind": "user", "attribute": "///", "op": "in", "values": ["a"], "negate": false}`,
   541  		},
   542  		{
   543  			name: "with segmentMatch operator",
   544  			clause: Clause{
   545  				Op:     OperatorSegmentMatch,
   546  				Values: []ldvalue.Value{ldvalue.String("a")},
   547  			},
   548  			jsonString: `{"attribute": "", "op": "segmentMatch", "values": ["a"], "negate": false}`,
   549  			// note, attribute is serialized as "" in this case, not omitted
   550  		},
   551  		{
   552  			name: "negated",
   553  			clause: Clause{
   554  				Attribute: ldattr.NewLiteralRef("key"),
   555  				Op:        OperatorIn,
   556  				Values:    []ldvalue.Value{ldvalue.String("a")},
   557  				Negate:    true,
   558  			},
   559  			jsonString: `{"attribute": "key", "op": "in", "values": ["a"], "negate": true}`,
   560  		},
   561  	}
   562  }
   563  
   564  func makeRolloutSerializationTestParams() []rolloutSerializationTestParams {
   565  	basicVariations := []WeightedVariation{{Variation: 1, Weight: 100000}}
   566  	basicVariationsJSON := `[{"variation": 1, "weight": 100000}]`
   567  
   568  	return []rolloutSerializationTestParams{
   569  		{
   570  			name:       "simple",
   571  			rollout:    Rollout{Variations: basicVariations},
   572  			jsonString: `{"variations": ` + basicVariationsJSON + `}`,
   573  		},
   574  		{
   575  			name: "with context kind",
   576  			rollout: Rollout{
   577  				ContextKind: ldcontext.Kind("org"),
   578  				Variations:  basicVariations,
   579  			},
   580  			jsonString: `{"contextKind": "org", "variations": ` + basicVariationsJSON + `}`,
   581  		},
   582  		{
   583  			name: "with bucketBy",
   584  			rollout: Rollout{
   585  				BucketBy:   ldattr.NewLiteralRef("name"),
   586  				Variations: basicVariations,
   587  			},
   588  			jsonString: `{"bucketBy": "name", "variations": ` + basicVariationsJSON + `}`,
   589  		},
   590  		{
   591  			name: "with contextKind and complex bucketBy ref",
   592  			rollout: Rollout{
   593  				ContextKind: ldcontext.Kind("user"),
   594  				BucketBy:    ldattr.NewRef("/attr1/subprop"),
   595  				Variations:  basicVariations,
   596  			},
   597  			jsonString: `{"contextKind": "user", "bucketBy": "/attr1/subprop", "variations": ` + basicVariationsJSON + `}`,
   598  		},
   599  		{
   600  			name: "bucketBy is treated as plain name and not path when kind is omitted",
   601  			rollout: Rollout{
   602  				BucketBy:   ldattr.NewLiteralRef("/attr1/subprop"), // note, NewLiteralRef rather than NewRef
   603  				Variations: basicVariations,
   604  			},
   605  			jsonString: `{"bucketBy": "/attr1/subprop", "variations": ` + basicVariationsJSON + `}`,
   606  		},
   607  		{
   608  			name: "invalid bucketBy ref",
   609  			rollout: Rollout{
   610  				ContextKind: ldcontext.Kind("user"),
   611  				BucketBy:    ldattr.NewRef("///"),
   612  				Variations:  basicVariations,
   613  			},
   614  			jsonString: `{"contextKind": "user", "bucketBy": "///", "variations": ` + basicVariationsJSON + `}`,
   615  		},
   616  		{
   617  			name: "simple experiment",
   618  			rollout: Rollout{
   619  				Kind:       RolloutKindExperiment,
   620  				Variations: basicVariations,
   621  			},
   622  			jsonString:    `{"kind": "experiment", "variations": ` + basicVariationsJSON + `}`,
   623  			jsonAltInputs: []string{`{"kind": "experiment", "seed": null, "variations": ` + basicVariationsJSON + `}`},
   624  		},
   625  		{
   626  			name: "experiment with seed",
   627  			rollout: Rollout{
   628  				Kind:       RolloutKindExperiment,
   629  				Seed:       ldvalue.NewOptionalInt(12345),
   630  				Variations: basicVariations,
   631  			},
   632  			jsonString: `{"kind": "experiment", "seed": 12345, "variations": ` + basicVariationsJSON + `}`,
   633  		},
   634  		{
   635  			name: "experiment with untracked",
   636  			rollout: Rollout{
   637  				Kind: RolloutKindExperiment,
   638  				Variations: []WeightedVariation{
   639  					{Variation: 0, Weight: 75000},
   640  					{Variation: 1, Weight: 25000, Untracked: true},
   641  				},
   642  			},
   643  			jsonString: `{"kind": "experiment", "variations": [` +
   644  				`{"variation": 0, "weight": 75000}, {"variation": 1, "weight": 25000, "untracked": true}]}`,
   645  		},
   646  	}
   647  }
   648  
   649  func mergeDefaultProperties(output json.RawMessage, defaults map[string]interface{}) json.RawMessage {
   650  	var parsedOutput map[string]interface{}
   651  	if err := json.Unmarshal(output, &parsedOutput); err != nil {
   652  		return output
   653  	}
   654  	outMap := make(map[string]interface{})
   655  	for k, v := range parsedOutput {
   656  		outMap[k] = v
   657  	}
   658  	for k, v := range defaults {
   659  		if _, found := outMap[k]; !found {
   660  			outMap[k] = v
   661  		}
   662  	}
   663  	data, err := json.Marshal(outMap)
   664  	if err != nil {
   665  		return output
   666  	}
   667  	return data
   668  }
   669  

View as plain text