From 9f507fe75392cddb891c11ac0812cb663dd1c1f7 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 7 Mar 2025 15:23:22 -0300 Subject: [PATCH] Update types and support evaluation for rule-based segments, i.e., without partitions --- src/dtos/types.ts | 21 +++++++++------- src/evaluator/Engine.ts | 2 +- src/evaluator/combiners/and.ts | 9 +++---- src/evaluator/combiners/ifelseif.ts | 16 ++++++------- src/evaluator/condition/index.ts | 24 +++++++++---------- .../matchers/__tests__/between.spec.ts | 4 ++-- .../__tests__/segment/client_side.spec.ts | 8 +++---- src/evaluator/matchers/index.ts | 2 +- src/evaluator/parser/index.ts | 6 ++--- src/evaluator/types.ts | 4 ++-- src/evaluator/value/index.ts | 4 ++-- src/evaluator/value/sanitize.ts | 6 ++--- src/sdkManager/index.ts | 2 +- src/utils/lang/index.ts | 2 +- 14 files changed, 57 insertions(+), 53 deletions(-) diff --git a/src/dtos/types.ts b/src/dtos/types.ts index a2ffaad5..423de3a8 100644 --- a/src/dtos/types.ts +++ b/src/dtos/types.ts @@ -66,6 +66,11 @@ interface IInSegmentMatcher extends ISplitMatcherBase { userDefinedSegmentMatcherData: IInSegmentMatcherData } +interface IInRBSegmentMatcher extends ISplitMatcherBase { + matcherType: 'IN_RULE_BASED_SEGMENT', + userDefinedSegmentMatcherData: IInSegmentMatcherData +} + interface IInLargeSegmentMatcher extends ISplitMatcherBase { matcherType: 'IN_LARGE_SEGMENT', userDefinedLargeSegmentMatcherData: IInLargeSegmentMatcherData @@ -176,7 +181,7 @@ export type ISplitMatcher = IAllKeysMatcher | IInSegmentMatcher | IWhitelistMatc ILessThanOrEqualToMatcher | IBetweenMatcher | IEqualToSetMatcher | IContainsAnyOfSetMatcher | IContainsAllOfSetMatcher | IPartOfSetMatcher | IStartsWithMatcher | IEndsWithMatcher | IContainsStringMatcher | IInSplitTreatmentMatcher | IEqualToBooleanMatcher | IMatchesStringMatcher | IEqualToSemverMatcher | IGreaterThanOrEqualToSemverMatcher | ILessThanOrEqualToSemverMatcher | IBetweenSemverMatcher | IInListSemverMatcher | - IInLargeSegmentMatcher + IInLargeSegmentMatcher | IInRBSegmentMatcher /** Split object */ export interface ISplitPartition { @@ -189,30 +194,30 @@ export interface ISplitCondition { combiner: 'AND', matchers: ISplitMatcher[] } - partitions: ISplitPartition[] - label: string - conditionType: 'ROLLOUT' | 'WHITELIST' + partitions?: ISplitPartition[] + label?: string + conditionType?: 'ROLLOUT' | 'WHITELIST' } export interface IRBSegment { name: string, changeNumber: number, status: 'ACTIVE' | 'ARCHIVED', + conditions: ISplitCondition[], excluded: { keys: string[], segments: string[] - }, - conditions: ISplitCondition[], + } } export interface ISplit { name: string, changeNumber: number, + status: 'ACTIVE' | 'ARCHIVED', + conditions: ISplitCondition[], killed: boolean, defaultTreatment: string, trafficTypeName: string, - conditions: ISplitCondition[], - status: 'ACTIVE' | 'ARCHIVED', seed: number, trafficAllocation?: number, trafficAllocationSeed?: number diff --git a/src/evaluator/Engine.ts b/src/evaluator/Engine.ts index 36f52cb4..233ecc2a 100644 --- a/src/evaluator/Engine.ts +++ b/src/evaluator/Engine.ts @@ -73,7 +73,7 @@ export class Engine { trafficAllocationSeed, attributes, splitEvaluator - ); + ) as MaybeThenable; // Evaluation could be async, so we should handle that case checking for a // thenable object diff --git a/src/evaluator/combiners/and.ts b/src/evaluator/combiners/and.ts index b229a22b..fd239753 100644 --- a/src/evaluator/combiners/and.ts +++ b/src/evaluator/combiners/and.ts @@ -2,10 +2,11 @@ import { findIndex } from '../../utils/lang'; import { ILogger } from '../../logger/types'; import { thenable } from '../../utils/promise/thenable'; import { MaybeThenable } from '../../dtos/types'; -import { IMatcher } from '../types'; +import { ISplitEvaluator } from '../types'; import { ENGINE_COMBINER_AND } from '../../logger/constants'; +import SplitIO from '../../../types/splitio'; -export function andCombinerContext(log: ILogger, matchers: IMatcher[]) { +export function andCombinerContext(log: ILogger, matchers: Array<(key: SplitIO.SplitKey, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) => MaybeThenable>) { function andResults(results: boolean[]): boolean { // Array.prototype.every is supported by target environments @@ -15,8 +16,8 @@ export function andCombinerContext(log: ILogger, matchers: IMatcher[]) { return hasMatchedAll; } - return function andCombiner(...params: any): MaybeThenable { - const matcherResults = matchers.map(matcher => matcher(...params)); + return function andCombiner(key: SplitIO.SplitKey, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator): MaybeThenable { + const matcherResults = matchers.map(matcher => matcher(key, attributes, splitEvaluator)); // If any matching result is a thenable we should use Promise.all if (findIndex(matcherResults, thenable) !== -1) { diff --git a/src/evaluator/combiners/ifelseif.ts b/src/evaluator/combiners/ifelseif.ts index b2bfdcd0..aaba4b27 100644 --- a/src/evaluator/combiners/ifelseif.ts +++ b/src/evaluator/combiners/ifelseif.ts @@ -1,4 +1,4 @@ -import { findIndex } from '../../utils/lang'; +import { findIndex, isBoolean } from '../../utils/lang'; import { ILogger } from '../../logger/types'; import { thenable } from '../../utils/promise/thenable'; import { UNSUPPORTED_MATCHER_TYPE } from '../../utils/labels'; @@ -18,14 +18,12 @@ export function ifElseIfCombinerContext(log: ILogger, predicates: IEvaluator[]): }; } - function computeTreatment(predicateResults: Array) { - const len = predicateResults.length; - - for (let i = 0; i < len; i++) { + function computeEvaluation(predicateResults: Array): IEvaluation | boolean | undefined { + for (let i = 0, len = predicateResults.length; i < len; i++) { const evaluation = predicateResults[i]; if (evaluation !== undefined) { - log.debug(ENGINE_COMBINER_IFELSEIF, [evaluation.treatment]); + if (!isBoolean(evaluation)) log.debug(ENGINE_COMBINER_IFELSEIF, [evaluation.treatment]); return evaluation; } @@ -35,7 +33,7 @@ export function ifElseIfCombinerContext(log: ILogger, predicates: IEvaluator[]): return undefined; } - function ifElseIfCombiner(key: SplitIO.SplitKey, seed?: number, trafficAllocation?: number, trafficAllocationSeed?: number, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) { + function ifElseIfCombiner(key: SplitIO.SplitKeyObject, seed?: number, trafficAllocation?: number, trafficAllocationSeed?: number, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) { // In Async environments we are going to have async predicates. There is none way to know // before hand so we need to evaluate all the predicates, verify for thenables, and finally, // define how to return the treatment (wrap result into a Promise or not). @@ -43,10 +41,10 @@ export function ifElseIfCombinerContext(log: ILogger, predicates: IEvaluator[]): // if we find a thenable if (findIndex(predicateResults, thenable) !== -1) { - return Promise.all(predicateResults).then(results => computeTreatment(results)); + return Promise.all(predicateResults).then(results => computeEvaluation(results)); } - return computeTreatment(predicateResults as IEvaluation[]); + return computeEvaluation(predicateResults as IEvaluation[]); } // if there is none predicates, then there was an error in parsing phase diff --git a/src/evaluator/condition/index.ts b/src/evaluator/condition/index.ts index 4fd6d372..5facaa5c 100644 --- a/src/evaluator/condition/index.ts +++ b/src/evaluator/condition/index.ts @@ -7,14 +7,14 @@ import SplitIO from '../../../types/splitio'; import { ILogger } from '../../logger/types'; // Build Evaluation object if and only if matchingResult is true -function match(log: ILogger, matchingResult: boolean, bucketingKey: string | undefined, seed: number | undefined, treatments: { getTreatmentFor: (x: number) => string }, label: string): IEvaluation | undefined { +function match(log: ILogger, matchingResult: boolean, bucketingKey: string | undefined, seed?: number, treatments?: { getTreatmentFor: (x: number) => string }, label?: string): IEvaluation | boolean | undefined { if (matchingResult) { - const treatment = getTreatment(log, bucketingKey as string, seed, treatments); - - return { - treatment, - label - }; + return treatments ? // Feature flag + { + treatment: getTreatment(log, bucketingKey as string, seed, treatments), + label: label! + } : // Rule-based segment + true; } // else we should notify the engine to continue evaluating @@ -22,12 +22,12 @@ function match(log: ILogger, matchingResult: boolean, bucketingKey: string | und } // Condition factory -export function conditionContext(log: ILogger, matcherEvaluator: (...args: any) => MaybeThenable, treatments: { getTreatmentFor: (x: number) => string }, label: string, conditionType: 'ROLLOUT' | 'WHITELIST'): IEvaluator { +export function conditionContext(log: ILogger, matcherEvaluator: (key: SplitIO.SplitKeyObject, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) => MaybeThenable, treatments?: { getTreatmentFor: (x: number) => string }, label?: string, conditionType?: 'ROLLOUT' | 'WHITELIST'): IEvaluator { - return function conditionEvaluator(key: SplitIO.SplitKey, seed?: number, trafficAllocation?: number, trafficAllocationSeed?: number, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) { + return function conditionEvaluator(key: SplitIO.SplitKeyObject, seed?: number, trafficAllocation?: number, trafficAllocationSeed?: number, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) { // Whitelisting has more priority than traffic allocation, so we don't apply this filtering to those conditions. - if (conditionType === 'ROLLOUT' && !shouldApplyRollout(trafficAllocation as number, (key as SplitIO.SplitKeyObject).bucketingKey as string, trafficAllocationSeed as number)) { + if (conditionType === 'ROLLOUT' && !shouldApplyRollout(trafficAllocation!, key.bucketingKey, trafficAllocationSeed!)) { return { treatment: undefined, // treatment value is assigned later label: NOT_IN_SPLIT @@ -41,10 +41,10 @@ export function conditionContext(log: ILogger, matcherEvaluator: (...args: any) const matches = matcherEvaluator(key, attributes, splitEvaluator); if (thenable(matches)) { - return matches.then(result => match(log, result, (key as SplitIO.SplitKeyObject).bucketingKey, seed, treatments, label)); + return matches.then(result => match(log, result, key.bucketingKey, seed, treatments, label)); } - return match(log, matches, (key as SplitIO.SplitKeyObject).bucketingKey, seed, treatments, label); + return match(log, matches, key.bucketingKey, seed, treatments, label); }; } diff --git a/src/evaluator/matchers/__tests__/between.spec.ts b/src/evaluator/matchers/__tests__/between.spec.ts index 5b76186b..eaf78106 100644 --- a/src/evaluator/matchers/__tests__/between.spec.ts +++ b/src/evaluator/matchers/__tests__/between.spec.ts @@ -19,6 +19,6 @@ test('MATCHER BETWEEN / should return true ONLY when the value is between 10 and expect(matcher(15)).toBe(true); // 15 is between 10 and 20 expect(matcher(20)).toBe(true); // 20 is between 10 and 20 expect(matcher(21)).toBe(false); // 21 is not between 10 and 20 - expect(matcher(undefined)).toBe(false); // undefined is not between 10 and 20 - expect(matcher(null)).toBe(false); // null is not between 10 and 20 + expect(matcher(undefined as any)).toBe(false); // undefined is not between 10 and 20 + expect(matcher(null as any)).toBe(false); // null is not between 10 and 20 }); diff --git a/src/evaluator/matchers/__tests__/segment/client_side.spec.ts b/src/evaluator/matchers/__tests__/segment/client_side.spec.ts index 5e192829..c4e3470e 100644 --- a/src/evaluator/matchers/__tests__/segment/client_side.spec.ts +++ b/src/evaluator/matchers/__tests__/segment/client_side.spec.ts @@ -29,8 +29,8 @@ test('MATCHER IN_SEGMENT / should return true ONLY when the segment is defined i } } as IStorageSync) as IMatcher; - expect(await matcherTrue()).toBe(true); // segment found in mySegments list - expect(await matcherFalse()).toBe(false); // segment not found in mySegments list + expect(await matcherTrue('key')).toBe(true); // segment found in mySegments list + expect(await matcherFalse('key')).toBe(false); // segment not found in mySegments list }); test('MATCHER IN_LARGE_SEGMENT / should return true ONLY when the segment is defined inside the segment storage', async function () { @@ -54,6 +54,6 @@ test('MATCHER IN_LARGE_SEGMENT / should return true ONLY when the segment is def largeSegments: undefined } as IStorageSync) as IMatcher; - expect(await matcherTrue()).toBe(true); // large segment found in mySegments list - expect(await matcherFalse()).toBe(false); // large segment storage is not defined + expect(await matcherTrue('key')).toBe(true); // large segment found in mySegments list + expect(await matcherFalse('key')).toBe(false); // large segment storage is not defined }); diff --git a/src/evaluator/matchers/index.ts b/src/evaluator/matchers/index.ts index d50c38dd..3fd840f4 100644 --- a/src/evaluator/matchers/index.ts +++ b/src/evaluator/matchers/index.ts @@ -64,5 +64,5 @@ export function matcherFactory(log: ILogger, matcherDto: IMatcherDto, storage?: let matcherFn; // @ts-ignore if (matchers[type]) matcherFn = matchers[type](value, storage, log); // There is no index-out-of-bound exception in JavaScript - return matcherFn; + return matcherFn as IMatcher; } diff --git a/src/evaluator/parser/index.ts b/src/evaluator/parser/index.ts index a398aa0b..d12edf1a 100644 --- a/src/evaluator/parser/index.ts +++ b/src/evaluator/parser/index.ts @@ -37,7 +37,7 @@ export function parser(log: ILogger, conditions: ISplitCondition[], storage: ISt } // Evaluator function. - return (key: string, attributes: SplitIO.Attributes | undefined, splitEvaluator: ISplitEvaluator) => { + return (key: SplitIO.SplitKey, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) => { const value = sanitizeValue(log, key, matcherDto, attributes); let result: MaybeThenable = false; @@ -71,12 +71,12 @@ export function parser(log: ILogger, conditions: ISplitCondition[], storage: ISt predicates.push(conditionContext( log, andCombinerContext(log, expressions), - Treatments.parse(partitions), + partitions && Treatments.parse(partitions), label, conditionType )); } - // Instanciate evaluator given the set of conditions using if else if logic + // Instantiate evaluator given the set of conditions using if else if logic return ifElseIfCombinerContext(log, predicates); } diff --git a/src/evaluator/types.ts b/src/evaluator/types.ts index db0d4e28..92806ddf 100644 --- a/src/evaluator/types.ts +++ b/src/evaluator/types.ts @@ -29,6 +29,6 @@ export type IEvaluationResult = IEvaluation & { treatment: string; impressionsDi export type ISplitEvaluator = (log: ILogger, key: SplitIO.SplitKey, splitName: string, attributes: SplitIO.Attributes | undefined, storage: IStorageSync | IStorageAsync) => MaybeThenable -export type IEvaluator = (key: SplitIO.SplitKey, seed?: number, trafficAllocation?: number, trafficAllocationSeed?: number, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) => MaybeThenable +export type IEvaluator = (key: SplitIO.SplitKeyObject, seed?: number, trafficAllocation?: number, trafficAllocationSeed?: number, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) => MaybeThenable -export type IMatcher = (...args: any) => MaybeThenable +export type IMatcher = (value: string | number | boolean | string[] | IDependencyMatcherValue, splitEvaluator?: ISplitEvaluator) => MaybeThenable diff --git a/src/evaluator/value/index.ts b/src/evaluator/value/index.ts index 95b4000c..06184aa5 100644 --- a/src/evaluator/value/index.ts +++ b/src/evaluator/value/index.ts @@ -4,7 +4,7 @@ import { ILogger } from '../../logger/types'; import { sanitize } from './sanitize'; import { ENGINE_VALUE, ENGINE_VALUE_NO_ATTRIBUTES, ENGINE_VALUE_INVALID } from '../../logger/constants'; -function parseValue(log: ILogger, key: string, attributeName: string | null, attributes?: SplitIO.Attributes) { +function parseValue(log: ILogger, key: SplitIO.SplitKey, attributeName: string | null, attributes?: SplitIO.Attributes) { let value = undefined; if (attributeName) { if (attributes) { @@ -23,7 +23,7 @@ function parseValue(log: ILogger, key: string, attributeName: string | null, att /** * Defines value to be matched (key / attribute). */ -export function sanitizeValue(log: ILogger, key: string, matcherDto: IMatcherDto, attributes?: SplitIO.Attributes) { +export function sanitizeValue(log: ILogger, key: SplitIO.SplitKey, matcherDto: IMatcherDto, attributes?: SplitIO.Attributes) { const attributeName = matcherDto.attribute; const valueToMatch = parseValue(log, key, attributeName, attributes); const sanitizedValue = sanitize(log, matcherDto.type, valueToMatch, matcherDto.dataType, attributes); diff --git a/src/evaluator/value/sanitize.ts b/src/evaluator/value/sanitize.ts index 630a4b38..5d0d6e28 100644 --- a/src/evaluator/value/sanitize.ts +++ b/src/evaluator/value/sanitize.ts @@ -69,9 +69,9 @@ function getProcessingFunction(matcherTypeID: number, dataType: string) { /** * Sanitize matcher value */ -export function sanitize(log: ILogger, matcherTypeID: number, value: string | number | boolean | Array | undefined, dataType: string, attributes?: SplitIO.Attributes) { +export function sanitize(log: ILogger, matcherTypeID: number, value: string | number | boolean | Array | SplitIO.SplitKey | undefined, dataType: string, attributes?: SplitIO.Attributes) { const processor = getProcessingFunction(matcherTypeID, dataType); - let sanitizedValue: string | number | boolean | Array | IDependencyMatcherValue | undefined; + let sanitizedValue: string | number | boolean | Array | IDependencyMatcherValue | undefined; switch (dataType) { case matcherDataTypes.NUMBER: @@ -88,7 +88,7 @@ export function sanitize(log: ILogger, matcherTypeID: number, value: string | nu sanitizedValue = sanitizeBoolean(value); break; case matcherDataTypes.NOT_SPECIFIED: - sanitizedValue = value; + sanitizedValue = value as any; break; default: sanitizedValue = undefined; diff --git a/src/sdkManager/index.ts b/src/sdkManager/index.ts index 82423016..831771c9 100644 --- a/src/sdkManager/index.ts +++ b/src/sdkManager/index.ts @@ -17,7 +17,7 @@ function collectTreatments(splitObject: ISplit) { // Localstorage mode could fall into a no rollout conditions state. Take the first condition in that case. if (!allTreatmentsCondition) allTreatmentsCondition = conditions[0]; // Then extract the treatments from the partitions - return allTreatmentsCondition ? allTreatmentsCondition.partitions.map(v => v.treatment) : []; + return allTreatmentsCondition ? allTreatmentsCondition.partitions!.map(v => v.treatment) : []; } function objectToView(splitObject: ISplit | null): SplitIO.SplitView | null { diff --git a/src/utils/lang/index.ts b/src/utils/lang/index.ts index 11b6afd0..4735e608 100644 --- a/src/utils/lang/index.ts +++ b/src/utils/lang/index.ts @@ -111,7 +111,7 @@ export function groupBy>(source: T[], prop: string /** * Checks if a given value is a boolean. */ -export function isBoolean(val: any): boolean { +export function isBoolean(val: any): val is boolean { return val === true || val === false; }