2016-10-25

MetaMindのRuby Clientをリリースしました

MetaMindのRuby Clientが無かったので作っちゃいました。

インストール&使い方

gem "metamind"

でインストールして、以下のようにして使います

require 'metamind'
 
# client = MetaMind::Client.new(cert: '{certificate path', 
#                              password: '{certificate password}', 
#                              email: '{emai for metamind account}')
client = MetaMind::Client.new(private_key: '{private key path}', 
                              password: '{private key password}', 
                              email: '{email for metamind account}')
client.get_all_datasets

certかprivate_keyどちらかのキーは必須。

苦労したところとか

MetaMindはファイルアップロード以外のAPIでもContent-Typeをmultipart/form-dataの指定をしなければならないのが面倒でした。

net/httpを利用するとなると簡単に対応できそうなライブラリがなかったので、以下のように手作り感のあるmultipart/form-dataで対応しました。Fileの方は間違ってるかも(素直にBase64とかにした方が良かったですかね)

def build_multipart_query params
  parts = []
  params.each do |k, v|
    lines = []
    if v.is_a?(File)
      lines << "--#{@boundary}"
      lines << %Q{Content-Disposition: attachment; name="#{k}"}
      lines << "Content-type: image/#{File.extname(v)[1..-1]}"
      lines << "Content-Transfer-Encoding: binary"
      lines << ""
      lines << v.read
    else
      lines << "--#{@boundary}"
      lines << %Q{Content-Disposition: form-data; name="#{k}"}
      lines << ""
      lines << v
    end
    parts << lines.join(CRLF)
  end
  parts.join(CRLF) + "#{CRLF}--#{@boundary}--"
end

Hashをクエリパラメータにする方法はActiveSupportのHash#to_queryを使うのがセオリーになっているっぽいですが、そのためだけにActiveSupportをインストールするのは微妙かな、と思ったのでcore_extのHashの特定部分だけ少し修正して使っています。オープンクラス便利だなー

class Object
  def to_query(key)
    "#{CGI.escape(key.to_s)}=#{CGI.escape(self.to_s)}"
  end
end

class Hash
  def to_query(namespace = nil)
    collect do |key, value|
      unless (value.is_a?(Hash) || value.is_a?(Array)) && value.empty?
        value.to_query(namespace ? "#{namespace}[#{key}]" : key)
      end
    end.compact * "&"
  end
end

HTTPリクエストのテストをする際のスタブはWebMockが使いやすいです

stub_request(:post, 'https://api.metamind.io/v1/oauth2/token')
  .to_return(body: { access_token: 'ACCESS_TOKEN_EXAMPLE'}.to_json, 
             status: 200, 
             headers: {'Content-Type' => 'application/json'})

# 何かしらのAPIコール
expect(WebMock).to have_requested(:post, 'https://api.metamind.io/v1/oauth2/token').
  with(body: { grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', assertion: jwt }.to_query,
       headers: { 'Content-Type' => 'application/x-www-form-urlencoded' })

ただし、WebMockはmultipart/form-dataに対するbodyのアサーションは対応していないようで、アサーションをかけようとすると以下のエラーが発生します。

WebMock does not support matching body 
for multipart/form-data requests yet

JWT生成のときにTime.nowを使っているため、テストコードではTimecopを利用。以下のように書くと、ブロック内のTime.now等の値が固定される。

Timecop.freeze(Time.now) do
  # ...
end

JWT生成はjwtライブラリが楽です。MetamindのAPIの認証方式はOAuth2.0のJWT bearer token flowになので、そこで使っています。

jwt = JWT.encode({
                    iss: 'developer.force.com',
                    sub: @email,
                    aud: 'https://api.metamind.io/v1/oauth2/token',
                    iat: Time.now.to_i,
                    exp: Time.now.to_i + 3600
                 }, @private_key, 'RS256')

Rubygemの作り方やgemspecの書き方は以下のサイトが詳しいです

あとはgem pushするときにmasterブランチにupstreamブランチを設定しておかないとエラーになるとか

metamind 0.1.0 built to pkg/metamind-0.1.0.gem.
Tagged v0.1.0.
Untagging v0.1.0 due to error.
rake aborted!
Couldn't git push. `git push ' failed with the following output:

fatal: The current branch master has no upstream branch.
To push the current branch and set the remote as upstream, use

    git push --set-upstream origin master

その他参考URL

このエントリーをはてなブックマークに追加