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::Attributes
API (more on that later)
- Nested attributes must not be defined as attributes in the step object.
- The class must implement the
permitted_params
method which returns an array of attributes. - If the
#save
method is overridden, super
must be called and its value must be used.
- The
#save
method must return a boolean value.
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.metadata(User.last)
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_resource
method must not set any instance variables. - The
#find_resource
method must only retrieve the record associated with the resource ID (#includes
is allowed), not a collection of records. - The
#find_resource
method 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_class
method returns the step object class associated with the current step. - Other instance variables can be set before calling
super
or inside the block.
Rules to follow:
- DO NOT implement the
new
, edit
or create
actions. - Always call
super
with a block in the show
and update
actions. - Always set the
@step_processor
instance variable inside the super
block. - DO NOT call
#save
manually, 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
└── views/
└── users/
├── banking_info.html.erb
├── objectives.html.erb
└── profile.html.erb
Rules to follow
- DO NOT create a view for the
show
action. - DO NOT create views named
new
or edit
.
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
Rules to follow
- The resource object must be passed to
form_for
, not the step processor - The form's method must be either
:put
or :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.