@featurevisor/sdk 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,533 @@
1
+ import { allConditionsAreMatched } from "./conditions";
2
+ import { Condition } from "@featurevisor/types";
3
+
4
+ describe("sdk: Conditions", function () {
5
+ it("should be a function", function () {
6
+ expect(typeof allConditionsAreMatched).toEqual("function");
7
+ });
8
+
9
+ describe("operators", function () {
10
+ it("should match with operator: equals", function () {
11
+ const conditions: Condition[] = [
12
+ {
13
+ attribute: "browser_type",
14
+ operator: "equals",
15
+ value: "chrome",
16
+ },
17
+ ];
18
+
19
+ // match
20
+ expect(allConditionsAreMatched(conditions, { browser_type: "chrome" })).toEqual(true);
21
+
22
+ // not match
23
+ expect(allConditionsAreMatched(conditions, { browser_type: "firefox" })).toEqual(false);
24
+ });
25
+
26
+ it("should match with operator: notEquals", function () {
27
+ const conditions: Condition[] = [
28
+ {
29
+ attribute: "browser_type",
30
+ operator: "notEquals",
31
+ value: "chrome",
32
+ },
33
+ ];
34
+
35
+ // match
36
+ expect(allConditionsAreMatched(conditions, { browser_type: "firefox" })).toEqual(true);
37
+
38
+ // not match
39
+ expect(allConditionsAreMatched(conditions, { browser_type: "chrome" })).toEqual(false);
40
+ });
41
+
42
+ it("should match with operator: greaterThan", function () {
43
+ const conditions: Condition[] = [
44
+ {
45
+ attribute: "age",
46
+ operator: "greaterThan",
47
+ value: 18,
48
+ },
49
+ ];
50
+
51
+ // match
52
+ expect(allConditionsAreMatched(conditions, { age: 19 })).toEqual(true);
53
+
54
+ // not match
55
+ expect(allConditionsAreMatched(conditions, { age: 17 })).toEqual(false);
56
+ });
57
+
58
+ it("should match with operator: lessThan", function () {
59
+ const conditions: Condition[] = [
60
+ {
61
+ attribute: "age",
62
+ operator: "lessThan",
63
+ value: 18,
64
+ },
65
+ ];
66
+
67
+ // match
68
+ expect(allConditionsAreMatched(conditions, { age: 17 })).toEqual(true);
69
+
70
+ // not match
71
+ expect(allConditionsAreMatched(conditions, { age: 19 })).toEqual(false);
72
+ });
73
+ });
74
+
75
+ describe("simple condition", function () {
76
+ it("should match with exact single condition", function () {
77
+ const conditions: Condition[] = [
78
+ {
79
+ attribute: "browser_type",
80
+ operator: "equals",
81
+ value: "chrome",
82
+ },
83
+ ];
84
+
85
+ // match
86
+ expect(
87
+ allConditionsAreMatched(conditions[0], {
88
+ browser_type: "chrome",
89
+ }),
90
+ ).toEqual(true);
91
+ });
92
+
93
+ it("should match with exact condition", function () {
94
+ const conditions: Condition[] = [
95
+ {
96
+ attribute: "browser_type",
97
+ operator: "equals",
98
+ value: "chrome",
99
+ },
100
+ ];
101
+
102
+ // match
103
+ expect(
104
+ allConditionsAreMatched(conditions, {
105
+ browser_type: "chrome",
106
+ }),
107
+ ).toEqual(true);
108
+ });
109
+
110
+ it("should not match with empty conditions", function () {
111
+ const conditions: Condition[] = [];
112
+
113
+ // match
114
+ expect(
115
+ allConditionsAreMatched(conditions, {
116
+ browser_type: "chrome",
117
+ }),
118
+ ).toEqual(true);
119
+
120
+ // not match
121
+ expect(
122
+ allConditionsAreMatched(conditions, {
123
+ browser_type: "firefox",
124
+ }),
125
+ ).toEqual(true);
126
+ });
127
+
128
+ it("should match with extra conditions that are not needed", function () {
129
+ const conditions: Condition[] = [
130
+ {
131
+ attribute: "browser_type",
132
+ operator: "equals",
133
+ value: "chrome",
134
+ },
135
+ ];
136
+
137
+ // match
138
+ expect(
139
+ allConditionsAreMatched(conditions, {
140
+ browser_type: "chrome",
141
+ browser_version: "1.0",
142
+ }),
143
+ ).toEqual(true);
144
+ });
145
+
146
+ it("should match with multiple conditions", function () {
147
+ const conditions: Condition[] = [
148
+ {
149
+ attribute: "browser_type",
150
+ operator: "equals",
151
+ value: "chrome",
152
+ },
153
+ {
154
+ attribute: "browser_version",
155
+ operator: "equals",
156
+ value: "1.0",
157
+ },
158
+ ];
159
+
160
+ // match
161
+ expect(
162
+ allConditionsAreMatched(conditions, {
163
+ browser_type: "chrome",
164
+ browser_version: "1.0",
165
+ foo: "bar",
166
+ }),
167
+ ).toEqual(true);
168
+ });
169
+ });
170
+
171
+ describe("AND condition", function () {
172
+ it("should match with one AND condition", function () {
173
+ const conditions: Condition[] = [
174
+ {
175
+ and: [
176
+ {
177
+ attribute: "browser_type",
178
+ operator: "equals",
179
+ value: "chrome",
180
+ },
181
+ ],
182
+ },
183
+ ];
184
+
185
+ // match
186
+ expect(
187
+ allConditionsAreMatched(conditions, {
188
+ browser_type: "chrome",
189
+ }),
190
+ ).toEqual(true);
191
+ });
192
+
193
+ it("should match with multiple conditions inside AND", function () {
194
+ const conditions: Condition[] = [
195
+ {
196
+ and: [
197
+ {
198
+ attribute: "browser_type",
199
+ operator: "equals",
200
+ value: "chrome",
201
+ },
202
+ {
203
+ attribute: "browser_version",
204
+ operator: "equals",
205
+ value: "1.0",
206
+ },
207
+ ],
208
+ },
209
+ ];
210
+
211
+ // match
212
+ expect(
213
+ allConditionsAreMatched(conditions, {
214
+ browser_type: "chrome",
215
+ browser_version: "1.0",
216
+ }),
217
+ ).toEqual(true);
218
+
219
+ // not match
220
+ expect(
221
+ allConditionsAreMatched(conditions, {
222
+ browser_type: "chrome",
223
+ }),
224
+ ).toEqual(false);
225
+ });
226
+ });
227
+
228
+ describe("OR condition", function () {
229
+ it("should match with one OR condition", function () {
230
+ const conditions: Condition[] = [
231
+ {
232
+ or: [
233
+ {
234
+ attribute: "browser_type",
235
+ operator: "equals",
236
+ value: "chrome",
237
+ },
238
+ ],
239
+ },
240
+ ];
241
+
242
+ // match
243
+ expect(
244
+ allConditionsAreMatched(conditions, {
245
+ browser_type: "chrome",
246
+ }),
247
+ ).toEqual(true);
248
+ });
249
+
250
+ it("should match with multiple conditions inside OR", function () {
251
+ const conditions: Condition[] = [
252
+ {
253
+ or: [
254
+ {
255
+ attribute: "browser_type",
256
+ operator: "equals",
257
+ value: "chrome",
258
+ },
259
+ {
260
+ attribute: "browser_version",
261
+ operator: "equals",
262
+ value: "1.0",
263
+ },
264
+ ],
265
+ },
266
+ ];
267
+
268
+ // match
269
+ expect(
270
+ allConditionsAreMatched(conditions, {
271
+ browser_version: "1.0",
272
+ }),
273
+ ).toEqual(true);
274
+
275
+ // not match
276
+ expect(
277
+ allConditionsAreMatched(conditions, {
278
+ browser_type: "firefox",
279
+ }),
280
+ ).toEqual(false);
281
+ });
282
+ });
283
+
284
+ describe("nested conditions", function () {
285
+ it("should match with OR inside AND", function () {
286
+ const conditions: Condition[] = [
287
+ {
288
+ and: [
289
+ {
290
+ attribute: "browser_type",
291
+ operator: "equals",
292
+ value: "chrome",
293
+ },
294
+ {
295
+ or: [
296
+ {
297
+ attribute: "browser_version",
298
+ operator: "equals",
299
+ value: "1.0",
300
+ },
301
+ {
302
+ attribute: "browser_version",
303
+ operator: "equals",
304
+ value: "2.0",
305
+ },
306
+ ],
307
+ },
308
+ ],
309
+ },
310
+ ];
311
+
312
+ // match
313
+ expect(
314
+ allConditionsAreMatched(conditions, {
315
+ browser_type: "chrome",
316
+ browser_version: "1.0",
317
+ }),
318
+ ).toEqual(true);
319
+
320
+ expect(
321
+ allConditionsAreMatched(conditions, {
322
+ browser_type: "chrome",
323
+ browser_version: "2.0",
324
+ }),
325
+ ).toEqual(true);
326
+
327
+ // not match
328
+ expect(
329
+ allConditionsAreMatched(conditions, {
330
+ browser_type: "chrome",
331
+ browser_version: "3.0",
332
+ }),
333
+ ).toEqual(false);
334
+
335
+ expect(
336
+ allConditionsAreMatched(conditions, {
337
+ browser_version: "2.0",
338
+ }),
339
+ ).toEqual(false);
340
+ });
341
+
342
+ it("should match with plain conditions, followed by OR inside AND", function () {
343
+ const conditions: Condition[] = [
344
+ {
345
+ attribute: "country",
346
+ operator: "equals",
347
+ value: "nl",
348
+ },
349
+ {
350
+ and: [
351
+ {
352
+ attribute: "browser_type",
353
+ operator: "equals",
354
+ value: "chrome",
355
+ },
356
+ {
357
+ or: [
358
+ {
359
+ attribute: "browser_version",
360
+ operator: "equals",
361
+ value: "1.0",
362
+ },
363
+ {
364
+ attribute: "browser_version",
365
+ operator: "equals",
366
+ value: "2.0",
367
+ },
368
+ ],
369
+ },
370
+ ],
371
+ },
372
+ ];
373
+
374
+ // match
375
+ expect(
376
+ allConditionsAreMatched(conditions, {
377
+ country: "nl",
378
+ browser_type: "chrome",
379
+ browser_version: "1.0",
380
+ }),
381
+ ).toEqual(true);
382
+
383
+ expect(
384
+ allConditionsAreMatched(conditions, {
385
+ country: "nl",
386
+ browser_type: "chrome",
387
+ browser_version: "2.0",
388
+ }),
389
+ ).toEqual(true);
390
+
391
+ // not match
392
+ expect(
393
+ allConditionsAreMatched(conditions, {
394
+ browser_type: "chrome",
395
+ browser_version: "3.0",
396
+ }),
397
+ ).toEqual(false);
398
+
399
+ expect(
400
+ allConditionsAreMatched(conditions, {
401
+ country: "us",
402
+ browser_version: "2.0",
403
+ }),
404
+ ).toEqual(false);
405
+ });
406
+
407
+ it("should match with AND inside OR", function () {
408
+ const conditions: Condition[] = [
409
+ {
410
+ or: [
411
+ {
412
+ attribute: "browser_type",
413
+ operator: "equals",
414
+ value: "chrome",
415
+ },
416
+ {
417
+ and: [
418
+ {
419
+ attribute: "device_type",
420
+ operator: "equals",
421
+ value: "mobile",
422
+ },
423
+ {
424
+ attribute: "orientation",
425
+ operator: "equals",
426
+ value: "portrait",
427
+ },
428
+ ],
429
+ },
430
+ ],
431
+ },
432
+ ];
433
+
434
+ // match
435
+ expect(
436
+ allConditionsAreMatched(conditions, {
437
+ browser_type: "chrome",
438
+ browser_version: "2.0",
439
+ }),
440
+ ).toEqual(true);
441
+
442
+ expect(
443
+ allConditionsAreMatched(conditions, {
444
+ browser_type: "firefox",
445
+ device_type: "mobile",
446
+ orientation: "portrait",
447
+ }),
448
+ ).toEqual(true);
449
+
450
+ // not match
451
+ expect(
452
+ allConditionsAreMatched(conditions, {
453
+ browser_type: "firefox",
454
+ browser_version: "2.0",
455
+ }),
456
+ ).toEqual(false);
457
+
458
+ expect(
459
+ allConditionsAreMatched(conditions, {
460
+ browser_type: "firefox",
461
+ device_type: "desktop",
462
+ }),
463
+ ).toEqual(false);
464
+ });
465
+
466
+ it("should match with plain conditions, followed by AND inside OR", function () {
467
+ const conditions: Condition[] = [
468
+ {
469
+ attribute: "country",
470
+ operator: "equals",
471
+ value: "nl",
472
+ },
473
+ {
474
+ or: [
475
+ {
476
+ attribute: "browser_type",
477
+ operator: "equals",
478
+ value: "chrome",
479
+ },
480
+ {
481
+ and: [
482
+ {
483
+ attribute: "device_type",
484
+ operator: "equals",
485
+ value: "mobile",
486
+ },
487
+ {
488
+ attribute: "orientation",
489
+ operator: "equals",
490
+ value: "portrait",
491
+ },
492
+ ],
493
+ },
494
+ ],
495
+ },
496
+ ];
497
+
498
+ // match
499
+ expect(
500
+ allConditionsAreMatched(conditions, {
501
+ country: "nl",
502
+ browser_type: "chrome",
503
+ browser_version: "2.0",
504
+ }),
505
+ ).toEqual(true);
506
+
507
+ expect(
508
+ allConditionsAreMatched(conditions, {
509
+ country: "nl",
510
+ browser_type: "firefox",
511
+ device_type: "mobile",
512
+ orientation: "portrait",
513
+ }),
514
+ ).toEqual(true);
515
+
516
+ // not match
517
+ expect(
518
+ allConditionsAreMatched(conditions, {
519
+ browser_type: "firefox",
520
+ browser_version: "2.0",
521
+ }),
522
+ ).toEqual(false);
523
+
524
+ expect(
525
+ allConditionsAreMatched(conditions, {
526
+ country: "de",
527
+ browser_type: "firefox",
528
+ device_type: "desktop",
529
+ }),
530
+ ).toEqual(false);
531
+ });
532
+ });
533
+ });
@@ -0,0 +1,73 @@
1
+ import { Attributes, Condition, PlainCondition } from "@featurevisor/types";
2
+
3
+ export function conditionIsMatched(condition: PlainCondition, attributes: Attributes): boolean {
4
+ const { attribute, operator, value } = condition;
5
+
6
+ if (operator === "equals") {
7
+ return attributes[attribute] === value;
8
+ } else if (operator === "notEquals") {
9
+ return attributes[attribute] !== value;
10
+ } else if (typeof attributes[attribute] === "string" && Array.isArray(value)) {
11
+ // array
12
+ const valueInAttributes = attributes[attribute] as string;
13
+
14
+ if (operator === "in") {
15
+ return value.indexOf(valueInAttributes) !== -1;
16
+ } else if (operator === "notIn") {
17
+ return value.indexOf(valueInAttributes) === -1;
18
+ }
19
+ } else if (typeof attributes[attribute] === "string" && typeof value === "string") {
20
+ // string
21
+ const valueInAttributes = attributes[attribute] as string;
22
+
23
+ if (operator === "contains") {
24
+ return valueInAttributes.indexOf(value) !== -1;
25
+ } else if (operator === "notContains") {
26
+ return valueInAttributes.indexOf(value) === -1;
27
+ } else if (operator === "startsWith") {
28
+ return valueInAttributes.startsWith(value);
29
+ } else if (operator === "endsWith") {
30
+ return valueInAttributes.endsWith(value);
31
+ }
32
+ } else if (typeof attributes[attribute] === "number" && typeof value === "number") {
33
+ // numeric
34
+ const valueInAttributes = attributes[attribute] as number;
35
+
36
+ if (operator === "greaterThan") {
37
+ return valueInAttributes > value;
38
+ } else if (operator === "greaterThanOrEquals") {
39
+ return valueInAttributes >= value;
40
+ } else if (operator === "lessThan") {
41
+ return valueInAttributes < value;
42
+ } else if (operator === "lessThanOrEquals") {
43
+ return valueInAttributes <= value;
44
+ }
45
+ }
46
+
47
+ return false;
48
+ }
49
+
50
+ export function allConditionsAreMatched(
51
+ conditions: Condition[] | Condition,
52
+ attributes: Attributes,
53
+ ): boolean {
54
+ if ("attribute" in conditions) {
55
+ return conditionIsMatched(conditions, attributes);
56
+ }
57
+
58
+ if ("and" in conditions && Array.isArray(conditions.and)) {
59
+ return conditions.and.every((c) => allConditionsAreMatched(c, attributes));
60
+ }
61
+
62
+ if ("or" in conditions && Array.isArray(conditions.or)) {
63
+ return conditions.or.some((c) => allConditionsAreMatched(c, attributes));
64
+ }
65
+
66
+ // @TODO: introduce `not`
67
+
68
+ if (Array.isArray(conditions)) {
69
+ return conditions.every((c) => allConditionsAreMatched(c, attributes));
70
+ }
71
+
72
+ return false;
73
+ }
@@ -0,0 +1,73 @@
1
+ import {
2
+ Feature,
3
+ Segment,
4
+ DatafileContent,
5
+ Attribute,
6
+ AttributeKey,
7
+ SegmentKey,
8
+ FeatureKey,
9
+ } from "@featurevisor/types";
10
+
11
+ export function parseJsonConditionsIfStringified<T>(record: T, key: string): T {
12
+ if (typeof record[key] === "string" && record[key] !== "*") {
13
+ try {
14
+ record[key] = JSON.parse(record[key]);
15
+ } catch (e) {
16
+ console.error("Error parsing JSON", e);
17
+ }
18
+ }
19
+
20
+ return record;
21
+ }
22
+
23
+ export class DatafileReader {
24
+ private schemaVersion: string;
25
+ private revision: string;
26
+ private attributes: Attribute[];
27
+ private segments: Segment[];
28
+ private features: Feature[];
29
+
30
+ constructor(datafileJson: DatafileContent) {
31
+ this.schemaVersion = datafileJson.schemaVersion;
32
+ this.revision = datafileJson.revision;
33
+ this.segments = datafileJson.segments;
34
+ this.attributes = datafileJson.attributes;
35
+ this.features = datafileJson.features;
36
+ }
37
+
38
+ getRevision(): string {
39
+ return this.revision;
40
+ }
41
+
42
+ getSchemaVersion(): string {
43
+ return this.schemaVersion;
44
+ }
45
+
46
+ getAllAttributes(): Attribute[] {
47
+ return this.attributes;
48
+ }
49
+
50
+ getAttribute(attributeKey: AttributeKey): Attribute | undefined {
51
+ return this.attributes.find((a) => a.key === attributeKey);
52
+ }
53
+
54
+ getSegment(segmentKey: SegmentKey): Segment | undefined {
55
+ const segment = this.segments.find((s) => s.key === segmentKey);
56
+
57
+ if (!segment) {
58
+ return undefined;
59
+ }
60
+
61
+ return parseJsonConditionsIfStringified(segment, "conditions");
62
+ }
63
+
64
+ getFeature(featureKey: FeatureKey): Feature | undefined {
65
+ const feature = this.features.find((s) => s.key === featureKey);
66
+
67
+ if (!feature) {
68
+ return undefined;
69
+ }
70
+
71
+ return feature;
72
+ }
73
+ }