Chapter 19

Finishing Up Our Controllers

In the previous chapters, we built a set of classes to help us create representations based on the client’s needs. Now that they are all ready to serve a greater purpose, we can switch our attention back to our controllers.

We currently only have one action implemented on the books controller, and we haven’t even created the authors and publishers controllers. Well, the wait is over. By the end of this chapter, we will have all those controllers ready.

Let’s get started.

19.1. The Books Controller

The books controller, like any good Rails controller, will need to handle the basic functions of listing, showing, creating, updating and deleting books.

In this section, we are going to add actions to the books controller to handle these various behavior one at a time. Then we’ll go through the authors and publishers controller quickly, since the logic is going to be exactly the same.

19.1.1. GET /api/books

We’ve built the index action that responds to the /api/books URI in the past chapters. There’s not much to change in it. For reference, here is the current code of the BooksController class.

# app/controllers/books_controller.rb
class BooksController < ApplicationController

  def index
    books = orchestrate_query(Book.all)
    serializer = Alexandria::Serializer.new(data: books,
                                            params: params,
                                            actions: [:fields, :embeds])  
    render json: serializer.to_json
  end

end

The way we use the Alexandria::Serializer is going to be shared with the other actions we are about to define. The only thing we are going to change in the index action is extracting the serialization part in the ApplicationController class.

# app/controllers/application_controller.rb
class ApplicationController < ActionController::API

  rescue_from QueryBuilderError, with: :builder_error
  rescue_from RepresentationBuilderError, with: :builder_error

  protected

  def builder_error # Hidden Code
  def orchestrate_query  # Hidden Code

  def serialize(data)
    {
      json: Alexandria::Serializer.new(data: data,
                                       params: params,
                                       actions: [:fields, :embeds]).to_json
    }
  end

end

Then, we can just use this new method in the books controller.

# app/controllers/books_controller.rb
class BooksController < ApplicationController

  def index
    books = orchestrate_query(Book.all)
    render serialize(books)
  end

end

Two lines of code. That’s pretty clean. But did we break anything?

rspec

Success (GREEN)

...

Finished in 2.99 seconds (files took 2.53 seconds to load)
82 examples, 0 failures

19.1.2. GET /api/books/:id

With the index action fully ready, let’s move on to the show action associated with the /api/books/:id URI template.

First, we need to define our expectations by writing a few automated tests.

The show action is simple and has two possible outcomes:

  1. The resource is found and sent back by the server.
  2. The resource cannot be found and the server will return 404.

Here are the tests matching those expectations.

# spec/requests/books_spec.rb
require 'rails_helper'

RSpec.describe 'Books', type: :request do

  let(:ruby_microscope) { create(:ruby_microscope) }
  let(:rails_tutorial) { create(:ruby_on_rails_tutorial) }
  let(:agile_web_dev) { create(:agile_web_development) }
  let(:books) { [ruby_microscope, rails_tutorial, agile_web_dev] }

  describe 'GET /api/books' # Hidden Code

  describe 'GET /api/books/:id' do

    context 'with existing resource' do
      before { get "/api/books/#{rails_tutorial.id}" }

      it 'gets HTTP status 200' do
        expect(response.status).to eq 200
      end

      it 'receives the "rails_tutorial" book as JSON' do
        expected = { data: BookPresenter.new(rails_tutorial, {}).fields.embeds }
        expect(response.body).to eq(expected.to_json)
      end
    end

    context 'with nonexistent resource' do
      it 'gets HTTP status 404' do
        get '/api/books/2314323'
        expect(response.status).to eq 404
      end
    end
  end # describe 'GET /api/books/:id'

end

Try to run them.

rspec spec/requests/books_spec.rb

Failure (RED)

...

Finished in 1.86 seconds (files took 2.5 seconds to load)
31 examples, 3 failures

...

Alright, failing! Let’s fix them.

Since the “resource not found” behavior will be needed for more than the show action, we are going to extract its logic and define it as a before_action filter. But first, we have to define the resource_not_found method in the ApplicationController that will halt the request and return 404 unless the resource is found.

