Chapter 18

Representation Builders

This chapter is all about the representation builders. This is the last chapter about builders; in the next one, we will use all the tools we’ve created to build our controllers super fast.

18.1. Updating the FieldPicker

Before we build any new representation builders, we are going to update the FieldPicker. Since we want all the builders to follow the same logic to avoid confusing the clients, the FieldPicker should raise an error when invalid fields are provided, instead of just removing them as it’s currently doing.

18.1.1. Refactoring the FieldPicker

Let’s write a test showcasing this expected behavior. I included the entire file because we also need to change our previous tests a tiny bit.

# spec/representation_builders/field_picker_spec.rb
require 'rails_helper'

RSpec.describe 'FieldPicker' do
  let(:rails_tutorial) { create(:ruby_on_rails_tutorial) }

  # We remove 'subtitle' here
  let(:params) { { fields: 'id,title' } }
  let(:presenter) { BookPresenter.new(rails_tutorial, params) }
  let(:field_picker) { FieldPicker.new(presenter) }

  before do
    allow(BookPresenter).to(
      receive(:build_attributes).and_return(['id', 'title', 'author_id'])
    )
  end

  describe '#pick' do

    # And here, in the test title
    context 'with the "fields" parameter containing "id,title"' do

      it 'updates the presenter "data" with the book "id" and "title"' do
        expect(field_picker.pick.data).to eq({
          'id'    => rails_tutorial.id,
          'title' => 'Ruby on Rails Tutorial'
        })
      end

      context 'with overriding method defined in presenter' do
        before { presenter.class.send(:define_method, :title) { 'Overridden!' } }

        it 'updates the presenter "data" with the title "Overridden!"' do
          expect(field_picker.pick.data).to eq({
            'id' => rails_tutorial.id,
            'title' => 'Overridden!'
          })
        end

        after { presenter.class.send(:remove_method, :title) }
      end
    end

    context 'with no "fields" parameter' do
      let(:params) { {} }

      it 'updates "data" with the fields ("id","title","author_id")' do
        expect(field_picker.pick.data).to eq({
          'id' => rails_tutorial.id,
          'title' => 'Ruby on Rails Tutorial',
          'author_id' => rails_tutorial.author.id
        })
      end
    end

    context 'with invalid attributes "fid"' do
      let(:params) { { fields: 'fid,title' } }

      it 'raises a "RepresentationBuilderError"' do
        expect { field_picker.pick }.to(
          raise_error(RepresentationBuilderError))
      end
    end
  end
end

Run the tests.

rspec spec/representation_builders/field_picker_spec.rb

Failure (RED)

...

Finished in 0.38694 seconds (files took 2.42 seconds to load)
4 examples, 1 failure

18.1.2. Adding a New Error: RepresentationBuilderError

Since we expect a RepresentationBuilderError, we need to create a new error class in our API.

touch app/errors/representation_builder_error.rb

The code is pretty similar to the QueryBuilderError class.

# app/errors/representation_builder_error.rb
class RepresentationBuilderError < StandardError
  attr_accessor :invalid_params

  def initialize(invalid_params)
    @invalid_params = invalid_params
    super
  end
end

Finally, we need to change the logic in the FieldPicker class. Instead of just removing unknown fields, we need to raise an exception any time one is passed. To do this, we need to do a bit of refactoring.

# app/representation_builders/field_picker.rb
class FieldPicker

  def initialize(presenter)
    @presenter = presenter
  end

  def pick
    build_fields
    @presenter
  end

  # We will need to access the fields from outside later on
  def fields
    @fields ||= validate_fields
  end

  private

  # We check each field to see if it's included in the
  # build_attributes array of the given presenter.
  def validate_fields
    return pickable if @presenter.params[:fields].blank?

    fields = if !@presenter.params[:fields].blank?
      @presenter.params[:fields].split(',')
    else
      []
    end

    return pickable if fields.blank?

    fields.each do |field|
      error!(field) unless pickable.include?(field)
    end

    fields
  end

  def build_fields
    fields.each do |field|
      target = @presenter.respond_to?(field) ? @presenter : @presenter.object
      @presenter.data[field] = target.send(field) if target
    end
  end

  def error!(field)
    build_attributes = @presenter.class.build_attributes.join(',')
    raise RepresentationBuilderError.new("fields=#{field}"),
    "Invalid Field Pick. Allowed field: (#{build_attributes})"
  end

  def pickable
    @pickable ||= @presenter.class.build_attributes
  end

