jsonapi-resourcesはこちら
cerebris/jsonapi-resources: A resource-focused Rails library for developing JSON API compliant servers.
下準備
インストールまで
いつものなのでサクサクいきます。
$ bundle init
# Gemfile source 'https://rubygems.org' gem 'rails', '5.0.0.rc1'
$ bundle install $ bundle exec rails new . --api # 上書きを確認されるので適当にYesしておく
# Gemfile # 以下を追記。 # rubygemsにあるのだとRails5に対応していないのでgithubから取得していることに注意。 gem 'jsonapi-resources', git: 'git@github.com:cerebris/jsonapi-resources.git', ref: '5e4fd81e516fe73395d8607520e96d6a013ff741'
設定変更
Controllerに機能追加(必須)
# app/controllers/application_controller.rb class ApplicationController < ActionController::Base include JSONAPI::ActsAsResourceController end
開発しやすいようにconfigいじっておく(任意)
# config/environments/development.rb Rails.application.configure do # 以下2つを変更。 config.eager_load = true config.consider_all_requests_local = false end
開発しやすいようにinitializersとしてちょっといじっておく(こっちも任意)
#config/initializers/jsonapi-resources.rb # 各設定の意味はこちらを参照: https://github.com/cerebris/jsonapi-resources/blob/master/lib/jsonapi/configuration.rb JSONAPI.configure do |config| # :underscored_key, :camelized_key, :dasherized_key, or custom # jsで扱い易いようにJSONのkeyをlowerCamelCaseにする。 config.json_key_format = :camelized_key # :none, :offset, :paged, or a custom paginator name # pagenationの形式をoffsetに変更 config.default_paginator = :offset end
実装
サンプルはtwitter風な何かを考えましょう。
Userがいて各UserはTweetを複数もちます。
Userはpasswordを直接DBにもちます(サンプルなのに真面目にやるのが面倒なので)
$ bin/rails g model User screen_name:string name:string password:string $ bin/rails g model Tweet user_id:integer text:text
associationも貼りましょう。
# app/models/User.rb class User < ApplicationRecord has_many :tweets end
# app/models/Tweet.rb class Tweet < ApplicationRecord belongs_to :user end
はい、ここからがJSONAPI::Resourcesを使う本番です。
app/resources配下にAPIを書いていくことになります。
attributes にAPIとして公開したい情報を書きます。
ここに記載されないものはAPIからは公開されません。
しかしassociationのものをここに書いてはいけません。 has_many や has_one を使って記述します
(※ これらは違う書き方もできます。詳しくは公式のREADMEを参照。)
# app/resources/user_resource.rb # class名はmodel名 + Resource class UserResource < JSONAPI::Resource # attributesでmodelにdispatchしたいメソッドを書く attributes :name, :screen_name # associationも書く has_many :tweets end
# app/resources/tweet_resource.rb class TweetResource < JSONAPI::Resource attributes :text end
これでAPIの定義はできたのでroutingとcontrollerを書いておきましょう。
actionは勝手に定義してくれるのでなんもなくてOKです。
$ bin/rails g controller users $ bin/rails g controller tweets
Rails.application.routes.draw do jsonapi_resources :users jsonapi_resources :tweets end
さて、では動作確認をしましょう。
$ bundle exec rake db:migrate
$ rake routes
Prefix Verb URI Pattern Controller#Action
user_relationships_tweets GET /users/:user_id/relationships/tweets(.:format) users#show_relationship {:relationship=>"tweets"}
POST /users/:user_id/relationships/tweets(.:format) users#create_relationship {:relationship=>"tweets"}
PUT|PATCH /users/:user_id/relationships/tweets(.:format) users#update_relationship {:relationship=>"tweets"}
DELETE /users/:user_id/relationships/tweets(.:format) users#destroy_relationship {:relationship=>"tweets"}
user_tweets GET /users/:user_id/tweets(.:format) tweets#get_related_resources {:relationship=>"tweets", :source=>"users"}
users GET /users(.:format) users#index
POST /users(.:format) users#create
user GET /users/:id(.:format) users#show
PATCH /users/:id(.:format) users#update
PUT /users/:id(.:format) users#update
DELETE /users/:id(.:format) users#destroy
tweets GET /tweets(.:format) tweets#index
POST /tweets(.:format) tweets#create
tweet GET /tweets/:id(.:format) tweets#show
PATCH /tweets/:id(.:format) tweets#update
PUT /tweets/:id(.:format) tweets#update
DELETE /tweets/:id(.:format) tweets#destroy
$ bin/rails s
意気揚々とchromeとかでアクセスするとエラーが出ます。
{"errors":[{"title":"Not acceptable","detail":"All requests must use the 'application/vnd.api+json' Accept without media type parameters. This request specified 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8'.","id":null,"href":null,"code":"406","source":null,"links":null,"status":"406","meta":null}]}
ちゃんとheaderを設定してcurlしてあげましょう。
$ curl -i -H "Accept: application/vnd.api+json" http://localhost:3000/users
HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Type: application/vnd.api+json; charset=utf-8
ETag: W/"8fe32e407a1038ee38753b70e5374b3a"
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: d80811e1-20b7-4259-8865-2ea1ad823899
X-Runtime: 0.012141
Transfer-Encoding: chunked
{"data":[]}
適当にデータを突っ込みます。(seedでいいのかっていうのはおいといて)
# db/seed.rb if User.count == 0 3.times do |i| User.create(name: "name#{i}", screen_name: "screen_name#{i}", password: "pass#{i}") end end if Tweet.count == 0 User.all.each do |u| 10.times do |i| Tweet.create(user_id: u.id, text: "#{u.name} < text#{i}") end end end
$ bundle exec rake db:seed
$ curl -H "Accept: application/vnd.api+json" http://localhost:3000/users | jq '.data | .[0]'
{
"id": "1",
"type": "users",
"links": {
"self": "http://localhost:3000/users/1"
},
"attributes": {
"name": "name0",
"screenName": "screen_name0"
},
"relationships": {
"tweets": {
"links": {
"self": "http://localhost:3000/users/1/relationships/tweets",
"related": "http://localhost:3000/users/1/tweets"
}
}
}
}
attributesに指定していないのでpasswordが含まれていませんね。
またhas_manyでtweetsをもつとしたのでrelationshipsをもっています。
$ curl -H "Accept: application/vnd.api+json" http://localhost:3000/tweets | jq '.data | .[0]'
{
"id": "1",
"type": "tweets",
"links": {
"self": "http://localhost:3000/tweets/1"
},
"attributes": {
"text": "name0 < text0"
}
}
tweetにはrelationshipsを指定していないのでattributesのみです。
注意点
belongs_toがない
has_oneを使いましょう。
APIリソースとして表現する以上、そのリソースを主体として考えるべきでbelongs_toなんてことはないんだ、という理由なのかと推測している。
attributeとしてassociationのもの指定してしまう
attributesでassociationを取れる名前を指定すればassociationの指定がなくてもとってくることは可能。
しかしN+1問題が起きてしまうためやるべきではないと思う
# app/resources/user_resource.rb class UserResource < JSONAPI::Resource attributes :name, :screen_name has_many :tweets end
$ curl -H "Accept: application/vnd.api+json" 'http://localhost:3000/users?include=tweets'
Started GET "/users?include=tweets" for ::1 at 2016-05-22 14:02:42 +0900
Processing by UsersController#index as API_JSON
Parameters: {"include"=>"tweets"}
User Load (0.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? OFFSET ? [["LIMIT", 10], ["OFFSET", 0]]
Tweet Load (0.4ms) SELECT "tweets".* FROM "tweets" WHERE "tweets"."user_id" IN (1, 2, 3)
(0.2ms) SELECT COUNT(*) FROM "users"
Completed 200 OK in 24ms (Views: 7.9ms | ActiveRecord: 0.8ms)
一方でattributesで指定したとき。
# app/resources/user_resource.rb class UserResource < JSONAPI::Resource attributes :name, :screen_name, :tweets end
$ curl -H "Accept: application/vnd.api+json" http://localhost:3000/users
Started GET "/users" for ::1 at 2016-05-22 14:04:04 +0900 Processing by UsersController#index as API_JSON User Load (0.1ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? OFFSET ? [["LIMIT", 10], ["OFFSET", 0]] (0.2ms) SELECT COUNT(*) FROM "users" Tweet Load (0.3ms) SELECT "tweets".* FROM "tweets" WHERE "tweets"."user_id" = ? [["user_id", 1]] Tweet Load (0.3ms) SELECT "tweets".* FROM "tweets" WHERE "tweets"."user_id" = ? [["user_id", 2]] Tweet Load (0.2ms) SELECT "tweets".* FROM "tweets" WHERE "tweets"."user_id" = ? [["user_id", 3]] Completed 200 OK in 59ms (Views: 28.0ms | ActiveRecord: 2.4ms)