# app/controllers/application_controller.rb
class ApplicationController < ActionController::API

  rescue_from QueryBuilderError, with: :builder_error
  rescue_from RepresentationBuilderError, with: :builder_error
  rescue_from ActiveRecord::RecordNotFound, with: :resource_not_found

  protected

  def builder_error # Hidden Code
  def orchestrate_query  # Hidden Code
  def serialize # Hidden Code

  def resource_not_found
    render(status: 404)
  end

end

In each of our controllers, we will define a method like the one below that will return the current entity being accessed or instantiate a new one based on the passed parameters.

Note that we use find_by! to raise an ActiveRecord::RecordNotFound exception that will be caught by the ApplicationController.

def book
  @book ||= params[:id] ? Book.find_by!(id: params[:id]) : Book.new(book_params)
end

Next to it, we will also define an alias as resource that will be used later on.

def book
  @book ||= params[:id] ? Book.find_by!(id: params[:id]) : Book.new(book_params)
end
alias_method :resource, :book

Defining those methods allows us to remove duplicated code from our actions and keep only the bare minimum. Our show action, for example, will be composed of only one line of code.

def show
  render serialize(book)
end

See how it all comes together in the books controller.

# app/controllers/books_controller.rb
class BooksController < ApplicationController

  def index
    books = orchestrate_query(Book.all)
    render serialize(books)
  end

  def show
    render serialize(book)
  end

  private

  def book
    @book ||= params[:id] ? Book.find_by!(id: params[:id]) : Book.new(book_params)
  end
  alias_method :resource, :book

end

Let’s try to run the tests to see if our expectations have been met by our implementation.

rspec spec/requests/books_spec.rb

Success (GREEN)

...

Finished in 1.41 seconds (files took 2.84 seconds to load)
31 examples, 0 failures

The show action is ready!

19.1.3. POST /api/books

Retrieving books is only possible if there are books to retrieve. With this in mind, we have to implement an action (create) for the /api/books URI associated with the POST HTTP method.

To test this URI, we need to write more tests. Since the create action receives parameters from the client, we need to test two different contexts: when the client sends valid data and when it does not. Let’s focus on the former first.

# spec/requests/books_spec.rb
require 'rails_helper'

RSpec.describe 'Books', type: :request do
  # Hidden Code

  describe 'GET /api/books' do  # Hidden Code
  describe 'GET /api/books/:id' do # Hidden Code

  describe 'POST /api/books' do
    let(:author) { create(:michael_hartl) }
    before { post '/api/books', params: { data: params } }

    context 'with valid parameters' do
      let(:params) do
        attributes_for(:ruby_on_rails_tutorial, author_id: author.id)
      end

      it 'gets HTTP status 201' do
        expect(response.status).to eq 201
      end

      it 'receives the newly created resource' do
        expect(json_body['data']['title']).to eq 'Ruby on Rails Tutorial'
      end

      it 'adds a record in the database' do
        expect(Book.count).to eq 1
      end

      it 'gets the new resource location in the Location header' do
        expect(response.headers['Location']).to eq(
          "http://www.example.com/api/books/#{Book.first.id}"
        )
      end
    end
  end # describe 'POST /api/books'
end

Try to run the tests.

rspec spec/requests/books_spec.rb

Failure (RED)

...

Finished in 2 seconds (files took 2.33 seconds to load)
35 examples, 4 failures

...

To make those tests pass, we need to implement the create action with the default behavior, when everything goes well and the client sends parameters that will allow the entity to be saved.

The create action is going to use the strong parameters feature of Ruby on Rails to ensure that only allowed parameters go through.

def book_params
  params.require(:data).permit(:title, :subtitle, :isbn_10, :isbn_13,
                               :description, :released_on, :publisher_id,
                               :author_id, :cover)
end

With those filtered parameters, we can then create a new book and return it to the client with a 201 status and the location of the URI of the new resource in the Location header as specified in the HTTP RFC.

def create
  book = Book.create(book_params)
  render serialize(book).merge(status: :created, location: book)
