Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Rule Engine

A small, extensible declarative rule engine written in TypeScript.
You configure rules in a central EngineConfig, the engine builds the rule pipeline automatically, and then your data flows through each rule in sequence.

How It Works

1. Declarative Configuration

All rules are defined in a single configuration object:

const config: EngineConfig = {
  rules: [
    {
      type: "synonyms",
      config: {
        groups: [
          { canonical: "A", variants: ["a1", "a2", "a3"] },
          { canonical: "B", variants: ["b1", "b3"] },
          { canonical: "C", variants: ["c1", "c4"] },
          { canonical: "Z", variants: ["z1"] },
        ],
      },
    },
    {
      type: "precedence",
      config: {
        precedence: ["A", "B", "C", "D", "E", "F", "Z"],
      },
    },
  ],
};

Each rule has:

  • a type (synonyms, precedence, …)
  • a custom config object

2. Rule Registry

Each rule type is associated with a factory that knows how to create its runtime class:

const ruleRegistry: Record<RuleType, RuleFactory<RuleContext>> = {
  synonyms: (def) => new SynonymRule(def.config as SynonymsRuleConfig),
  precedence: (def) =>
    new PrecedenceRule(def.config as PrecedenceRuleConfig, {
      maxTokens: 3,
    }),
};

This lets the engine dynamically build the correct rule objects at runtime.

3. Engine Construction

Your configuration is passed into buildEngineFromConfig, which:

  • Looks up the factory for each rule type.
  • Instantiates the runtime rule instance.
  • Creates a new RuleEngine with a list of rule objects.
export const engineFromConfig = buildEngineFromConfig(config);

4. Rule Execution Pipeline

The rule engine processes the initial context through each rule in order:

const initialCtx = { tokens: ["a1", "b1", "c1", "a2", "z1", "a3", "c4", "b3"] };
const result = await engineFromConfig.run(initialCtx);

Built-in Rules

1. SynonymRule

Normalizes tokens so all variants map to a canonical token.

["a1", "a3", "b1"]  ["A", "A", "B"]

2. PrecedenceRule

Sorts and filters tokens based on a precedence list, with an optional max token count.

Example:

precedence: ["A", "B", "C", "Z"]
tokens: ["A", "Z", "B", "A", "C"]
 ["A", "B", "C"]    // when maxTokens: 3

How to Add a New Rule

Adding a new rule follows 4 steps.

1.

In types.ts:

export interface MyNewRuleConfig {
  type: "my-new-rule";
  threshold: number;
}

2. Implement the Rule

Create rules/my-new-rule.ts:

import { Rule } from "../types";
import { RuleContext } from "../engine";
import { MyNewRuleConfig } from "../types";

export class MyNewRule implements Rule<RuleContext> {
  public readonly type = "my-new-rule" as const;

  constructor(private readonly config: MyNewRuleConfig) {}

  apply(ctx: RuleContext): RuleContext {
    const { threshold } = this.config;

    const tokens = ctx.tokens.filter((t) => t.length >= threshold);

    return {
      ...ctx,
      tokens,
    };
  }
}

3. Register the Rule

In rule-registry.ts:

import { MyNewRule } from "./rules/my-new-rule";

const ruleRegistry: Record<RuleType, RuleFactory<RuleContext>> = {
  ...,
  "my-new-rule": def =>
    new MyNewRule(def.config as MyNewRuleConfig)
};

4. Use It in Config

{
  type: "my-new-rule",
  config: { threshold: 2 }
}

Extending the Engine

You can easily extend the rule engine with additional capabilities:

  • Async rules: Rules can await external services or databases.
  • Branching / conditional rules: Evaluate rules only if a condition is met.
  • Rule groups: Run groups of rules parallelly or sequentially.
  • Rule debugging hooks: Add before, after, and onError logic.
  • Context enrichment: Rules may attach extra fields to context objects.

About

No description, website, or topics provided.

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages