Skip to content

onebeyond/poc-rule-engine

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 

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

Watchers

Forks

Releases

No releases published

Packages

No packages published