end

It all comes together in the books controller like this.

# app/controllers/books_controller.rb
class BooksController < ApplicationController

  def index # Hidden Code
  def show  # Hidden Code

  def create
    book = Book.create(book_params)
    render serialize(book).merge(status: :created, location: book)
  end

  private

  def book # Hidden Code
  alias_method :resource, :book

  def book_params
    params.require(:data).permit(:title, :subtitle, :isbn_10, :isbn_13,
                                 :description, :released_on, :publisher_id,
                                 :author_id, :cover)
  end

end

With this code, our tests are now working.

rspec spec/requests/books_spec.rb

Success (GREEN)

...

Finished in 1.62 seconds (files took 1.86 seconds to load)
35 examples, 0 failures

But now we need to think about clients sending invalid parameters which would prevent the entities from being saved in the database - for example, not giving a title to a book.

Let’s write some tests for this context where the request sent by the client contains invalid parameters.

# spec/requests/books_spec.rb
require 'rails_helper'

RSpec.describe 'Books', type: :request do
  # Hidden Code

  describe 'GET /api/books' do  # Hidden Code
  describe 'GET /api/books/:id' do # Hidden Code

  describe 'POST /api/books' do
    let(:author) { create(:michael_hartl) }
    before { post '/api/books', params: { data: params } }

    context 'with valid parameters' # Hidden Code

    context 'with invalid parameters' do
      let(:params) { attributes_for(:ruby_on_rails_tutorial, title: '') }

      it 'gets HTTP status 422' do
        expect(response.status).to eq 422
      end

      it 'receives the error details' do
        expect(json_body['error']['invalid_params']).to eq(
          {'author'=>['must exist', "can't be blank"], 'title'=>["can't be blank"]}
        )
      end

      it 'does not add a record in the database' do
        expect(Book.count).to eq 0
      end
    end # context 'with invalid parameters'

  end

end
rspec spec/requests/books_spec.rb

Failure (RED)

...

Finished in 1.42 seconds (files took 2.45 seconds to load)
38 examples, 2 failures

...

When the server receives invalid parameters like this, it should send back a 422 Unprocessable Entity status code. Since we will be using this behavior for other controllers and actions (update), we are going to add a method in ApplicationController to deal with it.

You can see the unprocessable_entity! method below. All it does is send a JSON document back to the client with the 422 status code.

The invalid_params key contains the errors that prevent the entity from being saved.

# app/controllers/application_controller.rb
class ApplicationController < ActionController::API

  rescue_from QueryBuilderError, with: :builder_error
  rescue_from RepresentationBuilderError, with: :builder_error
  rescue_from ActiveRecord::RecordNotFound, with: :resource_not_found

  protected

  def builder_error # Hidden Code

  def unprocessable_entity!(resource)
    render status: :unprocessable_entity, json: {
      error: {
        message: "Invalid parameters for resource #{resource.class}.",
        invalid_params: resource.errors
      }
    }
  end

  def serialize # Hidden Code
  def orchestrate_query # Hidden Code
  def resource_not_found # Hidden Code

end

We just have to change the logic a tiny bit to return a 422 Unprocessable Entity status to the client. To achieve this, we need to add a check to see if the book can be saved or not. We can now use the book method we created earlier to build the book for us.

# app/controllers/books_controller.rb
class BooksController < ApplicationController

  def index # Hidden Code
  def show # Hidden Code

  def create
    if book.save
      render serialize(book).merge(status: :created, location: book)
    else
      unprocessable_entity!(book)
    end
  end

  private

  def book # Hidden Code

  def book_params
    params.require(:data).permit(:title, :subtitle, :isbn_10, :isbn_13,
                                 :description, :released_on, :publisher_id,
                                 :author_id, :cover)
  end

end

Run the tests. They should all pass without any issues.

rspec spec/requests/books_spec.rb

Success (GREEN)

...

Finished in 1.7 seconds (files took 2.22 seconds to load)
38 examples, 0 failures

19.1.4. PUT /api/books/:id