end

This refactoring should let our tests pass without any issue.

rspec spec/representation_builders/field_picker_spec.rb

Success (GREEN)

...

FieldPicker
  #pick
    with the "fields" parameter containing "id,title"
      updates the presenter "data" with the book "id" and "title"
      with overriding method defined in presenter
        updates the presenter "data" with the title "Overridden!"
    with no "fields" parameter
      updates "data" with the fields ("id","title","author_id") in presenter
    with invalid attributes "fid"
      raises a "RepresentationBuilderError"

Finished in 0.3367 seconds (files took 2.08 seconds to load)
4 examples, 0 failures

Great! On to the books controller tests.

18.1.3. Updating the Controllers

We need to add tests checking the failure when an invalid field is requested. It’s very similar to what we implemented for the query builders, so you shouldn’t find any surprises in there.

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

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

  describe 'GET /api/books' do
    before { books }

    context 'default behavior' # Hidden Code

    describe 'field picking' do
      context 'with the fields parameter'  # Hidden Code
      context 'without the field parameter' # Hidden Code

      context 'with invalid field name "fid"' do
        before { get '/api/books?fields=fid,title,author_id' }

        it 'gets "400 Bad Request" back' do
          expect(response.status).to eq 400
        end

        it 'receives an error' do
          expect(json_body['error']).to_not be nil
        end

        it 'receives "fields=fid" as an invalid param' do
          expect(json_body['error']['invalid_params']).to eq 'fields=fid'
        end
      end # context "with invalid field name 'fid'" end
    end # End of describe 'field picking'

    describe 'pagination' # Hidden Code
    describe 'sorting' # Hidden Code
    describe 'filtering' # Hidden Code

  end
end

How is the books controller currently handling the exception?

rspec spec/requests/books_spec.rb

Failure (RED)

...

Finished in 2.22 seconds (files took 2.48 seconds to load)
24 examples, 3 failures

...

Not well, apparently! Let’s refactor the ApplicationController class. First, we will rename the query_builder_error method to builder_error and it will become a shared method once we add this line:

rescue_from RepresentationBuilderError, with: :builder_error

Now we can see it in action. Notice how we added type: error.class to let the client know what kind of exception it’s receiving.

# 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(error)
    render status: 400, json: {
      error: {
        type: error.class,
        message: error.message,
        invalid_params: error.invalid_params
      }
    }
  end

  def filter  # Hidden Code
  def sort  # Hidden Code
  def paginate  # Hidden Code
  def current_url # Hidden Code
end

All is well in the world of tests now.

rspec spec/requests/books_spec.rb

Success (GREEN)

...

Finished in 2.35 seconds (files took 2.68 seconds to load)
24 examples, 0 failures

The updated FieldPicker is now working properly. Well done!

18.2. EmbedPicker

Next up is the EmbedPicker - we mentioned it earlier. This builder will allow a client to specify when it wants to have a related resource embedded in the JSON document of the current resource. Take a look at the examples below.

Without embedding the author:

http://localhost:3000/api/books

{
  "data": [
    {
      "id": 1,
      "title": "Modular Rails",
      "author_id": 1
    },
    {},
    {}
  ]
}

With the author entity embedded:

http://localhost:3000/api/books?embed=author

{
  "data": [
    {
      "id": 1,
      "title": "Modular Rails",
      "author": {
        "id": 1,
        "given_name": "Thibault",
        "family_name": "Denizet"
      }
    },
    {},
    {}
  ]
}

Got the idea? Alright, let’s proceed. First, we need more files. Create them manually or with the command below.

18.2.1. Implementing the EmbedPicker

Create two new files…

touch app/representation_builders/embed_picker.rb \
      spec/representation_builders/embed_picker_spec.rb

and put this code in the embed_picker.rb file we just created.

# app/representation_builders/embed_picker.rb
class EmbedPicker
end

Now we are getting to your favorite part: writing tests! We follow the same pattern as before with three contexts:

  • Without any parameter for this builder
  • With a valid parameter (embed=author)
  • With an invalid parameter (embed=something)

Here are the implementations of all the tests for the EmbedPicker class.

# spec/representation_builders/embed_picker_spec.rb
require 'rails_helper'

