Padrino入門した

Sinatraのapp.rbがどんどん大きくなって仕方がなくなってきた。
が、Railsまでいかないので、よくその中間と言われるPadrinoの門を叩いてみた。
Padrinoについてぼくが知っていることといえばビュッフェスタイルということだけである。

$ ruby -v
ruby 2.0.0p247 (2013-06-27 revision 41674) [x86_64-darwin12.3.0]
$ padrino -v
Padrino v. 0.11.3

アプリ名「Daisy

$ padrino generate project Daisy
      create
      create  .gitignore
      create  config.ru
      create  config/apps.rb
      create  config/boot.rb
      create  public/favicon.ico
      create  public/images
      create  public/javascripts
      create  public/stylesheets
      create  tmp
      create  .components
      create  app
      create  app/app.rb
      create  app/controllers
      create  app/helpers
      create  app/views
      create  app/views/layouts
      create  Gemfile
      create  Rakefile
    skipping  orm component...
    skipping  test component...
    skipping  mock component...
    skipping  script component...
    applying  slim (renderer)...
       apply  renderers/slim
      insert  Gemfile
    skipping  stylesheet component...
   identical  .components
       force  .components
       force  .components

=================================================================
Daisy is ready for development!
=================================================================
$ cd ./Daisy
$ bundle
=================================================================
$ cd ./Daisy/
$ bundle install --path vendor/bundle
app/app.rb
module Daisy
  class App < Padrino::Application
    get '/' do
      "Hello World"
    end
  end
end
$ bundle exec rackup config.ru
$ open http://localhost:9292/

Hello World!


中を見てたらPadrinoってほんとにSinatraだった。
Railsっぽい構成でcontrollerが作れるから、たぶんいまのプロジェクトでも乗り換えた方がわかりやすくなるなー。

Railsで2個めのDBを作ってマイグレーションする