We don’t want to allow entire entities to be replaced. Instead, we prefer to only allow partial changes using the PATCH method.

19.1.5. PATCH /api/books/:id

Implementing the update action that responds to PATCH /api/books/:id is going to be very similar to the create action. We are using the same contexts actually.

Here are the tests for it.

# spec/requests/books_spec.rb
require 'rails_helper'

RSpec.describe 'Books', type: :request do
  # Hidden Code

  describe 'GET /api/books' # Hidden Code
  describe 'GET /api/books/:id' # Hidden Code
  describe 'POST /api/books' # Hidden Code

  describe 'PATCH /api/books/:id' do
    before { patch "/api/books/#{rails_tutorial.id}", params: { data: params } }

    context 'with valid parameters' do
      let(:params) { { title: 'The Ruby on Rails Tutorial' } }

      it 'gets HTTP status 200' do
        expect(response.status).to eq 200
      end

      it 'receives the updated resource' do
        expect(json_body['data']['title']).to eq(
          'The Ruby on Rails Tutorial'
        )
      end
      it 'updates the record in the database' do
        expect(Book.first.title).to eq 'The Ruby on Rails Tutorial'
      end
    end

    context 'with invalid parameters' do
      let(:params) { { title: '' } }

      it 'gets HTTP status 422' do
        expect(response.status).to eq 422
      end

      it 'receives the error details' do
        expect(json_body['error']['invalid_params']).to eq(
          { 'title'=>["can't be blank"] }
        )
      end

      it 'does not add a record in the database' do
        expect(Book.first.title).to eq 'Ruby on Rails Tutorial'
      end
    end
  end # describe 'PATCH /api/books/:id' end
end

Run the tests to see them fail.

rspec spec/requests/books_spec.rb

Failure (RED)

...

Finished in 2.41 seconds (files took 3 seconds to load)
44 examples, 6 failures

Here is the implementation in the books controller. There’s nothing much to say. We’ll try to update the specified resource, and if it fails we will return a 422 Unprocessable Entity error. If it succeeds, we can return the serialized book.

Here is the complete controller with the update action.

# app/controllers/books_controller.rb
class BooksController < ApplicationController

  def index # Hidden Code
  def show # Hidden Code
  def create # Hidden Code

  def update
    if book.update(book_params)
      render serialize(book).merge(status: :ok)
    else
      unprocessable_entity!(book)
    end
  end

  private

  def book # Hidden Code
  def book_params # Hidden Code

end

Finally, run the tests to ensure that everything is working properly.

rspec spec/requests/books_spec.rb

Success (GREEN)

...

Finished in 1.9 seconds (files took 3.07 seconds to load)
44 examples, 0 failures

19.1.6. DELETE /api/books/:id

We have two ways to implement the deletion of entities from the database. First, we can stick to the HTTP RFC and always return 204 No Content, even if the resource has already been deleted.

def destroy
  book = Book.where(id: params[:id]).first
  book.destroy if book
  render status: :no_content
end

The second option is more common. The idea is that trying to delete a nonexistent resource should result in a 404 Not Found status code.

def destroy
  Book.find_by!(id: params[:id]).destroy
  render status: :no_content
end

For this API, I’ve decided (arbitrarily) to go with the second option, so let’s write the tests for it.

# spec/requests/books_spec.rb
require 'rails_helper'

RSpec.describe 'Books', type: :request do
  # Hidden Code

  describe 'GET /api/books' # Hidden Code
  describe 'GET /api/books/:id' # Hidden Code
  describe 'POST /api/books' # Hidden Code
  describe 'PATCH /api/books/:id' # Hidden Code

  describe 'DELETE /api/books/:id' do
    context 'with existing resource' do
      before { delete "/api/books/#{rails_tutorial.id}" }

      it 'gets HTTP status 204' do
        expect(response.status).to eq 204
      end

      it 'deletes the book from the database' do
        expect(Book.count).to eq 0
      end
    end

    context 'with nonexistent resource' do
      it 'gets HTTP status 404' do
        delete '/api/books/2314323'
        expect(response.status).to eq 404
      end
    end
  end # describe 'DELETE /api/books/:id' end