RSpec.describe 'EmbedPicker' do
  let(:author) { create(:michael_hartl) }
  let(:ruby_microscope) { create(:ruby_microscope, author_id: author.id) }
  let(:rails_tutorial) { create(:ruby_on_rails_tutorial, author_id: author.id) }

  let(:params) { { } }
  let(:embed_picker) { EmbedPicker.new(presenter) }

  describe '#embed' do
    context 'with books (many-to-one) as the resource' do
      let(:presenter) { BookPresenter.new(rails_tutorial, params) }

      before do
        allow(BookPresenter).to(
          receive(:relations).and_return(['author'])
        )
      end

      context 'with no "embed" parameter' do
        it 'returns the "data" hash without changing it' do
          expect(embed_picker.embed.data).to eq presenter.data
        end
      end

      context 'with invalid relation "something"' do
        let(:params) { { embed: 'something' } }

        it 'raises a "RepresentationBuilderError"' do
          expect { embed_picker.embed }.to raise_error(RepresentationBuilderError)
        end
      end

      context 'with the "embed" parameter containing "author"' do
        let(:params) { { embed: 'author' } }

        it 'embeds the "author" data' do
          expect(embed_picker.embed.data[:author]).to eq({
            'id' => rails_tutorial.author.id,
            'given_name' => 'Michael',
            'family_name' => 'Hartl',
            'created_at' => rails_tutorial.author.created_at,
            'updated_at' => rails_tutorial.author.updated_at
          })
        end
      end

      context 'with the "embed" parameter containing "books"' do
        let(:params) { { embed: 'books' } }
        let(:presenter) { AuthorPresenter.new(author, params) }

        before do
          ruby_microscope && rails_tutorial
          allow(AuthorPresenter).to(
            receive(:relations).and_return(['books'])
          )
        end

        it 'embeds the "books" data' do
          expect(embed_picker.embed.data[:books].size).to eq(2)
          expect(embed_picker.embed.data[:books].first['id']).to eq(
            ruby_microscope.id
          )
          expect(embed_picker.embed.data[:books].last['id']).to eq(
            rails_tutorial.id
          )
        end
      end
    end
  end
end

Run the tests to paint some red.

rspec spec/representation_builders/embed_picker_spec.rb

Failure (RED)

...

Finished in 0.31034 seconds (files took 1.89 seconds to load)
4 examples, 4 failures

...

It’s now time to take a look at the EmbedPicker class. Just like the FieldPicker, this class will receive a presenter as parameter. From that presenter, it will extract the request parameters and, more specifically, the embed key.

If this key is not present or is empty, the EmbedPicker will return the presenter and its data variable unchanged.

# app/representation_builders/embed_picker.rb
class EmbedPicker

  def initialize(presenter)
    @presenter = presenter
  end

  def embed
    return @presenter unless embeds.any?

    # Hidden Code

However, if the client has requested some related entities to be embedded, the builder will start by validating the values.

# app/representation_builders/embed_picker.rb
class EmbedPicker

  def initialize # Hidden Code

  def embed
    return @presenter unless embeds.any?
    embeds.each { |embed| build_embed(embed) }
    @presenter
  end

  def embeds
    @embeds ||= validate_embeds
  end

  # Hidden Code
  def validate_embeds
    return [] if @presenter.params[:embed].blank?

    embeds = @presenter.params[:embed].try(:split, ',') || []
    return [] unless embeds.any?

    embeds.each do |embed|
      error!(embed) unless @presenter.class.relations.include?(embed)
    end

    embeds
  end

  def error!(embed)
    raise RepresentationBuilderError.new("embed=#{embed}"),
    "Invalid Embed. Allowed relations: (#{@presenter.class.relations.join(',')})"
  end

  # Hidden Code

Finally, if all is well, the builder will start embedding the related entities in the data variable of the passed presenter. The first thing the build_embed method will do is get the appropriate presenter by calling the relations method available below. Once it has the presenter and the relationship data, the method will proceed and start adding the related data by delegating the building phase to the FieldPicker.

def build_embed(embed)
  embed_presenter = "#{relations[embed].class_name}Presenter".constantize
  entity = @presenter.object.send(embed)

  @presenter.data[embed] = if relations[embed].collection?
    entity.order(:id).map do |embedded_entity|
      FieldPicker.new(embed_presenter.new(embedded_entity, {})).pick.data
    end
  else
    entity ? FieldPicker.new(embed_presenter.new(entity, {})).pick.data : {}
  end
end

The relations method does some reflection on the current model in order to check what are the available relationships.

def relations
  @relations ||= compute_relations
end

