Table of Contents

Discover the vocabulary

A rule editor has to know which operators exist, which take a value, and which functions this engine has. Don't hard-code that list. RuleSchemaCatalog and engine.FunctionCatalog hand it to you at run time, derived from the same tables the parser and validator use, so they can't disagree with what the engine accepts.

The whole vocabulary

using RuleWright.Serialization;

Console.WriteLine("Condition operators");
foreach (ConditionOperatorInfo op in RuleSchemaCatalog.ConditionOperators)
{
    Console.WriteLine($"  {op.JsonName,-19} value: {op.ValueKind,-9} expression left: {op.AllowsExpressionLeft,-5} needs name: {op.RequiresFunctionName}");
}

Console.WriteLine("Logical operators");
foreach (LogicalOperatorInfo op in RuleSchemaCatalog.LogicalOperators)
{
    Console.WriteLine($"  {op.JsonName,-4} children: {op.MinChildren}..{op.MaxChildren?.ToString() ?? "n"}");
}

Console.WriteLine("Expression operators");
foreach (ExpressionOperatorInfo op in RuleSchemaCatalog.ExpressionOperators)
{
    Console.WriteLine($"  {op.JsonName,-9} {op.Category,-12} operands: {op.MinOperands}..{op.MaxOperands?.ToString() ?? "n"}");
}

Console.WriteLine("Action types");
foreach (ActionTypeInfo action in RuleSchemaCatalog.ActionTypes)
{
    Console.WriteLine($"  {action.Name,-15} {action.Effect,-8} requires value: {action.RequiresValue}");
}

Output

Condition operators
  Equals              value: Scalar    expression left: True  needs name: False
  NotEquals           value: Scalar    expression left: True  needs name: False
  GreaterThan         value: Scalar    expression left: True  needs name: False
  GreaterThanOrEqual  value: Scalar    expression left: True  needs name: False
  LessThan            value: Scalar    expression left: True  needs name: False
  LessThanOrEqual     value: Scalar    expression left: True  needs name: False
  Contains            value: Text      expression left: True  needs name: False
  StartsWith          value: Text      expression left: True  needs name: False
  EndsWith            value: Text      expression left: True  needs name: False
  MatchesRegex        value: Text      expression left: True  needs name: False
  In                  value: Array     expression left: True  needs name: False
  NotIn               value: Array     expression left: True  needs name: False
  IsNull              value: None      expression left: True  needs name: False
  IsNotNull           value: None      expression left: True  needs name: False
  custom              value: Custom    expression left: False needs name: True
  Any                 value: Condition expression left: False needs name: False
  All                 value: Condition expression left: False needs name: False
  None                value: Condition expression left: False needs name: False
Logical operators
  AND  children: 1..n
  OR   children: 1..n
  NOT  children: 1..1
Expression operators
  add       Arithmetic   operands: 2..n
  subtract  Arithmetic   operands: 2..2
  multiply  Arithmetic   operands: 2..n
  divide    Arithmetic   operands: 2..2
  modulo    Arithmetic   operands: 2..2
  negate    Arithmetic   operands: 1..1
  concat    Text         operands: 2..n
  coalesce  NullHandling operands: 2..n
  count     Collection   operands: 1..1
Action types
  setOutput       Replace  requires value: True
  addToOutput     Add      requires value: True
  appendToOutput  Append   requires value: True
  removeOutput    Remove   requires value: False
Any needs an element condition: True

What each entry tells an editor

Catalog Entry Tells you
ConditionOperators ConditionOperatorInfo JsonName; ValueKind (which value editor to show: None, Scalar, Text, Array, Custom or Condition); RequiresValue; RequiresFunctionName; RequiresElementCondition; AllowsExpressionLeft.
LogicalOperators LogicalOperatorInfo JsonName, and MinChildren / MaxChildren (null means no limit).
ExpressionOperators ExpressionOperatorInfo JsonName, Category, and MinOperands / MaxOperands.
ActionTypes ActionTypeInfo Name, Effect (Replace, Add, Append, Remove), and RequiresValue.

Look one up by name with TryGetConditionOperator, TryGetExpressionOperator or TryGetActionType:

using RuleWright.Serialization;

if (RuleSchemaCatalog.TryGetConditionOperator("Any", out ConditionOperatorInfo any))
{
    Console.WriteLine($"Any needs an element condition: {any.RequiresElementCondition}");
}

Registrations are per engine

The operators are fixed. Three parts of the vocabulary depend on what each engine registered, so they live on the engine:

Registration Names Catalog
Custom functions (custom) engine.RegisteredFunctions engine.FunctionCatalog: Description and ValueKind per function
Value functions (call) engine.RegisteredValueFunctions engine.ValueFunctionCatalog: Description and RequiredOperandCount per function
Custom actions (action type) engine.RegisteredActions The names; validation accepts them alongside the built-in four
using RuleWright.Core;
using RuleWright.Execution;
using RuleWright.Json.SystemText;

RuleWrightEngine engine = new RuleWrightBuilder()
    .UseJsonReader(new SystemTextJsonReader())
    .RegisterFunction("IsBusinessDay", (field, value) =>
        field is DateTime d && d.DayOfWeek is not (DayOfWeek.Saturday or DayOfWeek.Sunday))
    .RegisterValueFunction(new RoundToFunction())
    .RegisterAction("setIfHigher", context => { /* see Custom actions */ })
    .Build();

Console.WriteLine($"custom functions: {string.Join(", ", engine.RegisteredFunctions)}");
Console.WriteLine($"value functions:  {string.Join(", ", engine.RegisteredValueFunctions)}");
Console.WriteLine($"custom actions:   {string.Join(", ", engine.RegisteredActions)}");

foreach (RuleValueFunctionDescriptor function in engine.ValueFunctionCatalog)
{
    Console.WriteLine($"  {function.Name}: operands {function.RequiredOperandCount?.ToString() ?? "any"} — {function.Description}");
}

Output

custom functions: IsBusinessDay
value functions:  RoundTo
custom actions:   setIfHigher
  RoundTo: operands 2 — Rounds a number to a digit count.

Building an editor

A rule editor usually needs three things from the server, and RuleWright provides all three:

  1. The vocabulary, from the two catalogs above, to build its palettes and value editors.
  2. Validation as you type, from engine.Validate, with JSON pointers to underline.
  3. A test run, from Evaluate with EnableTrace, to show which conditions a sample fact passes.

The web API tutorial serves the first two over HTTP. The RuleWright repository has a complete example: a Blazor WebAssembly rule builder with a drag-and-drop canvas, running the real engine in the browser. It stores node positions in each rule's layout key, which the engine ignores and the content hash excludes, so moving a node never recompiles a rule.

Next

Accept rules you didn't write.