end

Run the tests.

rspec spec/requests/books_spec.rb

Failure (RED)

...

Finished in 1.48 seconds (files took 2.37 seconds to load)
47 examples, 3 failures

...

Only two things to change. First, we need to run resource_not_found before the destroy action, so let’s change the list of actions to [:show, :update, :destroy]. The second change is the destroy action itself, which we’ve already talked about. There is nothing else to add here - the two lines composing it are pretty simple.

# app/controllers/books_controller.rb
class BooksController < ApplicationController

  def index # Hidden Code
  def show # Hidden Code
  def create # Hidden Code
  def update # Hidden Code

  def destroy
    book.destroy
    render status: :no_content
  end

  private

  def book # Hidden Code
  def book_params # Hidden Code
end

Run the tests to check that they are all passing.

rspec spec/requests/books_spec.rb

Success (GREEN)

...

Finished in 1.57 seconds (files took 2.94 seconds to load)
47 examples, 0 failures

19.2. The Authors Controller

Thanks to our preparation in the past chapters, the authors and publishers controllers are going to be super simple to implement. I’m actually not going to give you the tests this time - I’d like you to try writing them instead.

You don’t have to test everything but if you decide to write some tests, that would be awesome! I’m still going to give you the authors_spec file with the main context and you should be able to fill them as you see fit.

Note that if you purchased the medium or complete package, you have all the tests in the solutions folder.

First, let’s create the files.

touch app/controllers/authors_controller.rb \
      spec/requests/authors_spec.rb

We also need to add the routes or else the Rails router won’t match the URIs with our controller.

# config/routes.rb
Rails.application.routes.draw do
  scope :api do
    resources :books
    resources :authors
  end
end

And now, well, it’s time for you to write some tests ;). Below you will find the authors_spec file with all the contexts you need. These follow the same logic we implemented for the books controller tests, so don’t expect anything new. You just need to adapt the logic to make it work for authors. I’ve already prepared a bunch of let to help you get started.

# spec/requests/authors_spec.rb
require 'rails_helper'

RSpec.describe 'Authors', type: :request do

  let(:pat) { create(:author) }
  let(:michael) { create(:michael_hartl) }
  let(:sam) { create(:sam_ruby) }
  let(:authors) { [pat, michael, sam] }

  describe 'GET /api/authors' do    
    context 'default behavior' do
    end

    describe 'field picking' do
      context 'with the fields parameter' do
      end
      context 'without the field parameter' do
      end
      context 'with invalid field name "fid"' do
      end

    end

    describe 'pagination' do
      context 'when asking for the first page' do
      end
      context 'when asking for the second page' do
      end
      context 'when sending invalid "page" and "per" parameters' do
      end
    end

    describe 'sorting' do
      context 'with valid column name "id"' do
      end
      context 'with invalid column name "fid"' do
      end
    end

    describe 'filtering' do
      context 'with valid filtering param "q[given_name_cont]=Pat"' do
      end
      context 'with invalid filtering param "q[fgiven_name_cont]=Pat"' do
      end
    end

  end

  describe 'GET /api/authors/:id' do
    context 'with existing resource' do
    end
    context 'with nonexistent resource' do
    end
  end

  describe 'POST /api/authors' do
    context 'with valid parameters' do
    end
    context 'with invalid parameters' do
    end
  end

  describe 'PATCH /api/authors/:id' do
    context 'with valid parameters' do
    end
    context 'with invalid parameters' do
    end
  end

  describe 'DELETE /api/authors/:id' do
    context 'with existing resource' do
    end
    context 'with nonexistent resource' do
    end
  end

end

Now let’s see the AuthorsController implementation. There shouldn’t be anything surprising in there, as it works exactly like the books controller. We can even create an intermediary controller from which BooksController, AuthorsController and PublishersController could inherit. I’ll let you do that if you want to - it’s not really complicated.

