The Xing Work Flow - Backend
This is a general guide to creating a feature in Xing from the Backend to the Frontend (coming soon).
In this example, we will be creating feature that allows a user to read or write Dogs.
Setting up the Backend resource
There are basically two types of resource:
- OUTGOING: Resources that are being consumed by the Frontend
- INCOMING: Resources that are being received from the Frontend
OUTGOING
Outlined here are the files that you would need to touch to produce an outgoing Backend Resource. We generally tend to write files in this order.
- API Doc
- Request Spec
- Routes
- Controller Spec
- Controller
- Model Spec
- Model
- Factory
- Serializer Spec
- Serializer
API Doc
API_DOC/dog
Let's first consider the resource that you are trying to build for the Frontend to consume. For a show resource, it would look something like this:
{
links: {
self: "/dogs/1"
},
data: {
name: "Buddy",
age: 3,
breed: "Some kind of large rat"
}
}
Request Spec
backend/spec/requests/dog_show_spec.rb
Now that we know what we'd like to receive when we're trying to get information on a specific dog, we should write a request spec. The request spec tests that when we hit "/dogs/:id" we will get a response that matches the show resource we imagined above.
require 'spec_helper'
describe "dogs#show", :type => :request do
let :dog do
FactoryGirl.create(:dog)
end
describe "GET dogs/:id" do
it "shows page as json" do
json_get "dogs/#{dog.id}"
expect(response.status).to eq(200)
expect(response.body).to have_json_path('links/self')
expect(response.body).to have_json_path('data/name')
expect(response.body).to have_json_path('data/age')
expect(response.body).to have_json_path('data/breed')
end
end
end
Routes
backend/config/routes.rb
Add the following line to your routes file:
resources :dogs, :only => [:show]
Controller Spec
backend/spec/controllers/dogs_controller.rb
require 'spec_helper'
describe DogsController do
let :dog do
double Dog, :id => "123"
end
let :serializer do
double(DogSerializer)
end
########################################################################################
# GET SHOW
########################################################################################
describe "responding to GET show" do
it "should find the dog and pass it to a serializer" do
allow(Dog).to receive(:find).with(dog.id).and_return(dog)
allow(DogSerializer).to receive(:new).with(dog).and_return(serializer)
expect(controller).to receive(:render).
with(:json => serializer).
and_call_original
get :show, :id => 123
end
end
end
Controller
Note that our controller descends from Xing::Controllers::Base. This comes from the xing-backend gem that we should be including in our Gemfile. Check out the code at https://github.com/LRDesign/xing-backend.
For outgoing resources, the controller will render JSON that is output by its serializer (DogSerializer). We'll be looking at those shortly.
class DogsController < ApplicationController
# GET /dogs/:id
def show
dog = Dog.find(params[:id])
render :json => DogSerializer.new(dog)
end
You will find that all the controllers follow almost exactly the same pattern. If you find yourself straying from this pattern or adding methods here, you should consult your team leader.
Model Specs/Model/Factory
You'll need to create these as well. This part is pretty much the same as you've done in Rails.
Serializer Spec
backend/spec/serializers/dog_serializer_spec.rb
Serializers are where we construct our outgoing resources. It will take AR models and build JSON according to the instructions we write!
require 'spec_helper'
describe DogSerializer, :type => :serializer do
let :dog do
FactoryGirl.create(:dog, :age => 1
)
end
describe 'as_json' do
let :json do
DogSerializer.new(dog).to_json
end
it "should have the correct structure" do
expect(json).to have_json_path('links/self')
expect(json).to have_json_path('data/name')
expect(json).to have_json_path('data/age')
expect(json).to have_json_path('data/breed')
end
end
end
Serializer
backend/serializers/dog_serializer.rb
Note that the serializer descends from Xing::Serializers::Base. Again, this comes from the xing-backend gem. https://github.com/LRDesign/xing-backend
When you find that you need to nest serializers, the following classes will come in handy:
- Xing::Serializers::List
- Xing::Serializers::PagedList
Serializing and mapping nested resources are another article entirely, but know that these classes exist.
class DogSerializer < Xing::Serializers::Base
attributes :name, :age, :breed
def links
{ :self => routes.dog_path(object) }
end
end
High Five! We should now be able to hit the /dogs/:id resource and GET correctly formatted JSON.
INCOMING
Outlined here are the files that you would need to touch to receive JSON and write that data to the DB. We generally tend to write files in this order.
- API Doc
- Request Spec
- Routes
- Controller Spec
- Controller
- Model Spec
- Model
- Factory
- Serializer Spec
- Serializer
- Mapper Spec
- Mapper
API Doc
API_DOC/dog
Create/Update resources will have a similar structure to the show:
# GET /dogs/:id
{
links: {
self: "/dogs/1"
},
data: {
name: "Buddy",
age: 3,
breed: "Some kind of large rat"
}
}
# POST /dogs
{
links: {
},
data: {
name: "Pooh Bear",
age: 3,
breed: "Snorting Spinner"
}
}
# PUT /dogs/:id
{
links: {
self: "/dogs/1"
},
data: {
name: "Buddy McLovin",
age: 3,
breed: "Some kind of large rat"
}
}
Note that the POST resource does not require a self link, but a PUT does require a self link to know which record to update.
For the rest of the example, we will be working with a PUT resource.
Request Spec
backend/spec/requests/dog_update_spec.rb
Now that we know what we are receiving from the Frontend, we should write a request spec. The request spec tests that when we hit "/dogs/:id" with a JSON body, we will either successfully update a record or process a failed update correctly.
require 'spec_helper'
describe "dogs#update", :type => :request do
let! :dog do
FactoryGirl.create(:dog
)
end
let :json_body do
{
links: {
self: "/dogs/1"
},
data: {
name: "Buddy McLovin"
}
}.to_json
end
describe "Successful update"do
describe "PUT dogs/:id" do
it "is a 200 success including the serialized object" do
json_put "dogs/#{dog.id}", json_body
expect(response).to be_success
expect(response.body).to have_json_path('links/self')
expect(response.body).to have_json_path('data/name')
expect(response.body).to have_json_path('data/age')
expect(response.body).to have_json_path('data/breed')
expect(response.body).to be_json_eql("\"#{routes.dog_path(dog)}\"").at_path('links/self')
expect(response.body).to be_json_eql("\"Buddy McLovin\"").at_path("data/name")
end
end
end
describe "failing update" do
describe 'required information omitted' do
let :invalid_json do
{
links: {
self: "/dogs/1"
},
data: {
name: nil
}
}.to_json
end
describe "PUT dogs/:id" do
it "is a 422 with an error in response body" do
json_put "dogs/#{dog.id}", invalid_json
expect(response.status).to be(422)
expect(response.body).to be_json_eql("\"can't be blank\"").at_path("data/name/message")
end
end
end
end
end
Routes
backend/config/routes.rb
Add update to the dogs route:
resources :dogs, :only => [:show, :update]
Controller Spec
backend/spec/controllers/dogs_controller.rb
Test the update method in your controller spec. Notice that we use plenty of mocks, and that we now require a Mapper!
require 'spec_helper'
describe DogsController do
let :dog do
double Dog, :id => "123"
end
let :serializer do
double(DogSerializer)
end
let :json do
{ stuff: "like this", more: "like that" }.to_json
end
let :mock_mapper do
double(DogMapper)
end
let :mock_errors do
{ data: { stuff: "Is required" }}
end
########################################################################################
# GET SHOW
########################################################################################
describe "responding to GET show" do
it "should find the dog and pass it to a serializer" do
allow(Dog).to receive(:find).with(dog.id).and_return(dog)
allow(DogSerializer).to receive(:new).with(dog).and_return(serializer)
expect(controller).to receive(:render).
with(:json => serializer).
and_call_original
get :show, :id => 123
end
end
end
########################################################################################
# PUT UPDATE
########################################################################################
describe "responding to PUT update" do
it "should update with dog mapper and pass the JSON to it" do
allow(DogMapper).to receive(:new).with(json, dog.id).and_return(mock_mapper)
allow(mock_mapper).to receive(:save).and_return(true)
allow(mock_mapper).to receive(:dog).and_return(dog)
allow(DogSerializer).to receive(:new).with(dog).and_return(serializer)
expect(controller).to receive(:render).
with(:json => serializer).
and_call_original
put :update, json, { :id => 123 }
end
it "should render status 422 if not updated" do
allow(DogMapper).to receive(:new).with(json, dog.id).and_return(mock_mapper)
allow(mock_mapper).to receive(:dog).and_return(dog)
allow(mock_mapper).to receive(:save).and_return(false)
allow(mock_mapper).to receive(:errors).and_return(mock_errors)
allow(controller).to receive(:failed_to_process).with(mock_errors).and_call_original
post :update, json, { :id => 123 }
expect(response).to reject_as_unprocessable
Controller
An update controller is probably about as complicated of a controller as you will write.
It uses Mapper to process the JSON and params to update the record. If it is a successful update, it then uses a Serializer to respond with the updated record in JSON format.
class DogsController < ApplicationController
# GET /dogs/:id
def show
dog = Dog.find(params[:id])
render :json => DogSerializer.new(dog)
end
#PUT /dogs/:id
def update
mapper = DogMapper.new(json_body, params[:id])
if mapper.save
render :json => DogSerializer.new(mapper.dog)
else
failed_to_process(mapper.errors)
end
end
end
Again, you will find that all the controllers follow almost exactly the same pattern. If you find yourself straying from this pattern or adding methods here, you should consult your team leader.
Model Spec/Model/Factory/Serializer Spec/Serializer
If you had already created a show resource, You have all these things already! If not, see the code above regarding Serializers and Serializer Specs.
Mapper Spec
/backend/spec/mappers/dog_mapper_spec.rb
Here we test that our Mapper does what it is meant to do, which is to:
- take JSON
- break it down
- map it to the corresponding Active Record Model(s)
- check for errors
- save the record(s)
or
- send meaningful error messages to the Frontend
OK!
require 'spec_helper'
describe DogMapper, :type => :mapper do
let! :dog do
FactoryGirl.create(:dog, :name => "Fluffy FooFoo")
end
let :mapper do
DogMapper.new(json, dog.id)
end
let :valid_data do
{
data: {
name: "Buddy McLovin"
}
}
end
let :invalid_data do
{
data: {
name: nil
}
}
end
describe 'updating content' do
describe "valid data" do
let :json do
valid_data
end
it "should save the dog" do
expect do
mapper.save
end.to change{ dog.reload.name }.to("Buddy McLovin")
end
it "should be able to return dog" do
mapper.save
expect(mapper.dog).to be_a(Dog)
expect(mapper.dog).to be_persisted
end
end
describe "invalid data" do
let :json do
invalid_data.to_json
end
it "should insert an error into the errors hash without saving anything" do
expect do
mapper.save
end.not_to change{ dog.reload.name }
expect(mapper.errors).to eq(
{:data=>{:name=>{:type=>"required", :message=>"can't be blank"}}}
)
end
end
end
end
Mapper
/backend/mappers/dog_mapper.rb
Note that the Mapper descends from Xing::Mappers::Base. Xing::Mappers::Base has quite a few methods in it and it would be beneficial to give it some study to figure out how it works.
The following example is a deceptively simple case. The Mapper is often the most difficult part of a Backend Resource, especially if the resource has nested models and complicated validations.
If you find yourself needing to map lists back to the database, the following classes will come in handy:
- Xing::Builders::ListBuilder
- Xing::Builders::OrderedListBuilder
One day, we'll get around to writing docs on serializing and mapping nested resources. Until then, don't be afraid to ask for help!
class DogMapper < Xing::Mappers::Base
alias dog record
alias dog= record=
def record_class
Dog
end
def assign_values(data_hash)
@dog_data = data_hash
super
end
def update_record
dog.assign_attributes(@dog_data)
end
end
(∩`-´)⊃━☆゚.*・。゚ DONE!