cult3

What are Trailblazer Contracts?

May 18, 2016

Table of contents:

  1. What is the Contract?
  2. Separating out the Contract
  3. Conclusion

In last week’s tutorial we looked at defining our first Operation for creating new users.

A Trailblazer Operation encapsulates a given action of the application and takes responsibility for accepting data from the request, validating that data, and creating or updating the database.

It’s often the case that the create and update actions of a given resource are similar, but not quite the same. For example, each action will typically have the same properties, and perhaps many of the same validation rules.

But the create and update actions are usually not exactly the same.

In today’s tutorial we will be taking a closer look at the Contract aspect of the Operation.

What is the Contract?

An important part of last week’s tutorial was defining the Contract of the Operation:

require 'reform/form/validation/unique_validator.rb'

class User < ActiveRecord::Base
  class Create < Trailblazer::Operation
    include Model
    model User, :create

    contract do
      property :email
      property :password, virtual: true

      validates :email, presence: true, email: true, unique: true
      validates :password, presence: true
    end
  end
end

The Contract defines the properties and validation rules of the Operation.

But what is the Contract?

If you remember back to Trailblazer - A New Architecture For Rails [Review] I mentioned that the Operation, whilst on the surface seems like it has a lot of responsibility, actually is more of a conductor for the underlying composed collaborators.

The Contract is actually Reform in disguise! We’ve previously looked at Reform in Using Form Objects in Ruby on Rails with Reform.

So the Contract functionality of your Operations is actually implemented with Reform.

Separating out the Contract

In last week’s tutorial we defined the Contract inside of the Operation. This is a perfectly acceptable way of defining the Contract as it keeps everything in one file.

However, it is also possible to define the Contract as a separate class, and then set that class as the Contract for the Operation.

I find this makes it easier to see the properties and validation rules for a set of Operations that share many of the same properties.

To separate out the Contract, first create a new file called contract.rb under the concepts/user directory:

module User::Contract
end

First I’m going to define all of the shared properties and rules in a Base class that can be extended. Ruby does not have the idea of an abstract class, but this is essentially an abstract class because I won’t be using it directly, but instead, only using the children of this class:

require 'reform/form/validation/unique_validator.rb'

module User::Contract
  class Base < Reform::Form
    model User
    property :email
    property :password, virtual: true

    validates :email, email: true, unique: true
    validates :password, length: { minimum: 8 }
  end
end

As you can see, the Base class should inherit from Reform::Form. Finally we see how the Contract is actually just an instance of Reform in disguise!

Next I can basically just cut and paste the rules from the Operation that will always apply and add them to this class.

I’ve also added a password length validation just to show that business rules that should always be satisfied should be placed in this base class.

Next I can add the Create Contract:

require 'reform/form/validation/unique_validator.rb'

module User::Contract
  class Base < Reform::Form
    model User
    property :email
    property :password, virtual: true

    validates :email, email: true, unique: true
    validates :password, length: { minimum: 8 }
  end

  class Create < Base
    validates :email, presence: true
    validates :password, presence: true
  end
end

The Create class inherits from the Base class so we do not need to define the properties and the validation rules again. In this class we can simply add the requirements that the email and password must be present.

Finally I can add the Update class:

require 'reform/form/validation/unique_validator.rb'

module User::Contract
  class Base < Reform::Form
    model User
    property :email
    property :password, virtual: true

    validates :email, email: true, unique: true
    validates :password, length: { minimum: 8 }
  end

  class Create < Base
    validates :email, presence: true
    validates :password, presence: true
  end

  class Update < Base
  end
end

In this example I haven’t added any additional validation rules for the update action, but as you can probably imagine, in a more complex scenario you might have additional validation rules that must be satisfied to update a given resource. In that case you could add these rules here.

With the Contract defined, we can now add it to the Operation like this:

require 'bcrypt'

class User < ActiveRecord::Base
  class Create < Trailblazer::Operation
    include Model
    model User, :create

    contract User::Contract::Create

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

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

As you can see, I can remove all of the Contract specific code and replace it with following line to set the Contract on the Operation:

contract User::Contract::Create

If you run the tests again you should see that they all still pass!

Conclusion

If you are already familiar with Reform, it should come as no surprise that Operation classes in Trailblazer are really powerful. Once I understood that the Contract of an Operation was Reform, the pieces really started to fall into place.

The ability to define a “contract” and use inheritance really makes it easy to build out your business rules for a particular set of actions.

This is much easier than trying to do it in a Model class, especially if the rules are slightly different for create and update actions!

Philip Brown

@philipbrown

© Yellow Flag Ltd 2024.