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.
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.
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
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.
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!
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.
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:
embed=author
)
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
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
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.
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.
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
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.
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
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
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.