def compute_relations
  associations = @presenter.object.class.reflect_on_all_associations

  associations.each_with_object({}) do |r, hash|
    hash["#{r.name}"] = r
  end
end

Here is the complete EmbedPicker class with everything we just discussed.

# app/representation_builders/embed_picker.rb
class EmbedPicker

  def initialize(presenter)
    @presenter = presenter
  end

  def embed
    return @presenter unless embeds.any?
    embeds.each { |embed| build_embed(embed) }
    @presenter
  end

  def embeds
    @embeds ||= validate_embeds
  end

  private

  def validate_embeds
    return [] if @presenter.params[:embed].blank?

    embeds = @presenter.params[:embed].try(:split, ',') || []
    return [] unless embeds.any?

    embeds.each do |embed|
      error!(embed) unless @presenter.class.relations.include?(embed)
    end

    embeds
  end

  def build_embed(embed)
    embed_presenter = "#{relations[embed].class_name}Presenter".constantize
    entity = @presenter.object.send(embed)

    @presenter.data[embed] = if relations[embed].collection?
      entity.order(:id).map do |embedded_entity|
        FieldPicker.new(embed_presenter.new(embedded_entity, {})).pick.data
      end
    else
      entity ? FieldPicker.new(embed_presenter.new(entity, {})).pick.data : {}
    end
  end

  def error!(embed)
    raise RepresentationBuilderError.new("embed=#{embed}"),
    "Invalid Embed. Allowed relations: (#{@presenter.class.relations.join(',')})"
  end

  def relations
    @relations ||= compute_relations
  end

  def compute_relations
    associations = @presenter.object.class.reflect_on_all_associations

    associations.each_with_object({}) do |r, hash|
      hash["#{r.name}"] = r
    end
  end

end

Let’s run the tests to have the satisfaction of seeing them pass.

rspec spec/representation_builders/embed_picker_spec.rb

Success (GREEN)

...

EmbedPicker
  #embed
    with books (many-to-one) as the resource
      with no "embed" parameter
        returns the "data" hash without changing it
      with invalid relation "something"
        raises a "RepresentationBuilderError"
      with the "embed" parameter containing "author"
        embeds the "author" data
      with the "embed" parameter containing "books"
        embeds the "books" data

Finished in 0.45622 seconds (files took 2.43 seconds to load)
4 examples, 0 failures

18.2.2. Updating the Controllers

Now that this builder is ready, how about adding some methods to the BasePresenter class to make all the representation builders easier to use?

# app/presenters/base_presenter.rb
class BasePresenter
  # Hidden Code

  attr_accessor :object, :params, :data

  def initialize # Hidden Code
  def as_json # Hidden Code

  def fields
    FieldPicker.new(self).pick
  end

  def embeds
    EmbedPicker.new(self).embed
  end

end

With those methods, we can use something like:

BookPresenter.new(book, params).pick_fields.embed

Much simpler and less verbose!

The last step to ensure that the EmbedPicker is working is to add some tests to the books_spec file.

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

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

  describe 'GET /api/books' do
    before { books }

    context 'default behavior'  # Hidden code
    describe 'field picking'  # Hidden code

    describe 'embed picking' do

      context "with the 'embed' parameter" do
        before { get '/api/books?embed=author' }

        it 'gets the books with their authors embedded' do
          json_body['data'].each do |book|
            expect(book['author'].keys).to eq(
              ['id', 'given_name', 'family_name', 'created_at', 'updated_at']
            )
          end
        end
      end

      context 'with invalid "embed" relation "fake"' do
        before { get '/api/books?embed=fake,author' }

        it 'gets "400 Bad Request" back' do
          expect(response.status).to eq 400
        end

        it 'receives an error' do
          expect(json_body['error']).to_not be nil
        end

        it 'receives "fields=fid" as an invalid param' do
          expect(json_body['error']['invalid_params']).to eq 'embed=fake'
        end
      end # context "with invalid 'embed' relation 'fake'" end
    end # End of describe 'embed picking'

    describe 'pagination' # Hidden code
    describe 'sorting' # Hidden code
    describe 'filtering'  # Hidden code
  end
end

Then, watch them fail.

rspec spec/requests/books_spec.rb

Failure (RED)

...
Finished in 2.07 seconds (files took 2.33 seconds to load)
28 examples, 4 failures
...

