Validating records in Hanami
Now we have the ability to add and edit new words in our application. In this blog post we will learn how to validate records we want to update or create.
Why we have to add validation?
Now it’s possible to add a new record without a name or translation - maybe in the future we will add the possibility for adding words without translations, but for now, we want to make sure that both fields are filled.
How we validate records in Hanami
Code which responsibility is to perform validations has been factored out and lives in the gem called hanami-validations. Under the hood, it uses a great gem called dry-validation. I highly recommend to check this out - it’s one of my favorite gems and I use it even in my rails apps.
Hanami proposes a little different approach for validation than for example Rails. In Rails you initialize the object and then you check if this object is valid. In Hanami you validate the params first and only if the params are valid you create a new object. Thanks to that you avoid having objects that are in the invalid state (at least during initialization). You can read why this is a good thing in the Piotr Solnica blog post who is also the creator of the dry-validation gem.
Adding validation in Hanami
In Hanami we are validating params, so a controller is a place where we do this. To add validation to our project add this code:
params do
required(:word).schema do
required(:name).filled(:str?)
required(:translation).filled(:str?)
end
end
to both Create and Update actions.
In the above code, we define a schema and we expect that parameters passed to those actions will have key word and the key word will have keys nane and translation, which both will be strings.
Now we need to update the actions, to actually trigger the validations. Our apps/web/controllers/words/create.rb file should look like this:
module Web::Controllers::Words
class Create
include Web::Action
expose :word
params do
required(:word).schema do
required(:name).filled(:str?)
required(:translation).filled(:str?)
end
end
def call(params)
if params.valid?
@word = WordRepository.new.create(params[:word])
redirect_to '/'
else
@word = Word.new(params[:word])
self.status = 422
end
end
end
end
As we see, to validate params we need to call valid? method on params. If validation fails, we will return status code 422, which indicates that the record was incorrect and we will initialize a word object again, because we want to display the form once more with error messages.
Update action should look very similar:
module Web::Controllers::Words
class Update
include Web::Action
expose :word
params do
required(:word).schema do
required(:name).filled(:str?)
required(:translation).filled(:str?)
end
end
def call(params)
if params.valid?
@word = WordRepository.new.update(params[:id], params[:word])
redirect_to '/'
else
@word = WordRepository.new.find(params[:id])
self.status = 422
end
end
end
end
We have to also update slightly our Create and Update views - since we want to display the form again if the validation failed, we need to instruct the view which template it should use.
To do this add
template 'words/new'
to Create view and
template 'words/edit'
to Update view.
Next step will be adding the test that will reflect newly added logic. To do this lets open spec/web/controllers/words/create_spec.rb and add scenario where we pass invalid params.
require 'spec_helper'
require_relative '../../../../apps/web/controllers/words/create'
describe Web::Controllers::Words::Create do
let(:action) { Web::Controllers::Words::Create.new }
before do
WordRepository.new.clear
end
describe 'with valid params' do
let(:params) { Hash[word: { name: 'lew', translation: 'lion' }] }
it 'creates a new word' do
action.call(params)
action.word.id.wont_be_nil
action.word.name.must_equal params[:word][:name]
end
it 'redirects the user to the words listing' do
response = action.call(params)
response[0].must_equal 302
response[1]['Location'].must_equal '/'
end
end
describe 'with invalid params' do
let(:params) { Hash[word: {}] }
it 're-renders the words#new view' do
response = action.call(params)
response[0].must_equal 422
end
it 'sets errors attribute accordingly' do
response = action.call(params)
response[0].must_equal 422
action.params.errors[:word][:name].must_equal ['is missing']
action.params.errors[:word][:translation].must_equal ['is missing']
end
end
end
We’ve placed our previous tests inside with valid params descibe block and we’ve added with invalid params block.
All the test should pass at this stage.
Displaying error messages
Our validation works great now, but we would like to show to the user why adding or editing a word failed and what should be fixed. To do this we will update the partial with our form to include this code:
<% unless params.valid? %>
<div class="errors">
<h3>There was a problem with your submission</h3>
<ul>
<% params.error_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
The last part is to update our feature tests. We want to make sure that if the user will not be able to save a word he would see the error messages.
We have to update 2 files - spec/web/features/add_word_spec.rb and spec/web/feature/edit_word_spec.rb.
The first file should look like this:
require 'features_helper'
describe 'Add a word' do
before do
WordRepository.new.clear
end
it 'can create a new word' do
visit '/words/new'
within 'form#word-form' do
fill_in 'Name', with: 'lew'
fill_in 'Translation', with: 'lion'
click_button 'Create'
end
current_path.must_equal('/')
assert page.has_content?('All words')
assert page.has_content?('lion')
end
it 'displays list of errors when params contains errors' do
visit '/words/new'
within 'form#word-form' do
click_button 'Create'
end
current_path.must_equal('/words')
assert page.has_content?('There was a problem with your submission')
assert page.has_content?('Name must be filled')
assert page.has_content?('Translation must be filled')
end
end
and the second like this:
require 'features_helper'
describe 'Edit a word' do
before do
WordRepository.new.clear
@word = WordRepository.new.create(name: "lew", translation: "lion")
end
it 'can edit a word' do
visit "/words/#{@word.id}/edit"
within 'form#word-form' do
fill_in 'Name', with: 'lew_2'
fill_in 'Translation', with: 'lion_2'
click_button 'Update'
end
current_path.must_equal('/')
assert page.has_content?('All words')
assert page.has_content?('lion_2')
end
it 'displays list of errors when params contains errors' do
visit "/words/#{@word.id}/edit"
within 'form#word-form' do
fill_in 'Name', with: ''
fill_in 'Translation', with: ''
click_button 'Update'
end
current_path.must_equal("/words/#{@word.id}")
assert page.has_content?('There was a problem with your submission')
assert page.has_content?('Name must be filled')
assert page.has_content?('Translation must be filled')
end
end
As we can see, we’ve added to both files the test where parameters are invalid and we’ve made some assertion on what is visible on the page when validation failed.
Summary
Today we’ve added an important part of the application and now we can say that adding and editing words is completely finished. The last missing part from the basic functionalities we want to provide is deleting the records and this what we will be working on next.
Written on April 23, 2017