Trong việc module hóa cũng như tạo cấu trúc REST API bạn có thể tự tạo cho mình một framework riêng hoặc sử dụng một framework đã có và đang được phát triển và ưa dùng là grape. tham khảo tại đây
Grape is a REST-like API framework for Ruby. It's designed to run on Rack or complement existing web application frameworks such as Rails and Sinatra by providing a simple DSL to easily develop RESTful APIs. It has built-in support for common conventions, including multiple formats, subdomain/prefix restriction, content negotiation, versioning and much more.
Nói gọn lại là Grape là một framework tích hợp và ăn nhập với các server như rack, sinsatra để tạo ra hệ thống Restful APIs. Ưu điểm là nó rất nhanh hơn các rails-api thông thường.
Trong bài này tôi sẽ giới thiệu và demo một ứng dụng sử dụng grape api.
Với mong muốn đi sâu hơn vào grape và ứng dụng của grape với rails nên tôi sẽ có thể viết nối tiếp một số bài nữa.
Trong gemfile
1
gem 'grape'
Run bundle : bundle update
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
module Twitter
class API < Grape::API
version 'v1', using: :header, vendor: 'twitter'
format :json
prefix :api
helpers do
def current_user
@current_user ||= User.authorize!(env)
end
def authenticate!
error!('401 Unauthorized', 401) unless current_user
end
end
resource :statuses do
desc 'Return a public timeline.'
get :public_timeline do
Status.limit(20)
end
desc 'Return a personal timeline.'
get :home_timeline do
authenticate!
current_user.statuses.limit(20)
end
desc 'Return a status.'
params do
requires :id, type: Integer, desc: 'Status ID.'
end
route_param :id do
get do
Status.find(params[:id])
end
end
desc 'Create a status.'
params do
requires :status, type: String, desc: 'Your status.'
end
post do
authenticate!
Status.create!({
user: current_user,
text: params[:status]
})
end
desc 'Update a status.'
params do
requires :id, type: String, desc: 'Status ID.'
requires :status, type: String, desc: 'Your status.'
end
put ':id' do
authenticate!
current_user.statuses.find(params[:id]).update({
user: current_user,
text: params[:status]
})
end
desc 'Delete a status.'
params do
requires :id, type: String, desc: 'Status ID.'
end
delete ':id' do
authenticate!
current_user.statuses.find(params[:id]).destroy
end
end
end
end
1
2
3
app
|––controllers
|––api
base.rb
1
2
3
4
5
module API
class Base < Grape::API
mount API::V1::Base
end
end
cấu trúc thư mục
1
2
3
4
app
|––controllers
|––api
|––base.rb
mount: là cơ chế nói cho rails biết là grape đang tạo ra số lượng api tương ứng với file
1
Twitter::API.compile!
Đối với rails để mount một file và tạo ra routes ta cần làm như sau:
application.rb
1
2
config.paths.add File.join('app', 'api'), glob: File.join('**', '*.rb')
config.autoload_paths += Dir[Rails.root.join('app', 'api', '*')]
config/routes.rb
1
mount Twitter::API => '/'
config/initializers/inflections.rb
1
2
3
ActiveSupport::Inflector.inflections(:en) do |inflect|
inflect.acronym 'API'
end
Trong việc mount là một cơ chế kỳ ảo của grape đảm bảo việc nested và mở rộng module hóa cũng quản lý version.
Từ version 1 bạn có thể phát triển lên version 2, 3. Grape dễ dàng quản lý điều đó dựa trên các câu lệnh cùng thư mục
1
2
3
4
5
6
app
|––controllers
|––api
|––base.rb
|––v1
|––base.rb
Các khái niệm cơ bản:
There are four strategies in which clients can reach your API’s endpoints: :path, :header, :accept_version_header and :param. The default strategy is :path.
version 'v1', using: :pathcurl http://localhost:9292/v1/statuses/public_timeline1
version 'v1', using: :header, vendor: 'twitter'
use with curl:
1
curl -H Accept:application/vnd.twitter-v1+json http://localhost:9292/statuses/public_timeline
1
version 'v1', using: :accept_version_header
example:
curl -H "Accept-Version:v1" http://localhost:9292/statuses/public_timeline
1
version 'v1', using: :param, parameter: 'v'
ex:
curl http://localhost:9292/statuses/public_timeline?v=v1
Giả sử bạn cần mở rộng module v1 bằng việc khải báo thêm các api. bước 1: Tạo folder v1
1
2
3
4
5
app
|––controllers
|––api
|––base.rb
|––v1
bước 2: với mỗi version tạo một base.rb riêng:
1
2
3
4
5
6
app
|––controllers
|––api
|––base.rb
|––v1
|––base.rb
bước 3: Trong file base khai báo mount thư mục đồng cấp
1
2
3
4
5
6
7
8
module API
module V1
class Base < Grape::API
mount V1::Users
# mount API::V1::AnotherResource
end
end
end
bước 4: khai báo api tương ứng user.rb
1
2
3
4
5
6
7
app
|––controllers
|––api
|––base.rb
|––v1
|––base.rb
|––users.rb
Như ở trên khi bạn khai báo 1 api đồng cấp, đó cũng là api end point. Đối với một api end point:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
module API
module V1
class Users < Grape::API
include API::V1::Defaults
resource :users do
desc "Return all users"
get "", root: :users do
User.all
end
desc "Return a user"
params do
requires :id, type: String, desc: "ID of the user"
end
get ":id", root: "user" do
User.where(id: params[:user_id]).first
end
end
end
end
end
resource: khai báo router
desc: mô tả
get "", root: :users do khai báo url api lấy toàn bộ user
ex: http://localhost:3000/api/v1/users
tool: POSTMAN
được khai báo tại file defaults.rb, ex: app/controllers/api/v1/defaults.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
module API
module V1
module Defaults
extend ActiveSupport::Concern
included do
prefix "api"
version "v1", using: :path
default_format :json
format :json
formatter :json,
Grape::Formatter::ActiveModelSerializers
helpers do
def permitted_params
@permitted_params ||= declared(params,
include_missing: false)
end
def logger
Rails.logger
end
end
# check authentice_user
def authenticate_user!
uid = request.headers["Uid"]
token = request.headers["Access-Token"]
@current_user = User.find_by(uid: uid)
unless @current_user && @current_user.valid_token?(token)
api_error!("You need to log in to use the app.", "failure", 401, {})
end
end
# Hàm hiển thị errors message khi lỗi
def api_error!(message, error_code, status, header)
error!({message: message, code: error_code}, status, header)
end
# # Hàm raise errors message khi lỗi
def api_error_log(message)
@logger ||= Logger.new(ProjectLogger.log_path("project_api"))
@logger.info("=============#{Time.zone.now.to_s}==================\n")
@logger.info("#{message}\n")
end
rescue_from ActiveRecord::RecordNotFound do |e|
error_response(message: e.message, status: 404)
end
rescue_from ActiveRecord::RecordInvalid do |e|
error_response(message: e.message, status: 422)
end
rescue_from Grape::Exceptions::ValidationErrors do |e|
error_response(message: e.message, status: 400)
end
end
end
end
end
Grape vấn support đầy đủ các hàm call back như:
1. before
2. before_validation
3. validations
4. after_validation
5. the API call
6. after
7. finally
Example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class MyAPI < Grape::API
get '/' do
"root - #{@blah}"
end
namespace :foo do
before do
@blah = 'blah'
end
get '/' do
"root - foo - #{@blah}"
end
namespace :bar do
get '/' do
"root - foo - bar - #{@blah}"
end
end
end
end
kết quả
1
2
3
GET / # 'root - '
GET /foo # 'root - foo - blah'
GET /foo/bar # 'root - foo - bar - blah'
example version
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Test < Grape::API
resource :foo do
version 'v1', :using => :path do
before do
@output ||= 'v1-'
end
get '/' do
@output += 'hello'
end
end
version 'v2', :using => :path do
before do
@output ||= 'v2-'
end
get '/' do
@output += 'hello'
end
end
end
end
kết quả
1
2
GET /foo/v1 # 'v1-hello'
GET /foo/v2 # 'v2-hello'
Viết rồi kiểm thử làm sao ? -> viết rspec cho api ta có thể dùng gem airborne. install:
1
gem install airborne
cấu trúc thư mục
1
2
3
4
spec
|––api
|––v1
|––users_spec
nội dung file test:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
require "rails_helper"
require "airborne"
describe "API::V1::Users" do
after(:all){I18n.locale = :ja}
describe "POST api/v1/users" do
let!(:user) do
FactoryGirl.create :user, id: 1, email: "test@gmail.com", first_name: "James",
last_name: "Bond", provider: "email"
end
context "when user update successfully" do
let(:api_response){FactoryGirl.build(:api_update_user_success_response).deep_symbolize_keys}
before do
post("/api/v1/users", {first_name: "hitorri"},
{"Accept-Language": "en", "App-Version": "knt/1.0", "Uid": user.uid, "Access-Token": user.access_token})
end
it{expect_json(api_response)}
end
end
end
using gem rack-cors, cấu hình tại file config.ru
1
2
3
4
5
6
7
8
9
10
require 'rack/cors'
use Rack::Cors do
allow do
origins '*'
resource '*', headers: :any, methods: :get
end
end
run Twitter::API
Công việc của chúng ta là convert mảng sang JSON. Để làm được việc này chúng ta cài thêm gem grape-active_model_serializers
1
2
3
4
5
6
7
8
9
10
11
12
13
module API
module V1
module Defaults
extend ActiveSupport::Concern
included do
prefix "api"
version "v1", using: :path
default_format :json
format :json
formatter :json,
Grape::Formatter::ActiveModelSerializers
...
Create a directory, serializers, in the top level of your app. Create a graduate_serializer.rb file in that directory. Here is where our serializer will live.
app/serializers/graduate_serializer.rb
1
2
3
4
5
6
class GraduateSerializer < ActiveModel::Serializer
attributes :id, :first_name, :last_name, :cohort,
:current_job, :bio, :news, :website, :picture,
:created_at, :updated_at
end
bundle install.app/controllers/api/v1/base.rb thêm đoạn sau:
1
2
3
4
5
6
7
8
9
require "grape-swagger"
...
add_swagger_documentation(
api_version: "v1",
hide_documentation_path: true,
mount_path: "/api/v1/swagger_doc",
hide_format: true
)
end
config/routes.rb:
1
mount GrapeSwaggerRails::Engine, at: "/documentation"
vào url: http://localhost/documentation bạn sẽ redirect tới: http://localhost:3000/api/v1/swagger_doc
Có thể tham khảo thêm tại đây
Có thể tham khảo tại đây
Có thể tham khảo thêm tại đây