@daltonr/rulewrite
v0.1.0
Published
A composable predicate/rule library inspired by the Specification Design Pattern
Maintainers
Readme
rulewrite
A composable business rules library for TypeScript, inspired by the Specification Pattern.
Define rules once. Compose them freely. Evaluate and explain them anywhere.
Complex conditional logic is hard to read, hard to test, and hard to change. rulewrite lets you capture each business rule as a named, typed predicate and combine them using logical operators — producing rules that are self-describing, auditable, and reusable across your codebase.
Getting started
Install as a dependency
npm install rulewriteClone and run locally
git clone https://github.com/your-org/rulewrite.git
cd rulewrite
npm installRun the examples:
npm run example:checkout
npm run example:authorization
npm run example:attendanceRun the tests:
npm testRun tests in watch mode:
npm run test:watchType-check without building:
npm run typecheckBuild the package:
npm run buildQuick start
import { rule } from 'rulewrite';
const isAdult = rule<User>(u => u.age >= 18, 'IsAdult');
const isVerified = rule<User>(u => u.emailVerified, 'IsVerified');
const canRegister = isAdult.and(isVerified);
canRegister.isSatisfiedBy(user); // boolean
canRegister.evaluate(user); // full evaluation treeCore concepts
Defining a rule
const isActive = rule<Account>(a => a.status === 'active', 'IsActive');A rule is a named predicate over a single type. The label is used in evaluation output.
Composing rules
All operators return a new rule of the same type, so they chain freely.
const eligible = isAdult.and(isVerified).and(isActive);
const allowed = isOwner.or(isAdmin);
const flagged = hasPendingPayment.prevents(canWithdraw);
const policy = containsAlcohol.implies(isAgeVerified);| Method | Meaning |
|---|---|
| .and(B) | A and B must both be satisfied |
| .or(B) | A or B must be satisfied |
| .not() | A must not be satisfied |
| .implies(B) | If A is satisfied, B must also be satisfied |
| .prevents(B) | A and B cannot both be satisfied (NAND) |
Projecting into a context
Rules are defined over a single type. Use .on() to project them into a wider context when composing across types.
type OrderContext = { customer: User; product: Product };
const customerIsAdult = isAdult.on((ctx: OrderContext) => ctx.customer);
const customerIsVerified = isVerified.on((ctx: OrderContext) => ctx.customer);
const canPurchase = customerIsAdult.and(customerIsVerified);
canPurchase.isSatisfiedBy({ customer, product });Rules that span both types are written directly against the context:
const withinBudget = rule<OrderContext>(
ctx => ctx.product.price <= ctx.customer.creditLimit,
'WithinBudget'
);Collection combinators
Apply a rule across a collection using .someOf(), .allOf(), or .noneOf().
type Team = { members: User[] };
const anyAdult = isAdult.someOf<Team>(t => t.members);
const allAdults = isAdult.allOf<Team>(t => t.members);
const noAdults = isAdult.noneOf<Team>(t => t.members);| Method | Meaning | Empty collection |
|---|---|---|
| .someOf(sel) | At least one item satisfies the rule | false |
| .allOf(sel) | All items satisfy the rule | true (vacuous truth) |
| .noneOf(sel) | No items satisfy the rule | true |
Evaluating rules
.isSatisfiedBy() returns a boolean. .evaluate() returns a structured tree showing how every sub-rule was resolved — useful for diagnostics, audit logs, and UI feedback.
const result = canPurchase.evaluate({ customer, product });
result.satisfied // boolean
result.label // 'AND'
result.children // per-operand results, recursivelyExamples
E-commerce checkout
Access control policy
Venue attendance rules
API reference
rule<T>(predicate, label)
Creates an atomic rule.
function rule<T>(predicate: (value: T) => boolean, label: string): Rule<T>Rule<T>
interface Rule<T> {
isSatisfiedBy(value: T): boolean;
evaluate(value: T): EvaluationResult;
and(other: Rule<T>): Rule<T>;
or(other: Rule<T>): Rule<T>;
not(): Rule<T>;
implies(other: Rule<T>): Rule<T>;
prevents(other: Rule<T>): Rule<T>;
on<C>(selector: (ctx: C) => T): Rule<C>;
someOf<C>(selector: (ctx: C) => T[]): Rule<C>;
allOf<C>(selector: (ctx: C) => T[]): Rule<C>;
noneOf<C>(selector: (ctx: C) => T[]): Rule<C>;
}EvaluationResult
interface EvaluationResult {
satisfied: boolean;
label: string;
children?: EvaluationResult[];
}Design notes
Why not just use functions?
Plain predicate functions compose with && and ||, but you lose structure. rulewrite rules carry their composition tree, so you can evaluate and explain them — not just run them.
Why .implies()?
A.implies(B) is false only when A is satisfied and B is not. It captures conditional requirements naturally: if the order contains alcohol, the customer must be age-verified. An AND would require age verification on every order.
Why .on() instead of a context object?
Defining rules against a shared context type couples unrelated rules together. .on() lets rules stay focused on one type and be projected into a wider context only at the composition site. The same isAdult rule works on a User, a Department.leader, or any field of any context.
