Over the last couple of weeks we’ve started building a registration process using Ruby on Rails and Trailblazer.

In Using Inheritance for Trailblazer Operations we created the functionality to create new users and import users from an existing application.

When a new user signs up to the application they will need to click on a link that will be emailed to them in order to confirm their email address. We implemented this functionality in Building out a User Confirmation flow in Trailblazer.

However, we also need to confirm the imported users and give them an opportunity to pick a username and set a password.

This presents an awkward situation where we need to accept data based upon the state of the user. Normally this would require messy if statements in the Controller and the View.

Fortunately, thanks to Trailblazer’s abstraction, this should be pretty easy to deal with in this application!

In today’s tutorial we will be taking a look at how to build this functionality using Trailblazer and Ruby on Rails. If you missed the previous tutorials, you should probably take a look at those to set the context for this tutorial.

Adding the Routes

The first thing I’m going to do is to define the routes:

get "confirmation/confirm", to: "confirmation/confirm#new"  
post "confirmation/confirm", to: "confirmation/confirm#create"  

Firstly, I need a GET request route that will display the form when the user clicks on their confirmation link from the email I send them.

Secondly, I will need a POST route that will accept the form request. In the case of normal users, this will just consist of the user’s confirmation token, but in the case of imported users this will also include their chosen username and password.

Adding the Controller and the View

The next thing we need to do is to add the Controller for accepting the requests:

module Confirmation  
class ConfirmController < ApplicationController  
def new  
form Confirmation::Confirm::Operation::Base  
end

def create  
run Confirmation::Confirm::Operation::Base do |op|  
return redirect_to login_url  
end

render :new, status: 400  
end  
end  
end  

As you can see, I’m namespacing this Controller under Confirmation as I did with in last week’s tutorial. This Controller is basically the same as all of the Controllers we’ve looked at so far in this series.

The one thing to note is that I’m using Confirmation::Confirm::Operation::Base as the Operation. This is because I need to return the correct Operation class depending on the user who the confirmation token belongs to.

I’m also not sure I like these long ass namespaces, so that might change in the future.

We will also need a View for the #new method to keep Rails happy:

h1 Confirm your account  
= concept("confirmation/confirm/cell", @operation)  

As you can see, we’re going to be using a Cell as we did last week. We’ll be creating the Cell later in this tutorial.

Defining the Contract

Next up we need to define the Contract for the two Operations, one for imported users, and one for the default users:

module Confirmation::Confirm::Contract  
class Base < Reform::Form  
model User  
property :token, from: :confirmation_token  
end

class Imported < Base  
property :username  
property :password, virtual: true

validates :username, presence: true, username: true  
validates :password, presence: true, length: { minimum: 8 }  
validate :username_is_unique

def username_is_unique  
if User.find_by(username: username)  
errors.add(:username, :taken)  
end  
end  
end

class Default < Base; end  
end  

First I’m going to define the Base class that will act as an abstract class for both child classes. This class simply defines the token property so I can use it later.

In the Imported class we can define the username and password properties as well as the validation rules that should apply.

One thing to note is that I had to add a custom method to check for uniqueness of the username. However, this is a good example of how easy it is to define your own validation rules by simply defining a method on the Contract.

Finally, the Default Contract does not need any additional parameters or validations and so it can simply extend from the Base Contract class.

Creating the Operation

With the Contracts in place, we can now turn our attention to the Operation classes. Create a new file called operation.rb in the relevant concept directory:

module Confirmation::Confirm::Operation  
class Base < Trailblazer::Operation  
end

class Imported < Base  
end

class Default < Base  
end  
end  

As we saw in the Controller, we’re going to be using the Base class as the interface to the two underlying Operation implementations. The Base class needs to be able to delegate based upon whether the token belongs to an imported user or a default user.

Here is what the Base class looks like:

class Base < Trailblazer::Operation  
include Resolver  
include Model; model User

builds -> (model, policy, params) do  
return Confirmation::Confirm::Operation::Imported if model.imported?  
return Confirmation::Confirm::Operation::Default  
end

def self.model!(params)  
User.find_by!(confirmation_token: params[:token], confirmed_at: nil)  
end