# app/controllers/authors_controller.rb
class AuthorsController < ApplicationController

  def index
    authors = orchestrate_query(Author.all)
    render serialize(authors)
  end

  def show
    render serialize(author)
  end

  def create
    if author.save
      render serialize(author).merge(status: :created, location: author)
    else
      unprocessable_entity!(author)
    end
  end

  def update
    if author.update(author_params)
      render serialize(author).merge(status: :ok)
    else
      unprocessable_entity!(author)
    end
  end

  def destroy
    author.destroy
    render status: :no_content
  end

  private

  def author
    @author ||= params[:id] ? Author.find_by!(id: params[:id]) :
                              Author.new(author_params)
  end
  alias_method :resource, :author

  def author_params
    params.require(:data).permit(:given_name, :family_name)
  end

end

If you wrote the tests, run them to check that everything is working.

rspec spec/requests/authors_spec.rb

Success (GREEN)

...

Finished in 1.09 seconds (files took 2.76 seconds to load)
43 examples, 0 failures

19.3. The Publishers Controller

Implementing the publishers controller is going to be exactly like the authors controller. That’s why we will go through this section pretty fast.

First, create the files.

touch app/controllers/publishers_controller.rb \
      spec/requests/publishers_spec.rb

Add the publishers routes.

# config/routes.rb
Rails.application.routes.draw do
  scope :api do
    resources :books
    resources :authors
    resources :publishers
  end
end

To write some meaningful tests for the publishers controller, let’s add more factories.

# spec/factories/publishers.rb
FactoryBot.define do
  factory :publisher do
    name { "O'Reilly" }
  end

  factory :dev_media, class: Publisher do
    name { 'Dev Media' }
  end

  factory :super_books, class: Publisher do
    name { 'Super Books' }
  end
end

Now, it’s your time to shine (again). Write tests for the publishers controller. Here is a skeleton to get you started.

# spec/requests/publishers_spec.rb
require 'rails_helper'

RSpec.describe 'Publishers', type: :request do

  let(:oreilly) { create(:publisher) }
  let(:dev_media) { create(:dev_media) }
  let(:super_books) { create(:super_books) }
  let(:publishers) { [oreilly, dev_media, super_books] }

  describe 'GET /api/publishers' do
    context 'default behavior' do
    end

    describe 'field picking' do
      context 'with the fields parameter' do
      end
      context 'without the field parameter' do
      end
      context 'with invalid field name "fid"' do
      end
    end

    describe 'pagination' do
      context 'when asking for the first page' do
      end
      context 'when asking for the second page' do
      end
      context 'when sending invalid "page" and "per" parameters' do
      end
    end

    describe 'sorting' do
      context 'with valid column name "id"' do
      end
      context 'with invalid column name "fid"' do
      end
    end

    describe 'filtering' do
      context 'with valid filtering param "q[name_cont]=Reilly"' do
      end
      context 'with invalid filtering param "q[fname_cont]=Reilly"' do
      end
    end
  end

  describe 'GET /api/publishers/:id' do
    context 'with existing resource' do
    end
    context 'with nonexistent resource' do
    end
  end

  describe 'POST /api/publishers' do
    context 'with valid parameters' do
    end
    context 'with invalid parameters' do
    end
  end

  describe 'PATCH /api/publishers/:id' do
    context 'with valid parameters' do
    end
    context 'with invalid parameters' do
    end
  end

  describe 'DELETE /api/publishers/:id' do
    context 'with existing resource' do
    end
    context 'with nonexistent resource' do
    end
  end
end

And here is the PublishersController implementation.

# app/controllers/publishers_controller.rb
class PublishersController < ApplicationController

  def index
    publishers = orchestrate_query(Publisher.all)
    render serialize(publishers)
  end

  def show
    render serialize(publisher)
  end

  def create
    publisher = Publisher.new(publisher_params)

    if publisher.save
      render serialize(publisher).merge(status: :created, location: publisher)
    else
      unprocessable_entity!(publisher)
    end
  end

  def update
    if publisher.update(publisher_params)
      render serialize(publisher).merge(status: :ok)
    else
      unprocessable_entity!(publisher)
    end
  end

  def destroy
    publisher.destroy
    render status: :no_content
  end

  private

  def publisher
    @publisher ||= params[:id] ? Publisher.find_by!(id: params[:id]) :
                                 Publisher.new(publisher_params)
  end
  alias_method :resource, :publisher

  def publisher_params
    params.require(:data).permit(:name)
  end

