RubyOnBasis[12] 新人Railsエンジニア向け講座

このシリーズについて

RubyOnBasisはRuby on Railsを業務に利用する新人Webエンジニアのための、Railsを使わないテキストだ。この講座ではWEBrickを使ってWebアプリを書きながらHTTPを始めとする基礎知識をキャッチアップしてRailsがどうやって作られ動くのかを理解していく。

このシリーズを始めから読む

作ろう、商品!

今日はProductを新規登録をしてみよう。が、その前に一つ話しておくことがある。

以前の説明でHTTPはURLに対してGET, POST, PUT, DELETEメソッドを使うことでリソースを操作すると言ったな。すまん、ありゃ(一部)嘘だ。

俺が悪いんじゃない、悪いのはHTMLだ

HTTPの仕様はもちろんそうなっているんだが、実際にはなんとブラウザを利用するWebアプリではPUTメソッドもDELETEメソッドも使っていない。HTMLの規格でGET/POSTしか使えないんだ。

ただ、ブラウザを使わないアプリケーション同士でWebAPIとしてHTTPを使う場合はPUTやDELETEメソッドも使っている。例えばiPhoneTwitterクライアントを利用する場合、内部ではput/deleteメソッドが使われるだろう。

「嘘だっ! RailsのRoutesを書く時にPATCHやDELETEだとかのメソッドを指定しているのを見たことあるぞ。使えないわけあるか!」と反論するお友達もいるかもしれない。ではRailsがどうやってこの矛盾に対処するか説明しよう。

Railsに限ったことではないが、WebアプリでPUTやDELETEを使う場合にはメソッドオーバーライドをしているんだ。

メソッド・オーバーライド

メソッド・オーバーライドとは通信上POSTメソッドでデータを投げるが、投げるデータの中で自身のメソッドが何かを指定しておく。そして、Webアプリ内あるいはWebアプリへリクエストを渡すアプリ内で指定されたメソッドとしてリクエストを扱うんだ。メソッドを覆す(override)というワケ。

=> RailsにおけるオーバーライドについてQiitaで説明したぞ リクエストのメソッドを見るならrequest#methodは避けるべきかも

書こう、HTML

先にちょっとばかりHTMLを書いてみよう。フォームを作ってWebアプリにPOSTメソッドでリクエストを投げるんだ。書いたらどこかローカルに保存しておいてね。

<form method="post" action="http://localhost:3000/test">
    <input type="text" name="textArea">
    <input type="submit">
</form>

簡単に説明しよう。formタグのmethod要素は単純にget/postのどちらかを入れる。action要素はリクエストをどこのurlへ投げるかの指定だ。

知っての通りinputなどのタグで様々な値を指定できる。railsでもおなじみだね。type要素はどんな入力UIなのか(時刻、テキストフォーム、数字…)、そしてsubmitタイプはこのフォーム情報を実際にリクエストするためのボタンだ。

ところでリクエストを受けた時にどんな内容のものなのか、詳細を見たいよね。デバッグ用に/testというURLを作ろう。リクエストされるとそのリクエストの中身をレスポンスとして返すんだ。

# Webickのサーバ設定と起動
server = WEBrick::HTTPServer.new({:Port => 3000})
server.mount_proc('/products') do |req, res|
  res.body = list.map(&:inspect).join("\n")
end

server.mount_proc('/test') do |req, res|
  res.body = req.to_s # リクエスト情報を文字列化
end

trap(:INT){ server.shutdown }
server.start

さて、さっきのHTMLをローカルのどこかに保存してブラウザに開いてみよう。入力フォーム1つにsubmitボタン1つ。質素な佇まいだ、何か入力してからボタンを押してみよう。

POST /test HTTP/1.1
Host: localhost:3000
Connection: keep-alive
Content-Length: 13
Cache-Control: max-age=0
Origin: null
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.101 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: ja,en-US;q=0.8,en;q=0.6

textArea=neko

色々と情報が出て来るね。ここの情報も重要は重要だけど、今回は最後だけ注目してくれ。これはクエリ情報だ。つまりさっきフォームに入れた情報だね。これだけを取り出すことも出来る。req.queryとすることでクエリをHashオブジェクトで得られるんだ。これでProductを新規登録できるね!

HTMLを書き直してみよう。

<form method="post" action="http://localhost:3000/product">
  <p>名前 <input type="text" name="name"></p>
  <p>価格 <input type="number" name="price"></p>
  <p>在庫数 <input type="number" name="stock"></p>
  <input type="submit">
</form>

urlをtestからproductへ変えて入力フォームを増やしたよ。

require 'webrick'

# リソースのクラス定義
class Product
  attr_accessor :name, :price, :stock

  def initialize(attrs)
    attrs.each {|attr, value| instance_variable_set("@#{attr}", value) }
  end
end

# 初期データの設定
products = []
products << Product.new(name: "ぺんぎんのぬいぐるみ", price: 2900, stock: 1)
products << Product.new(name: "食ぱんクッション", price: 5600, stock: 21)

# Webickのサーバ設定と起動
server = WEBrick::HTTPServer.new({:Port => 3000})
server.mount_proc('/products') do |req, res|
  res.body = products.map(&:inspect).join("\n")
end

server.mount_proc('/product') do |req, res|
  q = req.query
  new_product = Product.new(name: q['name'], price: q['price'].to_i, stock: q['stock'].to_i)
  # 入力フォームの値が格納されるreq.queryのデータでProductオブジェクトを
  # 作成する。queryから取得する値は全て文字列型で取り出されるので数値は変換(to_i)が必要
  products << new_product
  res.body = new_product.inspect
end

trap(:INT){ server.shutdown }
server.start

さっそく、HTMLをブラウザで開いて適当なデータを入力してみよう! 無事に商品が登録されたかな? 商品一覧ページ(http://localhost:3000/products)へ行ってみよう。見事に商品が増えていれば成功だ!

#<Product:0x007fce73875c28 @name="ぺんぎんのぬいぐるみ", @price=2900, @stock=1>
#<Product:0x007fce738759d0 @name="食ぱんクッション", @price=5600, @stock=21>
#<Product:0x007fce73847350 @name="neko", @price=123, @stock=1>

ちょっとした問題

もしかして上手く行かなかったお友達がいるんじゃないかな。例えば日本語を入力したら文字化けしたとか! これはきっとエンコーディングのせいに違いない、きっとデコードすれば…と思ったお友達は実に優秀だ。確認して見るとこうなっている。

req.query['name'].encoding # 文字列のエンコード種類を調べる。
=> #<Encoding:ASCII-8BIT>

なぜかWebrickエンコードとしてASCII-8Bitを使ってくるようだ。Utf-8にしてくれ。が、試したところreq.query['name'].encode('utf-8')としても上手く行かない。force_encodingを使う必要がある。ここについて詳しく触れるのは避けよう。面倒なのでreq.queryハッシュの値すべてをutf-8で強制エンコードしてしまおう。

server.mount_proc('/product') do |req, res|
  req.query.each { |key, value| req.query[key] = value.force_encoding('utf-8') }
  q = req.query
  new_product = Product.new(name: q['name'], price: q['price'].to_i, stock: q['stock'].to_i)
  products << new_product
  res.body = new_product.inspect
end

これで日本語も上手く行っただろう。Hooray!

<< 前のチャプターへ戻る | 次のチャプターへ進む >>