Rule Validation
Ruleur provides a comprehensive validation framework to catch errors before rules are stored or executed. This helps ensure rules are correct, well-formed, and safe to run.
Why Validate Rules?
When rules are stored in a database or loaded from YAML files, you want to catch errors early:
- Structural errors: Missing fields, invalid node types, malformed conditions
- Semantic errors: Unknown operators, invalid references, unsupported actions
- Runtime errors: Rules that fail to execute due to logic errors
Validation catches these issues before they cause problems in production.
Quick Example
require 'ruleur'
# Load a rule from YAML
rule = Ruleur::Persistence::YAMLLoader.load_file('config/rules/my_rule.yml')
# Validate the rule
result = Ruleur::Validation.validate_rule(rule)
if result.valid?
puts 'Rule is valid!'
puts "Warnings: #{result.warnings.join(', ')}" unless result.warnings.empty?
else
puts 'Validation failed!'
result.errors.each { |error| puts " - #{error}" }
endValidation Methods
The Ruleur::Validation module provides four main validation methods:
validate_rule(rule, test_context: nil)
Validates a complete Ruleur::Rule object with structural, semantic, and optional execution tests.
rule = Ruleur.define do
rule 'test_rule' do
conditions do
all?(user(:admin?))
end
actions do
allow! :delete
end
end
end.rules.first
result = Ruleur::Validation.validate_rule(rule)
puts result.valid? # => true
puts result.errors # => []
puts result.warnings # => []Parameters:
rule: ARuleur::Ruleinstancetest_context: (Optional) A hash orContextto test execution
Returns: ValidationResult with valid?, errors, and warnings
validate_hash(rule_hash)
Validates a rule hash (serialized format) before deserialization. Useful for validating YAML before loading.
rule_hash = {
name: 'test_rule',
condition: { type: 'pred', op: 'truthy', left: { type: 'ref', root: 'user' }, right: nil },
execute: { set: { test: true } }
}
result = Ruleur::Validation.validate_hash(rule_hash)
if result.valid?
rule = Ruleur::Rule.deserialize(rule_hash)
# Safe to use rule
endParameters:
rule_hash: A hash in Ruleur's serialization format
Returns: ValidationResult
validate_condition(condition)
Validates a condition node (structural and semantic checks).
condition = Ruleur::DSL::Condition::Builders.all?(
Ruleur::DSL::Condition::Builders.truthy?(
Ruleur::DSL::Condition::Builders.ref(:user, :admin?)
)
)
result = Ruleur::Validation.validate_condition(condition)Parameters:
condition: ACondition::Nodeinstance
Returns: ValidationResult
validate_action(action_spec)
Validates an action specification.
action_spec = { set: { allow_create: true, priority: 'high' } }
result = Ruleur::Validation.validate_action(action_spec)
if result.valid?
# Action spec is safe to use
endParameters:
action_spec: A hash describing actions
Returns: ValidationResult
Validating YAML Files
The YAMLLoader includes a convenience method for validating YAML files directly:
# Quick validation (structural only)
result = Ruleur::Persistence::YAMLLoader.validate_file('config/rules/my_rule.yml')
if result[:valid]
puts 'YAML is structurally valid'
else
puts "Errors: #{result[:errors].join(', ')}"
endFor comprehensive validation, load the rule first and use validate_rule:
# Load from YAML
rule = Ruleur::Persistence::YAMLLoader.load_file('config/rules/my_rule.yml')
# Comprehensive validation
result = Ruleur::Validation.validate_rule(rule)
if result.valid?
# Safe to save to database
repository.save(rule)
endValidationResult Object
All validation methods return a ValidationResult object with:
valid?
Returns true if validation passed (no errors).
result = Ruleur::Validation.validate_rule(rule)
puts 'All checks passed' if result.valid?errors
Array of error messages. If present, the rule is invalid.
unless result.valid?
result.errors.each do |error|
puts "ERROR: #{error}"
end
endwarnings
Array of warning messages. Warnings don't make a rule invalid but indicate potential issues.
if result.valid? && result.warnings.any?
puts 'Rule is valid but has warnings:'
result.warnings.each { |w| puts " - #{w}" }
endto_h
Converts result to a hash for serialization:
result.to_h
# => { valid: true, errors: [], warnings: ["Test execution passed"] }Test Execution Validation
To ensure a rule works with actual data, provide a test context:
User = Struct.new(:admin)
Record = Struct.new(:status)
rule = Ruleur.define do
rule 'admin_access' do
conditions do
all?(user(:admin))
end
actions do
allow! :access
end
end
end.rules.first
# Validate with test data
test_context = {
user: User.new(true),
record: Record.new('active')
}
result = Ruleur::Validation.validate_rule(rule, test_context: test_context)
if result.valid?
puts 'Rule validated and executed successfully'
# Check warnings for execution feedback
puts result.warnings # => ["Test execution passed"]
endBenefits of Test Execution:
- Catches runtime errors (method missing, type errors)
- Verifies rule logic with sample data
- Confirms expected outcomes
Example with failure:
rule = Ruleur.define do
rule 'broken_rule' do
conditions do
all?(user(:nonexistent_method)) # This will fail
end
actions do
allow! :access
end
end
end.rules.first
test_context = {
user: User.new(true),
record: Record.new('active')
}
result = Ruleur::Validation.validate_rule(rule, test_context: test_context)
puts result.valid? # => false
puts result.errors # => ["Test execution failed: undefined method `nonexistent_method'..."]Validation Checks
The validation framework performs multiple levels of checks:
Structural Validation
Ensures the rule has valid structure:
- ✅ Rule name is present and non-empty
- ✅ Condition is present
- ✅ Action spec is present
- ✅ Salience is an integer
- ✅ Tags is an array
- ✅
no_loopis a boolean
Example errors:
- Rule name cannot be nil or empty
- Rule condition cannot be nil
- Rule action_spec cannot be nil
- Salience must be an Integer, got String
- Tags must be an Array, got String
- no_loop must be boolean, got StringCondition Validation
Validates the condition tree structure:
- ✅ Valid node types:
pred,all,any,not - ✅ Predicates have operators
- ✅ Composite conditions (
all/any) have children arrays - ✅
notconditions have a child - ✅ Values are valid types:
lit,ref,call
Example errors:
- Invalid condition type: "invalid"
- Predicate missing operator
- all condition must have children array
- Not condition must have child
- Invalid value type: unknownSemantic Validation
Validates that the rule makes sense:
- ✅ Operators exist in the operator registry
- ✅ Call nodes have valid receiver (must be Ref)
- ✅ Action specs use supported actions (currently only
set)
Example errors:
- Unknown operator: "invalid_op"
- Call receiver must be a Ref, got Lit
- Unsupported action type: executeAction Validation
Validates action specifications:
- ✅ Action spec is a non-empty hash
- ✅
setaction contains a hash of key-value pairs - ✅ Values are serializable types
Example errors:
- Action must be a Hash, got String
- Action cannot be empty
- Action 'set' must be a HashCommon Validation Patterns
Pattern 1: Validate Before Saving
Always validate rules before persisting to database:
def save_rule(rule, repository)
# Validate first
result = Ruleur::Validation.validate_rule(rule)
raise "Invalid rule: #{result.errors.join(', ')}" unless result.valid?
# Log warnings
result.warnings.each { |w| Rails.logger.warn("Rule warning: #{w}") }
# Safe to save
repository.save(rule)
endPattern 2: Validate YAML Imports
When importing rules from YAML, validate them in a pipeline:
def import_rules_from_directory(dir_path, repository)
Dir.glob("#{dir_path}/*.yml").each do |file|
# Load rule
rule = Ruleur::Persistence::YAMLLoader.load_file(file)
# Validate
result = Ruleur::Validation.validate_rule(rule)
if result.valid?
repository.save(rule)
puts "✓ Imported: #{rule.name}"
else
puts "✗ Failed: #{file}"
result.errors.each { |e| puts " - #{e}" }
end
end
endPattern 3: Validate with Test Suite
Create test cases for your rules and validate execution:
describe 'Permission Rules' do
it 'validates admin access rule' do
rule = Ruleur::Persistence::YAMLLoader.load_file('rules/admin_access.yml')
# Test case 1: Admin user
test_context = {
user: User.new(role: 'admin'),
record: Document.new(status: 'draft')
}
result = Ruleur::Validation.validate_rule(rule, test_context: test_context)
expect(result.valid?).to be true
end
endPattern 4: Continuous Validation
Periodically validate all stored rules to catch issues:
class RuleHealthCheck
def self.validate_all_rules(repository)
report = { valid: 0, invalid: 0, warnings: 0, errors: [] }
repository.all.each do |rule|
result = Ruleur::Validation.validate_rule(rule)
if result.valid?
report[:valid] += 1
report[:warnings] += result.warnings.size
else
report[:invalid] += 1
report[:errors] << {
rule: rule.name,
errors: result.errors
}
end
end
report
end
end
# Run in a rake task or scheduled job
report = RuleHealthCheck.validate_all_rules(repository)
puts "Valid: #{report[:valid]}, Invalid: #{report[:invalid]}"Best Practices
1. Always Validate Before Persistence
Never save a rule without validating it first:
# Good
result = Ruleur::Validation.validate_rule(rule)
repository.save(rule) if result.valid?
# Bad - no validation
repository.save(rule) # Might save broken rule!2. Use Test Context for Critical Rules
For important business rules, validate with test data:
# Create representative test cases
test_cases = [
{ user: admin_user, record: draft_doc },
{ user: regular_user, record: published_doc },
{ user: guest_user, record: private_doc }
]
test_cases.each do |context|
result = Ruleur::Validation.validate_rule(rule, test_context: context)
raise 'Failed test case' unless result.valid?
end3. Log Warnings
Even valid rules might have warnings worth investigating:
result = Ruleur::Validation.validate_rule(rule)
if result.valid?
repository.save(rule)
# Log warnings for investigation
result.warnings.each do |warning|
Rails.logger.warn("[Rule: #{rule.name}] #{warning}")
end
end4. Handle Validation Errors Gracefully
Provide clear feedback when validation fails:
def create_rule(params)
rule = build_rule_from_params(params)
result = Ruleur::Validation.validate_rule(rule)
if result.valid?
repository.save(rule)
{ success: true, rule: rule }
else
{
success: false,
errors: result.errors,
warnings: result.warnings
}
end
end5. Validate in CI/CD Pipeline
Add validation to your deployment pipeline:
# spec/rules_spec.rb
describe 'All YAML Rules' do
Dir.glob('config/rules/*.yml').each do |file|
it "validates #{File.basename(file)}" do
rule = Ruleur::Persistence::YAMLLoader.load_file(file)
result = Ruleur::Validation.validate_rule(rule)
expect(result.valid?).to be true,
"Rule validation failed: #{result.errors.join(', ')}"
end
end
endValidation in Web UI
If you're building a rule management UI, integrate validation:
class RulesController < ApplicationController
def create
rule_hash = params.require(:rule).permit!.to_h
# Validate hash before deserialization
result = Ruleur::Validation.validate_hash(rule_hash)
if result.valid?
rule = Ruleur::Rule.deserialize(rule_hash)
# Further validation with test execution
result = Ruleur::Validation.validate_rule(rule, test_context: build_test_context)
if result.valid?
@repository.save(rule)
render json: { success: true, warnings: result.warnings }
else
render json: { success: false, errors: result.errors }, status: :unprocessable_entity
end
else
render json: { success: false, errors: result.errors }, status: :bad_request
end
end
private
def build_test_context
{
user: User.new(role: 'test'),
record: Document.new(status: 'test')
}
end
endTroubleshooting
Common Validation Errors
"Rule name cannot be nil or empty"
- Ensure every rule has a non-empty name
"Unknown operator: 'custom_op'"
- The operator isn't registered in
Ruleur::Operators.registry - Check for typos (
truthynottrufy) - Use standard operators:
eq,ne,gt,gte,lt,lte,in,includes,matches,truthy,falsy,present,blank
"Call receiver must be a Ref, got Lit"
- Method calls must be on references, not literals
- Bad:
call(lit(123), :to_s) - Good:
call(ref(:record, :id), :to_s)
"Test execution failed: undefined method"
- The test context objects don't have the methods your rule expects
- Verify your test objects match your production objects
"all condition has no children"
- Empty composite conditions (warning, not error)
- Add child conditions or remove the composite
Next Steps
- YAML Rules: Load and validate rules from YAML
- Versioning: Store validated rules with audit trails
- Persistence: Save validated rules to database