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
text/typescript
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;
}