Skip to content

Commit d4f67a4

Browse files
Merge pull request #394 from splitio/rb_segments_evaluator
[Rule-based segments] Update evaluator to support rule-based segments
2 parents 3e2e2a9 + 9f507fe commit d4f67a4

File tree

14 files changed

+57
-53
lines changed

14 files changed

+57
-53
lines changed

src/dtos/types.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ interface IInSegmentMatcher extends ISplitMatcherBase {
6666
userDefinedSegmentMatcherData: IInSegmentMatcherData
6767
}
6868

69+
interface IInRBSegmentMatcher extends ISplitMatcherBase {
70+
matcherType: 'IN_RULE_BASED_SEGMENT',
71+
userDefinedSegmentMatcherData: IInSegmentMatcherData
72+
}
73+
6974
interface IInLargeSegmentMatcher extends ISplitMatcherBase {
7075
matcherType: 'IN_LARGE_SEGMENT',
7176
userDefinedLargeSegmentMatcherData: IInLargeSegmentMatcherData
@@ -176,7 +181,7 @@ export type ISplitMatcher = IAllKeysMatcher | IInSegmentMatcher | IWhitelistMatc
176181
ILessThanOrEqualToMatcher | IBetweenMatcher | IEqualToSetMatcher | IContainsAnyOfSetMatcher | IContainsAllOfSetMatcher | IPartOfSetMatcher |
177182
IStartsWithMatcher | IEndsWithMatcher | IContainsStringMatcher | IInSplitTreatmentMatcher | IEqualToBooleanMatcher | IMatchesStringMatcher |
178183
IEqualToSemverMatcher | IGreaterThanOrEqualToSemverMatcher | ILessThanOrEqualToSemverMatcher | IBetweenSemverMatcher | IInListSemverMatcher |
179-
IInLargeSegmentMatcher
184+
IInLargeSegmentMatcher | IInRBSegmentMatcher
180185

181186
/** Split object */
182187
export interface ISplitPartition {
@@ -189,30 +194,30 @@ export interface ISplitCondition {
189194
combiner: 'AND',
190195
matchers: ISplitMatcher[]
191196
}
192-
partitions: ISplitPartition[]
193-
label: string
194-
conditionType: 'ROLLOUT' | 'WHITELIST'
197+
partitions?: ISplitPartition[]
198+
label?: string
199+
conditionType?: 'ROLLOUT' | 'WHITELIST'
195200
}
196201

197202
export interface IRBSegment {
198203
name: string,
199204
changeNumber: number,
200205
status: 'ACTIVE' | 'ARCHIVED',
206+
conditions: ISplitCondition[],
201207
excluded: {
202208
keys: string[],
203209
segments: string[]
204-
},
205-
conditions: ISplitCondition[],
210+
}
206211
}
207212

208213
export interface ISplit {
209214
name: string,
210215
changeNumber: number,
216+
status: 'ACTIVE' | 'ARCHIVED',
217+
conditions: ISplitCondition[],
211218
killed: boolean,
212219
defaultTreatment: string,
213220
trafficTypeName: string,
214-
conditions: ISplitCondition[],
215-
status: 'ACTIVE' | 'ARCHIVED',
216221
seed: number,
217222
trafficAllocation?: number,
218223
trafficAllocationSeed?: number

src/evaluator/Engine.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export class Engine {
7373
trafficAllocationSeed,
7474
attributes,
7575
splitEvaluator
76-
);
76+
) as MaybeThenable<IEvaluation>;
7777

7878
// Evaluation could be async, so we should handle that case checking for a
7979
// thenable object

src/evaluator/combiners/and.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ import { findIndex } from '../../utils/lang';
22
import { ILogger } from '../../logger/types';
33
import { thenable } from '../../utils/promise/thenable';
44
import { MaybeThenable } from '../../dtos/types';
5-
import { IMatcher } from '../types';
5+
import { ISplitEvaluator } from '../types';
66
import { ENGINE_COMBINER_AND } from '../../logger/constants';
7+
import SplitIO from '../../../types/splitio';
78

