-
Notifications
You must be signed in to change notification settings - Fork 16
Model
CHANGES This page has been updated for the 9.x
branch.
Legacy instructions are gone - clone this wiki repo and checkout the 8.x
tag.
We will make a Post
model, with only a single title
field, as a dummy
API end point.
$ rails g model post title:string
A simple spec: the post
should have a title
field.
spec/models/post_spec.rb
:
require 'spec_helper'
describe Post do
it { should have_db_column :title }
end
Tests should fail.
Migrate the database, and restart guard
.
$ rake db:migrate db:test:prepare
$ guard
Tests should pass now.
We will expose two models to our ember app: post
will be public
accessible, but user
will require authentication. Both will expose index
and show
actions.
Normal response and authenticated responses should return a 200 OK
,
while unauthenticated requests should receive a 401 Unauthorized
.
spec/controllers/api/posts_controller_spec.rb
:
require 'spec_helper'
describe Api::PostsController do
let(:post) { Fabricate(:post) }
before { post } # initialize it
describe 'GET index' do
before { get :index }
it 'returns http 200' do
response.response_code.should == 200
end
end
describe 'GET show' do
before { get :show, id: post.id }
it 'returns http 200' do
response.response_code.should == 200
end
end
end
We have used a new Fabricator
here, so define it in
spec/fabricators/post_fabricator.rb
:
Fabricator(:post) do
title 'foo'
end
spec/controllers/api/users_controller_spec.rb
:
require 'spec_helper'
describe Api::UsersController do
let(:user) { Fabricate(:user) }
before { user } # initialize it
describe 'GET index' do
context 'unauthorized' do
before { get :index }
it 'returns http 401' do
response.response_code.should == 401
end
end
context 'authorized' do
before do
user.ensure_authentication_token!
get :index, auth_token: user.authentication_token
end
it 'returns http 200' do
response.response_code.should == 200
end
end
end
describe 'GET show' do
context 'unauthorized' do
before { get :show, id: user.id }
it 'returns http 401' do
response.response_code.should == 401
end
end
context 'authorized' do
before do
user.ensure_authentication_token!
get :show, id: user.id, auth_token: user.authentication_token
end
it 'returns http 200' do
response.response_code.should == 200
end
end
end
end
As outlined, we want a RESTful set of API end points for the resource posts
,
but only for index
and show
.
config/routes.rb
:
namespace :api do
# ...
resources :posts, only: [:index, :show]
resources :users, only: [:index, :show]
end
We will roll our own check for a signed in user, because Devise's native solution centers around cookies, while we want a pure token-only api.
First, our own current_user
method.
app/controllers/api/base_controller.rb
:
module Api
class BaseController < ApplicationController
# ...
def current_user
return nil unless params[:auth_token]
User.find_by authentication_token: params[:auth_token]
end
# ...
end
end
The guard clause is important, because we want to reject any request without
event providing an auth_token
param.
Our auth_only!
helper can then render a 401 Unauthorized
if current_user
cannot find any valid, authenticated user model.
module Api
class BaseController < ApplicationController
protected
# ...
def auth_only!
render json: {}, status: 401 unless current_user
end
# ...
end
end
A complete app/controllers/api/base_controller.rb
for reference:
module Api
class BaseController < ApplicationController
respond_to :json
before_action :default_json
def current_user
return nil unless params[:auth_token]
User.find_by authentication_token: params[:auth_token]
end
protected
def default_json
request.format = :json if params[:format].nil?
end
def auth_only!
render json: {}, status: 401 unless current_user
end
end
end
The PostsController
is standard rails.
app/controllers/api/posts_controller.rb
:
module Api
class PostsController < BaseController
def index
if params[:ids]
@posts = Post.find(params[:ids])
else
@posts = Post.all
end
respond_with @posts
end
def show
@post = Post.find(params[:id])
respond_with @post
end
end
end
The UsersController
is the same, except that we will use the auth_only!
helper to restrict access. In production you would probably want to limit
a user to accessing only one's own user model too.
app/controllers/api/users_controller.rb
:
module Api
class UsersController < BaseController
before_action :auth_only!
def index
if params[:ids]
@users = User.find(params[:ids])
else
@users = User.all
end
respond_with @users
end
def show
@user = User.find(params[:id])
respond_with @user
end
end
end
The tests should pass at this point, but we are not done yet.
We expect the response bodies to be JSON, and it should conform to what
ember-data
expects.
- the top level should be wrapped in a container variable, either the singular or the plural form of the model name as appropriate
- it should include the server side primary key as
id
- it should include fields that we want to load into the ember model
Bonus: we will be adding a param
field in our response. It is an
demonstration of server-side computed properties, and it will be used to
create more human-readable URLs later on.
We will add the specs in the 200
responses, as the unauthorized 401
responses should, logically, return nothing.
The posts#show
action first. spec/controllers/api/posts_controller_spec.rb
:
describe 'GET show' do
# ...
subject { JSON.parse response.body }
it 'wraps around post' do should include 'post' end
context 'inside post' do
subject { JSON.parse(response.body)['post'] }
it { should include 'id' }
it { should include 'title' }
it { should include 'param' }
end
# ...
end
The posts#index
action. We won't be repeating the individual post
specs,
because behind the scenes active_model_serializers
uses the same logic
to generate each individual post
.
describe 'GET index' do
# ...
subject { JSON.parse response.body }
it 'wraps around posts' do should include 'posts' end
# ...
end
Checkpoint: the full spec/controllers/api/posts_controller_spec.rb
file.
require 'spec_helper'
describe Api::PostsController do
let(:post) { Fabricate(:post) }
before { post } # initialize it
describe 'GET index' do
before { get :index }
subject { JSON.parse response.body }
it 'wraps around posts' do should include 'posts' end
it 'returns http 200' do
response.response_code.should == 200
end
end
describe 'GET show' do
before { get :show, id: post.id }
subject { JSON.parse response.body }
it 'wraps around post' do should include 'post' end
context 'inside post' do
subject { JSON.parse(response.body)['post'] }
it { should include 'id' }
it { should include 'title' }
it { should include 'param' }
end
it 'returns http 200' do
response.response_code.should == 200
end
end
end
A production users
model would probably return fields like name
, but
in our demo, we will just return an email
, along with the usual id
and
param
.
spec/controllers/api/users_controller_spec.rb
:
require 'spec_helper'
describe Api::UsersController do
let(:user) { Fabricate(:user) }
before { user } # initialize it
describe 'GET index' do
context 'unauthorized' do
before { get :index }
it 'returns http 401' do
response.response_code.should == 401
end
end
context 'authorized' do
before do
user.ensure_authentication_token!
get :index, auth_token: user.authentication_token
end
subject { JSON.parse response.body }
it 'wraps around users' do should include 'users' end
it 'returns http 200' do
response.response_code.should == 200
end
end
end
describe 'GET show' do
context 'unauthorized' do
before { get :show, id: user.id }
it 'returns http 401' do
response.response_code.should == 401
end
end
context 'authorized' do
before do
user.ensure_authentication_token!
get :show, id: user.id, auth_token: user.authentication_token
end
subject { JSON.parse response.body }
it 'wraps around user' do should include 'user' end
context 'inside user' do
subject { JSON.parse(response.body)['user'] }
it { should include 'id' }
it { should include 'email' }
it { should include 'param' }
end
it 'returns http 200' do
response.response_code.should == 200
end
end
end
end
Like the controller, some reusable logic can be implemented in a
BaseSerializer
. In this case, ember expects associations to be included in
embedded ID form, and supports sideloading them. We do not have any
association in this example, but it is useful to show them nonetheless.
app/serializers/base_serializer.rb
:
class BaseSerializer < ActiveModel::Serializer
embed :ids
end
The PostSerializer
will inherit from BaseSerializer
, although
this doesn't do anything yet for our simple application.
app/serializers/post_serializer.rb
:
class PostSerializer < BaseSerializer
end
We want to include the id
, title
and param
attributes.
class PostSerializer < BaseSerializer
attributes :id, :title, :param
end
The id
and title
methods can be found directly on the model, but we need
to define the param
method. Let's make it take the form 1-awesome-title
.
class PostSerializer < BaseSerializer
attributes :id, :title, :param # prev code
def param
"#{id}-#{title.dasherize.parameterize}"
end
end
Let's make a similar UserSerializer
, using the portion before the @
in
the email for our param
.
app/serializers/user_serializer.rb
:
class UserSerializer < BaseSerializer
attributes :id, :email, :param
def param
namePortion = email.split('@').first
"#{id}-#{namePortion.dasherize.parameterize}"
end
end
Tests should pass now.
If you detect codesmell due to duplication in this chapter, you are absolutely correct. You should probably refactor more of the models' common specs, serializer fields, etc out to a common mixin / helper.
For simplicity, this is left out of this tutorial.
Continue to Ember setup