diff --git a/go.mod b/go.mod index 75d29468fd3..44f0f24ab96 100644 --- a/go.mod +++ b/go.mod @@ -253,7 +253,7 @@ require ( go.opentelemetry.io/proto/otlp v1.4.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.31.0 // indirect - golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 // indirect + golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect golang.org/x/sync v0.10.0 // indirect golang.org/x/sys v0.28.0 // indirect golang.org/x/term v0.27.0 // indirect diff --git a/go.sum b/go.sum index 3c106b8d6a6..b2949b5a8ff 100644 --- a/go.sum +++ b/go.sum @@ -1074,8 +1074,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 h1:hNQpMuAJe5CtcUqCXaWga3FHu+kQvCqcsoVaQgSV60o= -golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= diff --git a/internal/flag/internal_flag_test.go b/internal/flag/internal_flag_test.go index a069a761ee1..3d7df8ced09 100644 --- a/internal/flag/internal_flag_test.go +++ b/internal/flag/internal_flag_test.go @@ -253,6 +253,48 @@ func TestInternalFlag_Value(t *testing.T) { }, }, }, + { + name: "Should return the variation specified in the rule if rule match (jsonLogic)", + flag: flag.InternalFlag{ + Variations: &map[string]*interface{}{ + "variation_A": testconvert.Interface(true), + "variation_B": testconvert.Interface(false), + }, + Rules: &[]flag.Rule{ + { + Name: testconvert.String("rule1"), + Query: testconvert.String(`{"==":[{"var":"key"},"user-key"]}`), + VariationResult: testconvert.String("variation_B"), + }, + }, + DefaultRule: &flag.Rule{ + VariationResult: testconvert.String("variation_A"), + }, + Metadata: &map[string]interface{}{ + "description": "this is a flag", + "issue-link": "https://issue.link/GOFF-1", + }, + }, + args: args{ + flagName: "my-flag", + user: ffcontext.NewEvaluationContext("user-key"), + flagContext: flag.Context{ + DefaultSdkValue: false, + }, + }, + want: false, + want1: flag.ResolutionDetails{ + Variant: "variation_B", + Reason: flag.ReasonTargetingMatch, + RuleIndex: testconvert.Int(0), + RuleName: testconvert.String("rule1"), + Cacheable: true, + Metadata: map[string]interface{}{ + "description": "this is a flag", + "issue-link": "https://issue.link/GOFF-1", + }, + }, + }, { name: "Should match the 2nd rule", flag: flag.InternalFlag{ @@ -301,6 +343,54 @@ func TestInternalFlag_Value(t *testing.T) { }, }, }, + { + name: "Should match the 2nd rule (jsonLogic)", + flag: flag.InternalFlag{ + Variations: &map[string]*interface{}{ + "variation_A": testconvert.Interface("value_A"), + "variation_B": testconvert.Interface("value_B"), + "variation_C": testconvert.Interface("value_C"), + }, + Rules: &[]flag.Rule{ + { + Name: testconvert.String("rule1"), + Query: testconvert.String(`{"==":[{"var":"key"},"not-user-key"]}`), + VariationResult: testconvert.String("variation_C"), + }, + { + Name: testconvert.String("rule2"), + Query: testconvert.String(`{"==":[{"var":"key"},"user-key"]}`), + VariationResult: testconvert.String("variation_C"), + }, + }, + DefaultRule: &flag.Rule{ + VariationResult: testconvert.String("variation_A"), + }, + Metadata: &map[string]interface{}{ + "description": "this is a flag", + "issue-link": "https://issue.link/GOFF-1", + }, + }, + args: args{ + flagName: "my-flag", + user: ffcontext.NewEvaluationContext("user-key"), + flagContext: flag.Context{ + DefaultSdkValue: "value_default", + }, + }, + want: "value_C", + want1: flag.ResolutionDetails{ + Variant: "variation_C", + Reason: flag.ReasonTargetingMatch, + RuleIndex: testconvert.Int(1), + RuleName: testconvert.String("rule2"), + Cacheable: true, + Metadata: map[string]interface{}{ + "description": "this is a flag", + "issue-link": "https://issue.link/GOFF-1", + }, + }, + }, { name: "Should match a rule with no name", flag: flag.InternalFlag{ @@ -346,6 +436,51 @@ func TestInternalFlag_Value(t *testing.T) { }, }, }, + { + name: "Should match a rule with no name (jsonLogic)", + flag: flag.InternalFlag{ + Variations: &map[string]*interface{}{ + "variation_A": testconvert.Interface("value_A"), + "variation_B": testconvert.Interface("value_B"), + "variation_C": testconvert.Interface("value_C"), + }, + Rules: &[]flag.Rule{ + { + Query: testconvert.String(`{"==":[{"var":"key"},"not-user-key"]}`), + VariationResult: testconvert.String("variation_C"), + }, + { + Query: testconvert.String(`{"==":[{"var":"key"},"user-key"]}`), + VariationResult: testconvert.String("variation_C"), + }, + }, + DefaultRule: &flag.Rule{ + VariationResult: testconvert.String("variation_A"), + }, + Metadata: &map[string]interface{}{ + "description": "this is a flag", + "issue-link": "https://issue.link/GOFF-1", + }, + }, + args: args{ + flagName: "my-flag", + user: ffcontext.NewEvaluationContext("user-key"), + flagContext: flag.Context{ + DefaultSdkValue: "value_default", + }, + }, + want: "value_C", + want1: flag.ResolutionDetails{ + Variant: "variation_C", + Reason: flag.ReasonTargetingMatch, + RuleIndex: testconvert.Int(1), + Cacheable: true, + Metadata: map[string]interface{}{ + "description": "this is a flag", + "issue-link": "https://issue.link/GOFF-1", + }, + }, + }, { name: "Should match only the first rule that apply (even if more than one can apply)", flag: flag.InternalFlag{ @@ -396,6 +531,56 @@ func TestInternalFlag_Value(t *testing.T) { }, }, }, + { + name: "Should match only the first rule that apply (even if more than one can apply) (jsonLogic)", + flag: flag.InternalFlag{ + Variations: &map[string]*interface{}{ + "variation_A": testconvert.Interface("value_A"), + "variation_B": testconvert.Interface("value_B"), + "variation_C": testconvert.Interface("value_C"), + "variation_D": testconvert.Interface("value_D"), + }, + Rules: &[]flag.Rule{ + { + Query: testconvert.String(`{"==":[{"var":"key"},"not-user-key"]}`), + VariationResult: testconvert.String("variation_C"), + }, + { + Query: testconvert.String(`{"==":[{"var":"company"},"go-feature-flag"]}`), + VariationResult: testconvert.String("variation_D"), + }, + { + Query: testconvert.String(`{"==":[{"var":"key"},"user-key"]}`), + VariationResult: testconvert.String("variation_C"), + }, + }, + DefaultRule: &flag.Rule{ + VariationResult: testconvert.String("variation_A"), + }, + Metadata: &map[string]interface{}{ + "description": "this is a flag", + "issue-link": "https://issue.link/GOFF-1", + }, + }, + args: args{ + flagName: "my-flag", + user: ffcontext.NewEvaluationContextBuilder("user-key").AddCustom("company", "go-feature-flag").Build(), + flagContext: flag.Context{ + DefaultSdkValue: "value_default", + }, + }, + want: "value_D", + want1: flag.ResolutionDetails{ + Variant: "variation_D", + Reason: flag.ReasonTargetingMatch, + RuleIndex: testconvert.Int(1), + Cacheable: true, + Metadata: map[string]interface{}{ + "description": "this is a flag", + "issue-link": "https://issue.link/GOFF-1", + }, + }, + }, { name: "Should ignore a rule that is disabled", flag: flag.InternalFlag{ @@ -488,6 +673,47 @@ func TestInternalFlag_Value(t *testing.T) { }, }, }, + { + name: "Should return an error if rule is invalid (jsonLogic)", + flag: flag.InternalFlag{ + Variations: &map[string]*interface{}{ + "variation_A": testconvert.Interface("value_A"), + "variation_B": testconvert.Interface("value_B"), + "variation_C": testconvert.Interface("value_C"), + "variation_D": testconvert.Interface("value_D"), + }, + Rules: &[]flag.Rule{ + { + Query: testconvert.String(`{"==":[{"var":"key"},"user-key"]}`), + Percentages: &map[string]float64{}, + }, + }, + DefaultRule: &flag.Rule{ + VariationResult: testconvert.String("variation_A"), + }, + Metadata: &map[string]interface{}{ + "description": "this is a flag", + "issue-link": "https://issue.link/GOFF-1", + }, + }, + args: args{ + flagName: "my-flag", + user: ffcontext.NewEvaluationContextBuilder("user-key").Build(), + flagContext: flag.Context{ + DefaultSdkValue: "value_default", + }, + }, + want: "value_default", + want1: flag.ResolutionDetails{ + Variant: flag.VariationSDKDefault, + Reason: flag.ReasonError, + ErrorCode: flag.ErrorFlagConfiguration, + Metadata: map[string]interface{}{ + "description": "this is a flag", + "issue-link": "https://issue.link/GOFF-1", + }, + }, + }, { name: "Should return an error if no targeting match and we have no default rule", flag: flag.InternalFlag{ diff --git a/internal/flag/rule_test.go b/internal/flag/rule_test.go index 00ae6e32f90..57477c48af8 100644 --- a/internal/flag/rule_test.go +++ b/internal/flag/rule_test.go @@ -497,6 +497,50 @@ func TestRule_Evaluate(t *testing.T) { args: args{}, wantErr: assert.Error, }, + { + name: "User does not match the query (JsonLogic)", + rule: flag.Rule{ + Name: testconvert.String("rule1"), + VariationResult: testconvert.String("variation_A"), + Query: testconvert.String(`{"==": [{"var": "key"}, "def"]}`), + }, + args: args{ + isDefault: false, + user: ffcontext.NewEvaluationContext("abc"), + }, + wantErr: assert.Error, + }, + { + name: "User match the query (JsonLogic)", + rule: flag.Rule{ + Name: testconvert.String("rule1"), + VariationResult: testconvert.String("variation_A"), + Query: testconvert.String(`{"==": [{"var": "key"}, "abc"]}`), + }, + args: args{ + isDefault: false, + user: ffcontext.NewEvaluationContext("abc"), + }, + want: "variation_A", + wantErr: assert.NoError, + }, + { + name: "Percentage + user match query (JsonLogic)", + rule: flag.Rule{ + Name: testconvert.String("rule1"), + Percentages: &map[string]float64{ + "variation_D": 70, + "variation_C": 10, + "variation_B": 20, + }, + Query: testconvert.String(`{"==": [{"var": "key"}, "96ac59e6-7492-436b-b15a-ba1d797d2423"]}`), + }, + args: args{ + user: ffcontext.NewEvaluationContext("96ac59e6-7492-436b-b15a-ba1d797d2423"), + }, + wantErr: assert.NoError, + want: "variation_B", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) {