Rule Builder
Visual predicate editor for constructing conditional WHEN/THEN rules, used in firewalls, filters, and policy engines.
Preview
Components in this category
Visual rule builder for constructing conditional logic.
ConditionGroupGroup of conditions with AND/OR combinator.
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:
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.
| Prop | Type | Default | Description |
|---|---|---|---|
fields | RuleField[] | — | Fields conditions may test. The first is used for new rows. |
actions | RuleActionDef[] | — | Actions the THEN side may fire. Omit for conditions only. |
value | Rule | — | Controlled rule value. |
defaultValue | Rule | — | Default rule when uncontrolled. |
onChange | (rule: Rule) => void | — | Rule change handler. |
maxDepth | number | 3 | Deepest nesting level for groups. |
readOnly | boolean | — | Displays static values instead of editing controls. |
header | ReactNode | — | Slot above WHEN (rule name, enable switch). |
footer | ReactNode | — | Slot below THEN (Save/Test buttons, validation summary). |
labels | { when?: ReactNode; then?: ReactNode } | — | Override the WHEN/THEN section labels. |
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.
| Prop | Type | Default | Description |
|---|---|---|---|
group | RuleGroup | — | The group to edit. |
fields | RuleField[] | — | Available fields. |
onChange | (group: RuleGroup) => void | — | Group change handler. |
onRemove | () => void | — | Remove handler. Omit for the root group. |
depth | number | 0 | Nesting depth. The root is 0. |
maxDepth | number | 3 | Deepest group level that may still nest another group. |
readOnly | boolean | — | Renders controls as static text. |
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.
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.
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.
{
"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
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
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
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