Inputs

Inputs are objects that represent user-entered data. They are populated with information that is available for modification, such as in HTML forms or in API payloads, and they are passed on to Repositories as arguments for persistence operations, as provided by the app user. Inputs have knowledge about which attributes should be present in a payload, among other constraints, and are able to tell if its own state is valid or not.

It is important to note that Inputs differ from Records for not representing domain entities, but simply data entered by the user. Inputs do not have numeric identifiers, for example, as these are generated by the system and not set by users. There are also no strong expectations in regards to data integrity for inputs, since user-entered data can contain any information of different types, or even not to be present at all.

User input validation is a core part of any app’s business logic. It ensures that incoming data is sane, proper, and respects a predefined schema. A default Rails app overloads Record objects with yet another responsibility: being the place where validation rules are written and checked. While there is value in making sure that database constraints are respected, input validation should happen as part of the business logic layer, before persistence is invoked with invalid input. Input objects are a great fit for that task. By leveraging validation utilities from Active Model, Input objects can not only perform the same validations as Records but also seamlessly integrate with view helpers such as Rails form builders.

Let’s change the existing Blog code to use Inputs. We create the Article Input that can hold title and body, and make these the actual argument for the create and update methods in the Article Repository. We will also move the validation rules currently present in the Blog’s Article Record all the way to the Input object.

# app/inputs/article_input.rb

class ArticleInput
  include ActiveModel::Model

  attr_accessor :title, :body

  validates :title, presence: true
  validates :body, presence: true, length: { minimum: 10 }
end
class ArticleRecord < ApplicationRecord
  self.table_name = 'articles'
end
class ArticleRepository
  def all
    ArticleRecord.all
  end

  def create(input)
    ArticleRecord.create!(title: input.title, body: input.body)
  end

  def find(id)
    ArticleRecord.find(id)
  end

  def update(id, input)
    record = find(id)
    record.update!(title: input.title, body: input.body)
    record
  end

  def delete(id)
    record = find(id)
    record.destroy!
  end
end

We can now replace the cases in which empty Article Records are used in the controller and views with the Article Input. These are also the methods that hold errors via Active Model’s Errors.

class ArticlesController < ApplicationController
  # GET /articles
  def index
    @articles = ArticleRepository.new.all
  end

  # GET /articles/1
  def show
    @article = ArticleRepository.new.find(params[:id])
  end

  # GET /articles/new
  def new
    @input = ArticleInput.new
  end

  # GET /articles/1/edit
  def edit
    article = ArticleRepository.new.find(params[:id])
    @input = ArticleInput.new(title: article.title, body: article.body)
  end

  # POST /articles
  def create
    @input = ArticleInput.new(article_params)

    if @input.valid?
      article = ArticleRepository.new.create(@input)
      redirect_to article_path(article.id), notice: 'Article was successfully created.'
    else
      render :new
    end
  end

  # PATCH/PUT /articles/1
  def update
    @input = ArticleInput.new(article_params)

    if @input.valid?
      article = ArticleRepository.new.update(params[:id], @input)
      redirect_to article_path(article.id), notice: 'Article was successfully updated.'
    else
      render :edit
    end
  end

  # DELETE /articles/1
  def destroy
    ArticleRepository.new.delete(params[:id])
    redirect_to articles_url, notice: 'Article was successfully destroyed.'
  end

  private

  # Only allow a list of trusted parameters through.
  def article_params
    params.require(:article_input).permit(:title, :body)
  end
end
<h1>New Article</h1>

<%= form_with(model: @input, url: articles_path) do |form| %>
  <% if @input.errors.present? %>
    <div id="error_explanation">
      <h2><%= pluralize(@input.errors.count, "error") %> prohibited this article from being saved:</h2>

      <ul>
        <% @input.errors.each do |error| %>
          <li><%= error.full_message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= form.label :title %>
    <%= form.text_field :title %>
  </div>

  <div class="field">
    <%= form.label :body %>
    <%= form.text_area :body %>
  </div>

  <div class="actions">
    <%= form.submit 'Create' %>
  </div>
<% end %>

<%= link_to 'Back', articles_path %>