end

If you wrote the tests, run them to check that everything is working.

rspec spec/requests/publishers_spec.rb

Success (GREEN)

Finished in 0.88656 seconds (files took 3.43 seconds to load)
43 examples, 0 failures

And that’s it! We now have three functional controllers in our API. Well done!

19.4. Uploading Book Covers

We can now create or update books. In an earlier chapter, we defined a process to upload pictures from anywhere without using a form; this was to make it easier for different clients to upload images. Now, it’s time to test our implementation.

Receiving images encoded in base64 means we will have huge strings of characters in the parameters. That’s not a problem, but I’d rather not have the logs spammed with those base64 encoded images. To avoid that, we just need to add the cover parameter to the list of parameters that must be filtered in the logs, just like the passwords.

To do this, add config.filter_parameters += [:cover] to the config/application.rb file.

# config/application.rb
require_relative 'boot'

require "rails"
require "active_model/railtie"
require "active_job/railtie"
require "active_record/railtie"
require "active_storage/engine"
require "action_controller/railtie"
require "action_mailer/railtie"
require "action_view/railtie"
require "action_cable/engine"
require "rails/test_unit/railtie"

require 'carrierwave'
require 'carrierwave/orm/activerecord'

Bundler.require(*Rails.groups)

module Alexandria
  class Application < Rails::Application
    config.load_defaults 5.2
    config.api_only = true
    config.filter_parameters += [:cover]
  end
end

We can use curl to upload a cover to a book. With the book, you should have received a folder named images in the resources archive. Copy this folder into your application folder and run the following curl request from inside your API folder to upload the cover image.

Start the server with rails s if it’s not already running.

Note that, when using carrierwave-base64, the base64 string needs to start with data:image/jpg;base64.

(echo -n '{"data":{"cover": "data:image/jpeg;base64,'; \
  base64 images/cover.jpg; echo '"}}') |
curl -X PATCH -H "Content-Type: application/json" -d @- \
  http://localhost:3000/api/books/1

Output

{
  "data":{
    "id":1,
    "title":"Ruby Under a Microscope",
    "subtitle":"An Illustrated Guide to Ruby Internals",
    "isbn_10":"1593275617",
    "isbn_13":"9781593275617",
    "description":"Ruby Under a Microscope is a cool book!",
    "released_on":"2013-09-01",
    "publisher_id":1,"author_id":1,
    "created_at":"2016-06-04T16:45:02.057Z",
    "updated_at":"2016-06-06T09:31:28.969Z",
    "cover":"/uploads/book/cover/1/cover.jpeg"
  }
}

If we checkout the cover path appended to the base url manually in a browser, we get the cover we just uploaded back as seen in Figure 1.

https://s3.amazonaws.com/devblast-mrwa-book/images/figures/19/01
Figure 1

It’s working! But we shouldn’t be satisfied just yet. Instead of only giving the path of the cover to the client, why not take one more step toward HATEOAS and give full URLs?

We can use the Rails.application.routes.url_helpers module to get the root URL. Include it in the BasePresenter class:

# app/presenters/base_presenter.rb
class BasePresenter
  include Rails.application.routes.url_helpers

  # Hidden Code
end

To be able to use the root_url method provided by Rails, we need to define the root in the routes files.

# config/routes.rb
Rails.application.routes.draw do
  scope :api do
    resources :books
    resources :authors
    resources :publishers
  end

  root to: 'books#index'
end

We also need to add some configuration defining what the base host URL is in the test and development environments.

# config/environments/test.rb
Rails.application.configure do
  # Hidden Code
  default_url_options[:host] = 'localhost:3000'
