Rails Valication Context, Decorators, Form Objects, and Ecto Changesets

When programming a web application, we often need to write validations to make sure business rules are satisfied, and integrity of the domain models is ensured. In Rails, it is easy to add these validations using the nice DSLs provided in the framework.

class User < ActiveRecord::Base
  validates_length_of :username, minimum: 6
end

The validation ensures that when our users sign up, their username cannot be shorter than six characters. Easy enough. Now let's say our business rules also state that an admin (from an Admin model) can update the username of a user to be shorter than six characters. We can achieve that by using a virtual attribute like this:

class User < ActiveRecord::Base
  attr_accessor: :edited_by_admin
  validates_length_of :username, minimum: 6, unless: -> (u) { u.edited_by_admin? }
  validates_length_of :username, minimum: 1, if:     -> (u) { u.edited_by_admin? }
end
class Admin::UsersController
  def edit
    @user = User.find(params[:id])
    @user.attributes = user_attributes
    @user.edited_by_admin = true
    if @user.save
      # ...
    else
      # ...
    end
  end
end

This works, but it's ugly in that we had to introduce an additional attribute into the picture. It turns out that Rails have provided Validation Context feature for this kind of situation (sort of).

Rails Validation Context

We would write validations like this:

class User < ActiveRecord::Base
  validates_length_of :username, minimum: 6, on: :user
  validates_length_of :username, minimum: 1, on: :admin
end

So when an admin saves a user, we add a context parameter to the save method.

class Admin::UsersController
  def edit
    @user = User.find(params[:id])
    @user.attributes = user_attributes
    if @user.save(context: :admin)
      # ...
    else
      # ...
    end
  end
end

While this approach is better than the first one, it quickly goes out of control when our business rules require us to add other validations.

validate :something, on: :create

The validation above won't get triggered from @user.save(context: :admin) call because the :created context is replaced by :admin context. It gets more complicated when we try hacking our way to make it work. We need something different.

Decorators

Decorators are good. They are compositional. They allow us to break down complexity using layers of simple objects. Ruby provides SimpleDelegator to easily create decorators. So let's see how it works.

# app/models/user.rb
class User < ActiveRecord::Base
  validate :something, on: :create
end

# app/models/admin_edited_user.rb
class AdminEditedUser < SimpleDelegator
  include ActiveModel::Validations

  validates_length_of :username, minimum: 1

  def save
    super if valid?
  end
end

# app/models/user_edited_user.rb
class UserEditedUser < SimpleDelegator
  include ActiveModel::Validations

  validates_length_of :username, minimum: 6

  def save
    super if valid?
  end
end

Here is how we use these decorators in a controller.

# in Admins::UsersController
@user = User.find(params[:id])
@user = AdminEditedUser.new(@user)

# in Users::UsersController
@user = User.find(params[:id])
@user = UserEditedUser.new(@user)

This code solves the problems we had with its predecessors. It is also flexible, allowing us to run any custom validations by introducing new decorators. It is also verbose and there are a few caveats as explained in this well written blog post. However, something just doesn't seem right to me. We've added a special user class that is only for the purpose of running some other validations when an admin is editing a user. We need something lighter.

Form Object

A form object is used to extract form-behavior out from the model into its own class. It sounds like a right fit for our problem. Let's see how it works.

class AdminEditUserForm
  include ActiveModel::Model

  attr_accessor :username
  validates_length_of :username, minimum: 1

  def initialize(user)
    @user = user
  end

  def submit(params)
    self.username = params[:username]
    if valid?
      @user.username = username
      @user.save(validate: false)
      true
    else
      false
    end
  end

  def persisted?
    false
  end
end

Then in the Admins::UsersController

@admin_edit_user_form = AdminEditUserForm.new(user)
@admin_edit_user_form.submit(params[:admin_edit_user_form])

This code is a bit more verbose than the decorator approach, but we have moved the responsibility to run validations to a form object. To add an additional validation is just a one liner of Rails validation DSL. Overall, I like this approach, albeit I don't like these lines.

# Need to explicitly assign username again
@user.username = username

# Need to bypass whatever validations are set for User class
@user.save(validate: false)

# Needed so form in view behaves
def persisted?
  false
end

They are distractions. Is there something better?

Introduce Ecto.Changeset

Ecto.Changeset is from the Ecto library. It's in Elixir language but the concept can be applied elsewhere. Similar to form objects, instead of coupling validations with models, we move validations to Changesets.

Here is a definition of Ecto.Changeset from the Programming Phoenix book.

Ecto also has a feature called changesets that holds all changes you want to perform on the database. It encapsulates the whole process of receiving external data, casting and validating it before writing it to the database.

Let's see how Changeset works

defmodule App.User do
  use App.Web, :model

  schema "users" do
    field :username, :string
    timestamps
  end

  def changeset(model, params \\ :empty) do
    model
    |> cast(params, ~w(), ~w())
    |> validate_length(:username, min: 6)
  end

  def admin_user_changeset(model, params \\ :empty) do
    model
    |> cast(params, ~w(), ~w())
    |> validate_length(:username, min: 1)
  end
end

Notice the two methods (names end with _changeset). They look like instance methods in Ruby, but they are class level methods. Simply put, In Elixir, there are no instance methods since there are no objects. In fact, I could have defined these two methods somewhere else. Both methods also return a changeset struct that will be used by Repo module to actually execute database operations.

Remember I mentioned that we run Changesets against a model? So when a user updates her username, we use User.changeset like this:

User.changeset(user, %{username: "batman"}) |> Repo.update

On the other hand, when an admin updates a user's username, we use User.admin_user_changeset method.

User.admin_user_changeset(user, %{username: "abc"}) |> Repo.update

That's it! If we want to add custom validations we can just add the validation in changesets accordingly.

# ...

def changeset(model, params \\ :empty) do
  model
  |> cast(params, ~w(), ~w())
  |> validate_length(:username, min: 6)
  |> validate_something_else
end

def admin_user_changeset(model, params \\ :empty) do
  model
  |> cast(params, ~w(), ~w())
  |> validate_length(:username, min: 1)
  |> validate_something_else
end

# ...

It's elegantly simple! The Changeset approach really shines because Ecto has made an effort to separate many responsibilities from models (unlike ActiveRecord). I expect to see something similar to this in Ruby, because the Rails communities have no short of great developers and there is already ember-changeset for EmberJS.

Loading comments...