Now, fix them by simply using the .embeds method on the instance of the BookPresenter.

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

  def index
    books = filter(sort(paginate(Book.all))).map do |book|
      BookPresenter.new(book, params).fields.embeds
    end

    render json: { data: books }.to_json
  end
end

Tadaaa!

rspec spec/requests/books_spec.rb

Success (GREEN)

...

Finished in 1.71 seconds (files took 1.81 seconds to load)
24 examples, 0 failures

18.2.3. Eager Loading

Remember when we created the EagerLoader? Well, it’s now time to see its power in full effect.

Start the server.

rails s

And access http://localhost:3000/api/books?embed=author

Now take a look at the logs. You should see something like in Figure 1.

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

Did you see all those SQL queries to get the author for each book?

Book Load (0.5ms)  SELECT  "books".* FROM "books" LIMIT ? OFFSET ?  
[["LIMIT", 10], ["OFFSET", 0]]
Author Load (0.4ms)  SELECT  "authors".* FROM "authors" WHERE
  "authors"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
Author Load (0.1ms)  SELECT  "authors".* FROM "authors" WHERE
  "authors"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
Author Load (0.1ms)  SELECT  "authors".* FROM "authors" WHERE
  "authors"."id" = ? LIMIT ?  [["id", 3], ["LIMIT", 1]]

Urgh. That’s not good. If we had 100 books, it would run 100 queries!

EagerLoader to the rescue! Let’s change the ApplicationController and the BooksController classes.

First, we need a shortcut to use the EagerLoader builder. Let’s add one to the ApplicationController and call it eager_load.

# 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 eager_load(scope)
    EagerLoader.new(scope, params).load
  end

  def filter  # Hidden Code
  def sort  # Hidden Code
  def paginate  # Hidden Code
  def current_url # Hidden Code
end

Then, use this new method in the books controller.

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

  def index
    books = eager_load(filter(sort(paginate(Book.all)))).map do |book|
      BookPresenter.new(book, params).fields.embeds
    end

    render json: { data: books }.to_json
  end
end

Try accessing the previous URL again ( /api/books?embed=author) and you should now see something like in Figure 2.

https://s3.amazonaws.com/devblast-mrwa-book/images/figures/18/02
Figure 2

Our problem is fixed and only one SQL query was made to get all the authors.

Book Load (0.2ms)  SELECT  "books".* FROM "books" LIMIT ? OFFSET ?  
[["LIMIT", 10], ["OFFSET", 0]]
Author Load (0.3ms)  SELECT "authors".* FROM "authors" WHERE
  "authors"."id" IN (1, 2, 3)

Run the tests to check that we didn’t break anything.

rspec

Success (GREEN)

...

Finished in 2.84 seconds (files took 3.76 seconds to load)
80 examples, 0 failures

18.3. QueryOrchestrator

I know I said we wouldn’t build another query builder, but this one is different. Implementing it is like putting the cherry on top of the cake!

So, what’s the QueryOrchestrator?

Well, its purpose is to act as the glue between our query builders to make them easier to use. Since they all behave in the same way (receive: scope and request params, return: scope), we can create a class that will handle them for us.

This class will become our interface to the query builders and we will be able to define which builders should be used by passing a list of symbols.

First, create a new file.

touch app/query_builders/query_orchestrator.rb

Here is the code for the QueryOrchestrator. To create it, we extracted the code from the ApplicationController class. Notice how we pass the actions as a parameter. We can give it [:paginate, :sort], for example. Or just use the :all which equates to [:paginate, :sort, :filter, :eager_load].

# app/query_builders/query_orchestrator.rb
class QueryOrchestrator
  ACTIONS = [:paginate, :sort, :filter, :eager_load]

  def initialize(scope:, params:, request:, response:, actions: :all)
    @scope = scope
    @params = params
    @request = request
    @response = response
    @actions = actions == :all ? ACTIONS : actions
  end

  def run
    @actions.each do |action|
      unless ACTIONS.include?(action)
        raise InvalidBuilderAction, "#{action} not permitted."
      end

      @scope = send(action)
    end
    @scope
  end

  private

  def paginate
    current_url = @request.base_url + @request.path
    paginator = Paginator.new(@scope, @request.query_parameters, current_url)
    @response.headers['Link'] = paginator.links
    paginator.paginate
  end

  def sort
    Sorter.new(@scope, @params).sort
  end

  def filter
    Filter.new(@scope, @params.to_unsafe_hash).filter
  end

  def eager_load
    EagerLoader.new(@scope, @params).load
  end

