diff --git a/src/evaluator/fallbackTreatmentsCalculator/__tests__/fallback-calculator.spec.ts b/src/evaluator/fallbackTreatmentsCalculator/__tests__/fallback-calculator.spec.ts new file mode 100644 index 00000000..786ecbe8 --- /dev/null +++ b/src/evaluator/fallbackTreatmentsCalculator/__tests__/fallback-calculator.spec.ts @@ -0,0 +1,66 @@ +import { FallbackTreatmentsCalculator } from '../'; +import type { FallbackTreatmentConfiguration } from '../../../../types/splitio'; // adjust path if needed + +describe('FallbackTreatmentsCalculator', () => { + + test('returns specific fallback if flag exists', () => { + const config: FallbackTreatmentConfiguration = { + byFlag: { + 'featureA': { treatment: 'TREATMENT_A', config: '{ value: 1 }' }, + }, + }; + const calculator = new FallbackTreatmentsCalculator(config); + const result = calculator.resolve('featureA', 'label by flag'); + + expect(result).toEqual({ + treatment: 'TREATMENT_A', + config: '{ value: 1 }', + label: 'fallback - label by flag', + }); + }); + + test('returns global fallback if flag is missing and global exists', () => { + const config: FallbackTreatmentConfiguration = { + byFlag: {}, + global: { treatment: 'GLOBAL_TREATMENT', config: '{ global: true }' }, + }; + const calculator = new FallbackTreatmentsCalculator(config); + const result = calculator.resolve('missingFlag', 'label by global'); + + expect(result).toEqual({ + treatment: 'GLOBAL_TREATMENT', + config: '{ global: true }', + label: 'fallback - label by global', + }); + }); + + test('returns control fallback if flag and global are missing', () => { + const config: FallbackTreatmentConfiguration = { + byFlag: {}, + }; + const calculator = new FallbackTreatmentsCalculator(config); + const result = calculator.resolve('missingFlag', 'label by noFallback'); + + expect(result).toEqual({ + treatment: 'CONTROL', + config: null, + label: 'fallback - label by noFallback', + }); + }); + + test('returns undefined label if no label provided', () => { + const config: FallbackTreatmentConfiguration = { + byFlag: { + 'featureB': { treatment: 'TREATMENT_B' }, + }, + }; + const calculator = new FallbackTreatmentsCalculator(config); + const result = calculator.resolve('featureB'); + + expect(result).toEqual({ + treatment: 'TREATMENT_B', + config: undefined, + label: undefined, + }); + }); +}); diff --git a/src/evaluator/fallbackTreatmentsCalculator/__tests__/fallback-sanitizer.spec.ts b/src/evaluator/fallbackTreatmentsCalculator/__tests__/fallback-sanitizer.spec.ts index e063a570..0f314e2a 100644 --- a/src/evaluator/fallbackTreatmentsCalculator/__tests__/fallback-sanitizer.spec.ts +++ b/src/evaluator/fallbackTreatmentsCalculator/__tests__/fallback-sanitizer.spec.ts @@ -14,49 +14,49 @@ describe('FallbacksSanitizer', () => { }); describe('isValidFlagName', () => { - it('returns true for a valid flag name', () => { + test('returns true for a valid flag name', () => { // @ts-expect-private-access expect((FallbacksSanitizer as any).isValidFlagName('my_flag')).toBe(true); }); - it('returns false for a name longer than 100 chars', () => { + test('returns false for a name longer than 100 chars', () => { const longName = 'a'.repeat(101); expect((FallbacksSanitizer as any).isValidFlagName(longName)).toBe(false); }); - it('returns false if the name contains spaces', () => { + test('returns false if the name contains spaces', () => { expect((FallbacksSanitizer as any).isValidFlagName('invalid flag')).toBe(false); }); }); describe('isValidTreatment', () => { - it('returns true for a valid treatment string', () => { + test('returns true for a valid treatment string', () => { expect((FallbacksSanitizer as any).isValidTreatment(validTreatment)).toBe(true); }); - it('returns false for null or undefined', () => { + test('returns false for null or undefined', () => { expect((FallbacksSanitizer as any).isValidTreatment(null)).toBe(false); expect((FallbacksSanitizer as any).isValidTreatment(undefined)).toBe(false); }); - it('returns false for a treatment longer than 100 chars', () => { + test('returns false for a treatment longer than 100 chars', () => { const long = { treatment: 'a'.repeat(101) }; expect((FallbacksSanitizer as any).isValidTreatment(long)).toBe(false); }); - it('returns false if treatment does not match regex pattern', () => { + test('returns false if treatment does not match regex pattern', () => { const invalid = { treatment: 'invalid treatment!' }; expect((FallbacksSanitizer as any).isValidTreatment(invalid)).toBe(false); }); }); describe('sanitizeGlobal', () => { - it('returns the treatment if valid', () => { + test('returns the treatment if valid', () => { expect(FallbacksSanitizer.sanitizeGlobal(validTreatment)).toEqual(validTreatment); expect(console.error).not.toHaveBeenCalled(); }); - it('returns undefined and logs error if invalid', () => { + test('returns undefined and logs error if invalid', () => { const result = FallbacksSanitizer.sanitizeGlobal(invalidTreatment); expect(result).toBeUndefined(); expect(console.error).toHaveBeenCalledWith( @@ -66,7 +66,7 @@ describe('FallbacksSanitizer', () => { }); describe('sanitizeByFlag', () => { - it('returns a sanitized map with valid entries only', () => { + test('returns a sanitized map with valid entries only', () => { const input = { valid_flag: validTreatment, 'invalid flag': validTreatment, @@ -79,7 +79,7 @@ describe('FallbacksSanitizer', () => { expect(console.error).toHaveBeenCalledTimes(2); // invalid flag + bad_treatment }); - it('returns empty object if all invalid', () => { + test('returns empty object if all invalid', () => { const input = { 'invalid flag': invalidTreatment, }; @@ -89,7 +89,7 @@ describe('FallbacksSanitizer', () => { expect(console.error).toHaveBeenCalled(); }); - it('returns same object if all valid', () => { + test('returns same object if all valid', () => { const input = { flag_one: validTreatment, flag_two: { treatment: 'valid_2', config: null }, diff --git a/src/evaluator/fallbackTreatmentsCalculator/fallbackSanitizer/index.ts b/src/evaluator/fallbackTreatmentsCalculator/fallbackSanitizer/index.ts index a8e07e34..06c7fe2e 100644 --- a/src/evaluator/fallbackTreatmentsCalculator/fallbackSanitizer/index.ts +++ b/src/evaluator/fallbackTreatmentsCalculator/fallbackSanitizer/index.ts @@ -1,4 +1,4 @@ -import { TreatmentWithConfig } from '../../../../types/splitio'; +import { FallbackTreatment } from '../../../../types/splitio'; import { FallbackDiscardReason } from '../constants'; @@ -10,21 +10,26 @@ export class FallbacksSanitizer { return name.length <= 100 && !name.includes(' '); } - private static isValidTreatment(t?: TreatmentWithConfig): boolean { - if (!t || !t.treatment) { + private static isValidTreatment(t?: FallbackTreatment): boolean { + if (!t) { return false; } - const { treatment } = t; + if (typeof t === 'string') { + if (t.length > 100) { + return false; + } + return FallbacksSanitizer.pattern.test(t); + } - if (treatment.length > 100) { + const { treatment } = t; + if (!treatment || treatment.length > 100) { return false; } - return FallbacksSanitizer.pattern.test(treatment); } - static sanitizeGlobal(treatment?: TreatmentWithConfig): TreatmentWithConfig | undefined { + static sanitizeGlobal(treatment?: FallbackTreatment): FallbackTreatment | undefined { if (!this.isValidTreatment(treatment)) { console.error( `Fallback treatments - Discarded fallback: ${FallbackDiscardReason.Treatment}` @@ -35,9 +40,9 @@ export class FallbacksSanitizer { } static sanitizeByFlag( - byFlagFallbacks: Record - ): Record { - const sanitizedByFlag: Record = {}; + byFlagFallbacks: Record + ): Record { + const sanitizedByFlag: Record = {}; const entries = Object.entries(byFlagFallbacks); entries.forEach(([flag, t]) => { diff --git a/src/evaluator/fallbackTreatmentsCalculator/index.ts b/src/evaluator/fallbackTreatmentsCalculator/index.ts new file mode 100644 index 00000000..30ef69e2 --- /dev/null +++ b/src/evaluator/fallbackTreatmentsCalculator/index.ts @@ -0,0 +1,49 @@ +import { FallbackTreatmentConfiguration, FallbackTreatment, IFallbackTreatmentsCalculator} from '../../../types/splitio'; + +export class FallbackTreatmentsCalculator implements IFallbackTreatmentsCalculator { + private readonly labelPrefix = 'fallback - '; + private readonly control = 'CONTROL'; + private readonly fallbacks: FallbackTreatmentConfiguration; + + constructor(fallbacks: FallbackTreatmentConfiguration) { + this.fallbacks = fallbacks; + } + + resolve(flagName: string, label?: string | undefined): FallbackTreatment { + const treatment = this.fallbacks.byFlag[flagName]; + if (treatment) { + return this.copyWithLabel(treatment, label); + } + + if (this.fallbacks.global) { + return this.copyWithLabel(this.fallbacks.global, label); + } + + return { + treatment: this.control, + config: null, + label: this.resolveLabel(label), + }; + } + + private copyWithLabel(fallback: FallbackTreatment, label: string | undefined): FallbackTreatment { + if (typeof fallback === 'string') { + return { + treatment: fallback, + config: null, + label: this.resolveLabel(label), + }; + } + + return { + treatment: fallback.treatment, + config: fallback.config, + label: this.resolveLabel(label), + }; + } + + private resolveLabel(label?: string | undefined): string | undefined { + return label ? `${this.labelPrefix}${label}` : undefined; + } + +} diff --git a/types/splitio.d.ts b/types/splitio.d.ts index 788029d3..f1ac014f 100644 --- a/types/splitio.d.ts +++ b/types/splitio.d.ts @@ -624,7 +624,7 @@ declare namespace SplitIO { /** * Fallback treatments to be used when the SDK is not ready or the flag is not found. */ - readonly fallbackTreatments?: FallbackTreatmentOptions; + readonly fallbackTreatments?: FallbackTreatmentConfiguration; } /** * Log levels. @@ -1232,15 +1232,26 @@ declare namespace SplitIO { * User consent status. */ type ConsentStatus = 'GRANTED' | 'DECLINED' | 'UNKNOWN'; + /** + * Fallback treatment can be either a string (treatment) or an object with treatment, config and label. + */ + type FallbackTreatment = string | { + treatment: string; + config?: string | null; + label?: string | null; + }; /** * Fallback treatments to be used when the SDK is not ready or the flag is not found. */ - type FallbackTreatmentOptions = { - global?: TreatmentWithConfig | Treatment, + type FallbackTreatmentConfiguration = { + global?: FallbackTreatment, byFlag: { - [key: string]: TreatmentWithConfig | Treatment + [key: string]: FallbackTreatment } } + type IFallbackTreatmentsCalculator = { + resolve(flagName: string, label?: string | undefined): FallbackTreatment; + } /** * Logger. Its interface details are not part of the public API. It shouldn't be used directly. */