了解如何在 Michael Hartl 的 Rails 5 教程中构建现代 API
Note 1: If you are looking for the regular readme, it's here.
Note 2: You can contribute to this tutorial by opening an issue or even sending a pull request!
Note 3: With the API I built, I went on and created the same app in Ember.
I will show how you can extend your Rails app and build an API without changing a single line of code from your existing app. We will be using Michael Hartl's Rails tutorial (I actually started learning Rails and subsequently Ruby from that tutorial, I really owe a beer to that guy) which is a classic Rails app and extend it by building an API for the app.
Designing an API is not an easy process. Usually it's very difficult to know beforehand what the client will need. However we will make our best to support most clients needs:
By the way, there is a long discussion about what REST means. Is just JSONAPI as REST as Joy Fielding's defined it? Definitely not. However, it's more resty than regular JSON response, plus it has a wide support in terms of libraries.
Moving forward, let's add our first resource, let it be a user. But before adding the controller let's add the routes first:
#api
namespace :api do
namespace :v1 do
resources :sessions, only: [:create, :show]
resources :users, only: [:index, :create, :show, :update, :destroy] do
post :activate, on: :collection
resources :followers, only: [:index, :destroy]
resources :followings, only: [:index, :destroy] do
post :create, on: :member
end
resource :feed, only: [:show]
end
resources :microposts, only: [:index, :create, :show, :update, :destroy]
end
end
All REST routes for each record, only GET method for collections (Rails muddles up collection REST routes with element REST routes in the same controllers) and a couple custom routes.
As you can see we have many routes. The idea is that the tutorial will mostly touch and show you a couple of them and you will manage to understand and see the rest from the code inside the repo. I think extended tutorials are boring :). However, if you find something weird or you don't understand something you are always welcomed to open an issue and ask :)
Let's create the users API controller and add support for the GET method on a single record:
The first thing we need to do is to separate our API from the rest of the app.
In order to do that we will create a new Controller under a different namespace.
Given that it's good to have versioned API let's go and create our first controller
under app/controllers/api/v1/
class Api::V1::BaseController hex).any?
end
end
and exactly after that we create the migration:
class AddTokenToUsers controller -> model -> controller -> serializer ->
output works ok.
That's why I feel API tests stand between unit tests and integration tests.
Note that since Michael has already added some model tests we don't have to be pedantic about it. We can skip models, and test
only API controllers.
``` ruby
describe Api::V1::UsersController, type: :api do
context :show do
before do
create_and_sign_in_user
@user = FactoryGirl.create(:user)
get api_v1_user_path(@user.id), format: :json
end
it 'returns the correct status' do
expect(last_response.status).to eql(200)
end
it 'returns the data in the body' do
body = JSON.parse(last_response.body, symbolize_names: true)
expect(body.dig(:data, :attributes, :name).to eql(@user.name)
expect(body.dig(:data, :attributes, :email).to eql(@user.name)
expect(body.dig(:data, :attributes, :admin).to eql(@user.admin)
expect(body.dig(:data, :attributes, :updated_at)).to eql(@user.created_at.iso8601)
expect(body.dig(:data, :attributes, :updated_at)).to eql(@user.updated_at.iso8601)
end
end
end
create_and_sign_in_user method comes from our authentication helper:
module AuthenticationHelper
def sign_in(user)
header('Authorization', "Token token=\"#{user.token}\"")
end
def create_and_sign_in_user
user = FactoryGirl.create(:user)
sign_in(user)
return user
end
alias_method :create_and_sign_in_another_user, :create_and_sign_in_user
def create_and_sign_in_admin
admin = FactoryGirl.create(:admin)
sign_in(admin)
return admin
end
alias_method :create_and_sign_in_admin_user, :create_and_sign_in_admin
end
RSpec.configure do |config|
config.include AuthenticationHelper, type: :api
end
…
ruby
require 'rails_helper'
describe Api::V1::UsersController, type: :api do
context :show do
before do
create_and_sign_in_user
FactoryGirl.create(:user)
@user = User.last!
get api_v1_user_path(@user.id)
end
it_returns_status(200)
it_returns_attribute_values(
resource: 'user', model: proc{@user}, attrs: [
:id, :name, :created_at, :microposts_count, :followers_count,
:followings_count
],
modifiers: {
created_at: proc{|i| i.in_time_zone('UTC').iso8601.to_s},
id: proc{|i| i.to_s}
}
)
end
end
This gem adds an automated way to test your JSONAPI (or any other API spec) respone by proviging you a simple API to test all attributes.
Furthermore, to have more robust tests, we can add rspec-json_schema that tests if the response follows a pre-defined JSON schema. For instance, the JSON schema for regular role, is the following:
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"properties": {
"data": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"type": {
"type": "string"
},
"attributes": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"email": {
"type": "string"
},
"created-at": {
"type": "string"
}
},
"required": [
"name",
"email",
"created-at",
]
},
"relationships": {
"type": "object",
"properties": {
"microposts": {
"type": "object",
"properties": {
"links": {
"type": "object",
"properties": {
"related": {
"type": "string"
}
},
"required": [
"related"
]
}
},
"required": [
"links"
]
},
"followers": {
"type": "object",
"properties": {
"links": {
"type": "object",
"properties": {
"related": {
"type": "string"
}
},
"required": [
"related"
]
}
},
"required": [
"links"
]
},
"followings": {
"type": "object",
"properties": {
"links": {
"type": "object",
"properties": {
"related": {
"type": "string"
}
},
"required": [
"related"
]
}
},
"required": [
"links"
]
}
},
"required": [
"microposts",
"followers",
"followings"
]
}
},
"required": [
"id",
"type",
"attributes",
"relationships"
]
}
},
"required": [
"data"
]
}
Notice on the required and additionalProperties properties which tighten the schema a lot.
Eventually the test spec for show action becomes:
require 'rails_helper'
describe Api::V1::UsersController, '#show', type: :api do
describe 'Authorization' do
context 'when as guest' do
before do
FactoryGirl.create(:user)
@user = User.last!
get api_v1_user_path(@user.id)
end
it_returns_status(401)
it_follows_json_schema('errors')
end
context 'when authenticated as a regular user' do
before do
create_and_sign_in_user
FactoryGirl.create(:user)
@user = User.last!
get api_v1_user_path(@user.id)
end
it_returns_status(200)
it_follows_json_schema('regular/user')
it_returns_attribute_values(
resource: 'user', model: proc{@user}, attrs: [
:id, :name, :created_at, :microposts_count, :followers_count,
:followings_count
],
modifiers: {
created_at: proc{|i| i.in_time_zone('UTC').iso8601.to_s},
id: proc{|i| i.to_s}
}
)
end
context 'when authenticated as an admin' do
before do
create_and_sign_in_admin
FactoryGirl.create(:user)
@user = User.last!
get api_v1_user_path(@user.id)
end
it_returns_status(200)
it_follows_json_schema('admin/user')
it_returns_attribute_values(
resource: 'user', model: proc{@user}, attrs: User.column_names,
modifiers: {
[:created_at, :updated_at] => proc{|i| i.in_time_zone('UTC').iso8601.to_s},
id: proc{|i| i.to_s}
}
)
end
end
end
…
ruby
class AddCacheCounters < ActiveRecord::Migration[5.0]
def change
add_column :users, :microposts_count, :integer, null: false, default: 0
add_column :users, :followers_count, :integer, null: false, default: 0
add_column :users, :followings_count, :integer, null: false, default: 0
end
end
Then inside Micropost model:
belongs_to :user, counter_cache: true
and inside Relationship model:
belongs_to :follower, class_name: "User", counter_cache: :followings_count
belongs_to :followed, class_name: "User", counter_cache: :followers_count
…
ruby
attribute :following_state
attribute :follower_state
def following_state
Relationship.where(
follower_id: current_user.id,
followed_id: object.id
).exists?
end
def follower_state
Relationship.where(
follower_id: object.id,
followed_id: current_user.id
).exists?
end
…
ruby
class Api::V1::UsersController < Api::V1::BaseController
include ActiveHashRelation
def index
auth_users = policy_scope(@users)
render jsonapi: auth_users.collection,
each_serializer: Api::V1::UserSerializer,
fields: {user: auth_users.fields},
meta: meta_attributes(auth_users.collection)
end
end
…
rvm use 2.3.3
bundle install
bundle exec rake db:create
bundle exec rake db:migrate
In test secion we can run all tests (both Michael's and API tests):
rake test
bundle exec rspec spec
…
ruby
class Api::V2::UsersController < Api::V1::UsersController
def index
#new overriden index here
end
end
…
ruby
set :build_dir, '../public/docs/'
and start writing your docs. You can take
暂无开放 Issues,或尚未同步最近议题。