Skip to content

Commit 381b458

Browse files
Merge pull request #441 from splitio/fme-10566
[FME-10566] Create FallbackTreatmentsCalculator
2 parents e39f22b + 1a05cee commit 381b458

File tree

5 files changed

+157
-26
lines changed

5 files changed

+157
-26
lines changed
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { FallbackTreatmentsCalculator } from '../';
2+
import type { FallbackTreatmentConfiguration } from '../../../../types/splitio'; // adjust path if needed
3+
4+
describe('FallbackTreatmentsCalculator', () => {
5+
6+
test('returns specific fallback if flag exists', () => {
7+
const config: FallbackTreatmentConfiguration = {
8+
byFlag: {
9+
'featureA': { treatment: 'TREATMENT_A', config: '{ value: 1 }' },
10+
},
11+
};
12+
const calculator = new FallbackTreatmentsCalculator(config);
13+
const result = calculator.resolve('featureA', 'label by flag');
14+
15+
expect(result).toEqual({
16+
treatment: 'TREATMENT_A',
17+
config: '{ value: 1 }',
18+
label: 'fallback - label by flag',
19+
});
20+
});
21+
22+
test('returns global fallback if flag is missing and global exists', () => {
23+
const config: FallbackTreatmentConfiguration = {
24+
byFlag: {},
25+
global: { treatment: 'GLOBAL_TREATMENT', config: '{ global: true }' },
26+
};
27+
const calculator = new FallbackTreatmentsCalculator(config);
28+
const result = calculator.resolve('missingFlag', 'label by global');
29+
30+
expect(result).toEqual({
31+
treatment: 'GLOBAL_TREATMENT',
32+
config: '{ global: true }',
33+
label: 'fallback - label by global',
34+
});
35+
});
36+
37+
test('returns control fallback if flag and global are missing', () => {
38+
const config: FallbackTreatmentConfiguration = {
39+
byFlag: {},
40+
};
41+
const calculator = new FallbackTreatmentsCalculator(config);
42+
const result = calculator.resolve('missingFlag', 'label by noFallback');
43+
44+
expect(result).toEqual({
45+
treatment: 'CONTROL',
46+
config: null,
47+
label: 'fallback - label by noFallback',
48+
});
49+
});
50+
51+
test('returns undefined label if no label provided', () => {
52+
const config: FallbackTreatmentConfiguration = {
53+
byFlag: {
54+
'featureB': { treatment: 'TREATMENT_B' },
55+
},
56+
};
57+
const calculator = new FallbackTreatmentsCalculator(config);
58+
const result = calculator.resolve('featureB');
59+
60+
expect(result).toEqual({
61+
treatment: 'TREATMENT_B',
62+
config: undefined,
63+
label: undefined,
64+
});
65+
});
66+
});

src/evaluator/fallbackTreatmentsCalculator/__tests__/fallback-sanitizer.spec.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,49 +14,49 @@ describe('FallbacksSanitizer', () => {
1414
});
1515

