今日はFaradayの話をする。
yao の開発の基本方針は、「必要のない依存をしない」だけれど、Faradayは数少ない依存gemに含まれている。
理由は、
- Faraday自体の依存gemが少ない
- 利用することでの、コードをスッキリさせるメリットが非常に大きい
と考えたから。
具体的には、FaradayはRackよろしくMiddlewareで機能追加をするパターンを採用しているため、ちょっとした仕事をさせるためのコードが、短く、わかりやすいものにできる。以下は具体例。
ログイントークンの保持と再発行
OpenStackのAPI利用の流れは、
- username、パスワード、テナント名(AWSでいうVPCひとつひとつみたいな感じ)を指定し、ログイン用エンドポイントを叩く
- そのエンドポイントから、ログイン用のトークンと、各サービス(compute、networkなど)それぞれのエンドポイントURL一覧をもらえる
- トークンとエンドポイント情報を利用してその後のAPIを叩く
という感じになっている。 OpenStack API Quick Start の通りである。
トークンは、 "ac4e1c05084ba4365c1c38bfb1350000"
みたいなMD5ハッシュ風の値で、これをヘッダに X-Auth-Token: "ac4e1c05084ba4365c1c38bfb1350000"
と含めばOK。なお、この認証方式、微妙にOAuth2と違うので既存のMiddlewareを使いまわせない...
今回は以下のようなMiddlewareを作った。
class Faraday::Request::OSToken def initialize(app, token) @app = app @token = token end def call(env) if @token.expired? @token.reflesh(Yao.default_client.default) end env[:request_headers]['X-Auth-Token'] = @token.to_s @app.call(env) end end Faraday::Request.register_middleware os_token: -> { Faraday::Request::OSToken }
ご覧の通り、これはなんというRack Middleware...という感じである。
ポイントは、 @token
自体は普通にTokenオブジェクトなので、expireしたかどうかの情報も所持している。なので、「Tokenがexpireしていたら、ハンドラーの中で勝手に再発行する」といった実装が可能になっているところ。
レスポンス JSON をそのままダンプする
OpenStackのドキュメントなどに想定されるレスポンスはあるんだけど、実際のものを見た方が理解が早い場合が多い。
こういうレスポンスを保存するやつとしては vcr/vcr · GitHub が有名だけど、リクエストも保存したりなど重量級なのでこんぐらいのコードで対応する。
class Faraday::Response::OSResponseRecorder < Faraday::Response::Middleware def on_complete(env) require 'pathname' root = Pathname.new(File.expand_path('../../../tmp', __FILE__)) path = [env.method.to_s.upcase, env.url.path.gsub('/', '-')].join("-") + ".json" puts root.join(path) File.open(root.join(path), 'w') do |f| f.write env.body end end end Faraday::Response.register_middleware os_response_recorder: -> { Faraday::Response::OSResponseRecorder }
レスポンスのMiddlewareの場合、Rackっぽくなくて、 on_complete(env)
が順次呼ばれ続けるというイメージ。その中でenvに副作用を起こせばいい。
このMiddlewareを有効にすると、通常通りAPIアクセスをしつつ、 PROJECT_ROOT/tmp
以下にそのままレスポンスのJSONが残る。
$ jq . < tmp/POST--v2.0-tokens.json { "access": { "token": { "issued_at": "2015-08-31T03:58:36.073232", "expires": "2015-09-01T03:58:36Z", "id": "31a5166533fd49f3b11b1cdce2000000", "tenant": { "description": "development environment", "enabled": true, "id": "b598bf98671c47e1b955f8c9660e0000", "name": "dev" } }, "serviceCatalog": [ { "endpoints": [ ....
エラー処理
OpenStack APIのエラーの扱いは、原則として 200..299
の範囲外のものであれば、ステータスコードに対応したエラーになる(300系はない。ないよね?)。
なので、アダプター側で共通化できる。
class Faraday::Response::OSErrorDetector < Faraday::Response::Middleware # TODO: Better handling, respecting official doc def on_complete(env) # Faraday::Env の組み込みメソッド # 本来、APIごとに正常なステータスコードの指定があるのだが、後でやる return if env.success? raise Yao::ServerError.detect(env) end end Faraday::Response.register_middleware os_error_detector: -> { Faraday::Response::OSErrorDetector }
module Yao class ServerError < ::StandardError def self.detect(env) case env.status when 400 if env.body && env.body["computeFault"] ComputeFault.new(extract_message(env.body), env) elsif env.body && env.body.to_a.join.include?('NetworkNotFound') NetworkNotFound.new(extract_message(env.body), env) else BadRequest.new(extract_message(env.body), env) end when 401 Unauthorized.new(extract_message(env.body), env) when 404 if env.body && env.body["itemNotFound"] ItemNotFound.new(extract_message(env.body), env) else NotFound.new("The resource could not be found.", env) end when 405 BadMethod.new(extract_message(env.body), env) # ... こんな感じで判定を頑張る end end end end
エラー種類の判定は、まあ頑張ってメタプログラミングしなくていいかと思ってこうしてる。この辺多分最近Go言語を書いてることが強く影響してそう。。
エラーについては、より注意深くハンドリングしたい場合もあるだろうが、その場合はRequest pathとか(というかenv自体)そういう情報も入れてるので、rescueしてそういうのを見るという設計方針で良さそう。
こんな感じでMiddlewareは簡単に書けて助かる。という話でした。
お知らせ
ノリで 0.0.2 をリリースしました。
yao | RubyGems.org | your community gem host
ConoHaのAPI でも動くんじゃないかという気がする。試してないけど。