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
111 lines (95 loc) • 3.35 kB
text/typescript
import { breakingSchemaChangesError, graphqlTypeNameKey } from "../constants";
import { Expression, Logs, Value } from "../types";
import nullThrows from "./nullThrows";
import { getExpressionEvaluationCountLogs, mergeLogs } from "./reductionLogs";
export const complexFormExpressionEvaluationError = `After evaluating your expression, the result was still in a complex form. ${breakingSchemaChangesError}`;
/**
* Expression evaluation takes a fully reduced expression in "normal form" and
* converts it to a JSON value. It returns this value with "logs" that track
* all of the expressions we've needed to evaluate to compute this final value.
*/
// TODO: Pass in Expr<Expr> instead of Expr<Expr | null> to avoid null checks?
export default function evaluate(
expression: Expression // Must be a fully reduced expression
): {
value: Value;
logs: Logs;
shouldLogEvaluation: boolean;
} {
const thisLogs = mergeLogs(
expression.logs,
getExpressionEvaluationCountLogs(expression)
);
switch (expression.type) {
case "NoOpExpression":
return { value: true, logs: thisLogs, shouldLogEvaluation: false };
case "BooleanExpression":
case "IntExpression":
case "FloatExpression":
case "StringExpression":
case "RegexExpression":
case "EnumExpression":
return {
value: expression.value,
logs: thisLogs,
shouldLogEvaluation: true,
};
case "ObjectExpression": {
const fieldEvaluations = Object.fromEntries(
Object.keys(expression.fields).map((fieldName) => [
fieldName,
evaluate(
nullThrows(expression.fields[fieldName], "null object field")
),
])
);
const value: Value = Object.fromEntries([
[graphqlTypeNameKey, expression.objectTypeName],
...Object.entries(fieldEvaluations).map(([fieldName, evaluation]) => [
fieldName,
evaluation.value,
]),
]);
const logs = mergeLogs(
thisLogs,
...Object.values(fieldEvaluations).map((evaluation) => evaluation.logs)
);
return { value, logs, shouldLogEvaluation: true };
}
case "ListExpression": {
const itemEvaluations = expression.items.map((item) =>
evaluate(nullThrows(item, "null list item"))
);
const value: Value = itemEvaluations.map(
(evaluation) => evaluation.value
);
const logs = mergeLogs(
thisLogs,
...itemEvaluations.map((evaluation) => evaluation.logs)
);
return { value, logs, shouldLogEvaluation: true };
}
case "GetFieldExpression":
case "UpdateObjectExpression":
case "SwitchExpression":
case "EnumSwitchExpression":
case "ComparisonExpression":
case "ArithmeticExpression":
case "RoundNumberExpression":
case "StringifyNumberExpression":
case "StringConcatExpression":
case "GetUrlQueryParameterExpression":
case "SplitExpression":
case "LogEventExpression":
case "FunctionExpression":
case "VariableExpression":
case "ApplicationExpression":
throw new Error(complexFormExpressionEvaluationError);
default: {
const neverExpression: never = expression;
throw new Error(
`unexpected expression: ${JSON.stringify(neverExpression)}`
);
}
}
}