Ruby on Rails使ってて、どうしても2個めのDBを作る必要があったのでメモ。
Railsのバージョンは4.0.0。
アプリ名は「Sakura」。

  • (1個目マイグレファイル)db/migrate/*

に加えて

  • (2個目DB名)sakura_second
  • (2個目マイグレファイル)db/second_migrate/*

としたい。

config/database.ymlに2個目の情報をかく

--- config/database.yml.org     2013-09-04 21:48:05.000000000 +0900
+++ config/database.yml 2013-09-04 21:48:54.000000000 +0900
@@ -37,3 +37,13 @@
   username: root
   password:
   socket: /tmp/mysql.sock
+
+# The second DB
+sakura2:
+  adapter: mysql2
+  encoding: utf8
+  database: sakura_second
+  pool: 5
+  username: root
+  password:
+  socket: /tmp/mysql.sock

テストとか開発用とか必要なら追加する。

config/environments/sakura2.rbを作る

config/database.ymlに"sakura2"を追加したので、RAILS_ENVの候補となるように、
config/environments/sakura2.rbを新規作成する。
その中でmigrationファイルを置くpathを変更する。

config/environments/sakura2.rb
Sakura::Application.configure do
  config.paths['db/migrate'] = ['db/second_migrate']
end

db/second_migrate/にマイグレファイルを作る

db/second_migrate/20130904121339_create_inveders.rb
class CreateInveders < ActiveRecord::Migration
  def change
    create_table :inveders do |t|
      t.string :name
      t.integer :age
    end
  end
end

2個目のDBをrakeで作る

$ bundle exec rake db:create RAILS_ENV=sakura2

2個目のDBにマイグレ

$ bundle exec rake db:migrate RAILS_ENV=sakura2


RAILS_ENVでマイグレ対象を切り替えられるようになった。

Rack::MethodOverrideを少しいじった話

よくある背景

よくある理由でIE対応を迫られて、ありがちな理由でPOSTなのにURLにパラメータがくっついてくるという
ありふれた問題にぶつかったまでは良かったのだが(悪かったのだが)
RESTfulなAPIに対応するという点で少しはまったのでメモ。

試したこと

最初はよくある解決策として、PUTで送りたいときに

POST
http://hostname/api/member/1?_method=put

と、POST_methodによるディスパッチを試みた。
とりあえすSinatraを使っていたので、公式サイトを見て

require "sinatra"

configure do
  enable :method_override
end

put "/" do
  "koreha put desu."
end

post "/" do
  "post desu. shippai desu."
end

と設定し、わくわくしながら試したけど、POST扱いになってしまうようだった。

$ curl -i -X POST http://localhost:4567/?_method=PUT
post desu. shippai desu.

調べた

実際に実装を覗いてみると、中でRack::MethodOverrideが使われていて、

#rack-1.5.2/lib/rack/methodoverride.rb

  method = req.POST[METHOD_OVERRIDE_PARAM_KEY] ||
    env[HTTP_METHOD_OVERRIDE_HEADER]

としてパラメータを取り出していた。
reqというのはRack::Requestオブジェクトだから、

#rack-1.5.2/lib/rack/request.rb

# Returns the data received in the request body.
#
# This method support both application/x-www-form-urlencoded and
# multipart/form-data.
def POST

というわけで、URLにくっついているクエリパラメータからは判断できないようになっていた。

解決策を考える

API内の一部の問題だったら、もう少し対応を考えたけど、
API全部が全部こんな感じだったので、素直に(?)Rack::MethodOverrideの方を再オープンして書き換えた。

require "sinatra"

configure do
  class Rack::MethodOverride
    def method_override(env)
#     req = Request.new(env)
      req = Rack::Request.new(env)
#     method = req.POST[METHOD_OVERRIDE_PARAM_KEY] ||
      method = req.params[METHOD_OVERRIDE_PARAM_KEY] ||
        env[HTTP_METHOD_OVERRIDE_HEADER]
      method.to_s.upcase
    end
  end
  enable :method_override
end

put "/" do
  "koreha put desu"
end

post "/" do
  "post desu. shippai desu."
end
$ curl -X POST http://localhost:4567/?_method=PUT
koreha put desu

動いた。

ちなみに

あと、Rack::Requestクラスを見て気がついたけど
paramsって

@params ||= self.GET.merge(self.POST)

ってURLのクエリパラメータとrequest bodyのパラメータが合体されているようである。
(今回はそれに置き換えた)

SinatraでJSONを返す

Sinatraと小さいAPIはとても相性がいい。
APIで返すものといえばJSONが最近は楽ですね。
まあ普通にrequire "json"して、#to_jsonすればよいのですが、
sinatra-contribにあるsinatra/jsonを使うのもよいかと思います。

Sinatraだけの場合

app.rb
require "sinatra"
require "json"

get '/' do
  content_type :json
  data = { foo: "bar" }
  data.to_json
end

Sinatra::JSONを使う場合

Gemfile
gem "sinatra"
gem "sinatra-contrib"
app.rb
require "sinatra"
require "sinatra/json"

get '/' do
  data = { foo: "bar" }
  json data
end

こんな感じ。

json_encoderオプション

json_encoderというオプションをjsonメソッドで渡すか、set :json_encoderで設定すればそのメソッドで最後仕上げてくれる。

# setで全体的な設定をする
set :json_encoder, :foooooooo

get '/' do
  data = { foo: "bar" }
  def data.foooooooo
    '{"hoge":"fuga"}'
  end
  # あるいは実行時に渡す(優先度はこっちが上)
  json data, :json_encoder => :foooooooo
end

json_content_typeオプション

これもjson_encoderと同じ形。APIで使うならデフォルトのまま

Content-Type: application/json;charset=utf-8

か、JSで使うように

# setで全体的な設定をする
set :json_content_type, :js

get '/' do
  data = { foo: "bar" }
  # あるいは実行時に渡す(優先度はこっちが上)
  json data, :content_type => :js
  #=> Content-Type: application/javascript;charset=utf-8
end

くらいがJSONP的な使いどころとして思いつくくらいでしょうか。

ドキュメントとちょっと違ってた

1

ドキュメントにはto_jsonのcallを試みると書いてあるけど、
特にそんなそぶりはない。

get '/' do
  data = { foo: "bar" }
  def data.to_json
    '{"hoge":"fuga"}'
  end
  json data
  #=> {"foo":"bar"}
end

実装も、デフォルトでは、MultiJSON#encodeが呼ばれて終わりだった。

小さいけど

content_typeのオーバーライドで

get '/' do
  json({:foo => 'bar'}, :encoder => :to_json, :content_type => :js)
end

とあったけど:encoderは:json_encoderの誤りだと思う。


ちょっと調べてからぷるりくしてみたい。

Sinatraとsinatra-contribのStreamingの違い

Sinatra標準のStreamingと、公式拡張sinatra-contribのStreamingの違いについてメモ。

Sinatra標準Streaming

使い方

# coding: utf-8
require "sinatra"

get '/' do
  stream do |out|
    out << "やあ<br>"
    sleep 1
    out << "こんにちは、<br>"
    sleep 1

    error_message = "お前はここで終わりだがな!"
    out.callback { p error_message }
    #=> closeされたときに実行される

    out << "世界"
  end
end

WEBrickだと対応してないので、thinで。

$ ruby classic_streaming.rb -s thin

まあデフォルトだと「<<」と「callback」しかできることはない。


他にはstreamに渡すときに第1引数をtrueにすると、そのコネクションが開きっぱなしになるらしい。
以下、超簡単なpub/sub実装。

require "sinatra"
connections = []

# subscribe側
get '/' do
  # trueならなんでもいい、そう、シンボルでもね。
  stream(:keep_open) do |out|
    connections << out
  end
end

# publish側
get '/send' do
  connections.each do |out|
    out << params[:message]+"<br>"
  end
  "send message: #{params[:message]}"
end

いいね!
おわり。

sinatra-contribのStreaming

これをもう少し拡張したのがsinatra-contribのStreamingで別途インストールが必要になる。

# Gemfile
gem "sinatra-contrib"

とはいえ、唯一の特徴がIOインスタンスっぽく扱える、というだけのようだ。


classicスタイルでの使い方

require "sinatra"
require "sinatra/streaming"

get "/" do
  stream do |out|
    out.write "妖怪が鍛えた<br>"
    sleep 1
    out.syswrite "この楼観剣に<br>"
    sleep 1
    out.write_nonblock "斬れぬものなど<br>"
    sleep 1
    out.puts "あんまり無い"
    out.putc(33)
    out.putc(33)
    out.printf("<%s>", "br")
    sleep 1
    out.print "#{out.pos} bytes"
  end
end

なんか公式の例で

out.flush

とできるようになっているけど、中身の実装はなかった。。。(華麗にスルー)


まあ特別IOっぽくしたいっていうことでも無い限り、標準の方で事足りると思う。

ちなみ

また、http://blog.uu59.org/2011-10-03-sinatra-stream.htmlの通り
ちょいちょい送信することを示す「Transfer-Encoding: chunked」がない(わざと実装していない?)みたいなので
あんまりきっちりしたことには使えないとも思う。

$ curl -i localhost:4567/
HTTP/1.1 200 OK
Content-Type: text/html;charset=utf-8
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Connection: close
Server: thin 1.5.1 codename Straight Razor

参考

http://blog.uu59.org/2011-10-03-sinatra-stream.html
ありがとうございます。

なぜRackのResponse BodyはStringではなくArrayなのか

run lambda { |env|
  [ 200,
    {"Content-Type" => "text/plain"},
    ["Hello World!"]
  ] }

この3つ目のbody。
PSGIでもそうだけど、なんでStringでも良さそうなのにArrayなのか。


ふと気になってググったら全く同じ内容がstachoverflowにあった。
ruby - Why is rack response body an array not a string? - Stack Overflow


元になってるWSGIから来てる理由と同じく、streamingに対応するためのようだ。
でっかいファイルを一気にメモリに読み込みたくないとか。

Stringだとどうしても1個だけという制限があるので、
イテレーションできるようにArrayになってるってことかな。

つまり

Rackの場合、3つ目のbodyは#eachを呼べて中身がStringならいいらしい。
ゆえに、

class Body
  def self.each
    yield "Hello World"
  end
end

run lambda { |env|
  [ 200,
    {"Content-Type" => "text/plain"},
    Body
  ] }

こういうのでも動く。

Sinatra Streaming Responses

Sinatra: README
Sinatraでちょっとずつclientに送るサンプルを見る。

get '/' do
  stream do |out|
    out << "It's gonna be legen -\n"
    sleep 0.5
    out << " (wait for it) \n"
    sleep 1
    out << "- dary!\n"
  end
end

実装としては3つ目のbodyのところにSinatra::Helpers::Streamオブジェクトがくる。
バージョンは1.4.3

sinatra/base.rb
class Stream
  def each(&front)
    @front = front
    @scheduler.defer do
      begin
        @back.call(self)
      rescue Exception => e
        @scheduler.schedule { raise e }
      end
      close unless @keep_open
    end
  end

  def <<(data)
    @scheduler.schedule { @front.call(data.to_s) }
    self
  end
end

@schedulerはenv["async.callback"]に何か入ってたらEMが使われるようになってた。
入ってなかったらダミーが動く。
WEBrickで動かなくてthinで動くのはたぶんその関係。
なるほどなー。