end

With the QueryOrchestrator, we can clean up the ApplicationController class and replace all our previous methods with a new one: orchestrate_query.

# 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(error)
    render status: 400, json: {
      error: {
        type: error.class,
        message: error.message,
        invalid_params: error.invalid_params
      }
    }
  end

  def orchestrate_query(scope, actions = :all)
    QueryOrchestrator.new(scope: scope,
                          params: params,
                          request: request,
                          response: response,
                          actions: actions).run
  end
end

Finally, let’s update the books controller with our new method.

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

  def index
    books = orchestrate_query(Book.all).map do |book|
      BookPresenter.new(book, params).fields.embeds
    end

    render json: { data: books }.to_json
  end

end

Thanks to the tests we wrote, we can check that using the QueryOrchestrator did not break anything.

rspec

Success (GREEN)

...

Finished in 3.96 seconds (files took 2.84 seconds to load)
80 examples, 0 failures

Looks good! Let’s talk about serializers now.

18.4. Serializer

Up to this point, we have created builders that “build” a hash with everything requested by the client. Now, we are getting to the part of serializing this data to send it back to the client in the format it wants.

The code we have used so far to build the final version of the representation was this:

render json: { data: books }.to_json

But we could totally make something more complicated or generate representations that follow the JSON API specification or the HAL standard.

We’ll get back to that later. For now, let’s keep the same logic and create a simple serializer that can be used for all our current needs.

mkdir -p app/serializers/alexandria && \
  touch app/serializers/alexandria/serializer.rb

All this serializer does is get the data from the appropriate presenters and put that data in a hash under the data key.

# app/serializers/alexandria/serializer.rb
module Alexandria
  class Serializer

    def initialize(data:, params:, actions:, options: {})
      @data = data
      @params = params
      @actions = actions
      @options = options
    end

    def to_json
      {
        data: build_data
      }.to_json
    end

    private

    def build_data
      if @data.respond_to?(:count)
        @data.map do |entity|
          presenter(entity).new(entity, @params).build(@actions)
        end
      else
        presenter(@data).new(@data, @params).build(@actions)
      end
    end

    def presenter(entity)
      @presenter ||= "#{entity.class}Presenter".constantize
    end

  end
end

In the code above, we are using a new method on the presenter instance:

presenter(entity).new(entity, @params).build(@actions)

Since we haven’t implemented this method yet, let’s add it now. Its goal is the same as the QueryOrchestrator logic: to receive a list of actions to take on the data.

# app/presenters/base_presenter.rb
class BasePresenter
  # Hidden Code

  attr_accessor :object, :params, :data

  def initialize # Hidden Code
  def as_json # Hidden Code

  def build(actions)
    actions.each { |action| send(action) }
    self
  end

  def fields
    FieldPicker.new(self).pick
  end

  def embeds
    EmbedPicker.new(self).embed
  end

end

Finally, it’s time to use our new serializer in the books controller. See how clean it looks now?

# 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

Let’s run all the tests one more time, just to be safe.

rspec

Success (GREEN)

...

Finished in 4.11 seconds (files took 2.58 seconds to load)
82 examples, 0 failures

18.5. Pushing our Changes

Let’s push the changes to GitHub. First, 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:   app/presenters/base_presenter.rb
	modified:   app/representation_builders/field_picker.rb
	modified:   spec/representation_builders/field_picker_spec.rb
	modified:   spec/requests/books_spec.rb

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

	app/errors/representation_builder_error.rb
	app/query_builders/query_orchestrator.rb
	app/representation_builders/embed_picker.rb
	app/serializers/
	spec/representation_builders/embed_picker_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 Representation Builders"

Output

[master a871fbb] Implement Representation Builders
 11 files changed, 314 insertions(+), 67 deletions(-)
 rewrite app/controllers/application_controller.rb (63%)
 create mode 100644 app/errors/representation_builder_error.rb
 create mode 100644 app/query_builders/query_orchestrator.rb
 create mode 100644 app/representation_builders/embed_picker.rb
 create mode 100644 app/serializers/alexandria/serializer.rb
 create mode 100644 spec/representation_builders/embed_picker_spec.rb

Push to GitHub.

git push origin master

18.6. Wrap Up

In this chapter, we built the remaining representation builders. We also added an orchestrator class to make the query builders easier to use. We also concluded the implementation of all the generic tools we needed to build our API.

In the next chapter, we will finish our controllers.