Wicked::Pipeline
A step by step pipeline system built on top of Wicked.
Table of Contents
Installation
Install the gem and add to the application's Gemfile by executing:
$ bundle add wicked-pipeline
If bundler is not being used to manage dependencies, install the gem by executing:
$ gem install wicked-pipeline
Compatibility
Currently this gem requires Rails >= 6.1
Usage
You can generate a pipeline using the dedicated generator:
$ rails generate wicked:pipeline Registration identification profile preferences
This will geneate the RegistrationPipeline, IdentificationStep, ProfileStep, and PreferencesStep classes under app/steps. It will also generate test files under spec/steps if RSpec is installed.
You can also generate individual steps:
$ rails generate wicked:pipeline:step identification email subscribe_to_newsletter:boolean interests:array
This will generate the IdentificationStep with the email, subscribe_to_newsletter and interests attributes. It will also generate a test file under spec/steps if RSpec is installed.
Step objects
A step object is very similar to a form object. It takes a "resource" (the ActiveRecord model) and optional params as arguments and will contain attribute definitions, validations for the step, a list of permitted params and a #save method. The validations are specific to the step but the resource will also be validated before being saved.
Step objects are subclasses of the Wicked::Pipeline::BaseStep class, and they live in the app/steps directory. Here are the rules that must be respected in a step object:
- The name of a step class must end with
Step - Attributes must be defined using the
ActiveModel::AttributesAPI (more on that later)- Nested attributes must not be defined as attributes in the step object.
- The class must implement the
permitted_paramsmethod which returns an array of attributes. - If the
#savemethod is overridden,supermust be called and its value must be used.- The
#savemethod must return a boolean value.
- The
The only method that needs to be implemented is the permitted_params private method, everything else is optional. Here is the most basic step object that can exist:
module Users
class ProfileStep < ::Wicked::Pipeline::BaseStep
private
def permitted_params
[]
end
end
end
That's it!
As mentioned before, step objects require a resource:
Users::ProfileStep.new(User.last)
Attributes
The attributes of a step object are defined using the .attribute method. The first argument is the name of the attribute, and the second argument is the type of the attribute. The type argument is optional but it must be specified for boolean attributes.
To define a String attributes as an Array, the second argument must be array: true and the :string type must not be specified.
module Users
class ProfileStep < ::Wicked::Pipeline::BaseStep
attribute :email
attribute :first_name
attribute :last_name
attribute :is_us_citizen, :boolean
attribute :investment_goals, array: true
private
def permitted_params
i[email first_name last_name is_us_citizen investment_goals]
end
end
end
The attributes must be in the list of permitted parameters!
Validations
Validations are used the same way as in ActiveRecord models. One exception is the uniqueness validation which is not available in step objects.
Hint: A custom validation method must be used for uniqueness validations, but usually uniqueness validations should be defined in the model.
module Users
class ProfileStep < ::Wicked::Pipeline::BaseStep
# ...
validates_presence_of :email, :first_name, :last_name
validates :is_us_citizen, inclusion: { in: [true, false] }
validate :full_name_must_not_be_too_long
private
def full_name_must_not_be_too_long
unless "#{first_name} #{last_name}".length <= 255
errors.add(:base, :too_long, count: 255)
end
end
# ...
end
end
Custom validation errors must be added to the step object not the resource itself, they will be merged into the resource's errors automatically.
Blocking
A blocking step will short-circuit a pipeline. In other words, all step following a blocking step will be inaccessible.
A step can be marked as "blocking" by overriding the blocking? predicate method:
module Users
class ProfileStep < ::Wicked::Pipeline::BaseStep
attribute :first_name
attribute :is_us_citizen, :boolean
def blocking?
first_name == "John" || is_us_citizen
end
# ...
end
end
Since the blocking? method is a predicate method, it must return a boolean value.
Blocking reason
To specify a reason why the step is marked as blocking, the blocking_reason method should be overridden:
module Users
class ProfileStep < ::Wicked::Pipeline::BaseStep
attribute :first_name
attribute :is_us_citizen, :boolean
def blocking?
first_name == "John" || is_us_citizen
end
def blocking_reason
return nil unless blocking?
if first_name == "John"
"Too cool for school"
elsif is_us_citizen
"Vive la France"
end
end
# ...
end
end
Step pipelines
A step pipeline class is a subclass of the Wicked::Pipeline::BasePipeline class and live in app/steps. At the most basic level should contain a list of step classes.
Note: Step pipeline should only be used with step objects!
class UserAccountPipeline < Wicked::Pipeline::BasePipeline
def steps
[
User::ProfileStep,
User::BankingInfoStep,
User::ObjectivesStep
]
end
end
The order of the steps will be used in the controller/views, so it's easy to reorder steps at any time.
Steps metadata can be accessed using the pipeline. This includes the name of a step, whether or not it is valid and whether or not it is accessible:
UserAccountPipeline.(User.last)
#=> {
# profile: { valid: true, accessible: true },
# banking_info: { valid: false, accessible: true },
# objectives: { valid: false, accessible: false }
# }
A step is accessible if the previous step is valid and accessible. The first step is always accessible.
Finally, pipelines are also used to check if all steps are valid for a given resource:
UserAccountPipeline.valid?(User.last)
Steps controllers
A steps controller is a subclass of Wicked::Pipeline::BaseStepsController, it can have any name and can be placed anywhere under the app/controllers directory. Unlike a regular controller, the :id parameter references the current step not the ID of a resource. For this reason, the name of the resource ID parameter must be specified using the #resource_param_name private method.
Steps controllers must implement the following private methods:
#resource_param_name: This method must return the name of the resource ID parameter (eg::user_id).#steps_pipeline: This method must return the pipeline class that will be used in the steps controller (eg:UserAccountPipeline)#find_resource: This method takes care of finding the resource object from the database.
class UsersController < Wicked::Pipeline::BaseStepsController
# ...
private
def resource_param_name
:user_id
end
def steps_pipeline
UserAccountPipeline
end
def find_resource
User.find(params[resource_param_name])
end
end
Rules to follow
- The
#find_resourcemethod must not set any instance variables. - The
#find_resourcemethod must only retrieve the record associated with the resource ID (#includesis allowed), not a collection of records. - The
#find_resourcemethod must return the record.
Routes
The param option must be specified when defining resource routes:
resources :users, only: [:show, :update], param: :user_id
DO NOT use nested routes with the step routes.
Nested routes
When the controller has an index action, nested routes can be defined in the following way:
resources :users, only: [:show, :update], param: :user_id
resources :users, only: [:index] do
resources :profiles
end
When the controller doesn't have an index action, nested routes should be defined in the following way instead:
resources :users, only: [:show, :update], param: :user_id
scope path: "users/:user_id" do
resources :profiles
end
Actions
A step controller has the show and update actions. It cannot have the new, edit and create actions! The index and destroy actions can be implemented but they will be independent of the pipeline.
Both action must call super with a block and set the @step_processor instance variable inside of that block:
class UsersController < Wicked::Pipeline::BaseStepsController
def show
super do
@user = find_resource
@step_processor = step_class.new(@user)
end
end
def update
super do
@user = find_resource
@step_processor = step_class.new(@user, params.require(:user))
end
end
# ...
end
Notes:
- The
step_classmethod returns the step object class associated with the current step. - Other instance variables can be set before calling
superor inside the block.
Rules to follow:
- DO NOT implement the
new,editorcreateactions. - Always call
superwith a block in theshowandupdateactions. - Always set the
@step_processorinstance variable inside thesuperblock. - DO NOT call
#savemanually, this will be done automatically. - DO NOT set the the following instance variable:
@step@next_step@previous_step
Flash messages
Flash messages can be set manually after calling super. Step objects have a #saved? method which can be used to verify that it was successfully saved. The method should be used before setting a flash messages:
class UsersController < Wicked::Pipeline::BaseStepsController
# ...
def update
super do
@user = find_resource
@step_processor = step_class.new(@user, params.require(:user))
end
if @step_processor.saved?
flash[:success] = t(".success")
end
end
# ...
end
Views
There must be a view for each step, not a partial, it must have the name of the step and it should live in the root directory of the controller's view directory:
app
Rules to follow
- DO NOT create a view for the
showaction. - DO NOT create views named
neworedit.
Forms
The form in step view must be declared in the following way:
<%= form_for @user, url: step_path, method: :patch do |f| %>
<% # ... %>
<% end %>
Notes
- The
step_pathmethod refers to the path of the current step. It can also be used to get the path of a specific step:
step_path(step_name: "objectives")
Rules to follow
- The resource object must be passed to
form_for, not the step processor - The form's method must be either
:putor:patch
Breadcrumbs
Step controllers provide a extended version of their pipeline's steps metadata with the following added info:
:url: The URL of a step:active: Whether or not a step is currently active
This information is made to be used to build breadcrumbs. Here is a basic way to use steps metadata to build breadcrumbs:
<nav aria-label="breadcrumb" class="p-0">
<ol class="breadcrumb p-0">
<% steps_metadata.each do |step_name, step_metadata| %>
<li class="<%= "active-step" if step_metadata[:active] %> <%= "user-#{step_name.dasherize}-step" %> py-2 px-4">
<% if step_metadata[:accessible] %>
<%= link_to step_metadata[:url] do %>
<i class="far <%= step_metadata[:valid] ? "fa-check-square" : "fa-square" %> mr-1"></i>
<%== t ".#{step_name}" %>
<% end %>
<% else %>
<span>
<i class="far fa-square mr-1"></i>
<%== t ".#{step_name}" %>
</span>
<% end %>
</li>
<% end %>
</ol>
</nav>
Development
After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests.
To install this gem onto your local machine, run bundle exec rake install.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/RobertAudi/wicked-pipeline.
The code is linted using Standard. To lint the code run bundle exec rake standard or bundle exec rake standard:fix to autocorrect offenses.
License
The gem is available as open source under the terms of the MIT License.