Rule Builder
Components

Rule Builder

Visual predicate editor for constructing conditional WHEN/THEN rules, used in firewalls, filters, and policy engines.

Preview

RuleBuilder

Components in this category

Rule data model

A Rule has a when (the condition tree) and a then (the actions to fire). The when tree is a recursive structure:

Rule structureTSX
type Rule = {
  when: RuleGroup; // root condition group
  then: RuleAction[]; // actions to fire
};

type RuleGroup = {
  id: string;
  kind: "group";
  combinator: "all" | "any" | "none"; // AND / OR / NOT
  children: RuleNode[]; // conditions or nested groups
};

type RuleCondition = {
  id: string;
  kind: "condition";
  field: string; // matches a RuleField.key
  operator: RuleOperator; // "eq" | "neq" | "in" | "contains" | "gt" | …
  value?: string; // for single-value operators
  values?: string[]; // for "in" / "not in" operators
};

type RuleNode = RuleCondition | RuleGroup;

type RuleAction = {
  id: string;
  type: string; // matches a RuleActionDef.type
  params?: Record<string, string>; // action parameters
};

RuleField defines what the conditions can test: key (path used in generated code, e.g. "request.path"), label (human-readable name), type ("string" | "number" | "enum" | "bool"), and optional group, options (for enum), operators (allowlist), and unit.

RuleOperator covers eq, neq, in, not_in, contains, not_contains, starts_with, ends_with, matches (regex), gt, gte, lt, lte, exists, not_exists, is_true, and is_false. Each operator has a defined arity ("none" | "one" | "many") and a set of field types it applies to.

RuleBuilder

RuleBuilder is the visual editor for a complete WHEN/THEN rule. It renders the condition tree on the left and the action list on the right. Every control is a Select or Input, so the rule value stays plain data.

RuleBuilderProps props
PropTypeDefaultDescription
fieldsRuleField[]—Fields conditions may test. The first is used for new rows.
actionsRuleActionDef[]—Actions the THEN side may fire. Omit for conditions only.
valueRule—Controlled rule value.
defaultValueRule—Default rule when uncontrolled.
onChange(rule: Rule) => void—Rule change handler.
maxDepthnumber3Deepest nesting level for groups.
readOnlyboolean—Displays static values instead of editing controls.
headerReactNode—Slot above WHEN (rule name, enable switch).
footerReactNode—Slot below THEN (Save/Test buttons, validation summary).
labels{ when?: ReactNode; then?: ReactNode }—Override the WHEN/THEN section labels.
ImportTSX
import {
  RuleBuilder,
  createRule,
  createGroup,
  createCondition,
  compileRule,
  evaluateRule,
  countConditions,
  findIncompleteConditions,
  dslRuleCompiler,
  typescriptRuleCompiler,
  jsonRuleCompiler,
} from "@solarius/onset";
import type {
  Rule,
  RuleField,
  RuleCondition,
  RuleGroup,
  RuleNode,
  RuleOperator,
  RuleOperatorDef,
  RuleCombinator,
  RuleAction,
  RuleActionDef,
  RuleActionParam,
  RuleFieldType,
  RuleCompiler,
} from "@solarius/onset";
import { useState } from "react";

const FIELDS: RuleField[] = [
  { key: "request.path", label: "Path", type: "string", group: "Request" },
  {
    key: "request.method",
    label: "Method",
    type: "enum",
    options: [
      { value: "GET", label: "GET" },
      { value: "POST", label: "POST" },
      { value: "PUT", label: "PUT" },
      { value: "DELETE", label: "DELETE" },
    ],
    group: "Request",
  },
  {
    key: "auth.user.role",
    label: "User role",
    type: "enum",
    options: [
      { value: "admin", label: "Admin" },
      { value: "member", label: "Member" },
      { value: "viewer", label: "Viewer" },
    ],
    group: "Auth",
  },
  { key: "auth.mfa", label: "MFA enabled", type: "bool", group: "Auth" },
  {
    key: "request.rate",
    label: "Rate",
    type: "number",
    unit: "req/min",
    group: "Request",
  },
];

const ACTIONS: RuleActionDef[] = [
  { type: "allow", label: "Allow", tone: "ok" },
  { type: "block", label: "Block", tone: "danger" },
  {
    type: "challenge",
    label: "Challenge",
    tone: "warn",
    params: [
      {
        key: "method",
        label: "Method",
        type: "enum",
        options: [
          { value: "mfa", label: "MFA" },
          { value: "captcha", label: "CAPTCHA" },
        ],
      },
    ],
  },
];

