Model

heartsentwined edited this page Oct 18, 2013 · 9 revisions

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.


Dummy post model

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.

The API end points

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

Routes

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

Authenticated-only helper

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

Model controllers

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

Response body specs

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

BaseSerializer

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

model serializers

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.

Footnote

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