ts-10-rule-engine
1.000
Proposed solution
```ts
export type Rule =
| { op: "and"; rules: Rule[] }
| { op: "or"; rules: Rule[] }
| { op: "not"; rule: Rule }
| { op: "eq" | "ne" | "lt" | "lte" | "gt" | "gte"; field: string; value: number | string }
| { op: "in"; field: string; values: Array<number | string> };
export class RuleError extends Error {}
function validateRule(rule: unknown): asserts rule is Rule {
if (!rule || typeof rule !== 'object') {
throw new RuleError('Invalid rule: not an object');
}
const ruleObj = rule as Record<string, unknown>;
if (!('op' in ruleObj)) {
throw new RuleError('Invalid rule: missing op');
}
const op = ruleObj.op;
if (typeof op !== 'string') {
throw new RuleError('Invalid rule: op must be a string');
}
switch (op) {
case 'and':
case 'or': {
if (!('rules' in ruleObj) || !Array.isArray(ruleObj.rules)) {
throw new RuleError(`Invalid rule: ${op} must have an array of rules`);
}
for (const subRule of ruleObj.rules) {
validateRule(subRule);
}
break;
}
case 'not': {
if (!('rule' in ruleObj) || typeof ruleObj.rule !== 'object') {
throw new RuleError('Invalid rule: not must have a child rule');
}
validateRule(ruleObj.rule);
break;
}
case 'eq':
case 'ne':
case 'lt':
case 'lte':
case 'gt':
case 'gte': {
if (!('field' in ruleObj) || typeof ruleObj.field !== 'string') {
throw new RuleError(`Invalid rule: ${op} must have a string field`);
}
if (!('value' in ruleObj) || (typeof ruleObj.value !== 'number' && typeof ruleObj.value !== 'string')) {
throw new RuleError(`Invalid rule: ${op} must have a number or string value`);
}
break;
}
case 'in': {
if (!('field' in ruleObj) || typeof ruleObj.field !== 'string') {
throw new RuleError('Invalid rule: in must have a string field');
}
if (!('values' in ruleObj) || !Array.isArray(ruleObj.values)) {
throw new RuleError('Invalid rule: in must have an array of values');
}
for (const value of ruleObj.values) {
if (typeof value !== 'number' && typeof value !== 'string') {
throw new RuleError('Invalid rule: in values must be numbers or strings');
}
}
break;
}
default:
throw new RuleError(`Invalid rule: unknown operation ${op}`);
}
}
export function evaluate(rule: Rule, record: Record<string, unknown>): boolean {
validateRule(rule);
switch (rule.op) {
case 'and':
return rule.rules.every(subRule => evaluate(subRule, record));
case 'or':
return rule.rules.some(subRule => evaluate(subRule, record));
case 'not':
return !evaluate(rule.rule, record);
case 'eq':
return record[rule.field] === rule.value;
case 'ne':
return record[rule.field] !== rule.value;
case 'lt':
const ltFieldValue = record[rule.field];
if (typeof ltFieldValue === 'number' && typeof rule.value === 'number') {
return ltFieldValue < rule.value;
}
if (typeof ltFieldValue === 'string' && typeof rule.value === 'string') {
return ltFieldValue < rule.value;
}
return false;
case 'lte':
const lteFieldValue = record[rule.field];
if (typeof lteFieldValue === 'number' && typeof rule.value === 'number') {
return lteFieldValue <= rule.value;
}
if (typeof lteFieldValue === 'string' && typeof rule.value === 'string') {
return lteFieldValue <= rule.value;
}
return false;
case 'gt':
const gtFieldValue = record[rule.field];
if (typeof gtFieldValue === 'number' && typeof rule.value === 'number') {
return gtFieldValue > rule.value;
}
if (typeof gtFieldValue === 'string' && typeof rule.value === 'string') {
return gtFieldValue > rule.value;
}
return false;
case 'gte':
const gteFieldValue = record[rule.field];
if (typeof gteFieldValue === 'number' && typeof rule.value === 'number') {
return gteFieldValue >= rule.value;
}
if (typeof gteFieldValue === 'string' && typeof rule.value === 'string') {
return gteFieldValue >= rule.value;
}
return false;
case 'in':
const inFieldValue = record[rule.field];
return rule.values.some(value => value === inFieldValue);
default:
throw new RuleError(`Unknown operation: ${(rule as any).op}`);
}
}
export function filter(
rule: Rule,
records: Array<Record<string, unknown>>,
): Array<Record<string, unknown>> {
return records.filter(record => evaluate(rule, record));
}
```Test output (stdout)
TAP version 13 # Subtest: eq / ne on numbers and strings ok 1 - eq / ne on numbers and strings --- duration_ms: 0.488353 type: 'test' ... # Subtest: eq does not coerce types (number vs string) ok 2 - eq does not coerce types (number vs string) --- duration_ms: 0.058471 type: 'test' ... # Subtest: numeric lt / lte / gt / gte ok 3 - numeric lt / lte / gt / gte --- duration_ms: 0.05992 type: 'test' ... # Subtest: lexicographic string ordering ok 4 - lexicographic string ordering --- duration_ms: 0.050821 type: 'test' ... # Subtest: ordering with missing field is false ok 5 - ordering with missing field is false --- duration_ms: 0.0463 type: 'test' ... # Subtest: ordering with type mismatch is false (no throw) ok 6 - ordering with type mismatch is false (no throw) --- duration_ms: 0.03861 type: 'test' ... # Subtest: eq with missing field is false, ne is true ok 7 - eq with missing field is false, ne is true --- duration_ms: 0.042791 type: 'test' ... # Subtest: in membership ok 8 - in membership --- duration_ms: 0.06858 type: 'test' ... # Subtest: not negates its child ok 9 - not negates its child --- duration_ms: 0.048681 type: 'test' ... # Subtest: empty and is true, empty or is false ok 10 - empty and is true, empty or is false --- duration_ms: 0.07605 type: 'test' ... # Subtest: and / or short-circuit semantics over multiple rules ok 11 - and / or short-circuit semantics over multiple rules --- duration_ms: 0.085081 type: 'test' ... # Subtest: deeply nested and / or / not ok 12 - deeply nested and / or / not --- duration_ms: 0.06089 type: 'test' ... # Subtest: filter keeps matching records in original order ok 13 - filter keeps matching records in original order --- duration_ms: 0.339462 type: 'test' ... # Subtest: filter with empty-and matches everything; empty-or matches nothing ok 14 - filter with empty-and matches everything; empty-or matches nothing --- duration_ms: 0.055181 type: 'test' ... # Subtest: RuleError thrown for an unknown op ok 15 - RuleError thrown for an unknown op --- duration_ms: 0.181051 type: 'test' ... # Subtest: RuleError thrown for malformed and / not / comparison nodes ok 16 - RuleError thrown for malformed and / not / comparison nodes --- duration_ms: 0.10565 type: 'test' ... # Subtest: filter propagates RuleError from an invalid rule ok 17 - filter propagates RuleError from an invalid rule --- duration_ms: 0.038031 type: 'test' ... 1..17 # tests 17 # suites 0 # pass 17 # fail 0 # cancelled 0 # skipped 0 # todo 0 # duration_ms 81.611489
System prompt
You are an expert programmer. Solve the task exactly as specified. Output your solution as fenced code blocks using the required file name(s) and the exact function/type signatures requested. Prefer correctness; do not include prose outside code unless asked.