Table of Contents

Collections

In checks one value against a fixed set. To reason about a collection in the fact, such as order lines, tags or payments, use a quantifier: Any, All or None, each with a condition applied to every element.

Quantifiers

{
  "field": "Order.Lines",
  "operator": "Any",
  "condition": { "field": "Category", "operator": "In", "value": ["alcohol", "tobacco"] }
}

field names the collection. condition is an ordinary condition tree (groups, nested quantifiers, computed expressions and custom functions all work), and its field paths resolve against each element, not the root fact.

Order line checks

{
  "name": "Order line checks",
  "rules": [
    {
      "id": "restricted-item",
      "condition": {
        "field": "Order.Lines",
        "operator": "Any",
        "condition": { "field": "Category", "operator": "In", "value": ["alcohol", "tobacco"] }
      },
      "actions": [
        { "type": "appendToOutput", "target": "Checks", "value": "verify age on delivery" }
      ]
    },
    {
      "id": "all-single-units",
      "condition": {
        "field": "Order.Lines",
        "operator": "All",
        "condition": { "field": "Quantity", "operator": "Equals", "value": 1 }
      },
      "actions": [
        { "type": "appendToOutput", "target": "Checks", "value": "one of each" }
      ]
    },
    {
      "id": "nothing-fragile",
      "condition": {
        "field": "Order.Lines",
        "operator": "None",
        "condition": { "field": "Category", "operator": "Equals", "value": "homeware" }
      },
      "actions": [
        { "type": "appendToOutput", "target": "Checks", "value": "standard packing" }
      ]
    },
    {
      "id": "priority-tag",
      "condition": {
        "field": "Order.Tags",
        "operator": "Any",
        "condition": { "field": "$", "operator": "Equals", "value": "priority" }
      },
      "actions": [
        { "type": "appendToOutput", "target": "Checks", "value": "ship today" }
      ]
    },
    {
      "id": "many-lines",
      "condition": {
        "expression": {
          "op": "count",
          "operands": [ { "field": "Order.Lines" } ]
        },
        "operator": "GreaterThan",
        "value": 2
      },
      "actions": [
        { "type": "appendToOutput", "target": "Checks", "value": "pick list" }
      ]
    }
  ]
}
using RuleWright.Core;
using RuleWright.Execution;
using RuleWright.Json.SystemText;

LoadedRuleSet rules = engine.LoadRuleSet(File.ReadAllText("collections.json"));

var empty = new Checkout();   // an order with no lines and no tags

foreach ((string name, Checkout checkout) in new[] { ("Aroha", Facts.Vip()), ("Ben", Facts.Newcomer()), ("Empty", empty) })
{
    RuleEvaluationResult result = engine.Evaluate(rules, checkout);
    string checks = result.Outputs.TryGetValue("Checks", out object? value)
        ? string.Join(", ", (List<object?>)value!)
        : "(none)";
    Console.WriteLine($"{name,-6} {checks}");
}

Output

Aroha  verify age on delivery, ship today, pick list
Ben    one of each, standard packing
Empty  one of each, standard packing

Empty and null collections

Operator True when Empty collection Null collection
Any at least one element matches false false
All every element matches true false
None no element matches true true

An empty order has all lines as single units and no homeware, which is why Empty got one of each and standard packing above. That is standard logic, but it surprises people. If an empty collection should not pass, combine the quantifier with a count:

{ "type": "group", "operator": "AND", "rules": [
  { "expression": { "op": "count", "operands": [ { "field": "Order.Lines" } ] }, "operator": "GreaterThan", "value": 0 },
  { "field": "Order.Lines", "operator": "All", "condition": { "field": "Quantity", "operator": "Equals", "value": 1 } }
] }

A null collection is the field's absence, so it follows the usual null rules: None is true, as NotIn is.

"$": the element itself

A list of strings or numbers has no member to name. "$" means the element being tested:

{ "field": "Order.Tags", "operator": "Any",
  "condition": { "field": "$", "operator": "Equals", "value": "priority" } }

$ is valid only inside a quantifier's condition. Every path that starts with $ is reserved, so $root.Total is a validation error rather than a member called $root.

Counting

count measures a collection through a computed left-hand side, as many-lines does above, or in an action value. It counts every element. Counting only the elements that match a condition isn't in the vocabulary yet: use a custom function for that.

Limits

  • An element condition can't read the root fact. "Any line whose price is above the order's average" isn't expressible yet.
  • A quantifier is one node in a trace. Its element condition appears in the node's description, such as Order.Lines Any (Category In ["alcohol", "tobacco"]), not as one result per element.

In C#

Quantifiers are built with the ConditionLeaf.Quantifier factory, and ConditionLeaf.ElementSelfPath is "$":

using RuleWright.Core;

// Quantifiers are built with a factory, because they take an element condition, not a value.
ConditionLeaf restricted = ConditionLeaf.Quantifier(
    "Order.Lines",
    ConditionOperator.Any,
    new ConditionLeaf("Category", ConditionOperator.In, new object?[] { "alcohol", "tobacco" }));

// "$" is the element itself, for a collection of scalars.
ConditionLeaf priority = ConditionLeaf.Quantifier(
    "Order.Tags",
    ConditionOperator.Any,
    new ConditionLeaf(ConditionLeaf.ElementSelfPath, ConditionOperator.Equal, "priority"));

var rule = new Rule(
    "restricted-priority",
    new ConditionGroup(LogicalOperator.And, new ConditionNode[] { restricted, priority }),
    new[] { new RuleAction(RuleAction.SetOutputType, "Hold", true) });

Next

Rule sets, priority and else.