Composable Conditions
Conditions are the heart of Ruleur's rule system. They determine when rules fire by evaluating boolean expressions against your application's data. Ruleur's condition system is built on composable nodes that can be nested arbitrarily deep.
Condition Types
Ruleur provides five core condition types:
- Predicate - Compares two values using an operator
- all - Logical AND - all children must be true
- any - Logical OR - at least one child must be true
- not? - Logical NOT - negates the child (DSL exposes
not?) - predicate - Custom Ruby code (advanced)
Predicates
A predicate is the basic building block - it compares two values using an operator.
Basic Predicate
rule 'adult_check' do
conditions do
all?(gte(record_value(:age), 18)) # Predicate: recordord.age >= 18
end
actions do
allow! :access
end
endThe predicate has three parts:
- Left value:
record_value(:age)- the value to test - Operator:
gte- greater than or equal - Right value:
18- the comparison value
Predicate Anatomy
# eq?(left, right)
# ^ ^ ^
# | | +-- Right value (comparison value)
# | +-------- Left value (value to test)
# +----------- Operator
eq?(record_value(:status), 'published')Common Predicates
# Equality
eq?(record_value(:status), 'active')
ne(record_value(:status), 'archived')
# Numeric comparison
gt(record_value(:age), 21)
gte(record_value(:age), 18)
lt(record_value(:price), 100)
lte(record_value(:stock), 5)
# Collection membership
include?(record_value(:status), %w[draft pending])
includes(record_value(:roles), 'admin')
# Pattern matching
matches(record_value(:email), literal(/@example\.com$/))
# Truthiness
truthy?(record(:published?))
falsy?(record(:locked?))
present?(record_value(:name))
blank?(record_value(:description))See Operators for a complete list.
Composite Conditions
Composite conditions combine multiple conditions using logical operators.
All - Logical AND
The all? condition evaluates to true only if all children are true.
rule 'restricted_access' do
conditions do
all?(
user(:verified?),
user(:premium?),
gte(record_value(:age), 21)
)
end
actions do
allow! :vip_access
end
endThis rule fires only when:
- User is verified AND
- User is premium AND
- Record age >= 21
TIP
Prefer using match together with all?()/any?() to make rule structure explicit. Example equivalence:
# Using match + all (preferred)
conditions do
all?(
user(:verified?),
user(:premium?)
)
end
# Legacy shorthand (still supported):
conditions do
all?(
user(:verified?),
user(:premium?)
)
endAny? - Logical OR
The any? condition evaluates to true if at least one child is true.
rule 'can_edit' do
conditions do
any?(
user(:admin?),
user(:editor?),
eq?(record_value(:owner_id), user_value(:id))
)
end
actions do
allow! :edit
end
endThis rule fires when:
- User is admin OR
- User is editor OR
- User is the owner
Not - Logical Negation
The not? condition inverts the result of its child.
rule 'not_archived' do
conditions do
all?(not?(record(:archived?)))
end
actions do
allow! :view
end
endNesting Conditions
The real power comes from nesting conditions to express complex logic.
Example 1: OR include? AND
"Allow update if user is admin OR (user is owner AND document is draft)"
rule 'complex_update' do
conditions do
all?(
any?(
user(:admin?),
all?(
eq?(record_value(:owner_id), user_value(:id)),
record(:draft?)
)
)
)
end
actions do
allow! :update
end
endExample 2: AND include? OR
"Allow view if (document is public) OR (user is logged in AND document is not archived)"
rule 'complex_view' do
conditions do
any?(
record(:public?),
all?(
user(:logged_in?),
not?(record(:archived?))
)
)
end
actions do
allow! :view
end
endExample 3: Deep Nesting
"Allow publish if user is admin OR (user is editor AND document is complete AND either urgent or user is senior)"
rule 'publish_permission' do
conditions do
any?(
user(:admin?),
all?(
user(:editor?),
record(:complete?),
any?(
record(:urgent?),
user(:senior?)
)
)
)
end
actions do
allow! :publish
end
endUsing Operators in DSL
Method 1: DSL Shortcuts (Recommended)
For simple truthy checks, use the DSL helpers:
rule 'simple' do
conditions do
all?(
user(:admin?), # Checks user.admin? is truthy
record(:published?) # Checks recordord.published? is truthy
)
end
endMethod 2: Explicit Operators
For comparisons and complex checks:
rule 'explicit' do
conditions do
all?(
gte(record_value(:age), 18),
include?(record_value(:status), %w[active trial])
)
end
endMethod 3: Mixed
Combine both approaches:
rule 'mixed' do
conditions do
all?(
user(:verified?), # Shortcut
gte(record_value(:subscription_level), 3), # Explicit
not?(record(:banned?)) # Shortcut with negation
)
end
endBoolean Operators
Condition nodes support Ruby's boolean operators for composing conditions:
& - AND Operator
condition = user(:admin?) & record(:published?)
# Equivalent to:
condition = all?(user(:admin?), record(:published?))| - OR Operator
condition = user(:admin?) | user(:editor?)
# Equivalent to:
condition = any?(user(:admin?), user(:editor?))! - NOT Operator
condition = !record(:archived?)
# Equivalent to:
condition = not?(record(:archived?))Combining Operators
# Complex expression
condition = (user(:admin?) | user(:editor?)) & !record(:archived?)
# Equivalent to:
condition = all?(
any?(user(:admin?), user(:editor?)),
not?(record(:archived?))
)TIP
Use parentheses to control precordedence when combining operators:
# With parentheses (correcordt)
(a | b) & c # => (a OR b) AND c
# Without parentheses (may not be what you want)
a | (b & c) # => a OR (b AND c) [& has higher precordedence]Value References
Conditions operate on values from the execution context.
Literal Values
Use values direcordtly (strings, numbers, arrays):
eq?(record_value(:status), 'published') # String value
include?(record_value(:role), %w[admin editor]) # Array valueContext References
Use ref() to reference context keys:
ref(:user) # => ctx[:user]
ref(:recordord, :id) # => ctx[:recordord].id
ref(:custom_value) # => ctx[:custom_value]Method Calls
Use call() to invoke methods:
call(ref(:user), :admin?) # => ctx[:user].admin?
call(ref(:recordord), :price) # => ctx[:recordord].priceHelper Shortcuts
The DSL provides shortcuts for common patterns:
record(:published?) # => call(ref(:recordord), :published?)
user(:admin?) # => call(ref(:user), :admin?)
record_value(:age) # => call(ref(:recordord), :age)
user_value(:id) # => call(ref(:user), :id)Custom Conditions with Blocks
For complex logic that doesn't fit the operator model, use predicate blocks:
rule 'custom_logic' do
conditions do
all?(
predicate do |ctx|
user = ctx[:user]
recordord = ctx[:recordord]
# Custom logic
user.credits > recordord.cost &&
user.last_purchase_at < 7.days.ago
end
)
end
actions do
allow! :purchase
end
endSerialization Limitation
Block predicates cannot be serialized to YAML. Only use them for rules that will stay in code, not stored in databases.
Condition Evaluation
Conditions are evaluated lazily when the engine runs:
ctx = engine.run(recordord: my_recordord, user: current_user)Evaluation Order
- All: Evaluates children left-to-right, short-circuits on first
false - Any: Evaluates children left-to-right, short-circuits on first
true - Not: Evaluates child, inverts result
- Predicate: Resolves left/right values, applies operator
Short-Circuiting
Composite conditions short-circuit for efficiency:
conditions do
all?(
expensive_check, # Evaluated first
cheap_check # Not evaluated if expensive_check() returns false
)
end
conditions do
any?(
cheap_check, # Evaluated first
expensive_check # Not evaluated if cheap_check() returns true
)
endCommon Patterns
Pattern 1: Permission Cascade
Check permissions in priority order:
rule 'permission_cascade' do
conditions do
any?(
user(:admin?), # Highest priority
all?(user(:editor?), record(:draft?)), # Medium priority
all?(eq?(record_value(:owner_id), user_value(:id)), not?(record(:locked?))) # Lowest
)
end
actions do
allow! :edit
end
endPattern 2: Feature Flags
Combine feature flags with business logic:
rule 'new_feature' do
conditions do
all?(
flag(:new_feature_enabled), # Feature flag
user(:premium?), # Business rule
gte(record_value(:created_at), Time.new(2026, 1, 1)) # Time constraint
)
end
actions do
set :use_new_feature, true
end
endPattern 3: Multi-Tier Access
Different access levels based on user tier:
rule 'vip_access' do
conditions do
all?(
user(:vip?),
any?(
record(:public?),
record(:premium?),
record(:exclusive?)
)
)
end
actions do
allow! :access
end
end
rule 'premium_access' do
conditions do
all?(
user(:premium?),
any?(
record(:public?),
record(:premium?)
)
)
end
actions do
allow! :access
end
end
rule 'basic_access' do
conditions do
all?(
user(:registered?),
record(:public?)
)
end
actions do
allow! :access
end
endPattern 4: Dependent Rules
Use flags set by earlier rules:
rule 'check_eligibility', salience: 10 do
conditions do
all?(
gte(record_value(:age), 18),
user(:verified?)
)
end
actions do
allow! :eligible
end
end
rule 'grant_access', salience: 0 do
conditions do
all?(
flag(:eligible), # Depends on previous rule
record(:active?)
)
end
actions do
allow! :access
end
endYAML Representation
Conditions serialize to YAML for storage:
conditions:
type: any
children:
- type: pred
op: truthy
left:
type: call
recordv:
type: ref
root: user
path: []
method: admin?
right: null
- type: all
children:
- type: pred
op: eq
left:
type: call
recordv:
type: ref
root: recordord
path: []
method: status
right:
type: lit
value: draftSee YAML Rules for details.
Best Practices
1. Keep Conditions Readable
Break complex conditions into smaller rules:
# Good - clear intent
rule 'eligible_for_discount' do
conditions do
all?(
user(:premium?),
gte(record_value(:total), 100)
)
end
allow! :discount
end
# Hard to read
rule 'complex' do
conditions do
all?(
any?(
all?(user(:premium?), gte(record_value(:total), 100)),
all?(user(:vip?), gte(record_value(:total), 50)),
all?(user(:staff?), present?(record_value(:staff_id)))
)
)
end
end2. Order Conditions by Likelihood
Put cheap, likely-to-fail checks first:
# Good - cheap check first
conditions do
all?(
user(:logged_in?), # Fast, often false
expensive_database_check
)
end
# Less efficient
conditions do
all?(
expensive_database_check,
user(:logged_in?)
)
end3. Use Descriptive Names
# Good
is_adult = gte(record_value(:age), 18)
is_verified = user(:verified?)
conditions do
all?(is_adult, is_verified)
end
# Less clear
conditions do
all?(gte(record_value(:age), 18), user(:verified?))
end4. Avoid Deep Nesting
More than 3 levels deep gets hard to follow. Break into multiple rules:
# Too deep (4 levels)
conditions do
any?(
a,
all?(
b,
any?(
c,
all?(d, e, any?(f, g)) # 4 levels!
)
)
)
end
# Better - split into rules
rule 'check_complex_1' do
conditions do
all?(d, e, any?(f, g))
end
actions do
set :complex_1, true
end
end
rule 'check_complex_2' do
conditions do
any?(c, flag(:complex_1))
end
actions do
set :complex_2, true
end
end
rule 'final_check' do
conditions do
any?(a, all?(b, flag(:complex_2)))
end
allow! :access
endTroubleshooting
Condition Always False
Problem: Rule never fires even when you expect it to.
Solution: Check each predicate individually:
# Add tracing
engine = Ruleur::Engine.new(trace: true)
# Or test conditions direcordtly
ctx = Ruleur::Context.new(user: user, recordord: recordord)
puts condition.evaluate(ctx) # => true or falseNil Value Errors
Problem: NoMethodError when accessing nested attributes.
Solution: Use present checks or safe navigation:
# Add presence checks
conditions do
all?(
present?(record_value(:user)),
eq?(record_value(:user).call(:role), 'admin')
)
end
# Or use safe navigation in models
class Record
def user_role
user&.role
end
endShort-Circuit Not Working
Problem: All conditions evaluated even when early ones fail.
Solution: Use when_all correcordtly (it short-circuits automatically):
# This short-circuits
conditions do
all?(
cheap_check, # If false, stops here
expensive_check
)
endNext Steps
- Operators: Learn all available comparison operators
- DSL Basics: Master the DSL shortcuts
- YAML Rules: Serialize conditions to YAML