成らぬは人の為さぬなりけり

エンジニアライフをエンジョイする為のブログ

Faradayを触ってみた

実はこれが、2014年最初のエントリー。サボりすぎ。

WebAPIを呼ぶスクリプト書くのにFaradayを使ってみたので、使い方などをメモっておこうと思います。 Faraday自体は前から知ってたんですが、リダイレクトとかめんどくさい事考えなくて良い場合に、 特にHTTPクライアントのライブラリ入れて使うより、Net::HTTPで十分じゃない?と思って使った事ありませんでした。 なんとなーく、Ruby書きたくなって、使ったことないgemを使ってみたかったので、使ってみた次第です。

ちなみに、読み方は ふぁらでい なんでしょうかね?

環境

使用するライブラリのバージョン

gem 'faraday', '~> 0.9.0'
gem 'sinatra', '~> 1.4.5'
gem 'sinatra-contrib', '~> 1.4.2'

今回のコードはこちら

Github yagince/faraday-sample

今回の流れ

  • まずはWebAPI作っとく
  • Faradayの基本的な使い方
  • Middlewareを使ってみる
  • Middlewareを作ってみる

まずはWebAPI作っとく

とりあえず、簡単にJSONでも返してくれるAPIが欲しいので、何でも良いのですが、 サーバサイドのデバッグもしてみたいので、自前で用意してみます。 といってもすっごい簡単な物で良いので、Sinatraさんにご協力頂き、さくっと作ります。

require "sinatra"
require "sinatra/reloader" if development?
require "sinatra/json"

get "/test" do
  json name: :hoge, age: 100
end

post "/test" do
  json params
end

post "/test.json" do
  json JSON.parse(request.body.read)
end

ポイント

  • reloder入れとくと、コード変えた時に再起動の必要なくなります。(Sinatra::Contrib)
  • 単純にrequire "json" して { key: value }.to_json 返しても良いんですが、それだけだとresponseのcontent-typeがtext/htmlになるので、Sinatra::Contribsinatra/jsonを使います
    • provides: :jsonすれば良いのですが、めんどくさいので

Faradayの基本的な使い方

先程作ったSinatraアプリは起動しておきます。

GETしてみる

require "faraday"
require "json"
require "pp"

res = Faraday.new(:url => "http://localhost:4567").get("/test")
body = JSON.parse(res.body)
pp body

※status:200以外の場合の事はまったく考えてません

とまぁ、こんな感じでgetする訳なんですが、これだと、Net::HTTPでもたいして変わらないんですよね。

require "net/http"
require "uri"
require "json"
require "pp"

res = Net::HTTP.get URI("http://localhost:4567/test")
body = JSON.parse res
pp body

URLを分割してなくて良い分、こちらの方が楽かも?。 ただ、Faradayの良い所はこれだけじゃわからないので、先に進みます。

POSTしてみる

Faraday

client = Faraday.new(:url => "http://localhost:4567")
res = client.post "/test", { hoge: 100, foo: :bar }
body = JSON.parse res.body
pp body

Net::HTTP

res = Net::HTTP.post_form URI("http://localhost:4567/test"), { hoge: 100, foo: :bar }
body = JSON.parse res.body
pp body

と、まぁ、ここまでもあまりメリット感じませんね。

Content-Type変えてみる

jsonをpostしてみます。

Faraday

client = Faraday.new(:url => "http://localhost:4567")
res = client.post do |req|
  req.url '/test.json'
  req.headers['Content-Type'] = 'application/json'
  req.body = { hoge: 100, foo: :bar }.to_json
end
body = JSON.parse res.body
pp body

Net::HTTP

uri = URI("http://localhost:4567/test")
req = Net::HTTP::Post.new uri, initheader = {'Content-Type' =>'application/json'}
req.body = { hoge: 100, foo: :bar }.to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: false) do |http|
  http.request req
end
body = JSON.parse res.body
pp body

少し楽になりましたね。 実はもっと楽になります。

Middlewareを使ってみる

FaradayにはMiddlewareという仕組みがあります。(Rackと同じような仕組みになってる) こいつを使う事で、拡張できるようになってます。 ここには、一番最初に書いてあるんですけどね。

request/responseをログ出力してみる

client = Faraday.new(:url => "http://localhost:4567") do |faraday|
  faraday.response :logger
  faraday.adapter  Faraday.default_adapter
end
res = client.get "/test"
body = JSON.parse res.body
pp body

ちなみにmiddleware使う場合、adapterは必ず指定しないとエラーになるっぽい??です。 ソースまで追ってないですが。

出力結果