def confirm  
model.confirmed_at = Time.now  
end  
end  

First I override the self.model! class method to find the user by the given token. If the user is not found an Exception will be thrown to halt the process. This sets the model on the Operation.

The next thing to note is the builds block that will return the correct implementation based upon if the model is imported or not.

Finally I’m including a confirm method that will set the confirmed_at timestamp on the model because I’m going to need it in both of the child classes.

Next up I will add the Imported class:

class Imported < Base  
contract Confirmation::Confirm::Contract::Imported

def process(params)  
validate(params[:user]) do |f|  
generate_digest  
confirm  
f.save  
end  
end

def generate_digest  
model.password_digest = BCrypt::Password.create(contract.password)  
end  
end  

This is pretty much a standard Operation that we’ve seen a couple of times now so there isn’t really much to explain.

First I set the contract that we defined earlier.

Next in the process method we first validate the incoming parameters. If the validation passes we generate the password digest, confirm the user and then call save.

Here is the Default class:

class Default < Base  
contract Confirmation::Confirm::Contract::Default

def process(params)  
validate(params) do |f|  
confirm  
f.save  
end  
end  
end  

Again there is even less to explain here. First I set the contract from earlier again. Next inside the process method I confirm the user and then call save.

Here is this file in full:

require "bcrypt"

module Confirmation::Confirm::Operation  
class Base < Trailblazer::Operation  
include Resolver  
include Model; model User

builds -> (model, policy, params) do  
return Confirmation::Confirm::Operation::Imported if model.imported?  
return Confirmation::Confirm::Operation::Default  
end

def self.model!(params)  
User.find_by!(confirmation_token: params[:token], confirmed_at: nil)  
end

def confirm  
model.confirmed_at = Time.now  
end  
end

class Imported < Base  
contract Confirmation::Confirm::Contract::Imported

def process(params)  
validate(params[:user]) do |f|  
generate_digest  
confirm  
f.save  
end  
end

def generate_digest  
model.password_digest = BCrypt::Password.create(contract.password)  
end  
end

class Default < Base  
contract Confirmation::Confirm::Contract::Default

def process(params)  
validate(params) do |f|  
confirm  
f.save  
end  
end  
end  
end  

With the Operation classes in place, now I’ll write some tests to make sure everything is working as it should be:

require "test_helper"

module Confirmation::Confirm::OperationTest  
class TestCase < ActiveSupport::TestCase  
def setup  
@default = User::Create::Operation::Default.(user: attributes_for(:user)).model  
@imported = User::Create::Operation::Imported.(user: attributes_for(:imported_user)).model  
end  
end  
end  

First I’m going to define a TestCase class so I can reuse the same setup method in each of my implementation test classes.

Here I’m simply creating a default user and an imported user using the correct Operation implementation for each as a factory.

I’m not sure if this a best practice or not, but it works pretty well.

Next up I will write a couple of tests for the Base class:

class BaseTest < TestCase  
test "throw exception on invalid token" do  
assert_raises ActiveRecord::RecordNotFound do  
Confirmation::Confirm::Operation::Base.run(token: "abc")  
end  
end

test "build Imported for imported user" do  
op = Confirmation::Confirm::Operation::Base.present(token: @imported.confirmation_token)

assert_instance_of(Confirmation::Confirm::Operation::Imported, op)  
end

test "build Default for default user" do  
op = Confirmation::Confirm::Operation::Base.present(token: @default.confirmation_token)

assert_instance_of(Confirmation::Confirm::Operation::Default, op)  
end  
end  

First I make sure a ActiveRecord::RecordNotFound Exception is thrown if the confirmation token does not exist. Next I assert that the Base class instantiates the correct child class given a user token.

Next up I will write some tests for the Imported class:

class ImportedTest < TestCase  
test "require presence of username and password" do  
res, op = Confirmation::Confirm::Operation::Imported.run(  
token: @imported.confirmation_token, user: {})

assert_not(res)  
assert_includes(op.errors[:username], "can’t be blank")  
assert_includes(op.errors[:password], "can’t be blank")  
end

