Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 51 additions & 2 deletions packages/flag-evaluation/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ export interface ContextFilter {
field: string;
operator: ContextFilterOperator;
values?: string[];
valueSet?: Set<string>;
}

/**
Expand Down Expand Up @@ -286,6 +287,7 @@ export function evaluate(
fieldValue: string,
operator: ContextFilterOperator,
values: string[],
valueSet?: Set<string>,
): boolean {
const value = values[0];

Expand Down Expand Up @@ -331,9 +333,11 @@ export function evaluate(
case "IS_NOT":
return fieldValue !== value;
case "ANY_OF":
return values.includes(fieldValue);
return valueSet ? valueSet.has(fieldValue) : values.includes(fieldValue);
case "NOT_ANY_OF":
return !values.includes(fieldValue);
return valueSet
? !valueSet.has(fieldValue)
: !values.includes(fieldValue);
case "IS_TRUE":
return fieldValue == "true";
case "IS_FALSE":
Expand Down Expand Up @@ -362,6 +366,7 @@ function evaluateRecursively(
context[filter.field],
filter.operator,
filter.values || [],
filter.valueSet,
);
case "rolloutPercentage": {
if (!(filter.partialRolloutAttribute in context)) {
Expand Down Expand Up @@ -463,3 +468,47 @@ export function evaluateFeatureRules<T extends RuleValue>({
missingContextFields,
};
}

export function newEvaluator<T extends RuleValue>(rules: Rule<T>[]) {
function translateRule(rule: RuleFilter): RuleFilter {
if (rule.type === "group") {
return {
...rule,
filters: rule.filters.map(translateRule),
};
}

if (
rule.type === "context" &&
(rule.operator === "ANY_OF" || rule.operator === "NOT_ANY_OF")
) {
return {
...rule,
valueSet: new Set(rule.values ?? []),
};
}

return { ...rule };
}

const translatedRules = rules.map((rule) => {
const { filter } = rule;
const translatedFilter = translateRule(filter);

return {
...rule,
filter: translatedFilter,
};
});

return function evaluateOptimized(
context: Record<string, unknown>,
featureKey: string,
) {
return evaluateFeatureRules({
context,
featureKey,
rules: translatedRules,
});
};
}
110 changes: 110 additions & 0 deletions packages/flag-evaluation/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
EvaluationParams,
flattenJSON,
hashInt,
newEvaluator,
unflattenJSON,
} from "../src";

Expand Down Expand Up @@ -239,6 +240,115 @@ describe("evaluate feature targeting integration ", () => {
ruleEvaluationResults: [false],
});
});

it("evaluates optimized rule evaluations correctly", async () => {
const res = newEvaluator([
{
value: true,
filter: {
type: "group",
operator: "and",
filters: [
{
type: "context",
field: "company.id",
operator: "IS",
values: ["company1"],
},
{
type: "rolloutPercentage",
key: "flag",
partialRolloutAttribute: "company.id",
partialRolloutThreshold: 99999,
},
{
type: "group",
operator: "or",
filters: [
{
type: "context",
field: "company.id",
operator: "ANY_OF",
values: ["company2"],
},
{
type: "negation",
filter: {
type: "context",
field: "company.id",
operator: "IS",
values: ["company3"],
},
},
],
},
{
type: "negation",
filter: {
type: "constant",
value: false,
},
},
],
},
},
])(
{
"company.id": "company1",
},
"feature",
);

expect(res).toEqual({
value: true,
context: {
"company.id": "company1",
},
featureKey: "feature",
missingContextFields: [],
reason: "rule #0 matched",
ruleEvaluationResults: [true],
});
});

it.each([
{
context: { "company.id": "company1" },
expected: true,
},
{
context: { "company.id": "company2" },
expected: true,
},
{
context: { "company.id": "company3" },
expected: false,
},
])(
"%#: evaluates optimized rule evaluations correctly",
async ({ context, expected }) => {
const evaluator = newEvaluator([
{
value: true,
filter: {
type: "group",
operator: "and",
filters: [
{
type: "context",
field: "company.id",
operator: "ANY_OF",
values: ["company1", "company2"],
},
],
},
},
]);

const res = evaluator(context, "feature");
expect(res.value ?? false).toEqual(expected);
},
);
});

describe("operator evaluation", () => {
Expand Down