8-
export function andCombinerContext(log: ILogger, matchers: IMatcher[]) {
9+
export function andCombinerContext(log: ILogger, matchers: Array<(key: SplitIO.SplitKey, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) => MaybeThenable<boolean>>) {
910

1011
function andResults(results: boolean[]): boolean {
1112
// Array.prototype.every is supported by target environments
@@ -15,8 +16,8 @@ export function andCombinerContext(log: ILogger, matchers: IMatcher[]) {
1516
return hasMatchedAll;
1617
}
1718

18-
return function andCombiner(...params: any): MaybeThenable<boolean> {
19-
const matcherResults = matchers.map(matcher => matcher(...params));
19+
return function andCombiner(key: SplitIO.SplitKey, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator): MaybeThenable<boolean> {
20+
const matcherResults = matchers.map(matcher => matcher(key, attributes, splitEvaluator));
2021

2122
// If any matching result is a thenable we should use Promise.all
2223
if (findIndex(matcherResults, thenable) !== -1) {

src/evaluator/combiners/ifelseif.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { findIndex } from '../../utils/lang';
1+
import { findIndex, isBoolean } from '../../utils/lang';
22
import { ILogger } from '../../logger/types';
33
import { thenable } from '../../utils/promise/thenable';
44
import { UNSUPPORTED_MATCHER_TYPE } from '../../utils/labels';
@@ -18,14 +18,12 @@ export function ifElseIfCombinerContext(log: ILogger, predicates: IEvaluator[]):
1818
};
1919
}
2020

21-
function computeTreatment(predicateResults: Array<IEvaluation | undefined>) {
22-
const len = predicateResults.length;
23-
24-
for (let i = 0; i < len; i++) {
21+
function computeEvaluation(predicateResults: Array<IEvaluation | boolean | undefined>): IEvaluation | boolean | undefined {
22+
for (let i = 0, len = predicateResults.length; i < len; i++) {
2523
const evaluation = predicateResults[i];
2624

2725
if (evaluation !== undefined) {
28-
log.debug(ENGINE_COMBINER_IFELSEIF, [evaluation.treatment]);
26+
if (!isBoolean(evaluation)) log.debug(ENGINE_COMBINER_IFELSEIF, [evaluation.treatment]);
2927

3028
return evaluation;
3129
}
@@ -35,18 +33,18 @@ export function ifElseIfCombinerContext(log: ILogger, predicates: IEvaluator[]):
3533
return undefined;
3634
}
3735

38-
function ifElseIfCombiner(key: SplitIO.SplitKey, seed?: number, trafficAllocation?: number, trafficAllocationSeed?: number, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) {
36+
function ifElseIfCombiner(key: SplitIO.SplitKeyObject, seed?: number, trafficAllocation?: number, trafficAllocationSeed?: number, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) {
3937
// In Async environments we are going to have async predicates. There is none way to know
4038
// before hand so we need to evaluate all the predicates, verify for thenables, and finally,
4139
// define how to return the treatment (wrap result into a Promise or not).
4240
const predicateResults = predicates.map(evaluator => evaluator(key, seed, trafficAllocation, trafficAllocationSeed, attributes, splitEvaluator));
4341

4442
// if we find a thenable
4543
if (findIndex(predicateResults, thenable) !== -1) {
46-
return Promise.all(predicateResults).then(results => computeTreatment(results));
44+
return Promise.all(predicateResults).then(results => computeEvaluation(results));
4745
}
4846

49-
return computeTreatment(predicateResults as IEvaluation[]);
47+
return computeEvaluation(predicateResults as IEvaluation[]);
5048
}
5149

5250
// if there is none predicates, then there was an error in parsing phase

src/evaluator/condition/index.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,27 @@ import SplitIO from '../../../types/splitio';
77
import { ILogger } from '../../logger/types';
88

99
// Build Evaluation object if and only if matchingResult is true
10-
function match(log: ILogger, matchingResult: boolean, bucketingKey: string | undefined, seed: number | undefined, treatments: { getTreatmentFor: (x: number) => string }, label: string): IEvaluation | undefined {
10+
function match(log: ILogger, matchingResult: boolean, bucketingKey: string | undefined, seed?: number, treatments?: { getTreatmentFor: (x: number) => string }, label?: string): IEvaluation | boolean | undefined {
1111
if (matchingResult) {
12-
const treatment = getTreatment(log, bucketingKey as string, seed, treatments);
13-
14-
return {
15-
treatment,
16-
label
17-
};
12+
return treatments ? // Feature flag
13+
{
14+
treatment: getTreatment(log, bucketingKey as string, seed, treatments),
15+
label: label!
16+
} : // Rule-based segment
17+
true;
1818
}
1919

2020
// else we should notify the engine to continue evaluating
2121
return undefined;
2222
}
2323

2424
// Condition factory
25-
export function conditionContext(log: ILogger, matcherEvaluator: (...args: any) => MaybeThenable<boolean>, treatments: { getTreatmentFor: (x: number) => string }, label: string, conditionType: 'ROLLOUT' | 'WHITELIST'): IEvaluator {
25+
export function conditionContext(log: ILogger, matcherEvaluator: (key: SplitIO.SplitKeyObject, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) => MaybeThenable<boolean>, treatments?: { getTreatmentFor: (x: number) => string }, label?: string, conditionType?: 'ROLLOUT' | 'WHITELIST'): IEvaluator {
2626

27-
return function conditionEvaluator(key: SplitIO.SplitKey, seed?: number, trafficAllocation?: number, trafficAllocationSeed?: number, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) {
27+
return function conditionEvaluator(key: SplitIO.SplitKeyObject, seed?: number, trafficAllocation?: number, trafficAllocationSeed?: number, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) {
2828

2929
// Whitelisting has more priority than traffic allocation, so we don't apply this filtering to those conditions.
30-
if (conditionType === 'ROLLOUT' && !shouldApplyRollout(trafficAllocation as number, (key as SplitIO.SplitKeyObject).bucketingKey as string, trafficAllocationSeed as number)) {
30+
if (conditionType === 'ROLLOUT' && !shouldApplyRollout(trafficAllocation!, key.bucketingKey, trafficAllocationSeed!)) {
3131
return {
3232
treatment: undefined, // treatment value is assigned later
3333
label: NOT_IN_SPLIT
@@ -41,10 +41,10 @@ export function conditionContext(log: ILogger, matcherEvaluator: (...args: any)
4141
const matches = matcherEvaluator(key, attributes, splitEvaluator);
4242

4343
if (thenable(matches)) {
44-
return matches.then(result => match(log, result, (key as SplitIO.SplitKeyObject).bucketingKey, seed, treatments, label));
44+
return matches.then(result => match(log, result, key.bucketingKey, seed, treatments, label));
4545
}
4646

47-
return match(log, matches, (key as SplitIO.SplitKeyObject).bucketingKey, seed, treatments, label);
47+
return match(log, matches, key.bucketingKey, seed, treatments, label);
4848
};
4949

5050
}

src/evaluator/matchers/__tests__/between.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,6 @@ test('MATCHER BETWEEN / should return true ONLY when the value is between 10 and
1919
expect(matcher(15)).toBe(true); // 15 is between 10 and 20
2020
expect(matcher(20)).toBe(true); // 20 is between 10 and 20
2121
expect(matcher(21)).toBe(false); // 21 is not between 10 and 20
22-
expect(matcher(undefined)).toBe(false); // undefined is not between 10 and 20
23-
expect(matcher(null)).toBe(false); // null is not between 10 and 20
22+
expect(matcher(undefined as any)).toBe(false); // undefined is not between 10 and 20
23+
expect(matcher(null as any)).toBe(false); // null is not between 10 and 20
2424
});

src/evaluator/matchers/__tests__/segment/client_side.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ test('MATCHER IN_SEGMENT / should return true ONLY when the segment is defined i
2929
}
3030
} as IStorageSync) as IMatcher;
3131

32-
expect(await matcherTrue()).toBe(true); // segment found in mySegments list
33-
expect(await matcherFalse()).toBe(false); // segment not found in mySegments list
32+
expect(await matcherTrue('key')).toBe(true); // segment found in mySegments list
33+
expect(await matcherFalse('key')).toBe(false); // segment not found in mySegments list
3434
});
3535

3636
test('MATCHER IN_LARGE_SEGMENT / should return true ONLY when the segment is defined inside the segment storage', async function () {
@@ -54,6 +54,6 @@ test('MATCHER IN_LARGE_SEGMENT / should return true ONLY when the segment is def
5454
largeSegments: undefined
5555
} as IStorageSync) as IMatcher;
5656

57-
expect(await matcherTrue()).toBe(true); // large segment found in mySegments list
58-
expect(await matcherFalse()).toBe(false); // large segment storage is not defined
57+
expect(await matcherTrue('key')).toBe(true); // large segment found in mySegments list
58+
expect(await matcherFalse('key')).toBe(false); // large segment storage is not defined
5959
});

src/evaluator/matchers/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,5 +64,5 @@ export function matcherFactory(log: ILogger, matcherDto: IMatcherDto, storage?:
6464
let matcherFn;
6565
// @ts-ignore
6666
if (matchers[type]) matcherFn = matchers[type](value, storage, log); // There is no index-out-of-bound exception in JavaScript
67-
return matcherFn;
67+
return matcherFn as IMatcher;
6868
}

src/evaluator/parser/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export function parser(log: ILogger, conditions: ISplitCondition[], storage: ISt
3737
}
3838

3939
// Evaluator function.
40-
return (key: string, attributes: SplitIO.Attributes | undefined, splitEvaluator: ISplitEvaluator) => {
40+
return (key: SplitIO.SplitKey, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) => {
4141
const value = sanitizeValue(log, key, matcherDto, attributes);
4242
let result: MaybeThenable<boolean> = false;
4343

@@ -71,12 +71,12 @@ export function parser(log: ILogger, conditions: ISplitCondition[], storage: ISt
7171
predicates.push(conditionContext(
7272
log,
7373
andCombinerContext(log, expressions),
74-
Treatments.parse(partitions),
74+
partitions && Treatments.parse(partitions),
7575
label,
7676
conditionType
7777
));
7878
}
7979

80-
// Instanciate evaluator given the set of conditions using if else if logic
80+
// Instantiate evaluator given the set of conditions using if else if logic
8181
return ifElseIfCombinerContext(log, predicates);
8282
}

src/evaluator/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,6 @@ export type IEvaluationResult = IEvaluation & { treatment: string; impressionsDi
2929

3030
export type ISplitEvaluator = (log: ILogger, key: SplitIO.SplitKey, splitName: string, attributes: SplitIO.Attributes | undefined, storage: IStorageSync | IStorageAsync) => MaybeThenable<IEvaluation>
3131

32-
export type IEvaluator = (key: SplitIO.SplitKey, seed?: number, trafficAllocation?: number, trafficAllocationSeed?: number, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) => MaybeThenable<IEvaluation | undefined>
32+
export type IEvaluator = (key: SplitIO.SplitKeyObject, seed?: number, trafficAllocation?: number, trafficAllocationSeed?: number, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) => MaybeThenable<IEvaluation | boolean | undefined>
3333

34-
export type IMatcher = (...args: any) => MaybeThenable<boolean>
34+
export type IMatcher = (value: string | number | boolean | string[] | IDependencyMatcherValue, splitEvaluator?: ISplitEvaluator) => MaybeThenable<boolean>

0 commit comments

Comments
 (0)