test "username should be a valid username" do  
res, op = Confirmation::Confirm::Operation::Imported.run(  
token: @imported.confirmation_token, user: {username: "invalid username"})

assert_not(res)  
assert_includes(op.errors[:username], "is invalid")  
end

test "username should be unique" do  
res, op = Confirmation::Confirm::Operation::Imported.run(  
token: @imported.confirmation_token, user: {username: @default.username})

assert_not(res)  
assert_includes(op.errors[:username], "has already been taken")  
end

test "password should be greater than 8 characters" do  
res, op = Confirmation::Confirm::Operation::Imported.run(  
token: @imported.confirmation_token, user: {password: "abc"})

assert_not(res)  
assert_includes(op.errors[:password], "is too short (minimum is 8 characters)")  
end

test "confirm user" do  
res, op = Confirmation::Confirm::Operation::Imported.run(  
token: @imported.confirmation_token, user: {username: "username", password: "password"})

assert(res)  
assert(op.model.confirmed?)  
end  
end  

If you have been following along with this series these tests should be fairly familiar to you now.

Finally I will write a quick test for the Default class too:

class DefaultTest < TestCase  
test "confirm user" do  
res, op = Confirmation::Confirm::Operation::Default.run(token: @default.confirmation_token)

assert(res)  
assert(op.model.confirmed?)  
end  
end  

Here is that test file in full:

require "test_helper"

module Confirmation::Confirm::OperationTest  
class TestCase < ActiveSupport::TestCase  
def setup  
@default = User::Create::Operation::Default.(user: attributes_for(:user)).model  
@imported = User::Create::Operation::Imported.(user: attributes_for(:imported_user)).model  
end  
end

class BaseTest < TestCase  
test "throw exception on invalid token" do  
assert_raises ActiveRecord::RecordNotFound do  
Confirmation::Confirm::Operation::Base.run(token: "abc")  
end  
end

test "build Imported for imported user" do  
op = Confirmation::Confirm::Operation::Base.present(token: @imported.confirmation_token)

assert_instance_of(Confirmation::Confirm::Operation::Imported, op)  
end

test "build Default for default user" do  
op = Confirmation::Confirm::Operation::Base.present(token: @default.confirmation_token)

assert_instance_of(Confirmation::Confirm::Operation::Default, op)  
end  
end

class ImportedTest < TestCase  
test "require presence of username and password" do  
res, op = Confirmation::Confirm::Operation::Imported.run(  
token: @imported.confirmation_token, user: {})

assert_not(res)  
assert_includes(op.errors[:username], "can’t be blank")  
assert_includes(op.errors[:password], "can’t be blank")  
end

test "username should be a valid username" do  
res, op = Confirmation::Confirm::Operation::Imported.run(  
token: @imported.confirmation_token, user: {username: "invalid username"})

assert_not(res)  
assert_includes(op.errors[:username], "is invalid")  
end

test "username should be unique" do  
res, op = Confirmation::Confirm::Operation::Imported.run(  
token: @imported.confirmation_token, user: {username: @default.username})

assert_not(res)  
assert_includes(op.errors[:username], "has already been taken")  
end

test "password should be greater than 8 characters" do  
res, op = Confirmation::Confirm::Operation::Imported.run(  
token: @imported.confirmation_token, user: {password: "abc"})

assert_not(res)  
assert_includes(op.errors[:password], "is too short (minimum is 8 characters)")  
end

test "confirm user" do  
res, op = Confirmation::Confirm::Operation::Imported.run(  
token: @imported.confirmation_token, user: {username: "username", password: "password"})

assert(res)  
assert(op.model.confirmed?)  
end  
end

class DefaultTest < TestCase  
test "confirm user" do  
res, op = Confirmation::Confirm::Operation::Default.run(token: @default.confirmation_token)

assert(res)  
assert(op.model.confirmed?)  
end  
end  
end  

Building the View

The final piece of this puzzle is to display the correct form based upon whether the user has been imported or not. Normally this would require a messy if statement in the View, but we’re going to be using Cells so we don’t have to deal with this nastiness.

Create a new file called cell.rb in the concept directory:

