DSL Basics
The Ruleur DSL provides a fluent, readable Ruby interface for defining business rules. It strikes a balance between expressiveness and safety, avoiding metaprogramming hazards while keeping the syntax clean.
Quick Example
require 'ruleur'
engine = Ruleur.define do
rule 'admin_create', no_loop: true do
conditions do
any?(
user(:admin?),
all?(record(:updatable?), record(:draft?))
)
end
actions do
set :create, true
end
end
end
ctx = engine.run(record: record, user: user)
ctx[:create] # => true (if rule fired) or nilWith Ruleur, context values are only set when rules fire. If no rule matches, the value remains nil.
Defining Engines
Use Ruleur.define to create an engine with rules:
engine = Ruleur.define do
rule 'rule_name' do
# conditions
# actions
end
rule 'another_rule', salience: 10 do
# ...
end
endDefining Rules
Each rule has:
- Name: A unique identifier (string or symbol)
- Conditions: When the rule should fire
- Actions: What the rule does when it fires
- Options: salience, tags, no_loop (optional)
Basic Structure
rule 'rule_name', salience: 10, tags: ['permissions'], no_loop: true do
conditions do
all?
# conditions go here
end
actions do
set :create, true
end
endRule Options
salience: Priority (higher = fires first). Default: 0tags: Array of strings for categorization. Default: []no_loop: Prevent rule from firing twice in same execution. Default: false
rule 'high_priority', salience: 100 do
# This rule fires before others
end
rule 'admin_crud', tags: %w[permissions admin] do
# Tagged for organization
end
rule 'once_only', no_loop: true do
# Won't fire again even if conditions remain true
endDSL Shortcuts
Ruleur provides convenient helper methods to keep your rules readable.
record(method_name) - Record Method Check
Checks if a method on the record returns truthy:
record(:admin?) # => truthy?(record.admin?)
record(:published?) # => truthy?(record.published?)TIP
record(method) is shorthand for truthy?(ref(:record).call(method)). The truthy operator checks if the value is not nil or false.
user(method_name) - User Method Check
Checks if a method on the user returns truthy:
user(:admin?) # => truthy?(user.admin?)
user(:verified?) # => truthy?(user.verified?)
:::
### `record_value(method_name)` - Record Value Reference
Gets the actual value (not truthy check) from a record method:
```ruby
eq?(record_value(:age), 18)
includes(literal(%w[draft pending]), record_value(:status))user_value(method_name) - User Value Reference
Gets the actual value from a user method:
eq?(user_value(:role), 'admin')
gte(user_value(:subscription_level), 3)flag(name) - Context Flag Check
Checks if a flag was set by another rule:
flag(:create) # => truthy?(:create)
flag(:update) # => truthy?(:update)This is useful for chaining rules - one rule sets :create, another checks it:
rule 'admin_create' do
conditions do
any?(user(:admin?))
end
actions do
set :create, true
end
end
rule 'draft_update' do
conditions do
all?(
flag(:create),
record(:draft?)
)
end
actions do
set :update, true
end
endConditions
Conditions determine when a rule fires. Use match with all/any builders or the legacy when_all/when_any helpers.
when_all - All Conditions Must Be True
rule 'admin_update' do
conditions do
all?(
user(:admin?),
record(:published?),
not?(record(:locked?))
)
end
actions do
set :update, true
end
endAll conditions must be truthy for the rule to fire.
when_any - At Least One Condition True
rule 'editor_show' do
conditions do
any?(
user(:admin?),
record(:public?),
eq?(record_value(:owner_id), user_value(:id))
)
end
actions do
set :show, true
end
endIf any condition is truthy, the rule fires.
Nesting Conditions
You can nest all and any within when_all or when_any:
rule 'editor_update' do
conditions do
all?(
any?(
user(:admin?),
user(:editor?)
),
all?(
record(:published?),
not?(record(:archived?))
)
)
end
actions do
set :update, true
end
endUsing Operators
For more complex comparisons, use operators directly:
rule 'premium_purchase' do
conditions do
all?(
gte(record_value(:age), 18),
eq?(record_value(:country), 'US'),
includes(literal(%w[active trial]), record_value(:status))
)
end
actions do
set :purchase, true
end
endSee Operators for a complete list.
Actions
Actions define what happens when a rule fires. Use the set method or action block.
set(key, value) - Set a Context Value
rule 'set_discount' do
conditions do
all?(user(:premium?))
end
actions do
set :discount, 0.20
end
endassert(hash) - Set Multiple Values
rule 'set_defaults' do
conditions do
all?(record(:new?))
end
actions do
assert(
status: 'draft',
priority: 'low',
assignee: nil
)
end
endCustom Action Block
For more complex logic, use an action block:
rule 'calculate_total' do
conditions do
all?(record(:items))
end
actions do |ctx|
items = ctx[:record].items
total = items.sum(&:price)
tax = total * 0.1
ctx[:total] = total
ctx[:tax] = tax
end
endTIP
The action method provides a block for executing code:
rule 'apply_discount' do
conditions do
all?(user(:premium?))
end
actions do |ctx|
ctx[:discount] = 0.20
end
endContext Variables
The execution context holds all facts and values during rule evaluation:
ctx = engine.run(
record: my_record,
user: current_user,
custom_value: 123
)
ctx[:record] # => my_record
ctx[:user] # => current_user
ctx[:custom_value] # => 123
ctx[:update] # => true (if rule fired) or nil (denied)
ctx[:discount] # => 0.20 (if rule set it)Rules can reference any context key using ref:
rule 'check_custom' do
conditions do
all?(
eq?(ref(:custom_value), 123)
)
end
actions do
set :custom_check, true
end
endComplete Example
Here's a real-world permission system:
require 'ruleur'
Document = Struct.new(:status, :owner_id, :locked) do
def draft? = status == 'draft'
def published? = status == 'published'
def locked? = !!locked
end
User = Struct.new(:id, :role) do
def admin? = role == 'admin'
def editor? = role == 'editor'
end
engine = Ruleur.define do
rule 'admin_crud', salience: 100 do
conditions do
all?(user(:admin?))
end
actions do
set :create, true
set :show, true
set :update, true
set :destroy, true
end
end
rule 'editor_create_update', salience: 50 do
conditions do
all?(
user(:editor?),
record(:draft?)
)
end
actions do
set :create, true
set :update, true
end
end
rule 'owner_update' do
conditions do
all?(
record(:draft?),
not?(record(:locked?)),
eq?(record_value(:owner_id), user_value(:id))
)
end
actions do
set :update, true
end
end
rule 'editor_published_update' do
conditions do
all?(
record(:published?),
any?(user(:admin?), user(:editor?))
)
end
actions do
set :update, true
end
end
end
doc = Document.new('draft', 123, false)
user = User.new(123, 'user')
ctx = engine.run(record: doc, user: user)
puts ctx[:create] # => true (editor can create drafts)
puts ctx[:update] # => true (owner can update own draft)
puts ctx[:destroy] # => nil (no permission)Best Practices
1. Use Descriptive Names
rule 'admin_destroy' do
# ...
end
rule 'user_destroy' do
# ...
end2. Keep Rules Focused
Each rule should have a single responsibility:
rule 'admin_create' do
conditions do
all?(user(:admin?))
end
actions do
set :create, true
end
end
rule 'verified_user_create' do
conditions do
all?(user(:verified?))
end
actions do
set :create, true
end
end3. Use Salience for Priority
Higher salience rules fire first:
rule 'set_default_discount', salience: 0 do
actions do
set :discount, 0.0
end
end
rule 'apply_premium_discount', salience: 10 do
conditions do
all?(user(:premium?))
end
actions do
set :discount, 0.15
end
end
rule 'apply_vip_discount', salience: 20 do
conditions do
all?(user(:vip?))
end
actions do
set :discount, 0.30
end
end4. Use no_loop to Prevent Infinite Firing
If a rule's action could make its own condition true again, use no_loop:
rule 'increment_counter', no_loop: true do
conditions do
all?(lt(ref(:counter), 100))
end
actions do |ctx|
ctx[:counter] = (ctx[:counter] || 0) + 1
end
end6. Tag Rules for Organization
rule 'admin_create', tags: %w[permissions admin] do
set :create, true
end
rule 'editor_update', tags: %w[permissions editor] do
set :update, true
end
engine.rules_with_tag('admin')Next Steps
- Conditions: Deep dive into composable conditions
- Operators: Complete list of comparison operators
- YAML Rules: Define rules in YAML for database storage
- Validation: Validate rules before execution