YAML Rules
Ruleur supports defining rules in YAML format, enabling database-first rule management. This is essential for:
- Storing rules in databases
- Loading rules dynamically at runtime
- Version controlling rules separately from code
- Non-developers authoring rules
Loading YAML Rules
Single File
require 'ruleur'
rule = Ruleur::Persistence::YAMLLoader.load_file('config/rules/allow_create.yml')
engine = Ruleur::Engine.new(rules: [rule])Multiple Files (Directory)
rules = Ruleur::Persistence::YAMLLoader.load_directory('config/rules/*.yml')
engine = Ruleur::Engine.new(rules: rules)From String
yaml_string = File.read('my_rule.yml')
rule = Ruleur::Persistence::YAMLLoader.load_string(yaml_string)YAML Format
Basic Structure
name: rule_name
salience: 0 # Optional: priority (default 0)
tags: [] # Optional: array of tags
no_loop: false # Optional: prevent infinite loops
conditions:
# Condition tree (see below)
actions:
# Action specification (see below)Simple Example
name: allow_admin_access
salience: 10
tags:
- permissions
- admin
no_loop: true
conditions:
type: pred
op: truthy
left:
type: call
recv:
type: ref
root: user
path: []
method: admin?
args: []
right: null
actions:
set:
allow_access: trueCondition Types
Predicate (pred)
Evaluates a comparison between two values:
conditions:
type: pred
op: eq # Operator: eq, ne, gt, gte, lt, lte, truthy, etc.
left:
# Value specification
right:
# Value specification (can be null)All (all)
All child conditions must be true (AND logic):
conditions:
type: all
children:
- type: pred
op: truthy
left: { type: ref, root: user, path: [roles] }
right: null
- type: pred
op: eq
left: { type: ref, root: record, path: [status] }
right: { type: lit, value: draft }Any (any)
At least one child condition must be true (OR logic):
conditions:
type: any
children:
- type: pred
op: truthy
left:
type: call
recv: { type: ref, root: user }
method: admin?
right: null
- type: pred
op: truthy
left:
type: call
recv: { type: ref, root: user }
method: moderator?
right: nullNot (not)
Negates a condition:
conditions:
type: not
child:
type: pred
op: blank
left: { type: ref, root: record, path: [deleted_at] }
right: nullValue Types
Literal (lit)
A static value:
type: lit
value: 42 # Can be number, string, boolean, nullReference (ref)
Access a fact from context:
type: ref
root: user # Root object name
path: [profile, email] # Optional: nested pathMethod Call (call)
Call a method on an object:
type: call
recv:
type: ref
root: record
path: []
method: active? # Method name
args: [] # Optional: method argumentsAction Specifications
Currently, only set actions are supported for YAML rules:
actions:
set:
allow_create: true
allow_update: false
approval_required: trueMultiple facts can be set in a single action.
Complete Examples
Permission Rule
name: allow_create
salience: 10
tags:
- permissions
- create
no_loop: true
conditions:
type: any
children:
# Admin can always create
- type: pred
op: truthy
left:
type: call
recv:
type: ref
root: user
path: []
method: admin?
args: []
right: null
# Or: updatable AND draft
- type: all
children:
- type: pred
op: truthy
left:
type: call
recv:
type: ref
root: record
path: []
method: updatable?
args: []
right: null
- type: pred
op: truthy
left:
type: call
recv:
type: ref
root: record
path: []
method: draft?
args: []
right: null
actions:
set:
allow_create: trueWorkflow Rule
name: auto_approve_small_amounts
salience: 5
tags:
- workflow
- approval
conditions:
type: all
children:
# Amount less than 1000
- type: pred
op: lt
left:
type: call
recv:
type: ref
root: invoice
method: amount
right:
type: lit
value: 1000
# Already reviewed
- type: pred
op: truthy
left:
type: call
recv:
type: ref
root: invoice
method: reviewed?
right: null
actions:
set:
auto_approved: true
approval_status: approvedExporting DSL Rules to YAML
You can convert DSL-defined rules to YAML:
# Define rule with DSL
engine = Ruleur.define do
rule 'my_rule', salience: 10 do
conditions do
any?(user(:admin?))
end
actions do
allow! :access
end
end
end
# Export to YAML file
Ruleur::Persistence::YAMLLoader.save_file(
engine.rules.first,
'config/rules/my_rule.yml',
include_metadata: true # Adds helpful comments
)
# Or get YAML string
yaml_string = Ruleur::Persistence::YAMLLoader.to_yaml(engine.rules.first)
puts yaml_stringWith Metadata
When include_metadata: true, the YAML file includes helpful comments:
# Ruleur Rule: my_rule
# Salience: 10
# Tags: permissions
# No-loop: true
# Generated: 2026-03-20T10:30:00Z
name: my_rule
salience: 10
tags:
- permissions
no_loop: true
conditions:
# ...
actions:
# ...Validating YAML
Structural Validation
Validate YAML syntax and basic structure:
result = Ruleur::Persistence::YAMLLoader.validate_file('config/rules/my_rule.yml')
if result[:valid]
puts 'Rule is valid!'
else
puts "Errors: #{result[:errors].join(', ')}"
endFull Validation
For comprehensive validation including semantics and test execution:
rule = Ruleur::Persistence::YAMLLoader.load_file('config/rules/my_rule.yml')
result = Ruleur::Validation.validate_rule(rule)
if result.valid?
puts 'Rule is valid!'
puts "Warnings: #{result.warnings}" unless result.warnings.empty?
else
puts 'Errors:'
result.errors.each { |error| puts " - #{error}" }
endSee Validation for more details.
Loading from Database
Typical workflow for database-backed rules:
# Store YAML in database
class RuleRecord < ActiveRecord::Base
# has columns: name, yaml_content, active
end
# Load and parse
active_rules = RuleRecord.where(active: true).map do |record|
Ruleur::Persistence::YAMLLoader.load_string(record.yaml_content)
end
# Create engine
engine = Ruleur::Engine.new(rules: active_rules)Or use the built-in VersionedActiveRecordRepository for full version tracking.
YAML Best Practices
✅ Do
- Use descriptive names:
allow_admin_createnotrule1 - Add meaningful tags: Group related rules
- Set appropriate salience: Higher priority rules fire first
- Validate before storing: Always validate YAML before saving
- Version control: Track YAML files in git
- Document complex logic: Add comments to explain why
❌ Don't
- Don't store arbitrary code: YAML rules can't contain Ruby lambdas
- Don't nest too deeply: Keep condition trees manageable
- Don't use dynamic values: YAML is static (no interpolation)
- Don't skip validation: Invalid rules will fail at runtime
- Don't forget error handling: Wrap loading in begin/rescue
Example: Well-Structured Rule
# Purpose: Allow document editing for admins and authors of drafts
# Dependencies: Requires user.admin? and document.draft? methods
# Author: alice@example.com
# Created: 2026-03-20
name: allow_document_edit
salience: 10
tags:
- documents
- permissions
- edit
no_loop: true
conditions:
type: any
children:
# Rule 1: Admins can always edit
- type: pred
op: truthy
left:
type: call
recv: { type: ref, root: user }
method: admin?
right: null
# Rule 2: Authors can edit their drafts
- type: all
children:
- type: pred
op: eq
left:
type: call
recv: { type: ref, root: document }
method: author_id
right:
type: call
recv: { type: ref, root: user }
method: id
- type: pred
op: truthy
left:
type: call
recv: { type: ref, root: document }
method: draft?
right: null
actions:
set:
allow_edit: trueTroubleshooting
Common Errors
"Invalid YAML syntax"
- Check for proper indentation (2 spaces)
- Ensure colons have space after them
- Quote strings with special characters
"Missing required field: condition"
- Every rule must have name, condition, and action
- Check spelling and nesting
"Unknown operator: xyz"
- See Operators for valid operators
- Check spelling (case-sensitive)
"Invalid condition type"
- Valid types:
pred,all,any,not - Check spelling and nesting structure
Next Steps
- Validation - Learn to validate YAML rules
- Versioning - Track YAML rule changes
- API Reference: YAMLLoader - Detailed API docs