module Confirmation::Confirm  
class Cell < Trailblazer::Cell  
builds do |op, options|  
op.model.imported? ? Imported : Default  
end

class Imported < Culttt::Cells::Form  
def show  
render  
end  
end

class Default < Culttt::Cells::Form  
def show  
render  
end  
end  
end  
end  

Here again we use a similar technique as to what we used in the Operation to “build” the correct implementation given the state of the user.

Both of the components are going to be forms so we the Cells implementations look pretty similar to the code from last week other than the higher level Cell that does the building.

Here are the actual Slim templates we’re going to need. First up we have the imported user form:

– if form.errors.any?  
ul  
– form.errors.full_messages.each do |msg|  
li = msg  
= form_for form, html: {class: "imported-confirmation-form"}, url: confirmation_confirm_url, method: :post do |f|  
div  
= f.label :username, class: "qa-username-label"  
= f.text_field :username, class: "qa-username-input"  
div  
= f.label :password, class: "qa-password-label"  
= f.password_field :password, class: "qa-password-input"  
div  
= hidden_field_tag :token, form.token, class: "qa-token-input"  
div  
= f.submit "Confirm", class: "qa-submit"  

As you can see, this form contains input elements for the username and password. I’ve also included the token as a hidden input field.

The default user form is pretty much the same but without the extra form elements:

– if form.errors.any?  
ul  
– form.errors.full_messages.each do |msg|  
li = msg  
= form_for form, html: {class: "default-confirmation-form"}, url: confirmation_confirm_url, method: :post do |f|  
div  
= hidden_field_tag :token, form.token, class: "qa-token-input"  
div  
= f.submit "Confirm", class: "qa-submit"  

With the Cell in place, I’ll next write some tests to make sure it’s work as I expect it to. First I will write some tests to make sure that the correct implementation is instantiated correctly:

require "test_helper"

module Confirmation::Confirm::CellTest  
class CellTest < ActiveSupport::TestCase  
def setup  
@default = User::Create::Operation::Default.(user: attributes_for(:user)).model  
@imported = User::Create::Operation::Imported.(user: attributes_for(:imported_user)).model  
end

test "build imported cell for imported user" do  
cell = Confirmation::Confirm::Cell.(  
Confirmation::Confirm::Operation::Base.present(token: @imported.confirmation_token))

assert_instance_of(Confirmation::Confirm::Cell::Imported, cell)  
end

test "build default cell for default user" do  
cell = Confirmation::Confirm::Cell.(  
Confirmation::Confirm::Operation::Base.present(token: @default.confirmation_token))

assert_instance_of(Confirmation::Confirm::Cell::Default, cell)  
end  
end  
end  

Next I will write a test to make sure the Imported cell has the correct markup:

class ImportedTest < Cell::TestCase  
controller Confirmation::ConfirmController

def setup  
@user = User::Create::Operation::Imported.(user: attributes_for(:imported_user)).model  
@operation = Confirmation::Confirm::Operation::Imported.present(token: @user.confirmation_token)  
end

test "has correct markup" do  
html = concept("confirmation/confirm/cell", @operation).()

html.must_have_selector("form.imported-confirmation-form")  
html.must_have_selector("label.qa-username-label")  
html.must_have_selector("input.qa-username-input")  
html.must_have_selector("label.qa-password-label")  
html.must_have_selector("input.qa-password-input")  
html.must_have_selector("input.qa-submit")  
end  
end  

And finally I will do the same for the Default class:

class DefaultTest < Cell::TestCase  
controller Confirmation::ConfirmController

def setup  
@user = User::Create::Operation::Default.(user: attributes_for(:user)).model  
@operation = Confirmation::Confirm::Operation::Default.present(token: @user.confirmation_token)  
end

test "has correct markup" do  
html = concept("confirmation/confirm/cell", @operation).()

html.must_have_selector("form.default-confirmation-form")  
html.wont_have_selector("label.qa-username-label")  
html.wont_have_selector("input.qa-username-input")  
html.wont_have_selector("label.qa-password-label")  
html.wont_have_selector("input.qa-password-input")  
html.must_have_selector("input.qa-submit")  
end  
end  

