Garageというgemを使ってWeb APIを作るときに、知っておくと便利なことを紹介したいと思います。
今回はモデル、クエリについて見ていきます。
Garage gemとは?
Garage gemはクックパッド社が開発・公開しているRESTful Web API用のgemで、Railsに組み込んで使用します。
Garageのインストール方法やモデル、コントローラ、テストの書き方などはクックパッド社のこちらのブログを参考にしてください。
下準備
今回はGarageのサンプルを使用します。git cloneして、README.mdに従って準備します。なお執筆時点の最新コミットはbc9fa4cです。
モデルの書き方
属性の宣言
モデルの情報をAPIで公開する場合には、下記の3つの宣言(クラスメソッド)を利用できます。
-
propertyUser#idやUser#nameといったモデルの属性や、Post#userといった単体のアソシエーション向けのメソッド -
collectionUser#postsといった複数のアソシエーション向けのメソッド -
linkuserが所有しているpostへのリンク(
/v1/users/1/posts)といった、リンク情報のためのメソッド
collection宣言
ここでは他であまり触れられていないcollectionを使ってみます。userにcollection :postsを追記し、postを2つ作成します。
# app/models/user.rb
class User < ActiveRecord::Base
property :id
property :name
property :email
+ collection :posts
link(:posts) { user_posts_path(self) }
...
end
> bundle exec rails runner 'Post.create(title: "My string", body: "My text.", user_id: 1)'
> bundle exec rails runner 'Post.create(title: "Your string", body: "Your text.", user_id: 1)'
/v1/usersにアクセスすると、"posts"というキーが追加され、postの配列が取得できるようになっています。
> curl -XGET -H "Authorization: Bearer $ACCESS_TOKEN" http://localhost:3000/v1/users | json_pp
[
{
"email" : "alice@example.com",
"_links" : {
"posts" : {
"href" : "/v1/users/1/posts"
}
},
"name" : "alice",
"posts" : [
{
"body" : "My text.",
"published_at" : null,
"title" : "My string",
"id" : 1
},
{
"body" : "Your text.",
"published_at" : null,
"title" : "Your string",
"id" : 2
}
],
"id" : 1
}
]
属性宣言のオプション
-
if属性をレスポンスに含めるかどうかの条件を設定するためのオプション
-
selectable属性を選択可能にするためのオプション
-
as属性の名前(キー名)を変更するためのオプション
ifオプション
引数として#callメソッドが実装されたオブジェクトをとります。オブジェクトはrepresenterとresponderの二つ引数とともに呼び出されます。呼び出しの結果が偽のとき、その属性は結果に含まれなくなります。representerは自分自身のオブジェクトです。responderはGarage::AppResponderオブジェクトです。
emailに次のような条件を設定してみます。
# app/models/user.rb
class User < ActiveRecord::Base
property :id
property :name
- property :email
+ property :email, if: ->(representer, responder) { representer.name != 'alice' }
link(:posts) { user_posts_path(self) }
...
end
idが1のuserは名前がaliceなので、"email"属性が表示されなくなります。
> curl -XGET -H "Authorization: Bearer $ACCESS_TOKEN" http://localhost:3000/v1/users | json_pp
[
{
"_links" : {
"posts" : {
"href" : "/v1/users/1/posts"
}
},
"name" : "alice",
"id" : 1
}
]
例えばユーザーの特定の情報(名前やメールアドレスなど)を公開するかどうか、各ユーザーが設定できるようにするときなど、このオプションを利用することができます。
このオプションはproperty、collectionだけでなくlinkでも使用可能です。
selectableオプション
その属性を選択可能な属性にします。選択可能な属性は、デフォルトでは非表示側に倒れます。そのためクエリに明示する必要があります(クエリについては後述します)。
引数として、true/falseもしくは#callメソッドが実装されたオブジェクトをとります。#callメソッドが実装されたオブジェクトの呼び出しなどは、ifと同様です。
postではuserがselectableになっています。
# app/models/post.rb
class Post < ActiveRecord::Base
property :id
property :title
property :body
property :published_at
property :user, selectable: true
...
end
通常のリクエストではuserに関する情報は表示されません。
> curl -XGET -H "Authorization: Bearer $ACCESS_TOKEN" http://localhost:3000/v1/posts/1 | json_pp
{
"body" : "My text.",
"published_at" : null,
"title" : "My string",
"id" : 1
}
{"fields":"*"}を渡して全部取得するようにすると、userの情報も取得できます。
> curl -XGET -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-Type: application/json" -d '{"fields":"*"}' http://localhost:3000/v1/posts/1 | json_pp
{
"body" : "My text.",
"published_at" : null,
"user" : {
"email" : "alice@example.com",
"_links" : {
"posts" : {
"href" : "/v1/users/1/posts"
}
},
"name" : "alice",
"id" : 1
},
"title" : "My string",
"id" : 1
}
このオプションはproperty、collectionで使用可能です。
asオプション
レスポンスに表示したい属性の名前(キー名)をシンボルか文字列で渡します。property宣言とcollection宣言は、第一引数と同名のメソッドをモデルのオブジェクトに対して呼び出し、第一引数をキー名に使用します。
upcased_nameというメソッドを定義し、asオプションを指定してみます。
# app/models/user.rb
class User < ActiveRecord::Base
property :id
- property :name
+ property :upcased_name, as: :name
property :email
link(:posts) { user_posts_path(self) }
+ def upcased_name
+ name.upcase
+ end
+
...
end
"upcased_name"ではなく"name"属性として表示されます。
> curl -XGET -H "Authorization: Bearer $ACCESS_TOKEN" http://localhost:3000/v1/users | json_pp
[
{
"email" : "alice@example.com",
"_links" : {
"posts" : {
"href" : "/v1/users/1/posts"
}
},
"name" : "ALICE",
"id" : 1
}
]
このオプションはproperty、collectionで使用可能です。
アクセス権の制御
Garageのdocにも書いてありますが、モデルのアクセス権についても説明します。モデルには"クラスレベル/オブジェクトレベル"で、"read/wirte"のアクセス権が設定できます。"クラスレベル/オブジェクトレベル"というのは"クラスメソッド/インスタンスメソッド"としてそれぞれbuild_permissionsを実装することです。"read/wirte"のアクセス権はbuild_permissions内でperms.permits! :read/:writeすることで設定できます。
このように設定が分割されているのは、"index/create"、"show/update/destroy"といったコントローラのアクションごとに必要なアクセス権が異なるからです。
下記のコードのコメントに書いてあるように、クラスメソッドでは"index/create"の設定を、インスタンスメソッドでは"show/update/destroy"の設定を行います。
# app/models/post.rb
class Post < ActiveRecord::Base
...
def self.build_permissions(perms, other, target)
perms.permits! :read # index
perms.permits! :write # create
end
def build_permissions(perms, other)
# 公開されていれば誰でも読めるが、非公開のときはpostの作成者しか読めない
if published_at?
perms.permits! :read # show
elsif user == other
perms.permits! :read # show
end
perms.permits! :write # update destroy
end
...
end
クエリの書き方
クエリを渡すときはContent-Type: application/jsonにして、fieldsに必要なパラメータを記述します。
全ての属性を取得する
全ての属性を取得するには*を使用します。
> curl -XGET -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-Type: application/json" -d '{"fields":"*"}' http://localhost:3000/v1/posts/1 | json_pp
{
"body" : "My text.",
"published_at" : null,
"user" : {
"email" : "alice@example.com",
"_links" : {
"posts" : {
"href" : "/v1/users/1/posts"
}
},
"name" : "alice",
"id" : 1
},
"title" : "My string",
"id" : 1
}
デフォルトの属性のみを取得する
デフォルトの属性を取得するには__default__を使用します。fieldsを指定しないときはデフォルトと同じ挙動になります。
なお__default__は*と異なり、selectableな属性は取得しません。
> curl -XGET -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-Type: application/json" -d '{"fields":"__default__"}' http://localhost:3000/v1/posts/1 | json_pp
{
"body" : "My text.",
"published_at" : null,
"title" : "My string",
"id" : 1
}
特定の属性のみを取得する
取得したい属性を,区切りで指定します。
> curl -XGET -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-Type: application/json" -d '{"fields":"id,body"}' http://localhost:3000/v1/posts/1 | json_pp
{
"body" : "My text.",
"id" : 1
}
属性をネストして指定する
[]を使用することで、ネストした属性指定が可能です。たとえばpostのuserのemailとnameのみが必要なときは、user[email,name]と書きます。
> curl -XGET -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-Type: application/json" -d '{"fields":"body,user[email,name]"}' http://localhost:3000/v1/posts/1 | json_pp
{
"body" : "My text.",
"user" : {
"email" : "alice@example.com",
"name" : "alice"
}
}
まとめ
今回はモデルの設定方法と読み出しかた(クエリ)を中心にみてみました。Garageはモデルの属性レベルで制御ができるところが魅力的だと思います。