I, [2014-05-10T23:45:34.114079 #12029]  INFO -- : get http://localhost:4567/test
D, [2014-05-10T23:45:34.114147 #12029] DEBUG -- request: User-Agent: "Faraday v0.9.0"
I, [2014-05-10T23:45:34.117054 #12029]  INFO -- Status: 200
D, [2014-05-10T23:45:34.117099 #12029] DEBUG -- response: content-type: "application/json"
content-length: "25"
x-content-type-options: "nosniff"
server: "WEBrick/1.3.1 (Ruby/2.1.1/2014-02-24)"
date: "Sat, 10 May 2014 14:45:34 GMT"
connection: "close"
{"name"=>"hoge", "age"=>100}

こんな感じで出力されます。

FaradayMiddlewre

faraday_middlewareっていうgemが用意されていて、使えるmiddlewareが既に結構用意されています。 これを使うと、さっきのjsonパターンは数倍楽になります。 bundler使っている場合は、Gemfileに以下を追加してインストールしておきます。

gem 'faraday_middleware', '~> 0.9.1'

使ってみます。

require "faraday"
require "faraday_middleware"
require "json"
require "pp"

client = Faraday.new(:url => "http://localhost:4567") do |faraday|
  faraday.request :json # ここ
  faraday.adapter Faraday.default_adapter
end
res = client.post "/test.json", { hoge: 100, foo: :bar }
body = JSON.parse res.body
pp body

middlewareでrequestのmiddlewareとして:jsonを入れとくだけで

  • postする時のbodyをjsonエンコード
  • Content-Typeヘッダをapplication/jsonにする

っていう事をやってくれます。

この辺でハマったこと

こんな事をするとContent-Typeapplication/jsonになりません。

client = Faraday.new(:url => "http://localhost:4567") do |faraday|
  faraday.request :url_encoded
  faraday.request :json
  faraday.adapter Faraday.default_adapter
end

この辺で、ちゃんとソース読んだ方が良さそうなので、見てみます。 まずは、

https://github.com/lostisland/faraday/blob/master/lib/faraday/rack_builder.rb#L87

この辺。

requestにmiddlewareの設定を渡すと use_symbol -> use という流れでここに来ます。 @handlersというインスタンス変数にpushしていってますね。

次は、

https://github.com/lostisland/faraday/blob/master/lib/faraday/rack_builder.rb#L162

この辺。

app -> to_app という流れでここに来ると、先程の@handlersreverseしてinjectしてます。 その中でbuildしているので、Hanlderbuildを見ると、

https://github.com/lostisland/faraday/blob/master/lib/faraday/rack_builder.rb#L47

klassをnewして(引数にapp渡してる)返してますね。

つまり、最初useした時は

という順番で追加しているので、これをreverseしてbuildすると

  • jsonをnew
    • 大本のapp(to_appに渡してるlambda)を持ってる
  • url_endoeをnew
    • jsonのappを持ってる

https://github.com/lostisland/faraday/blob/master/lib/faraday/rack_builder.rb#L150

結果、↑ここの@appはurl_encodeのオブジェクトになる為、

  1. url_encode
  2. json
  3. 大本のapp

という順番で実行されるわけですね。

Middlewareを作ってみる

というわけで、とりあえず簡単なMiddlewareを作ってみたいと思います。 デフォルトのloggerだとbodyを出力してくれないので、bodyを出力するmiddlewareを書いてみます。

require "faraday"
require "json"
require 'logger'

class BodyDumper
  def initialize app
    @app = app
    @logger = Logger.new(STDOUT)
  end

  def call env
    dump_body("request", env)
    @app.call(env).on_complete do
      dump_body("response", env)
    end
  end

  private
  def dump_body phase, env
    @logger.info("#{phase} : #{env.body}") if env.body
  end
end

client = Faraday.new(:url => "http://localhost:4567") do |faraday|
  faraday.request  :url_encoded
  faraday.use BodyDumper
  faraday.adapter  Faraday.default_adapter
end
res = client.post "/test", { hoge: 100, foo: :bar }

まとめ

FaradayはMiddlewareなどでの拡張性(柔軟性)がうりな気がします。

(どっかのWebAPIを呼ぶクライアントを作ったりとか向いてるかも)

adapterも結構用意されていて、em_httpもあるみたいですね。

あとはfaraday_middlewareoatuh2のmiddlewareがあって、 Authorizationヘッダー埋め込んでくれたりとか、地味に便利でした。

もう少し踏み込んで、拡張して使ってみたいと思います。

次回は気が向いたら、parameterのescape周りを修正してみたい、、、かも。 デフォルトだと半角スペースが+にescapeされるので。