Using Steps in Actions
The steps functionality allows you to compose complex actions by breaking them down into sequential, reusable steps. Each step can expect data from the parent context or previous steps, and expose data for subsequent steps.
Basic Concepts
What are Steps?
Steps are a way to organize action logic into smaller, focused pieces that:
- Execute in a defined order
- Can share data between each other
- Handle failures gracefully with error prefixing
- Can be reused across different actions
How Steps Work
- Step Definition: Define steps using the
step
class method - Execution Order: Steps execute sequentially in the order they're defined
- Data Flow: Each step can expect and expose data
- Error Handling: Step failures are caught and can trigger error handlers
Defining Steps
Using the step
Method
The step
method allows you to define steps inline with blocks:
class UserRegistration
include Axn
expects :email, :password, :name
exposes :user_id, :welcome_message
step :validate_input, expects: [:email, :password, :name], exposes: [:validated_data] do
# Validation logic
fail! "Email is invalid" unless email.match?(/\A[^@\s]+@[^@\s]+\z/)
fail! "Password too short" if password.length < 8
fail! "Name is required" if name.blank?
expose :validated_data, { email: email.downcase, password: password, name: name.strip }
end
step :create_user, expects: [:validated_data], exposes: [:user_id] do
user = User.create!(validated_data)
expose :user_id, user.id
end
step :send_welcome, expects: [:user_id, :validated_data], exposes: [:welcome_message] do
WelcomeMailer.send_welcome(user_id, validated_data[:email]).deliver_now
expose :welcome_message, "Welcome #{validated_data[:name]}!"
end
def call
# Steps handle execution automatically
end
end
Using the steps
Method
The steps
method allows you to compose existing action classes:
class ValidateInput
include Axn
expects :email, :password, :name
exposes :validated_data
def call
fail! "Email is invalid" unless email.match?(/\A[^@\s]+@[^@\s]+\z/)
fail! "Password too short" if password.length < 8
fail! "Name is required" if name.blank?
expose :validated_data, { email: email.downcase, password: password, name: name.strip }
end
end
class CreateUser
include Axn
expects :validated_data
exposes :user_id
def call
user = User.create!(validated_data)
expose :user_id, user.id
end
end
class SendWelcome
include Axn
expects :user_id, :validated_data
exposes :welcome_message
def call
WelcomeMailer.send_welcome(user_id, validated_data[:email]).deliver_now
expose :welcome_message, "Welcome #{validated_data[:name]}!"
end
end
class UserRegistration
include Axn
expects :email, :password, :name
exposes :user_id, :welcome_message
# Use existing action classes as steps
steps(ValidateInput, CreateUser, SendWelcome)
end
Mixed Approach
You can combine both approaches:
class UserRegistration
include Axn
expects :email, :password, :name
exposes :user_id, :welcome_message
# Use existing action for validation
steps(ValidateInput)
# Define custom step for user creation
step :create_user, expects: [:validated_data], exposes: [:user_id] do
user = User.create!(validated_data)
expose :user_id, user.id
end
# Use existing action for welcome email
steps(SendWelcome)
end
Data Flow Between Steps
Expecting Data
Steps can expect data from:
- Parent context: Data passed to the parent action
- Previous steps: Data exposed by earlier steps
step :step1, expects: [:input], exposes: [:processed_data] do
expose :processed_data, input.upcase
end
step :step2, expects: [:processed_data], exposes: [:final_result] do
# This step can access both 'input' (from parent) and 'processed_data' (from step1)
expose :final_result, "Result: #{processed_data}"
end
Exposing Data
Steps expose data using the expose
method:
step :calculation, expects: [:base_value], exposes: [:doubled_value, :final_result] do
doubled = base_value * 2
expose :doubled_value, doubled
expose :final_result, doubled + 10
end
Using expose_return_as
For simple calculations, you can use expose_return_as
:
step :calculation, expects: [:input], expose_return_as: :result do
input * 2 + 10 # Return value is automatically exposed as 'result'
end
Error Handling
Automatic Error Prefixing
When a step fails, error messages are automatically prefixed with the step name:
step :validation, expects: [:input] do
fail! "Input too short"
end
# If this step fails, the error message becomes: "validation step: Input too short"
Step Failure Propagation
When a step fails:
- The step's exception is caught
- The parent action fails with the prefixed error message
- The
on_exception
handlers are triggered appropriately
Exception Handling
Steps can raise exceptions that will be caught and handled:
step :risky_operation, expects: [:input] do
raise StandardError, "Something went wrong with #{input}"
end
# The exception is caught and the error message becomes: "risky_operation step: Something went wrong with [input]"
Best Practices
1. Keep Steps Focused
Each step should have a single responsibility:
# ❌ Bad: Step does too many things
step :process_user, expects: [:user_data], exposes: [:user_id, :welcome_sent] do
user = User.create!(user_data)
WelcomeMailer.send_welcome(user.id).deliver_now
expose :user_id, user.id
expose :welcome_sent, true
end
# ✅ Good: Steps are focused
step :create_user, expects: [:user_data], exposes: [:user_id] do
user = User.create!(user_data)
expose :user_id, user.id
end
step :send_welcome, expects: [:user_id], exposes: [:welcome_sent] do
WelcomeMailer.send_welcome(user_id).deliver_now
expose :welcome_sent, true
end
2. Use Descriptive Step Names
Step names should clearly indicate what the step does:
# ❌ Bad: Unclear names
step :step1, expects: [:input] do
# ...
end
# ✅ Good: Descriptive names
step :validate_email_format, expects: [:input] do
# ...
end
3. Handle Failures Gracefully
Use fail!
for expected failures and raise exceptions for unexpected errors:
step :validation, expects: [:input] do
# Expected failure - use fail!
fail! "Input too short" if input.length < 3
# Unexpected error - raise exception
raise StandardError, "Database connection failed" if database_unavailable?
end
4. Expose Only Necessary Data
Only expose data that subsequent steps actually need:
# ❌ Bad: Exposing unnecessary data
step :validation, expects: [:input], exposes: [:input, :validated, :timestamp] do
expose :input, input
expose :validated, true
expose :timestamp, Time.current
end
# ✅ Good: Only exposing what's needed
step :validation, expects: [:input], exposes: [:validated_input] do
expose :validated_input, input.strip
end
Common Use Cases
API Request Processing
class ProcessAPIRequest
include Axn
expects :request_data
exposes :response_data
step :authenticate, expects: [:request_data], exposes: [:authenticated_user] do
# Authentication logic
expose :authenticated_user, authenticate_user(request_data[:token])
end
step :authorize, expects: [:authenticated_user, :request_data], exposes: [:authorized] do
# Authorization logic
fail! "Access denied" unless authorized_user?(authenticated_user, request_data[:action])
expose :authorized, true
end
step :process_request, expects: [:request_data, :authenticated_user], exposes: [:response_data] do
# Process the actual request
expose :response_data, process_user_request(request_data, authenticated_user)
end
end
Troubleshooting
Common Issues
- Steps not executing: Ensure the Steps module is properly included
- Data not flowing: Check that step names match between
expects
andexposes
- Error messages unclear: Verify step names are descriptive
Debugging Tips
- Use descriptive step names for better error messages
- Check that data is properly exposed between steps
- Verify that step dependencies are correctly specified
Summary
The steps functionality provides a powerful way to compose complex actions from smaller, focused pieces. By following the patterns and best practices outlined here, you can create maintainable, testable, and reusable action compositions.