読者です 読者をやめる 読者になる 読者になる

ローファイ日記

出てくるコード片、ぼくが書いたものは断りがない場合 MIT License としています http://udzura.mit-license.org/

Faradayの話 - OpenStack クライアント開発日記 (3)

今日は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: 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 でも動くんじゃないかという気がする。試してないけど。