今日はFaradayの話をする。
github.com
yao の開発の基本方針は、「必要のない依存をしない」だけれど、Faradayは数少ない依存gemに含まれている。
github.com
理由は、
- 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
def on_complete(env)
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 でも動くんじゃないかという気がする。試してないけど。