UNPKG

hypertune

Version:

[Hypertune](https://www.hypertune.com/) is the most flexible platform for feature flags, A/B testing, analytics and app configuration. Built with full end-to-end type-safety, Git-style version control and local, synchronous, in-memory flag evaluation. Opt

667 lines (627 loc) 18.7 kB
import { ArithmeticOperator, BaseExpressionFields, BooleanExpression, BooleanValueType, ComparisonOperator, ContinuousDimensionType, DiscreteDimensionType, EnumExpression, Expression, FloatExpression, FloatValueType, FunctionValueType, IntExpression, IntValueType, ListValueType, NoOpExpression, ObjectValueType, Parameter, RegexExpression, StringExpression, StringValueType, ValueType, VariableExpression, VoidValueType, } from "../types"; type FoldPartialResult<TFoldResult> = | null | NoOpExpression | BooleanExpression | IntExpression | FloatExpression | StringExpression | RegexExpression | EnumExpression | (BaseExpressionFields & { type: "ObjectExpression"; valueType: ObjectValueType; objectTypeName: string; fields: { [fieldName: string]: TFoldResult }; }) | (BaseExpressionFields & { type: "GetFieldExpression"; valueType: ValueType; object: TFoldResult; fieldPath: string | null; }) | (BaseExpressionFields & { type: "UpdateObjectExpression"; valueType: ObjectValueType; object: TFoldResult; updates: { [fieldName: string]: TFoldResult }; }) | (BaseExpressionFields & { type: "ListExpression"; valueType: ListValueType; items: TFoldResult[]; }) | (BaseExpressionFields & { type: "SwitchExpression"; valueType: ValueType; control: TFoldResult; cases: { id: string; when: TFoldResult; then: TFoldResult; }[]; default: TFoldResult; }) | (BaseExpressionFields & { type: "EnumSwitchExpression"; valueType: ValueType; control: TFoldResult; cases: { [enumValue: string]: TFoldResult }; }) | (BaseExpressionFields & { type: "ComparisonExpression"; valueType: BooleanValueType; operator: ComparisonOperator | null; a: TFoldResult; b: TFoldResult; }) | (BaseExpressionFields & { type: "ArithmeticExpression"; valueType: IntValueType | FloatValueType; operator: ArithmeticOperator | null; a: TFoldResult; b: TFoldResult; }) | (BaseExpressionFields & { type: "RoundNumberExpression"; valueType: IntValueType; number: TFoldResult; }) | (BaseExpressionFields & { type: "StringifyNumberExpression"; valueType: StringValueType; number: TFoldResult; }) | (BaseExpressionFields & { type: "StringConcatExpression"; valueType: StringValueType; strings: TFoldResult; }) | (BaseExpressionFields & { type: "GetUrlQueryParameterExpression"; valueType: StringValueType; url: TFoldResult; queryParameterName: TFoldResult; }) | (BaseExpressionFields & { type: "SplitExpression"; valueType: ValueType; splitId: string | null; dimensionId: string | null; expose: TFoldResult; unitId: TFoldResult; dimensionMapping: | { type: typeof DiscreteDimensionType; cases: { [armId: string]: TFoldResult }; } | { type: typeof ContinuousDimensionType; function: TFoldResult; }; eventObjectTypeName: string | null; eventPayload: TFoldResult; }) | (BaseExpressionFields & { type: "LogEventExpression"; valueType: VoidValueType; eventObjectTypeName: string | null; eventPayload: TFoldResult; eventTypeId: string | null; unitId: TFoldResult; }) | (BaseExpressionFields & { type: "FunctionExpression"; valueType: FunctionValueType; parameters: Parameter[]; body: TFoldResult; }) | VariableExpression | (BaseExpressionFields & { type: "ApplicationExpression"; valueType: ValueType; function: TFoldResult; arguments: TFoldResult[]; }); type FoldFunction<TFoldResult> = ( partialResult: FoldPartialResult<TFoldResult> ) => TFoldResult; // Expensive export default function fold<TFoldResult>( f: FoldFunction<TFoldResult>, expression: Expression | null ): TFoldResult { if (!expression) { return f(expression); } switch (expression.type) { case "NoOpExpression": case "BooleanExpression": case "IntExpression": case "FloatExpression": case "StringExpression": case "RegexExpression": case "EnumExpression": return f(expression); case "ObjectExpression": return f({ ...expression, fields: Object.fromEntries( Object.entries(expression.fields).map(([fieldName, field]) => [ fieldName, fold(f, field), ]) ), }); case "GetFieldExpression": return f({ ...expression, object: fold(f, expression.object), }); case "UpdateObjectExpression": return f({ ...expression, object: fold(f, expression.object), updates: Object.fromEntries( Object.entries(expression.updates).map(([fieldName, field]) => [ fieldName, fold(f, field), ]) ), }); case "ListExpression": return f({ ...expression, items: expression.items.map((item) => fold(f, item)), }); case "SwitchExpression": return f({ ...expression, control: fold(f, expression.control), cases: expression.cases.map((item) => ({ id: item.id, when: fold(f, item.when), then: fold(f, item.then), })), default: fold(f, expression.default), }); case "EnumSwitchExpression": return f({ ...expression, control: fold(f, expression.control), cases: Object.fromEntries( Object.entries(expression.cases).map( ([enumValue, caseExpression]) => [ enumValue, fold(f, caseExpression), ] ) ), }); case "ComparisonExpression": case "ArithmeticExpression": return f({ ...expression, a: fold(f, expression.a), b: fold(f, expression.b), }); case "RoundNumberExpression": case "StringifyNumberExpression": return f({ ...expression, number: fold(f, expression.number), }); case "StringConcatExpression": return f({ ...expression, strings: fold(f, expression.strings), }); case "GetUrlQueryParameterExpression": return f({ ...expression, url: fold(f, expression.url), queryParameterName: fold(f, expression.queryParameterName), }); case "SplitExpression": return f({ ...expression, expose: fold(f, expression.expose), unitId: fold(f, expression.unitId), dimensionMapping: expression.dimensionMapping.type === "discrete" ? { type: "discrete", cases: Object.fromEntries( Object.entries(expression.dimensionMapping.cases).map( ([armId, caseExpression]) => [ armId, fold(f, caseExpression), ] ) ), } : { type: "continuous", function: fold(f, expression.dimensionMapping.function), }, eventObjectTypeName: expression.eventObjectTypeName, eventPayload: fold(f, expression.eventPayload), }); case "LogEventExpression": return f({ ...expression, unitId: fold(f, expression.unitId), eventPayload: fold(f, expression.eventPayload), }); case "FunctionExpression": return f({ ...expression, body: fold(f, expression.body), }); case "VariableExpression": return f(expression); case "ApplicationExpression": return f({ ...expression, function: fold(f, expression.function), arguments: expression.arguments.map((argument) => fold(f, argument)), }); default: { const neverExpression: never = expression; throw new Error( `Unexpected expression: ${JSON.stringify(neverExpression)}` ); } } } // Applies f to children, constructs new expression, applies f to it and returns // it along with merged map results // Expensive export function mapExpressionWithResult<TMapResult>( fn: (expr: Expression | null) => { newExpression: Expression | null; mapResult: TMapResult; }, combineResults: (...results: TMapResult[]) => TMapResult, expression: Expression | null ): { newExpression: Expression | null; mapResult: TMapResult; } { type TFoldResult = { newExpression: Expression | null; mapResult: TMapResult; }; // eslint-disable-next-line func-style const foldFunction: FoldFunction<TFoldResult> = (partialResult) => { if (!partialResult) { return fn(partialResult); } switch (partialResult.type) { case "NoOpExpression": case "BooleanExpression": case "IntExpression": case "FloatExpression": case "StringExpression": case "RegexExpression": case "EnumExpression": return fn(partialResult); case "ObjectExpression": { const thisResult = fn({ ...partialResult, fields: Object.fromEntries( Object.entries(partialResult.fields).map( ([fieldName, fieldResult]) => [ fieldName, fieldResult.newExpression, ] ) ), }); return { newExpression: thisResult.newExpression, mapResult: combineResults( thisResult.mapResult, ...Object.values(partialResult.fields).map( (fieldResult) => fieldResult.mapResult ) ), }; } case "GetFieldExpression": { const thisResult = fn({ ...partialResult, object: partialResult.object.newExpression, }); return { newExpression: thisResult.newExpression, mapResult: combineResults( thisResult.mapResult, partialResult.object.mapResult ), }; } case "UpdateObjectExpression": { const thisResult = fn({ ...partialResult, object: partialResult.object.newExpression, updates: Object.fromEntries( Object.entries(partialResult.updates).map( ([fieldName, updateResult]) => [ fieldName, updateResult.newExpression, ] ) ), }); return { newExpression: thisResult.newExpression, mapResult: combineResults( thisResult.mapResult, partialResult.object.mapResult, ...Object.values(partialResult.updates).map( (updateResult) => updateResult.mapResult ) ), }; } case "ListExpression": { const thisResult = fn({ ...partialResult, items: partialResult.items.map( (itemResult) => itemResult.newExpression ), }); return { newExpression: thisResult.newExpression, mapResult: combineResults( thisResult.mapResult, ...partialResult.items.map((itemResult) => itemResult.mapResult) ), }; } case "SwitchExpression": { const thisResult = fn({ ...partialResult, control: partialResult.control.newExpression, cases: partialResult.cases.map((caseResult) => ({ id: caseResult.id, when: caseResult.when.newExpression, then: caseResult.then.newExpression, })), default: partialResult.default.newExpression, }); return { newExpression: thisResult.newExpression, mapResult: combineResults( thisResult.mapResult, partialResult.control.mapResult, ...partialResult.cases.map((caseResult) => combineResults( caseResult.when.mapResult, caseResult.then.mapResult ) ), partialResult.default.mapResult ), }; } case "EnumSwitchExpression": { const thisResult = fn({ ...partialResult, control: partialResult.control.newExpression, cases: Object.fromEntries( Object.entries(partialResult.cases).map( ([enumValue, caseResult]) => [enumValue, caseResult.newExpression] ) ), }); return { newExpression: thisResult.newExpression, mapResult: combineResults( thisResult.mapResult, partialResult.control.mapResult, ...Object.values(partialResult.cases).map( (caseResult) => caseResult.mapResult ) ), }; } case "ComparisonExpression": case "ArithmeticExpression": { const thisResult = fn({ ...partialResult, a: partialResult.a.newExpression, b: partialResult.b.newExpression, }); return { newExpression: thisResult.newExpression, mapResult: combineResults( thisResult.mapResult, partialResult.a.mapResult, partialResult.b.mapResult ), }; } case "RoundNumberExpression": case "StringifyNumberExpression": { const thisResult = fn({ ...partialResult, number: partialResult.number.newExpression, }); return { newExpression: thisResult.newExpression, mapResult: combineResults( thisResult.mapResult, partialResult.number.mapResult ), }; } case "StringConcatExpression": { const thisResult = fn({ ...partialResult, strings: partialResult.strings.newExpression, }); return { newExpression: thisResult.newExpression, mapResult: combineResults( thisResult.mapResult, partialResult.strings.mapResult ), }; } case "GetUrlQueryParameterExpression": { const thisResult = fn({ ...partialResult, url: partialResult.url.newExpression, queryParameterName: partialResult.queryParameterName.newExpression, }); return { newExpression: thisResult.newExpression, mapResult: combineResults( thisResult.mapResult, partialResult.url.mapResult, partialResult.queryParameterName.mapResult ), }; } case "SplitExpression": { const thisResult = fn({ ...partialResult, expose: partialResult.expose.newExpression, unitId: partialResult.unitId.newExpression, dimensionMapping: partialResult.dimensionMapping.type === "discrete" ? { type: "discrete", cases: Object.fromEntries( Object.entries(partialResult.dimensionMapping.cases).map( ([armId, caseResult]) => [armId, caseResult.newExpression] ) ), } : { type: "continuous", function: partialResult.dimensionMapping.function.newExpression, }, eventObjectTypeName: partialResult.eventObjectTypeName, eventPayload: partialResult.eventPayload.newExpression, featuresMapping: {}, }); return { newExpression: thisResult.newExpression, mapResult: combineResults( thisResult.mapResult, partialResult.expose.mapResult, partialResult.unitId.mapResult, partialResult.eventPayload.mapResult, ...(partialResult.dimensionMapping.type === "discrete" ? Object.values(partialResult.dimensionMapping.cases).map( (caseMapResult) => caseMapResult.mapResult ) : [partialResult.dimensionMapping.function.mapResult]) ), }; } case "LogEventExpression": { const thisResult = fn({ ...partialResult, eventObjectTypeName: partialResult.eventObjectTypeName, eventPayload: partialResult.eventPayload.newExpression, unitId: partialResult.unitId.newExpression, featuresMapping: {}, }); return { newExpression: thisResult.newExpression, mapResult: combineResults( thisResult.mapResult, partialResult.unitId.mapResult, partialResult.eventPayload.mapResult ), }; } case "FunctionExpression": { const thisResult = fn({ ...partialResult, body: partialResult.body.newExpression, }); return { newExpression: thisResult.newExpression, mapResult: combineResults( thisResult.mapResult, partialResult.body.mapResult ), }; } case "VariableExpression": return fn(partialResult); case "ApplicationExpression": { const thisResult = fn({ ...partialResult, function: partialResult.function.newExpression, arguments: partialResult.arguments.map( (argumentResult) => argumentResult.newExpression ), }); return { newExpression: thisResult.newExpression, mapResult: combineResults( thisResult.mapResult, partialResult.function.mapResult, ...partialResult.arguments.map( (argumentResult) => argumentResult.mapResult ) ), }; } default: { const neverPartialResult: never = partialResult; throw new Error( `Unexpected partial result: ${JSON.stringify(neverPartialResult)}` ); } } }; return fold(foldFunction, expression); } // Expensive export function mapExpression( mapper: (expr: Expression | null) => Expression | null, expression: Expression | null ): Expression | null { const result = mapExpressionWithResult<null>( (expr) => ({ newExpression: mapper(expr), mapResult: null, }), () => null, expression ); return result.newExpression; }