Here is that test class in full:

require "test_helper"

module Confirmation::Confirm::CellTest  
class CellTest < ActiveSupport::TestCase  
def setup  
@default = User::Create::Operation::Default.(user: attributes_for(:user)).model  
@imported = User::Create::Operation::Imported.(user: attributes_for(:imported_user)).model  
end

test "build imported cell for imported user" do  
cell = Confirmation::Confirm::Cell.(  
Confirmation::Confirm::Operation::Base.present(token: @imported.confirmation_token))

assert_instance_of(Confirmation::Confirm::Cell::Imported, cell)  
end

test "build default cell for default user" do  
cell = Confirmation::Confirm::Cell.(  
Confirmation::Confirm::Operation::Base.present(token: @default.confirmation_token))

assert_instance_of(Confirmation::Confirm::Cell::Default, cell)  
end  
end

class ImportedTest < Cell::TestCase  
controller Confirmation::ConfirmController

def setup  
@user = User::Create::Operation::Imported.(user: attributes_for(:imported_user)).model  
@operation = Confirmation::Confirm::Operation::Imported.present(token: @user.confirmation_token)  
end

test "has correct markup" do  
html = concept("confirmation/confirm/cell", @operation).()

html.must_have_selector("form.imported-confirmation-form")  
html.must_have_selector("label.qa-username-label")  
html.must_have_selector("input.qa-username-input")  
html.must_have_selector("label.qa-password-label")  
html.must_have_selector("input.qa-password-input")  
html.must_have_selector("input.qa-submit")  
end  
end

class DefaultTest < Cell::TestCase  
controller Confirmation::ConfirmController

def setup  
@user = User::Create::Operation::Default.(user: attributes_for(:user)).model  
@operation = Confirmation::Confirm::Operation::Default.present(token: @user.confirmation_token)  
end

test "has correct markup" do  
html = concept("confirmation/confirm/cell", @operation).()

html.must_have_selector("form.default-confirmation-form")  
html.wont_have_selector("label.qa-username-label")  
html.wont_have_selector("input.qa-username-input")  
html.wont_have_selector("label.qa-password-label")  
html.wont_have_selector("input.qa-password-input")  
html.must_have_selector("input.qa-submit")  
end  
end  
end  

Adding some Controller tests

Finally, to make sure that everything is working correctly, I will write a couple of Controller tests as verification:

require "test_helper"

module Confirmation  
class ConfirmControllerTest < ActionController::TestCase  
def setup  
@default = User::Create::Operation::Default.(user: attributes_for(:user)).model  
@imported = User::Create::Operation::Imported.(user: attributes_for(:imported_user)).model  
end

test "return 404 on invalid token" do  
assert_raises ActiveRecord::RecordNotFound do  
get :new  
end  
end

test "display imported user confirmation form" do  
get :new, token: @imported.confirmation_token

assert_response(:success)  
end

test "display default user confirmation form" do  
get :new, token: @default.confirmation_token

assert_response(:success)  
end

test "fail with invalid imported user data" do  
post :create, token: @imported.confirmation_token, user: {}

assert_response(400)  
end

test "confirm imported user" do  
post :create, token: @imported.confirmation_token, user: {username: "username", password: "password"}

assert_response(302)  
end

test "confirm default user" do  
post :create, token: @default.confirmation_token

assert_response(302)  
assert_redirected_to(login_url)  
end  
end  
end  

First I check to make sure that I’m returned a 404 when the token does not exist. Next I do a quick to make sure the form is returned correctly.

Next I check to make sure the user is redirected when attempting to confirm an imported user with invalid data. And finally I make sure that confirming both an imported user and a default user works as it should.

Conclusion

Phew, finally finished! Well done for getting this far.

This might seem like a bit of a contrived example because it is so specific to my application, but hopefully you can see how powerful the polymorphism aspect of Trailblazer’s ability to build the correct implementation of an Operation or a Cell.

Instead of having to deal with nasty conditionals we can move that logic inside of the class and let good old object-oriented programming deal with creating the class.

This provides a beautifully simple interface and it prevents that logic from slipping out into the controller or the view.