Skip to content

Commit

Permalink
Merge pull request #2779 from flipt-io/entity-id-constraint
Browse files Browse the repository at this point in the history
feat(flipt): add entityId as a constraint type and operator
  • Loading branch information
yquansah authored Feb 19, 2024
2 parents fd927a5 + e99fef4 commit f9978cf
Show file tree
Hide file tree
Showing 19 changed files with 555 additions and 248 deletions.
28 changes: 22 additions & 6 deletions build/testing/integration/readonly/readonly_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ func TestReadOnly(t *testing.T) {
NamespaceKey: namespace,
})
require.NoError(t, err)
require.Len(t, flags.Flags, 55)
require.Len(t, flags.Flags, 56)

flag := flags.Flags[0]
assert.Equal(t, namespace, flag.NamespaceKey)
Expand Down Expand Up @@ -145,7 +145,7 @@ func TestReadOnly(t *testing.T) {

if flags.NextPageToken == "" {
// ensure last page contains 3 entries (boolean and disabled)
assert.Len(t, flags.Flags, 5)
assert.Len(t, flags.Flags, 6)

found = append(found, flags.Flags...)

Expand All @@ -160,7 +160,7 @@ func TestReadOnly(t *testing.T) {
nextPage = flags.NextPageToken
}

require.Len(t, found, 55)
require.Len(t, found, 56)
})
})

Expand Down Expand Up @@ -205,7 +205,7 @@ func TestReadOnly(t *testing.T) {
})

require.NoError(t, err)
require.Len(t, segments.Segments, 52)
require.Len(t, segments.Segments, 53)

t.Run("Paginated (page size 10)", func(t *testing.T) {
var (
Expand All @@ -225,7 +225,7 @@ func TestReadOnly(t *testing.T) {
found = append(found, segments.Segments...)

if segments.NextPageToken == "" {
assert.Len(t, segments.Segments, 2)
assert.Len(t, segments.Segments, 3)
break
}

Expand All @@ -234,7 +234,7 @@ func TestReadOnly(t *testing.T) {
nextPage = segments.NextPageToken
}

require.Len(t, found, 52)
require.Len(t, found, 53)
})
})

Expand Down Expand Up @@ -498,6 +498,22 @@ func TestReadOnly(t *testing.T) {
assert.Equal(t, evaluation.EvaluationReason_UNKNOWN_EVALUATION_REASON, response.Reason)
})

t.Run("entity id matching", func(t *testing.T) {
response, err := sdk.Evaluation().Variant(ctx, &evaluation.EvaluationRequest{
NamespaceKey: namespace,
FlagKey: "flag_using_entity_id",
EntityId: "[email protected]",
Context: map[string]string{},
})
require.NoError(t, err)

assert.Equal(t, true, response.Match)
assert.Equal(t, "variant_001", response.VariantKey)
assert.Equal(t, "flag_using_entity_id", response.FlagKey)
assert.Equal(t, evaluation.EvaluationReason_MATCH_EVALUATION_REASON, response.Reason)
assert.Contains(t, response.SegmentKeys, "segment_entity_id")
})

