Garageことはじめ


Garageというgemを使ってWeb APIを作るときに、知っておくと便利なことを紹介したいと思います。
今回はモデル、クエリについて見ていきます。

## Garage gemとは？

[Garage gem](https://github.com/cookpad/garage)はクックパッド社が開発・公開しているRESTful Web API用のgemで、Railsに組み込んで使用します。
Garageのインストール方法やモデル、コントローラ、テストの書き方などは[クックパッド社のこちらのブログ](http://techlife.cookpad.com/entry/2014/11/06/100000)を参考にしてください。

## 下準備

今回は[Garageのサンプル](https://github.com/taiki45/garage-example)を使用します。`git clone`して、README.mdに従って準備します。なお執筆時点の最新コミットは`bc9fa4c`です。

## モデルの書き方

### 属性の宣言

モデルの情報をAPIで公開する場合には、下記の３つの宣言(クラスメソッド)を利用できます。

+ `property`

	`User#id`や`User#name`といったモデルの属性や、`Post#user`といった単体のアソシエーション向けのメソッド

+ `collection`

	`User#posts`といった複数のアソシエーション向けのメソッド

+ `link`

	userが所有しているpostへのリンク(`/v1/users/1/posts`)といった、リンク情報のためのメソッド

#### collection宣言

ここでは他であまり触れられていない`collection`を使ってみます。userに`collection :posts`を追記し、postを２つ作成します。

```ruby
# app/models/user.rb
 class User < ActiveRecord::Base
   property :id
   property :name
   property :email
+  collection :posts
   link(:posts) { user_posts_path(self) }
...
 end
```

```shell
> 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の配列が取得できるようになっています。

```shell
> 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`に次のような条件を設定してみます。

```ruby
# 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"`属性が表示されなくなります。

```shell
> 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`になっています。

```ruby
# app/models/post.rb
 class Post < ActiveRecord::Base
  property :id
  property :title
  property :body
  property :published_at
  property :user, selectable: true
...
 end
```

通常のリクエストでは`user`に関する情報は表示されません。

```shell
> 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`の情報も取得できます。

```shell
> 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`オプションを指定してみます。

```ruby
# 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"`属性として表示されます。

```shell
> 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](https://github.com/cookpad/garage/blob/master/doc%2Frestful_actions.md#access--permission)にも書いてありますが、モデルのアクセス権についても説明します。モデルには"クラスレベル/オブジェクトレベル"で、"read/wirte"のアクセス権が設定できます。"クラスレベル/オブジェクトレベル"というのは"クラスメソッド/インスタンスメソッド"としてそれぞれ`build_permissions`を実装することです。"read/wirte"のアクセス権は`build_permissions`内で`perms.permits! :read/:write`することで設定できます。

このように設定が分割されているのは、"index/create"、"show/update/destroy"といったコントローラのアクションごとに必要なアクセス権が異なるからです。

下記のコードのコメントに書いてあるように、クラスメソッドでは"index/create"の設定を、インスタンスメソッドでは"show/update/destroy"の設定を行います。

```ruby
# 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`に必要なパラメータを記述します。

### 全ての属性を取得する

全ての属性を取得するには`*`を使用します。

```shell
> 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`な属性は取得しません。

```shell
> 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
}
```

### 特定の属性のみを取得する

取得したい属性を`,`区切りで指定します。

```shell
> 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]`と書きます。

```shell
> 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はモデルの属性レベルで制御ができるところが魅力的だと思います。