Skip to content

Commit 91ff817

Browse files
Evaluate targeting rules with semver string format (FF-1433) (#38)
* Evaluate targeting rules with semver string format (FF-1433) * anon func * bump to v2.2.0
1 parent b04f796 commit 91ff817

File tree

5 files changed

+115
-4
lines changed

5 files changed

+115
-4
lines changed

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@eppo/js-client-sdk-common",
3-
"version": "2.1.1",
3+
"version": "2.2.0",
44
"description": "Eppo SDK for client-side JavaScript applications (base for both web and react native)",
55
"main": "dist/index.js",
66
"files": [
@@ -41,6 +41,7 @@
4141
"devDependencies": {
4242
"@types/jest": "^29.5.11",
4343
"@types/md5": "^2.3.2",
44+
"@types/semver": "^7.5.6",
4445
"@typescript-eslint/eslint-plugin": "^5.13.0",
4546
"@typescript-eslint/parser": "^5.13.0",
4647
"eslint": "^8.17.0",
@@ -65,6 +66,7 @@
6566
"dependencies": {
6667
"axios": "^1.6.0",
6768
"lru-cache": "^10.0.1",
68-
"md5": "^2.3.0"
69+
"md5": "^2.3.0",
70+
"semver": "^7.5.4"
6971
}
7072
}

src/dto/rule-dto.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ export enum OperatorType {
88
NOT_ONE_OF = 'NOT_ONE_OF',
99
}
1010

11+
export enum OperatorValueType {
12+
PLAIN_STRING = 'PLAIN_STRING',
13+
STRING_ARRAY = 'STRING_ARRAY',
14+
SEM_VER = 'SEM_VER',
15+
NUMERIC = 'NUMERIC',
16+
}
17+
1118
export interface Condition {
1219
operator: OperatorType;
1320
attribute: string;

src/rule_evaluator.spec.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,21 @@ describe('findMatchingRule', () => {
2121
},
2222
],
2323
};
24+
const semverRule: IRule = {
25+
allocationKey: 'test',
26+
conditions: [
27+
{
28+
operator: OperatorType.GTE,
29+
attribute: 'version',
30+
value: '1.0.0',
31+
},
32+
{
33+
operator: OperatorType.LTE,
34+
attribute: 'version',
35+
value: '2.0.0',
36+
},
37+
],
38+
};
2439
const ruleWithMatchesCondition: IRule = {
2540
allocationKey: 'test',
2641
conditions: [
@@ -47,6 +62,13 @@ describe('findMatchingRule', () => {
4762
expect(findMatchingRule({ totalSales: 100 }, rules, false)).toEqual(numericRule);
4863
});
4964

65+
it('returns the rule for semver conditions', () => {
66+
const rules = [semverRule];
67+
expect(findMatchingRule({ version: '1.1.0' }, rules, false)).toEqual(semverRule);
68+
expect(findMatchingRule({ version: '2.0.0' }, rules, false)).toEqual(semverRule);
69+
expect(findMatchingRule({ version: '2.1.0' }, rules, false)).toBeNull();
70+
});
71+
5072
it('returns null if there is no attribute for the condition', () => {
5173
const rules = [numericRule];
5274
expect(findMatchingRule({ unknown: 'test' }, rules, false)).toEqual(null);

src/rule_evaluator.ts

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
2-
import { Condition, OperatorType, IRule } from './dto/rule-dto';
2+
import {
3+
valid as validSemver,
4+
gt as semverGt,
5+
lt as semverLt,
6+
gte as semverGte,
7+
lte as semverLte,
8+
} from 'semver';
9+
10+
import { Condition, OperatorType, IRule, OperatorValueType } from './dto/rule-dto';
311
import { decodeBase64, getMD5Hash } from './obfuscation';
412

513
export function findMatchingRule(
@@ -42,15 +50,30 @@ function evaluateRuleConditions(
4250

4351
function evaluateCondition(subjectAttributes: Record<string, any>, condition: Condition): boolean {
4452
const value = subjectAttributes[condition.attribute];
53+
54+
const conditionValueType = targetingRuleConditionValuesTypesFromValues(condition.value);
55+
4556
if (value != null) {
4657
switch (condition.operator) {
4758
case OperatorType.GTE:
59+
if (conditionValueType === OperatorValueType.SEM_VER) {
60+
return compareSemVer(value, condition.value, semverGte);
61+
}
4862
return compareNumber(value, condition.value, (a, b) => a >= b);
4963
case OperatorType.GT:
64+
if (conditionValueType === OperatorValueType.SEM_VER) {
65+
return compareSemVer(value, condition.value, semverGt);
66+
}
5067
return compareNumber(value, condition.value, (a, b) => a > b);
5168
case OperatorType.LTE:
69+
if (conditionValueType === OperatorValueType.SEM_VER) {
70+
return compareSemVer(value, condition.value, semverLte);
71+
}
5272
return compareNumber(value, condition.value, (a, b) => a <= b);
5373
case OperatorType.LT:
74+
if (conditionValueType === OperatorValueType.SEM_VER) {
75+
return compareSemVer(value, condition.value, semverLt);
76+
}
5477
return compareNumber(value, condition.value, (a, b) => a < b);
5578
case OperatorType.MATCHES:
5679
return new RegExp(condition.value as string).test(value as string);
@@ -78,15 +101,29 @@ function evaluateObfuscatedCondition(
78101
{},
79102
);
80103
const value = hashedSubjectAttributes[condition.attribute];
104+
const conditionValueType = targetingRuleConditionValuesTypesFromValues(value);
105+
81106
if (value != null) {
82107
switch (condition.operator) {
83108
case getMD5Hash(OperatorType.GTE):
109+
if (conditionValueType === OperatorValueType.SEM_VER) {
110+
return compareSemVer(value, decodeBase64(condition.value), semverGte);
111+
}
84112
return compareNumber(value, Number(decodeBase64(condition.value)), (a, b) => a >= b);
85113
case getMD5Hash(OperatorType.GT):
114+
if (conditionValueType === OperatorValueType.SEM_VER) {
115+
return compareSemVer(value, decodeBase64(condition.value), semverGt);
116+
}
86117
return compareNumber(value, Number(decodeBase64(condition.value)), (a, b) => a > b);
87118
case getMD5Hash(OperatorType.LTE):
119+
if (conditionValueType === OperatorValueType.SEM_VER) {
120+
return compareSemVer(value, decodeBase64(condition.value), semverLte);
121+
}
88122
return compareNumber(value, Number(decodeBase64(condition.value)), (a, b) => a <= b);
89123
case getMD5Hash(OperatorType.LT):
124+
if (conditionValueType === OperatorValueType.SEM_VER) {
125+
return compareSemVer(value, decodeBase64(condition.value), semverLt);
126+
}
90127
return compareNumber(value, Number(decodeBase64(condition.value)), (a, b) => a < b);
91128
case getMD5Hash(OperatorType.MATCHES):
92129
return new RegExp(decodeBase64(condition.value)).test(value as string);
@@ -115,10 +152,48 @@ function compareNumber(
115152
attributeValue: any,
116153
conditionValue: any,
117154
compareFn: (a: number, b: number) => boolean,
118-
) {
155+
): boolean {
119156
return (
120157
typeof attributeValue === 'number' &&
121158
typeof conditionValue === 'number' &&
122159
compareFn(attributeValue, conditionValue)
123160
);
124161
}
162+
163+
function compareSemVer(
164+
attributeValue: any,
165+
conditionValue: any,
166+
compareFn: (a: string, b: string) => boolean,
167+
): boolean {
168+
return (
169+
!!validSemver(attributeValue) &&
170+
!!validSemver(conditionValue) &&
171+
compareFn(attributeValue, conditionValue)
172+
);
173+
}
174+
175+
function targetingRuleConditionValuesTypesFromValues(
176+
value: number | string | string[],
177+
): OperatorValueType {
178+
// Check if input is a number
179+
if (typeof value === 'number') {
180+
return OperatorValueType.NUMERIC;
181+
}
182+
183+
if (Array.isArray(value)) {
184+
return OperatorValueType.STRING_ARRAY;
185+
}
186+
187+
// Check if input is a string that represents a SemVer
188+
if (validSemver(value)) {
189+
return OperatorValueType.SEM_VER;
190+
}
191+
192+
// Check if input is a string that represents a number
193+
if (!isNaN(Number(value))) {
194+
return OperatorValueType.NUMERIC;
195+
}
196+
197+
// If none of the above, it's a general string
198+
return OperatorValueType.PLAIN_STRING;
199+
}

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -786,6 +786,11 @@
786786
dependencies:
787787
undici-types "~5.26.4"
788788

789+
"@types/semver@^7.5.6":
790+
version "7.5.6"
791+
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.6.tgz#c65b2bfce1bec346582c07724e3f8c1017a20339"
792+
integrity sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==
793+
789794
"@types/stack-utils@^2.0.0":
790795
version "2.0.3"
791796
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8"

0 commit comments

Comments
 (0)