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 %>