function PolicyEditor() {
  const [rule, setRule] = useState<Rule>(() => createRule(FIELDS));

  return (
    <RuleBuilder
      fields={FIELDS}
      actions={ACTIONS}
      value={rule}
      onChange={setRule}
      footer={
        <div>
          <button
            onClick={() => console.log(compileRule(rule, jsonRuleCompiler))}
          >
            Save
          </button>
        </div>
      }
    />
  );
}

ConditionGroup

ConditionGroup renders one all/any/none group and its rows, recursing for nested groups. It is exported so a rule tree can be embedded without the WHEN/THEN shell that RuleBuilder puts around it — useful for dialogs where only the condition tree is relevant.

ConditionGroupProps props
PropTypeDefaultDescription
groupRuleGroup—The group to edit.
fieldsRuleField[]—Available fields.
onChange(group: RuleGroup) => void—Group change handler.
onRemove() => void—Remove handler. Omit for the root group.
depthnumber0Nesting depth. The root is 0.
maxDepthnumber3Deepest group level that may still nest another group.
readOnlyboolean—Renders controls as static text.
ImportTSX
import { ConditionGroup, createGroup, createCondition } from "@solarius/onset";

// Embed just the condition tree (no WHEN/THEN shell):
<ConditionGroup
  group={rule.when}
  fields={fields}
  onChange={(when) => setRule({ ...rule, when })}
/>;

Compilers

Three reference compilers ship with the library, to be passed to compileRule(rule, compiler). The traversal, operator lookup, and param resolution are shared; everything about the output format comes from the compiler you pass in.

dslRuleCompiler

Emits a readable block-structured policy, in the shape most policy services store and diff on review. Output is a plain text string.

DSL output exampleTSX
rule "block-login-abuse" {
  when
    all {
      request.path == "/login"
      auth.mfa is false
    }

  then {
    block()
    challenge(method: "mfa")
  }
}

typescriptRuleCompiler

Emits a predicate function over a ctx object shaped like the field keys, for inlining the rule into a worker or service instead of interpreting it. Output is a TypeScript code string.

TypeScript output exampleTSX
export function blockLoginAbuse(ctx: RequestContext): RuleAction[] | null {
  const matched = ctx.request.path === "/login" && ctx.auth.mfa === false;

  if (!matched) return null;
  return [{ type: "block" }, { type: "challenge", method: "mfa" }];
}

jsonRuleCompiler

Emits a normalised payload for a policy API; editor ids and empty params are dropped. Returns objects rather than text; the caller chooses whether to stringify. Output is a serialisable JSON object.

JSON output exampleTSX
{
  "name": "block-login-abuse",
  "match": {
    "all": [
      { "field": "request.path", "op": "eq", "value": "/login" },
      { "field": "request.rate", "op": "gt", "value": "100" }
    ]
  },
  "actions": [
    { "type": "block" },
    { "type": "challenge", "method": "mfa" }
  ]
}

Helper functions

Tree construction

Creating rulesTSX
import { createRule, createGroup, createCondition } from "@solarius/onset";

// Start with a blank rule:
const rule = createRule(fields);

// Programmatically build:
const group = createGroup("any", [
  createCondition(fields[0]), // picks the first field
  createCondition(fields[1]),
]);

Evaluation & introspection

Running and inspecting rulesTSX
import {
  evaluateRule,
  countConditions,
  findIncompleteConditions,
} from "@solarius/onset";

// Evaluate against a context object:
const ctx = {
  request: { path: "/login", method: "POST" },
  auth: { mfa: false },
};
evaluateRule(rule.when, ctx); // boolean

// Count conditions:
countConditions(rule.when); // number

// Find conditions missing a field or required value:
findIncompleteConditions(rule.when); // RuleCondition[]

Operator lookup

Operator helpersTSX
import { ruleOperators, ruleOperatorsFor, ruleDepthOf } from "@solarius/onset";

// Full operator definition table:
ruleOperators["gt"]; // { label: ">", arity: "one", types: ["number"] }

// Operators valid for a given field:
ruleOperatorsFor(fields[0]); // RuleOperator[]

// Find how deep a node is nested:
ruleDepthOf(rule.when, conditionId); // number