end
# config/environments/development.rb
Rails.application.configure do
  # Hidden Code
  default_url_options[:host] = 'localhost:3000'
end

Restart the server with CTRL-C and rails s to get these changes loaded.

Next, we can update the cover method in the book presenter to use the root_url.

# app/presenters/book_presenter.rb
class BookPresenter < BasePresenter
  build_with    :id, :title, :subtitle, :isbn_10, :isbn_13, :description,
                :released_on, :publisher_id, :author_id, :created_at, :updated_at,
                :cover
  related_to    :publisher, :author
  sort_by       :id, :title, :released_on, :created_at, :updated_at
  filter_by     :id, :title, :isbn_10, :isbn_13, :released_on, :publisher_id,
                :author_id

  def cover
    path = @object.cover.url.to_s
    path[0] = '' if path[0] == '/'
    "#{root_url}#{path}"
  end

end

Let’s see how the representation looks like now.

curl http://localhost:3000/api/books/1
{
  "data":{
    "id":1,
    "title":"Ruby Under a Microscope",
    "subtitle":"An Illustrated Guide to Ruby Internals",
    "isbn_10":"1593275617",
    "isbn_13":"9781593275617",
    "description":"Ruby Under a Microscope is a cool book!",
    "released_on":"2013-09-01",
    "publisher_id":1,
    "author_id":1,
    "created_at":"2016-06-04T16:45:02.057Z",
    "updated_at":"2016-06-06T09:31:28.969Z",
    "cover":"http://localhost:3000/uploads/book/cover/1/file.jpg"
  }
}

We are now getting the full URL which makes it easier for our clients, and removes the need for them to build the URL themselves.

Let’s not forget to add /public/uploads to the .gitignore file to avoid having our uploads being added to Git. Let’s also include the images/ folder we moved inside the project that only contains our sample cover; we don’t need to version that.

# Ignore bundler config.
/.bundle

# Ignore the default SQLite database.
/db/*.sqlite3
/db/*.sqlite3-journal

# Ignore all logfiles and tempfiles.
/log/*
/tmp/*
!/log/.keep
!/tmp/.keep

# Ignore Byebug command history file.
.byebug_history

/public/uploads
/images

19.5. Pushing Our Changes

Let’s run the tests first before pushing our changes.

rspec

Success (GREEN)

...

Finished in 6.35 seconds (files took 2.23 seconds to load)
189 examples, 0 failures

Great! Here is the list of steps to push the code.

Check the changes.

git status

Output

On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	modified:   app/controllers/application_controller.rb
	modified:   app/controllers/books_controller.rb
	modified:   config/routes.rb
	modified:   spec/factories/publishers.rb
	modified:   spec/requests/books_spec.rb
  modified:   app/presenters/base_presenter.rb
  modified:   app/presenters/book_presenter.rb
  modified:   config/application.rb
  modified:   config/environments/development.rb
  modified:   config/environments/test.rb

Untracked files:
  (use "git add <file>..." to include in what will be committed)

	app/controllers/authors_controller.rb
	app/controllers/publishers_controller.rb
	spec/requests/authors_spec.rb
	spec/requests/publishers_spec.rb

no changes added to commit (use "git add" and/or "git commit -a")

Stage them.

git add .

Commit the changes.

git commit -m "Implement Books, Authors and Publishers controllers"

Output

[master 88a387b] Implement Books, Authors and Publishers controllers
 15 files changed, 916 insertions(+), 14 deletions(-)
 create mode 100644 app/controllers/authors_controller.rb
 rewrite app/controllers/books_controller.rb (60%)
 create mode 100644 app/controllers/publishers_controller.rb
 create mode 100644 spec/requests/authors_spec.rb
 create mode 100644 spec/requests/publishers_spec.rb

Push to GitHub.

git push origin master

19.6. Wrap Up

In this chapter, we learned how to implement controllers using the tools we built in the previous chapters.

In the next chapter, we will implement a feature that any e-commerce website needs: a way to search for any product using any string of characters. Essentially, we’ll implement full-text search.