verdict-rules
Version:
A small, zero-dependency, async-native rule-evaluation engine. Compose conditions into one explainable pass/fail verdict, with sequential evaluation and real short-circuiting as a guarantee.
193 lines (188 loc) • 5.61 kB
JavaScript
;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
AndRule: () => AndRule,
FunctionRule: () => FunctionRule,
OrRule: () => OrRule,
RulesEngine: () => RulesEngine,
UnknownLookupError: () => UnknownLookupError
});
module.exports = __toCommonJS(index_exports);
// src/errors.ts
var UnknownLookupError = class extends Error {
/** Whether the missing thing was a rule name or a group label. */
kind;
/** The name or label that matched nothing. */
key;
constructor(kind, key) {
super(
kind === "rule" ? `No rule named '${key}' in this engine` : `No rules in group '${key}' in this engine`
);
this.name = "UnknownLookupError";
this.kind = kind;
this.key = key;
}
};
// src/engine.ts
var RulesEngine = class {
#rules;
#byName;
#byGroup;
constructor(rules) {
this.#rules = [...rules];
this.#byName = new Map(rules.map((r) => [r.name, r]));
this.#byGroup = /* @__PURE__ */ new Map();
for (const rule of rules) {
if (rule.group === void 0 || rule.group === "") continue;
const existing = this.#byGroup.get(rule.group);
if (existing === void 0) this.#byGroup.set(rule.group, [rule]);
else existing.push(rule);
}
}
/** Every rule name registered here, in registration order. */
get ruleNames() {
return [...this.#byName.keys()];
}
/** Every group label carried by at least one rule, in first-seen order. */
get groupNames() {
return [...this.#byGroup.keys()];
}
/** Evaluate every registered rule. Never short-circuits. */
async runAll(context) {
const results = [];
for (const rule of this.#rules) {
results.push(await rule.evaluate(context));
}
return { passed: results.every((r) => r.passed), results };
}
/**
* Evaluate one rule by name, or return `undefined` if no such rule exists.
*
* `undefined` means absent, never failed.
*/
async tryRunNamed(name, context) {
const rule = this.#byName.get(name);
if (rule === void 0) return void 0;
return rule.evaluate(context);
}
/**
* Evaluate exactly one rule, looked up by name.
*
* @throws {UnknownLookupError} if no rule has this name.
*/
async runNamed(name, context) {
const result = await this.tryRunNamed(name, context);
if (result === void 0) {
throw new UnknownLookupError("rule", name);
}
return result;
}
/**
* Evaluate a group, or return `undefined` if no such group exists.
*
* `undefined` means absent, never vacuously passed.
*/
async tryRunGroup(group, context) {
const rules = this.#byGroup.get(group);
if (rules === void 0 || rules.length === 0) return void 0;
const results = [];
for (const rule of rules) {
results.push(await rule.evaluate(context));
}
return { passed: results.every((r) => r.passed), results };
}
/**
* Evaluate every rule sharing a group label. Never short-circuits.
*
* @throws {UnknownLookupError} if no rule carries this label.
*/
async runGroup(group, context) {
const result = await this.tryRunGroup(group, context);
if (result === void 0) {
throw new UnknownLookupError("group", group);
}
return result;
}
};
// src/rule.ts
var FunctionRule = class {
name;
group;
#predicate;
constructor(name, predicate, group) {
this.name = name;
this.group = group;
this.#predicate = predicate;
}
/** Runs the wrapped predicate and returns whatever it returns, unchanged. */
evaluate(context) {
return this.#predicate(context);
}
};
var AndRule = class {
name;
group;
#rules;
constructor(name, rules, group) {
this.name = name;
this.group = group;
this.#rules = rules;
}
async evaluate(context) {
const subResults = [];
for (const rule of this.#rules) {
const result = await rule.evaluate(context);
subResults.push(result);
if (!result.passed) {
const detail = result.detail ? `'${rule.name}' failed: ${result.detail}` : `'${rule.name}' failed`;
return { ruleName: this.name, passed: false, detail, data: subResults };
}
}
return { ruleName: this.name, passed: true, data: subResults };
}
};
var OrRule = class {
name;
group;
#rules;
constructor(name, rules, group) {
this.name = name;
this.group = group;
this.#rules = rules;
}
async evaluate(context) {
const subResults = [];
for (const rule of this.#rules) {
const result = await rule.evaluate(context);
subResults.push(result);
if (result.passed) {
return { ruleName: this.name, passed: true, data: subResults };
}
}
return {
ruleName: this.name,
passed: false,
detail: "no sub-rule passed",
data: subResults
};
}
};
//# sourceMappingURL=index.cjs.map