前書き
この記事はエムティーアイ Advent Calendar 2016 の19日目です。
rails5.0から、プロジェクトを作る際にview部分などを省くオプションが選べるようになりました。
jsonを返すだけのWeb APIが簡単に作れるそうなので、ハンズオン的にAPIを作ってみます。
作るもの
Web APIは、サーバにあるリソースをHTTPを経由して簡単に操作するための仕組みです。
今回は、サーバサイドに植物の状態を持たせ、HTTPリクエストとして水をやることが出来るようにします。
- 新しく植物を植える
- 現在の状態を見る
- 水をやり、成長させる
上記の3つを実装しましょう。
下準備
必要なもの
- ruby2.2.2以降(rails5はこれ以前だと動きません)
- rubygems
- bundler
- curl
環境にあわせ、yumなりbrewなりでインストールしましょう。
windowsでやると辛いかもしれません。
ここでは飛ばしますが、rbenvを用いると複数のバージョンのrubyが容易に切り替えられるようになり、環境の管理がとても楽になります。
興味があれば試してみてください。
初期構築
railsもruby同様、複数バージョンを入れておかないとアプリごとに動いたり動かなかったりすることがあります。
開発環境と本番環境で差異があっても危険ですね。
bundlerを使うと、アプリごとに使うライブラリ(rubyのライブラリはgemと呼ばれます)のバージョンを指定できます。
ということで、まずAPI用のディレクトリを作成します。
今回作るAPIの名前はplant_factoryにします。
$ mkdir plant_factory
$ cd plant_factory
次に、bundlerが管理するgemの情報を記述したGemfileを作ります。
$ bundle init
生成されたGemfileを開き、railsのコメントアウトをはずします。
\# frozen_string_literal: true
source "https://rubygems.org"
gem 'rails'
railsを今いるディレクトリ内にローカルインストールします。
$ bundle install --path vendor/bundle
ローカルのrailsを使って、現在のディレクトリにapiプロジェクトを作ります。
bundle exec hoge
はグローバルにインストールされたhogeでなく、今居る場所で有効なhogeを使用するコマンドです。
Gemfileの上書きをして良いか聞かれるので、yを入力します。
$ bundle exec rails new . --api
--apiオプションをつけると、従来のrails newと違いview関係がまるまる削られ、デフォルトでcontrollerがjsonレスポンスを返すようになっています。
詳しい差異はこちらの記事で詳しく紹介されていますので、興味がある方は覗いてみてください。
これだけでは完全に骨組みしかありませんので、必要なmodelやcontrollerを作る必要があります。
植物の状態plantを管理するクラスを作りましょう。
以下のコマンドで基本的な部分を自動で作成することが出来ます。
$ bundle exec rails g scaffold plant
この段階で、デフォルトで使用されるデータベースSQLiteにplantテーブルが作成されますが、
まだidと作成/更新日時のカラムしか用意されていません。
ここに名前、水分量、大きさを追加します。
カラムを追加したい場合は以下のようにmigrationファイル(DBの変更差分)を作ります。
$ bundle exec rails g migration AddColumnToPlant name:string height:int water:int
これでカラム追加が出来ますが、デフォルト値の設定を追加しておきましょう。
/db/migrateに作成時刻から始まるファイルが出来ているので、デフォルト値を追加します。
class AddColumnToPlant < ActiveRecord::Migration[5.0]
def change
add_column :plants, :name, :string
add_column :plants, :height, :int, default: 1
add_column :plants, :water, :int, default: 0
end
end
この差分をDBに適用します。
rails5からrakeコマンドがrailsコマンドに統合されたので、従来rake db:migrateだったところがrailsコマンドでいけます。
$ bundle exec rails db:migrate
ここまでで初期構築が完了し、植物の状態をデータベースで管理出来る状態になりました。
これをベースに作り変えていきます。
植える
scaffoldした段階で、createメソッドがコントローラに追加されています。
どこにどんなリクエストを投げればよいか、ルーティングを見てみましょう。
$ bundle exec rails routes
Prefix Verb URI Pattern Controller#Action
plants GET /plants(.:format) plants#index
POST /plants(.:format) plants#create
plant GET /plants/:id(.:format) plants#show
PATCH /plants/:id(.:format) plants#update
PUT /plants/:id(.:format) plants#update
DELETE /plants/:id(.:format) plants#destroy
現状でも/plants にPOSTリクエストを送ることで植えることが出来ますが、せっかくなので名前をつけて植えられるようにします。
controllerでPOSTパラメータを受け取れるようにするため、plant_paramsメソッドにpermitするパラメータを追加します。
# Only allow a trusted parameter "white list" through.
def plant_params
params.fetch(:plant, {}).permit(:name)
end
この状態でPOSTパラメータに'{"plant":{"name":"flowey"}}'
といったjsonを設定してリクエストを送ると、名前をつけて植えることが出来ます。
railsにはWEBrickと言うサーバが標準で搭載されており、コマンドラインから起動することが出来ます。
起動してローカルからリクエストを投げてみましょう。デフォルトでは3000番ポートを使います。
$ bundle exec rails s
$ curl -X POST -H "Content-Type: application/json" -d '{"plant":{"name":"flowey"}}' http://localhost:3000/plants
=> {"id":1,"created_at":"2016-12-17T21:10:02.657Z","updated_at":"2016-12-17T21:10:02.657Z","name":"flowey","height":1,"water":0}
お花のfloweyちゃんが植えられました。Howdy!
観察して水をやる量を調整したいですが、showメソッドで返ってくる結果がいまいちわかりにくいです。
ちょっと書き換えましょう。
render json:はハッシュを与えてもうまくパースしてくれます。
# GET /plants/1
def show
message =
case @plant.water
when -(Float::INFINITY)..0 then "もっと水が必要だ"
when 1..3 then "水が必要だ"
when 4..6 then "もう少し水が必要だ"
else "水をやる必要はなさそうだ"
end
render json: {"名前": @plant.name, "大きさ": "#{@plant.height}cm", "状態":message}
end
$ curl localhost:3000/plants/1
{"名前":"flowey","大きさ":"1cm","状態":"もっと水が必要だ"}
水をやる
水をやるためのメソッドwaterとルーティングを追加します。
scaffoldで追加されたupdateメソッドは単なる上書き更新なので、ここでは無視します。
植物は大きいほど多くの水を必要としますが、多すぎたり少なすぎたりすると縮んでしまうようにします。
(大きさが0になったら枯れてしまったと思ってください)
まずmodelに受け取った水の量に応じて状態が変化するメソッドを追加します。
def grow(water)
# 成長量を水の量で決める
self.height +=
case self.water += water
when 1..3 then 1 # 少なめ
when 4..6 then 3 # ちょうどよい
when 7..9 then 1 # 多め
else -1 # 多すぎ、少なすぎ
end
# 水を消費する
self.water -=
case self.height
when 1..10 then 1
when 11..20 then 2
when 21..50 then 3
else 0
end
end
次にcontrollerにwaterメソッドを追加します。
# PUT /plants/1/water
def water
@plant.grow(params.fetch(:water, {}))
if @plant.save
render json: {"名前" => @plant.name, "大きさ": "#{@plant.height}cm", "水分量": @plant.water}
else
render json: @plant.errors, status: :unprocessable_entity
end
end
ルーティングを追加します。
Rails.application.routes.draw do
resources :plants do
member do
put 'water'
end
end
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end
rails routesしてみます。
$ bundle exec rails routes
Prefix Verb URI Pattern Controller#Action
water_plant PUT /plants/:id/water(.:format) plants#water
plants GET /plants(.:format) plants#index
POST /plants(.:format) plants#create
plant GET /plants/:id(.:format) plants#show
PATCH /plants/:id(.:format) plants#update
PUT /plants/:id(.:format) plants#update
DELETE /plants/:id(.:format) plants#destroy
/plants/id/waterにPUTリクエストで水があげられるようになりました。
植える>観察する>水をやる>観察する...の流れをやってみましょう。
$ curl -X PUT -H "Content-Type: application/json" -d '{"water":2}' http://localhost:3000/plants/1/water
{"名前":"flowey","大きさ":"2cm","水分量":1}
$ curl localhost:3000/plants/1
{"名前":"flowey","大きさ":"2cm","状態":"水が必要だ"}
$ curl -X PUT -H "Content-Type: application/json" -d '{"water":5}' http://localhost:3000/plants/1/water
{"名前":"flowey","大きさ":"5cm","水分量":5}
$ curl localhost:3000/plants/1
{"名前":"flowey","大きさ":"5cm","状態":"もう少し水が必要だ"}
テストを書く
scaffold時にテストコードも作られています。
model部分にロジックらしきものを入れたので、テストを追加します。
(ちゃんとやると長くなるので、実行できるか試すだけにします)
require 'test_helper'
class PlantTest < ActiveSupport::TestCase
def test_growing
plant1 = Plant.new(:name => "plant1", :height => "1", :water => "2")
plant1.grow(3)
assert_equal 4, plant1.height
assert_equal 4, plant1.water
end
end
test/models以下のテストのみを実行します。
$ bundle exec rails test test/models
Running via Spring preloader in process 27212
Run options: --seed 52500
# Running:
.
Finished in 0.021373s, 46.7877 runs/s, 93.5754 assertions/s.
1 runs, 2 assertions, 0 failures, 0 errors, 0 skips
assertが成功しました。
後書き
簡単なAPIであれば、ほとんど自動構築で作れてしまいますね。
自動構築自体はrails4以前でも出来ましたが、リクエストがviewにひもづいてしまうのでいちいちjsonを返すようにしたりgemを入れたりと結構面倒でした。
これを使ってボードゲームを作りたいなと思っています。
rails5.0の新機能はほとんど紹介できてませんが、これを期にぜひ試してみてください。
参考リンク
Ruby on Rails ガイド (5.0 対応)
http://railsguides.jp/
rbenvおよびbundlerの基本的な使用方法
https://www.qoosky.io/techs/8a0f1d8d9e
Ruby on Rails 5のAPIモードと非APIモードのファイル差分
http://qiita.com/61503891/items/d4eda649a10a3494830c