1616
describe('isValidFlagName', () => {
17-
it('returns true for a valid flag name', () => {
17+
test('returns true for a valid flag name', () => {
1818
// @ts-expect-private-access
1919
expect((FallbacksSanitizer as any).isValidFlagName('my_flag')).toBe(true);
2020
});
2121

22-
it('returns false for a name longer than 100 chars', () => {
22+
test('returns false for a name longer than 100 chars', () => {
2323
const longName = 'a'.repeat(101);
2424
expect((FallbacksSanitizer as any).isValidFlagName(longName)).toBe(false);
2525
});
2626

27-
it('returns false if the name contains spaces', () => {
27+
test('returns false if the name contains spaces', () => {
2828
expect((FallbacksSanitizer as any).isValidFlagName('invalid flag')).toBe(false);
2929
});
3030
});
3131

3232
describe('isValidTreatment', () => {
33-
it('returns true for a valid treatment string', () => {
33+
test('returns true for a valid treatment string', () => {
3434
expect((FallbacksSanitizer as any).isValidTreatment(validTreatment)).toBe(true);
3535
});
3636

37-
it('returns false for null or undefined', () => {
37+
test('returns false for null or undefined', () => {
3838
expect((FallbacksSanitizer as any).isValidTreatment(null)).toBe(false);
3939
expect((FallbacksSanitizer as any).isValidTreatment(undefined)).toBe(false);
4040
});
4141

42-
it('returns false for a treatment longer than 100 chars', () => {
42+
test('returns false for a treatment longer than 100 chars', () => {
4343
const long = { treatment: 'a'.repeat(101) };
4444
expect((FallbacksSanitizer as any).isValidTreatment(long)).toBe(false);
4545
});
4646

47-
it('returns false if treatment does not match regex pattern', () => {
47+
test('returns false if treatment does not match regex pattern', () => {
4848
const invalid = { treatment: 'invalid treatment!' };
4949
expect((FallbacksSanitizer as any).isValidTreatment(invalid)).toBe(false);
5050
});
5151
});
5252

5353
describe('sanitizeGlobal', () => {
54-
it('returns the treatment if valid', () => {
54+
test('returns the treatment if valid', () => {
5555
expect(FallbacksSanitizer.sanitizeGlobal(validTreatment)).toEqual(validTreatment);
5656
expect(console.error).not.toHaveBeenCalled();
5757
});
5858

59-
it('returns undefined and logs error if invalid', () => {
59+
test('returns undefined and logs error if invalid', () => {
6060
const result = FallbacksSanitizer.sanitizeGlobal(invalidTreatment);
6161
expect(result).toBeUndefined();
6262
expect(console.error).toHaveBeenCalledWith(
@@ -66,7 +66,7 @@ describe('FallbacksSanitizer', () => {
6666
});
6767

6868
describe('sanitizeByFlag', () => {
69-
it('returns a sanitized map with valid entries only', () => {
69+
test('returns a sanitized map with valid entries only', () => {
7070
const input = {
7171
valid_flag: validTreatment,
7272
'invalid flag': validTreatment,
@@ -79,7 +79,7 @@ describe('FallbacksSanitizer', () => {
7979
expect(console.error).toHaveBeenCalledTimes(2); // invalid flag + bad_treatment
8080
});
8181

82-
it('returns empty object if all invalid', () => {
82+
test('returns empty object if all invalid', () => {
8383
const input = {
8484
'invalid flag': invalidTreatment,
8585
};
@@ -89,7 +89,7 @@ describe('FallbacksSanitizer', () => {
8989
expect(console.error).toHaveBeenCalled();
9090
});
9191

92-
it('returns same object if all valid', () => {
92+
test('returns same object if all valid', () => {
9393
const input = {
9494
flag_one: validTreatment,
9595
flag_two: { treatment: 'valid_2', config: null },

src/evaluator/fallbackTreatmentsCalculator/fallbackSanitizer/index.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { TreatmentWithConfig } from '../../../../types/splitio';
1+
import { FallbackTreatment } from '../../../../types/splitio';
22
import { FallbackDiscardReason } from '../constants';
33

44

@@ -10,21 +10,26 @@ export class FallbacksSanitizer {
1010
return name.length <= 100 && !name.includes(' ');
1111
}
1212

13-
private static isValidTreatment(t?: TreatmentWithConfig): boolean {
14-
if (!t || !t.treatment) {
13+
private static isValidTreatment(t?: FallbackTreatment): boolean {
14+
if (!t) {
1515
return false;
1616
}
1717

18-
const { treatment } = t;
18+
if (typeof t === 'string') {
19+
if (t.length > 100) {
20+
return false;
21+
}
22+
return FallbacksSanitizer.pattern.test(t);
23+
}
1924

20-
if (treatment.length > 100) {
25+
const { treatment } = t;
26+
if (!treatment || treatment.length > 100) {
2127
return false;
2228
}
23-
2429
return FallbacksSanitizer.pattern.test(treatment);
2530
}
2631

27-
static sanitizeGlobal(treatment?: TreatmentWithConfig): TreatmentWithConfig | undefined {
32+
static sanitizeGlobal(treatment?: FallbackTreatment): FallbackTreatment | undefined {
2833
if (!this.isValidTreatment(treatment)) {
2934
console.error(
3035
`Fallback treatments - Discarded fallback: ${FallbackDiscardReason.Treatment}`
@@ -35,9 +40,9 @@ export class FallbacksSanitizer {
3540
}
3641

3742
static sanitizeByFlag(
38-
byFlagFallbacks: Record<string, TreatmentWithConfig>
39-
): Record<string, TreatmentWithConfig> {
40-
const sanitizedByFlag: Record<string, TreatmentWithConfig> = {};
43+
byFlagFallbacks: Record<string, FallbackTreatment>
44+
): Record<string, FallbackTreatment> {
45+
const sanitizedByFlag: Record<string, FallbackTreatment> = {};
4146

4247
const entries = Object.entries(byFlagFallbacks);
4348
entries.forEach(([flag, t]) => {
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { FallbackTreatmentConfiguration, FallbackTreatment, IFallbackTreatmentsCalculator} from '../../../types/splitio';
2+
3+
export class FallbackTreatmentsCalculator implements IFallbackTreatmentsCalculator {
4+
private readonly labelPrefix = 'fallback - ';
5+
private readonly control = 'CONTROL';
6+
private readonly fallbacks: FallbackTreatmentConfiguration;
7+
8+
constructor(fallbacks: FallbackTreatmentConfiguration) {
9+
this.fallbacks = fallbacks;
10+
}
11+
12+
resolve(flagName: string, label?: string | undefined): FallbackTreatment {
13+
const treatment = this.fallbacks.byFlag[flagName];
14+
if (treatment) {
15+
return this.copyWithLabel(treatment, label);
16+
}
17+
18+
if (this.fallbacks.global) {
19+
return this.copyWithLabel(this.fallbacks.global, label);
20+
}
21+
22+
return {
23+
treatment: this.control,
24+
config: null,
25+
label: this.resolveLabel(label),
26+
};
27+
}
28+
29+
private copyWithLabel(fallback: FallbackTreatment, label: string | undefined): FallbackTreatment {
30+
if (typeof fallback === 'string') {
31+
return {
32+
treatment: fallback,
33+
config: null,
34+
label: this.resolveLabel(label),
35+
};
36+
}
37+
38+
return {
39+
treatment: fallback.treatment,
40+
config: fallback.config,
41+
label: this.resolveLabel(label),
42+
};
43+
}
44+
45+
private resolveLabel(label?: string | undefined): string | undefined {
46+
return label ? `${this.labelPrefix}${label}` : undefined;
47+
}
48+
49+
}

types/splitio.d.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -624,7 +624,7 @@ declare namespace SplitIO {
624624
/**
625625
* Fallback treatments to be used when the SDK is not ready or the flag is not found.
626626
*/
627-
readonly fallbackTreatments?: FallbackTreatmentOptions;
627+
readonly fallbackTreatments?: FallbackTreatmentConfiguration;
628628
}
629629
/**
630630
* Log levels.
@@ -1232,15 +1232,26 @@ declare namespace SplitIO {
12321232
* User consent status.
12331233
*/
12341234
type ConsentStatus = 'GRANTED' | 'DECLINED' | 'UNKNOWN';
1235+
/**
1236+
* Fallback treatment can be either a string (treatment) or an object with treatment, config and label.
1237+
*/
1238+
type FallbackTreatment = string | {
1239+
treatment: string;
1240+
config?: string | null;
1241+
label?: string | null;
1242+
};
12351243
/**
12361244
* Fallback treatments to be used when the SDK is not ready or the flag is not found.
12371245
*/
1238-
type FallbackTreatmentOptions = {
1239-
global?: TreatmentWithConfig | Treatment,
1246+
type FallbackTreatmentConfiguration = {
1247+
global?: FallbackTreatment,
12401248
byFlag: {
1241-
[key: string]: TreatmentWithConfig | Treatment
1249+
[key: string]: FallbackTreatment
12421250
}
12431251
}
1252+
type IFallbackTreatmentsCalculator = {
1253+
resolve(flagName: string, label?: string | undefined): FallbackTreatment;
1254+
}
12441255
/**
12451256
* Logger. Its interface details are not part of the public API. It shouldn't be used directly.
12461257
*/

0 commit comments

Comments
 (0)