Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ufc] add null operator and more fixes #50

Merged
merged 11 commits into from
Apr 15, 2024
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
2 changes: 0 additions & 2 deletions src/client/eppo-client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
import { IAssignmentLogger } from '../assignment-logger';
import { IConfigurationStore } from '../configuration-store';
import { MAX_EVENT_QUEUE_SIZE, POLL_INTERVAL_MS, POLL_JITTER_PCT } from '../constants';
import { Evaluator } from '../evaluator';
import FlagConfigurationRequestor from '../flag-configuration-requestor';
import HttpClient from '../http-client';
import { Flag, VariationType } from '../interfaces';
Expand Down Expand Up @@ -301,7 +300,6 @@ describe('EppoClient E2E test', () => {
mock.setup();
mock.get(flagEndpoint, (_req, res) => {
const ufc = readMockUFCResponse(OBFUSCATED_MOCK_UFC_RESPONSE_FILE);
console.log(ufc);
return res.status(200).body(JSON.stringify(ufc));
});
await init(storage);
Expand Down
31 changes: 30 additions & 1 deletion src/client/eppo-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import HttpClient from '../http-client';
import { Flag, VariationType } from '../interfaces';
import { getMD5Hash } from '../obfuscation';
import initPoller, { IPoller } from '../poller';
import { AttributeType } from '../types';
import { AttributeType, ValueType } from '../types';
import { validateNotBlank } from '../validation';
import { LIB_VERSION } from '../version';

Expand Down Expand Up @@ -364,6 +364,13 @@ export default class EppoClient implements IEppoClient {
result.flagKey = flagKey;
}

if (
result?.variation &&
!this.checkValueTypeMatch(expectedVariationType, result.variation.value)
) {
return noneResult(flagKey, subjectKey, subjectAttributes);
}

try {
if (result?.doLog) {
this.logAssignment(result);
Expand All @@ -386,6 +393,28 @@ export default class EppoClient implements IEppoClient {
return expectedType === undefined || actualType === expectedType;
}

private checkValueTypeMatch(expectedType: VariationType | undefined, value: ValueType): boolean {
if (expectedType == undefined) {
return true;
}

switch (expectedType) {
case VariationType.STRING:
return typeof value === 'string';
case VariationType.BOOLEAN:
return typeof value === 'boolean';
case VariationType.INTEGER:
return typeof value === 'number' && Number.isInteger(value);
case VariationType.NUMERIC:
return typeof value === 'number';
case VariationType.JSON:
// note: converting to object downstream
return typeof value === 'string';
default:
return false;
}
}

public getFlagKeys() {
/**
* Returns a list of all flag keys that have been initialized.
Expand Down
116 changes: 106 additions & 10 deletions src/evaluator.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Evaluator, hashKey, isInShardRange } from './evaluator';
import { Evaluator, hashKey, isInShardRange, matchesRules } from './evaluator';
import { Flag, Variation, Shard, VariationType } from './interfaces';
import { getMD5Hash } from './obfuscation';
import { ObfuscatedOperatorType, OperatorType } from './rules';
import { ObfuscatedOperatorType, OperatorType, Rule } from './rules';
import { DeterministicSharder } from './sharders';

describe('Evaluator', () => {
Expand Down Expand Up @@ -380,7 +380,6 @@ describe('Evaluator', () => {
allocations: [
{
key: 'first',
rules: [],
splits: [
{
variationKey: 'a',
Expand All @@ -403,7 +402,6 @@ describe('Evaluator', () => {
},
{
key: 'default',
rules: [],
splits: [
{
variationKey: 'c',
Expand Down Expand Up @@ -454,8 +452,8 @@ describe('Evaluator', () => {
allocations: [
{
key: 'default',
startAt: new Date(now.getFullYear() + 1, 0, 1),
endAt: new Date(now.getFullYear() + 1, 1, 1),
startAt: new Date(now.getFullYear() + 1, 0, 1).toISOString(),
endAt: new Date(now.getFullYear() + 1, 1, 1).toISOString(),
rules: [],
splits: [
{
Expand Down Expand Up @@ -486,8 +484,8 @@ describe('Evaluator', () => {
allocations: [
{
key: 'default',
startAt: new Date(now.getFullYear() - 1, 0, 1),
endAt: new Date(now.getFullYear() + 1, 0, 1),
startAt: new Date(now.getFullYear() - 1, 0, 1).toISOString(),
endAt: new Date(now.getFullYear() + 1, 0, 1).toISOString(),
rules: [],
splits: [
{
Expand Down Expand Up @@ -518,8 +516,8 @@ describe('Evaluator', () => {
allocations: [
{
key: 'default',
startAt: new Date(now.getFullYear() - 2, 0, 1),
endAt: new Date(now.getFullYear() - 1, 0, 1),
startAt: new Date(now.getFullYear() - 2, 0, 1).toISOString(),
endAt: new Date(now.getFullYear() - 1, 0, 1).toISOString(),
rules: [],
splits: [
{
Expand Down Expand Up @@ -554,3 +552,101 @@ describe('Evaluator', () => {
expect(isInShardRange(1, { start: 1, end: 1 })).toBeFalsy();
});
});

describe('matchesRules', () => {
describe('matchesRules function', () => {
it('should return true when there are no rules', () => {
const rules: Rule[] = [];
const subjectAttributes = { id: 'test-subject' };
const obfuscated = false;
expect(matchesRules(rules, subjectAttributes, obfuscated)).toBeTruthy();
});

it('should return true when a rule matches', () => {
const rules: Rule[] = [
{
conditions: [
{
attribute: 'age',
operator: OperatorType.GTE,
value: 18,
},
],
},
];
const subjectAttributes = { id: 'test-subject', age: 20 };
const obfuscated = false;
expect(matchesRules(rules, subjectAttributes, obfuscated)).toBeTruthy();
});

it('should return true when one of two rules matches', () => {
const rules: Rule[] = [
{
conditions: [
{
attribute: 'age',
operator: OperatorType.GTE,
value: 18,
},
],
},
{
conditions: [
{
attribute: 'age',
operator: OperatorType.LTE,
value: 10,
},
],
},
];
const subjectAttributes = { id: 'test-subject', age: 10 };
const obfuscated = false;
expect(matchesRules(rules, subjectAttributes, obfuscated)).toBeTruthy();
});

it('should return true when null or rule is passed', () => {
const rules: Rule[] = [
{
conditions: [
{
attribute: 'age',
operator: OperatorType.IS_NULL,
value: true,
},
],
},
{
conditions: [
{
attribute: 'age',
operator: OperatorType.GTE,
value: 20,
},
],
},
];
const obfuscated = false;
expect(matchesRules(rules, { id: 'test-subject', age: 20 }, obfuscated)).toBeTruthy();
expect(matchesRules(rules, { id: 'test-subject', age: 10 }, obfuscated)).toBeFalsy();
expect(matchesRules(rules, { id: 'test-subject', country: 'UK' }, obfuscated)).toBeTruthy();
});

it('should return false when no rules match', () => {
const rules: Rule[] = [
{
conditions: [
{
attribute: 'age',
operator: OperatorType.GTE,
value: 18,
},
],
},
];
const subjectAttributes = { id: 'test-subject', age: 16 };
const obfuscated = false;
expect(matchesRules(rules, subjectAttributes, obfuscated)).toBeFalsy();
});
});
});
26 changes: 16 additions & 10 deletions src/evaluator.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Flag, Shard, Range, Variation } from './interfaces';
import { matchesRule } from './rules';
import { Rule, matchesRule } from './rules';
import { MD5Sharder, Sharder } from './sharders';
import { SubjectAttributes } from './types';

export interface FlagEvaluation {
flagKey: string;
subjectKey: string;
subjectAttributes: Record<string, string | number | boolean>;
subjectAttributes: SubjectAttributes;
allocationKey: string | null;
variation: Variation | null;
extraLogging: Record<string, string>;
Expand All @@ -22,7 +23,7 @@ export class Evaluator {
evaluateFlag(
flag: Flag,
subjectKey: string,
subjectAttributes: Record<string, string | number | boolean>,
subjectAttributes: SubjectAttributes,
obfuscated: boolean,
): FlagEvaluation {
if (!flag.enabled) {
Expand All @@ -31,14 +32,11 @@ export class Evaluator {

const now = new Date();
for (const allocation of flag.allocations) {
if (allocation.startAt && now < allocation.startAt) continue;
if (allocation.endAt && now > allocation.endAt) continue;
if (allocation.startAt && now < new Date(allocation.startAt)) continue;
if (allocation.endAt && now > new Date(allocation.endAt)) continue;

if (
!allocation.rules.length ||
allocation.rules.some((rule) =>
matchesRule(rule, { id: subjectKey, ...subjectAttributes }, obfuscated),
)
matchesRules(allocation?.rules ?? [], { id: subjectKey, ...subjectAttributes }, obfuscated)
) {
for (const split of allocation.splits) {
if (
Expand Down Expand Up @@ -78,7 +76,7 @@ export function hashKey(salt: string, subjectKey: string): string {
export function noneResult(
flagKey: string,
subjectKey: string,
subjectAttributes: Record<string, string | number | boolean>,
subjectAttributes: SubjectAttributes,
): FlagEvaluation {
return {
flagKey,
Expand All @@ -90,3 +88,11 @@ export function noneResult(
doLog: false,
};
}

export function matchesRules(
rules: Rule[],
subjectAttributes: SubjectAttributes,
obfuscated: boolean,
): boolean {
return !rules.length || rules.some((rule) => matchesRule(rule, subjectAttributes, obfuscated));
}
6 changes: 3 additions & 3 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ export interface Split {

export interface Allocation {
key: string;
rules: Rule[];
startAt?: Date;
endAt?: Date;
rules?: Rule[];
startAt?: string; // ISO 8601
endAt?: string; // ISO 8601
splits: Split[];
doLog: boolean;
}
Expand Down
Loading
Loading