Skip to content

Commit ac8b546

Browse files
committed
feat: re-implemented-in-condition
1 parent 88777e6 commit ac8b546

4 files changed

Lines changed: 127 additions & 18 deletions

File tree

flagsmith-engine/evaluationContext/models.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export type SegmentKey = SegmentContext['key'];
2222
export type SegmentName = SegmentContext['name'];
2323
export type SegmentRuleType = SegmentRule['type'];
2424
export type ConditionOperator = SegmentCondition['operator'] | InSegmentCondition['operator'];
25-
export type ConditionProperty = SegmentCondition['property'];
25+
export type ConditionProperty = SegmentCondition['property'] | InSegmentCondition['property'];
2626
export type ConditionValue = SegmentCondition['value'] | InSegmentCondition['value'];
2727

2828
export type FeatureKey = FeatureContext['feature_key'];

flagsmith-engine/segments/evaluators.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as jsonpath from 'jsonpath';
22
import {
33
EvaluationContext,
4+
InSegmentCondition,
45
SegmentCondition,
56
SegmentContext,
67
SegmentRule
@@ -49,16 +50,14 @@ export function evaluateIdentityInSegment(
4950
* @returns true if the condition matches
5051
*/
5152
export function traitsMatchSegmentCondition(
52-
condition: SegmentCondition,
53+
condition: SegmentCondition | InSegmentCondition,
5354
segmentKey: string,
5455
context?: EvaluationContext
5556
): boolean {
56-
// This could be any context value and identity key is the fallback ($.environment.key / $.environment.name ...) => getContextValue
57-
// We need to re-implement the IN operator for context values (especially because of the JSONEncodedList + context values)
58-
const identityKey = context?.identity?.key || '';
59-
6057
if (condition.operator === PERCENTAGE_SPLIT) {
61-
const hashedPercentage = getHashedPercentageForObjIds([segmentKey, identityKey]);
58+
const contextValueKey =
59+
getContextValue(condition.property, context) || context?.identity?.key;
60+
const hashedPercentage = getHashedPercentageForObjIds([segmentKey, contextValueKey]);
6261
return hashedPercentage <= parseFloat(String(condition.value));
6362
}
6463
if (!condition.property) {
@@ -160,7 +159,7 @@ function getTraitValue(property: string, context?: EvaluationContext): any {
160159
* @returns The resolved value, or undefined if path doesn't exist or is invalid
161160
*/
162161
export function getContextValue(jsonPath: string, context?: EvaluationContext): any {
163-
if (!context || !jsonPath.startsWith('$.')) return undefined;
162+
if (!context || !jsonPath?.startsWith('$.')) return undefined;
164163

165164
try {
166165
const results = jsonpath.query(context, jsonPath);

flagsmith-engine/segments/models.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,12 @@ export class SegmentConditionModel {
6565
};
6666

6767
operator: string;
68-
value: string | null | undefined;
68+
value: string | null | undefined | string[];
6969
property: string | null | undefined;
7070

7171
constructor(
7272
operator: string,
73-
value?: string | null | undefined,
73+
value?: string | null | undefined | string[],
7474
property?: string | null | undefined
7575
) {
7676
this.operator = operator;
@@ -88,21 +88,32 @@ export class SegmentConditionModel {
8888
);
8989
},
9090
evaluateRegex: (traitValue: any) => {
91-
return !!this.value && !!traitValue?.toString().match(new RegExp(this.value));
91+
return (
92+
!!this.value &&
93+
!!traitValue?.toString().match(new RegExp(this.value?.toString()))
94+
);
9295
},
9396
evaluateModulo: (traitValue: any) => {
9497
if (isNaN(parseFloat(traitValue)) || !this.value) {
9598
return false;
9699
}
97-
const parts = this.value.split('|');
100+
const parts = this.value?.toString().split('|');
98101
const [divisor, reminder] = [parseFloat(parts[0]), parseFloat(parts[1])];
99102
return traitValue % divisor === reminder;
100103
},
101-
evaluateIn: (traitValue: any) => {
102-
// Looks for a list => all good but checks if it's a list of string
103-
// If it's a string => Assume it's a json encoded list
104-
// Fallback to the old logic
105-
// Add some tests
104+
evaluateIn: (traitValue: string[] | string) => {
105+
if (Array.isArray(this.value)) {
106+
return this.value.includes(traitValue.toString());
107+
}
108+
109+
if (typeof this.value === 'string') {
110+
try {
111+
const parsed = JSON.parse(this.value);
112+
if (Array.isArray(parsed)) {
113+
return parsed.includes(traitValue.toString());
114+
}
115+
} catch {}
116+
}
106117
return this.value?.split(',').includes(traitValue.toString());
107118
}
108119
};

tests/engine/unit/segments/segment_evaluators.test.ts

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,12 @@ import { getHashedPercentageForObjIds } from '../../../../flagsmith-engine/utils
1717
import { getEvaluationContext } from '../../../../flagsmith-engine/evaluationContext/mappers.js';
1818
import {
1919
EvaluationContext,
20+
InSegmentCondition,
2021
SegmentCondition,
22+
SegmentCondition1,
2123
SegmentContext
2224
} from '../../../../flagsmith-engine/evaluationContext/evaluationContext.types.js';
25+
import { SegmentConditionModel } from '../../../../flagsmith-engine/segments/models.js';
2326

2427
// todo: work out how to implement this in a test function or before hook
2528
vi.mock('../../../../flagsmith-engine/utils/hashing', () => ({
@@ -203,6 +206,98 @@ describe('getIdentitySegments integration', () => {
203206
});
204207
});
205208

209+
describe('IN operator', () => {
210+
const mockContext: EvaluationContext = {
211+
environment: { key: 'env', name: 'test' },
212+
identity: {
213+
key: 'test-user',
214+
identifier: 'test',
215+
traits: { name: 'test' }
216+
},
217+
segments: {},
218+
features: {}
219+
};
220+
221+
test.each([
222+
// Array of strings
223+
[
224+
{
225+
property: '$.identity.identifier',
226+
operator: CONDITION_OPERATORS.IN,
227+
value: ['test', 'john-doe']
228+
},
229+
true
230+
],
231+
[
232+
{
233+
property: '$.identity.identifier',
234+
operator: CONDITION_OPERATORS.IN,
235+
value: ['john-doe']
236+
},
237+
false
238+
],
239+
240+
// JSON encoded
241+
[
242+
{
243+
property: '$.identity.identifier',
244+
operator: CONDITION_OPERATORS.IN,
245+
value: '["test", "john-doe"]'
246+
},
247+
true
248+
],
249+
[
250+
{
251+
property: '$.identity.identifier',
252+
operator: CONDITION_OPERATORS.IN,
253+
value: '["john-doe"]'
254+
},
255+
false
256+
],
257+
258+
// Legacy value string to split
259+
[
260+
{
261+
property: '$.identity.identifier',
262+
operator: CONDITION_OPERATORS.IN,
263+
value: 'test,john-doe'
264+
},
265+
true
266+
],
267+
[
268+
{
269+
property: '$.identity.identifier',
270+
operator: CONDITION_OPERATORS.IN,
271+
value: 'john-doe'
272+
},
273+
false
274+
],
275+
// Fails because the value is split in middle
276+
[
277+
{
278+
property: '$.identity.identifier',
279+
operator: CONDITION_OPERATORS.IN,
280+
value: 'te,st,john-doe'
281+
},
282+
false
283+
],
284+
285+
// Edge cases
286+
[{ property: '$.identity.identifier', operator: CONDITION_OPERATORS.IN, value: '' }, false],
287+
[{ property: '$.identity.identifier', operator: CONDITION_OPERATORS.IN, value: [] }, false],
288+
[
289+
{ property: '$.identity.identifier', operator: CONDITION_OPERATORS.IN, value: '[]' },
290+
false
291+
]
292+
] as Array<[SegmentCondition | InSegmentCondition, boolean]>)(
293+
'evaluates IN condition %j to %s',
294+
(condition: SegmentCondition | InSegmentCondition, expected: boolean) => {
295+
const result = traitsMatchSegmentCondition(condition, 'segment', mockContext);
296+
expect(result).toBe(expected);
297+
}
298+
);
299+
});
300+
206301
describe('evaluateIdentityInSegment', () => {
207302
const mockContext: EvaluationContext = {
208303
environment: { key: 'env', name: 'test' },
@@ -361,7 +456,11 @@ describe('percentage split operator', () => {
361456
])('percentage %d with threshold %d returns %s', (hashedValue, threshold, expected) => {
362457
const mockHashFn = getHashedPercentageForObjIds;
363458
mockHashFn.mockReturnValue(hashedValue);
364-
const condition = { property: 'any', operator: 'PERCENTAGE_SPLIT', value: threshold };
459+
const condition = {
460+
property: 'any',
461+
operator: 'PERCENTAGE_SPLIT',
462+
value: threshold.toString()
463+
} as SegmentCondition1 | InSegmentCondition;
365464
const result = traitsMatchSegmentCondition(condition, 'seg1', mockContext);
366465

367466
expect(result).toBe(expected);

0 commit comments

Comments
 (0)