t.Run("flag disabled", func(t *testing.T) {
result, err := sdk.Evaluation().Variant(ctx, &evaluation.EvaluationRequest{
NamespaceKey: namespace,
Expand Down
22 changes: 22 additions & 0 deletions build/testing/integration/readonly/testdata/main/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15623,6 +15623,19 @@ flags:
segment:
key: segment_no_constraints
value: true
- key: flag_using_entity_id
name: FLAG_USING_ENTITY_ID
type: VARIANT_FLAG_TYPE
description: Flag using entity id
enabled: true
variants:
- key: variant_001
name: VARIANT_001
rules:
- segment: segment_entity_id
distributions:
- variant: variant_001
rollout: 100
segments:
- key: segment_001
name: SEGMENT_001
Expand Down Expand Up @@ -16283,3 +16296,12 @@ segments:
operator: eq
value: segment
match_type: ALL_MATCH_TYPE
- key: segment_entity_id
name: SEGMENT_ENTITY_ID
description: Some Segment Description
constraints:
- type: ENTITY_ID_COMPARISON_TYPE
property: entity
operator: eq
value: [email protected]
match_type: ALL_MATCH_TYPE
28 changes: 23 additions & 5 deletions build/testing/integration/readonly/testdata/main/production.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15597,7 +15597,7 @@ flags:
percentage: 50
value: true
- key: flag_boolean_and_segments
name: FLAG_BOOLEAN
name: FLAG_BOOLEAN_AND_SEGMENTS
type: BOOLEAN_FLAG_TYPE
description: And segments for boolean flags
enabled: false
Expand All @@ -15624,15 +15624,24 @@ flags:
segment:
key: segment_no_constraints
value: true
- key: flag_using_entity_id
name: FLAG_USING_ENTITY_ID
type: VARIANT_FLAG_TYPE
description: Flag using entity id
enabled: true
variants:
- key: variant_001
name: VARIANT_001
rules:
- segment: segment_entity_id
distributions:
- variant: variant_001
rollout: 100
segments:
- key: segment_001
name: SEGMENT_001
description: Some Segment Description
constraints:
- type: STRING_COMPARISON_TYPE
property: in_segment
operator: eq
value: segment_001
- type: STRING_COMPARISON_TYPE
property: in_segment
operator: eq
Expand Down Expand Up @@ -16288,3 +16297,12 @@ segments:
operator: eq
value: segment
match_type: ALL_MATCH_TYPE
- key: segment_entity_id
name: SEGMENT_ENTITY_ID
description: Some Segment Description
constraints:
- type: ENTITY_ID_COMPARISON_TYPE
property: entity
operator: eq
value: [email protected]
match_type: ALL_MATCH_TYPE
2 changes: 1 addition & 1 deletion internal/cue/extended.cue
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#Flag: {
description: =~"^.+$"
}
}
6 changes: 6 additions & 0 deletions internal/cue/flipt.cue
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,10 @@ close({
value?: string
description?: string
operator: "eq" | "neq" | "present" | "notpresent" | "le" | "lte" | "gt" | "gte"
} | {
type: "ENTITY_ID_COMPARISON_TYPE"
property: string & =~"^.+$"
value?: string
description?: string
operator: "eq" | "neq" | "isoneof" | "isnotoneof"
})
2 changes: 2 additions & 0 deletions internal/server/evaluation/data/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ func toEvaluationConstraintComparisonType(c flipt.ComparisonType) evaluation.Eva
return evaluation.EvaluationConstraintComparisonType_DATETIME_CONSTRAINT_COMPARISON_TYPE
case flipt.ComparisonType_BOOLEAN_COMPARISON_TYPE:
return evaluation.EvaluationConstraintComparisonType_BOOLEAN_CONSTRAINT_COMPARISON_TYPE
case flipt.ComparisonType_ENTITY_ID_COMPARISON_TYPE:
return evaluation.EvaluationConstraintComparisonType_ENTITY_ID_CONSTRAINT_COMPARISON_TYPE
}
return evaluation.EvaluationConstraintComparisonType_UNKNOWN_CONSTRAINT_COMPARISON_TYPE
}
Expand Down
2 changes: 1 addition & 1 deletion internal/server/evaluation/evaluation.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ func (s *Server) boolean(ctx context.Context, flag *flipt.Flag, r *rpcevaluation

for k, v := range rollout.Segment.Segments {
segmentKeys = append(segmentKeys, k)
matched, reason, err := matchConstraints(r.Context, v.Constraints, v.MatchType)
matched, reason, err := matchConstraints(r.Context, v.Constraints, v.MatchType, r.EntityId)
if err != nil {
return nil, err
}
Expand Down
57 changes: 57 additions & 0 deletions internal/server/evaluation/evaluation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,63 @@ func TestBoolean_SegmentMatch_MultipleConstraints(t *testing.T) {
assert.Equal(t, flagKey, res.FlagKey)
}

func TestBoolean_SegmentMatch_Constraint_EntityId(t *testing.T) {
var (
flagKey = "test-flag"
namespaceKey = "test-namespace"
store = &evaluationStoreMock{}
logger = zaptest.NewLogger(t)
s = New(logger, store)
)

store.On("GetFlag", mock.Anything, storage.NewResource(namespaceKey, flagKey)).Return(
&flipt.Flag{
NamespaceKey: "test-namespace",
Key: "test-flag",
Enabled: true,
Type: flipt.FlagType_BOOLEAN_FLAG_TYPE,
}, nil)

store.On("GetEvaluationRollouts", mock.Anything, storage.NewResource(namespaceKey, flagKey)).Return([]*storage.EvaluationRollout{
{
NamespaceKey: namespaceKey,
RolloutType: flipt.RolloutType_SEGMENT_ROLLOUT_TYPE,
Rank: 1,
Segment: &storage.RolloutSegment{
Value: true,
SegmentOperator: flipt.SegmentOperator_OR_SEGMENT_OPERATOR,
Segments: map[string]*storage.EvaluationSegment{
"test-segment": {
SegmentKey: "test-segment",
MatchType: flipt.MatchType_ANY_MATCH_TYPE,
Constraints: []storage.EvaluationConstraint{
{
Type: flipt.ComparisonType_ENTITY_ID_COMPARISON_TYPE,
Property: "entity",
Operator: flipt.OpEQ,
Value: "[email protected]",
},
},
},
},
},
},
}, nil)

res, err := s.Boolean(context.TODO(), &rpcevaluation.EvaluationRequest{
FlagKey: flagKey,
EntityId: "[email protected]",
NamespaceKey: namespaceKey,
Context: map[string]string{},
})

require.NoError(t, err)

assert.Equal(t, true, res.Enabled)
assert.Equal(t, rpcevaluation.EvaluationReason_MATCH_EVALUATION_REASON, res.Reason)
assert.Equal(t, flagKey, res.FlagKey)
}

func TestBoolean_SegmentMatch_MultipleSegments_WithAnd(t *testing.T) {
var (
flagKey = "test-flag"
Expand Down
6 changes: 4 additions & 2 deletions internal/server/evaluation/legacy_evaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ func (e *Evaluator) Evaluate(ctx context.Context, flag *flipt.Flag, r *evaluatio
segmentMatches := 0

for k, v := range rule.Segments {
matched, reason, err := matchConstraints(r.Context, v.Constraints, v.MatchType)
matched, reason, err := matchConstraints(r.Context, v.Constraints, v.MatchType, r.EntityId)
if err != nil {
resp.Reason = flipt.EvaluationReason_ERROR_EVALUATION_REASON
return resp, err
Expand Down Expand Up @@ -222,7 +222,7 @@ func (e *Evaluator) Evaluate(ctx context.Context, flag *flipt.Flag, r *evaluatio

// matchConstraints is a utility function that will return if all or any constraints have matched for a segment depending
// on the match type.
func matchConstraints(evalCtx map[string]string, constraints []storage.EvaluationConstraint, segmentMatchType flipt.MatchType) (bool, string, error) {
func matchConstraints(evalCtx map[string]string, constraints []storage.EvaluationConstraint, segmentMatchType flipt.MatchType, entityId string) (bool, string, error) {
constraintMatches := 0

var reason string
Expand All @@ -244,6 +244,8 @@ func matchConstraints(evalCtx map[string]string, constraints []storage.Evaluatio
match, err = matchesBool(c, v)
case flipt.ComparisonType_DATETIME_COMPARISON_TYPE:
match, err = matchesDateTime(c, v)
case flipt.ComparisonType_ENTITY_ID_COMPARISON_TYPE:
match = matchesString(c, entityId)
default:
return false, reason, errs.ErrInvalid("unknown constraint type")
}
Expand Down
65 changes: 65 additions & 0 deletions internal/server/evaluation/legacy_evaluator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2309,6 +2309,71 @@ func TestEvaluator_MatchAny_RolloutDistribution_MultiRule(t *testing.T) {
assert.Equal(t, flipt.EvaluationReason_MATCH_EVALUATION_REASON, resp.Reason)
}

func TestEvaluator_MatchEntityId(t *testing.T) {
var (
store = &evaluationStoreMock{}
logger = zaptest.NewLogger(t)
s = NewEvaluator(logger, store)
)

store.On("GetEvaluationRules", mock.Anything, storage.NewResource("", "foo")).Return(
[]*storage.EvaluationRule{
{
ID: "1",
FlagKey: "foo",
Rank: 0,
Segments: map[string]*storage.EvaluationSegment{
"subscribers": {
SegmentKey: "subscribers",
MatchType: flipt.MatchType_ANY_MATCH_TYPE,
Constraints: []storage.EvaluationConstraint{
{
ID: "2",
Type: flipt.ComparisonType_ENTITY_ID_COMPARISON_TYPE,
Property: "entity",
Operator: flipt.OpEQ,
Value: "[email protected]",
},
},
},
},
},
}, nil)

store.On("GetEvaluationDistributions", mock.Anything, storage.NewID("1")).Return(
[]*storage.EvaluationDistribution{
{
ID: "4",
RuleID: "1",
VariantID: "5",
Rollout: 50,
VariantKey: "released",
},
{
ID: "6",
RuleID: "1",
VariantID: "7",
Rollout: 50,
VariantKey: "unreleased",
},
}, nil)

resp, err := s.Evaluate(context.TODO(), enabledFlag, &evaluation.EvaluationRequest{
FlagKey: "foo",
EntityId: "[email protected]",
Context: map[string]string{},
})

assert.NoError(t, err)

assert.NotNil(t, resp)
assert.True(t, resp.Match)
assert.Equal(t, "subscribers", resp.SegmentKey)
assert.Equal(t, "foo", resp.FlagKey)
assert.NotEmpty(t, resp.Value)
assert.Equal(t, flipt.EvaluationReason_MATCH_EVALUATION_REASON, resp.Reason)
}

func TestEvaluator_MatchAny_NoConstraints(t *testing.T) {
var (
store = &evaluationStoreMock{}
Expand Down
Loading

0 comments on commit f9978cf

Please sign in to comment.