16
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

エムティーアイAdvent Calendar 2016

Day 19

Ruby on Rails 5のAPIモードで植物を育てる

Posted at

前書き

この記事はエムティーアイ 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のコメントアウトをはずします。

Gemfile
\# 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に作成時刻から始まるファイルが出来ているので、デフォルト値を追加します。

db/migrate/xxxxxx_add_column_to_plant.rb
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するパラメータを追加します。

app/controllers/plants_controller.rb
# 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:はハッシュを与えてもうまくパースしてくれます。

app/controllers/plants_controller.rb
  # 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に受け取った水の量に応じて状態が変化するメソッドを追加します。

app/models/plant.rb
  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メソッドを追加します。

app/controllers/plants_controller.rb
  # 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

ルーティングを追加します。

config/routes.rb
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部分にロジックらしきものを入れたので、テストを追加します。
(ちゃんとやると長くなるので、実行できるか試すだけにします)

test/models/plant_test.rb
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

16
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?