RottenSoftware

Random thoughs about software development

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
>