逃げる8回で会心の一撃

Web エンジニアのサトウリョウスケが開発とか色々書くブログです

my_api_client v0.13.0 をリリースしました🚀

つい先日、v0.12.0 をリリースしたばかり ですが、 v0.13.0 をリリースしましたので、含まれる PR の内容について解説していきます。 より詳しい使い方は README.jp.md をご参照ください。

github.com

#180 Stub response on raising error (@ryz310)

今回はこの PR のみの更新です。 仕事で spec 書いてる最中に「API リクエストで例外が発生した際にレスポンス内容を保存する処理のスタブ化できないじゃん」ってなって作りました。

my_api_client では作成した API Client クラスを stub_api_client または stub_api_client_all というメソッドでスタブ化できます。 例えば以下のような ExampleApiClient というクラスを定義した時:

class ExampleApiClient < MyApiClient::Base
  endpoint 'https://example.com'

  error_handling status_code: 400..499, raise: MyApiClient::ClientError
  error_handling status_code: 500..599, raise: MyApiClient::ServerError

  # GET https://example.com/path/to/resouce
  def request
     get 'path/to/resouce'
  end
end

stub_api_client_all を実行すると ExampleApiClientインスタンスが全てスタブ化されるようになります。 以下の例だと、 #request を実行した時、API から { "message": "Hello world!" } という JSON が返ってきた時と同じ振る舞いをするようになります。

stub_api_client_all(
  ExampleApiClient,
  request: { response: { message: 'Hello world!' } }
)

api_client = ExampleApiClient.new
response = api_client.request
response.message # => 'Hello world!'

で、 error_handlingステータスコード400..499 の時は MyApiClient::ClientError という例外が発生する、という定義になっているのですが、このような例外が発生した時のテストを書くために、 raise のスタブ化も出来るようになっています。

stub_api_client_all(
  ExampleApiClient,
  request: { raise: MyApiClient::ClientError }
)

begin
  api_client = ExampleApiClient.new
  response = api_client.request
rescue MyApiClient::ClientError
  puts '4xx error!'
end

大抵の場合、これらのスタブ化ができれば問題ないのですが、例外発生時のレスポンスを見たい、というケースも無くはないかと思います。 仕様として、例外インスタンス#params#matadata というメソッドからリクエストパラメータとレスポンスパラメータを参照できるようになっています。

従来のスタブ化メソッドでも一応指定できなくはなかったんですが、結構手間だったので raise オプションの指定を以下のように拡張しました。 raise と一緒に指定した responseAPI のレスポンスとして返されて、それが例外として処理された、というスタブ化になります。

stub_api_client_all(
  ExampleApiClient,
  request: { 
    raise: MyApiClient::ClientError,
    response: { error_code: 10 }
  }
)

begin
  api_client = ExampleApiClient.new
  response = api_client.request
rescue MyApiClient::ClientError => e
  e.params.response.data.error_code #=> 10
end

my_api_client は内部で Sawyer を使っています。

github.com

e.params.responseSawyer::Response をそのまま返しているので、Sawyer::Response#data からレスポンスボディを参照できます。 Sawyer::Response#data では、レスポンスの JSONOpenStruct) のようにメソッドアクセスできるように変換してくれます。

ただし、 V1.0.0Sawyer の依存を無くしたいと考えているので、いずれ #data を挟む書き方は変更になるかもしれません。 一応こういう使い方もできますよ、という新機能でした。

my_api_client v0.12.0 をリリースしました🚀

my_api_client v0.12.0 に含まれる PR の内容について解説していきます。 より詳しい使い方は README.jp.md をご参照ください。

#173 Avoid sleep on testing

my_api_client では以下のように書くと、任意の例外を補足して自動的に API リクエストをリトライしてくれます。 ネットワーク系のエラーとか、 API Rate Limit に引っかかった時とかに便利なやつですね。

ActiveJob の retry_on とほぼ同じ使い方になっています。

class ExampleApiClient < MyApiClient::Base
  endpoint 'https://example.com'

  retry_on MyApiClient::ApiLimitError, wait: 1.minute, attempts: 3
  error_handling json: { '$.errors.code': 20 }, raise: MyApiClient::ApiLimitError

  # GET https://example.com/users
  def get_users
    get 'users'
  end
end

それは良いんですが、 rspec 上で wait が効いてしまっていたので、上記のコードだとリトライ3回分 wait するので、合計 3 分も待たされてしまっていました。 一応 rspecsleep を stub するとかやれば回避できますが、そもそもテストでは wait を無視して欲しいですよね。

my_api_client では be_handled_as_an_error という rspec の matcher を用意しているのですが、今回の対応で、この macher を経由してリトライが実行された場合は wait を無視するようになりました。

RSpec.describe ExampleApiClient, type: :api_client do
  let(:api_client) { described_class.new }

  # NOTE: レスポンスで `{ "errors": { "code": 20 } }` を受診した際、3 回リトライが実行された後に `MyApiClient::ApiLimitError` として例外処理される。
  it do
    expect { api_request! }
      .to be_handled_as_an_error(MyApiClient::ApiLimitError)
      .after_retry(3).times
      .when_receive(body: { errors: { code: 20 } }.to_json)
  end
end

また、 ExampleApiClientstub_api_clientstub_api_client_all を使用するとスタブ化できます。 スタブ化した状態だと任意の API レスポンスを返すか、任意の例外を発生させる、という動作になってリトライが発生しなくなるので、上記の問題はありませんでした。

stub_api_client_all(ExampleApiClient, get_users: { users: [{ id: 1 }, { id: 2 }, { id: 3 }] })

response = ExampleApiClient.new.get_users
response.users # => [{ id: 1 }, { id: 2 }, { id: 3 }]

#175 Verify arguments on error handling definition

error_handling の定義でレスポンスのステータスコードを指定することができるんですが、このオプション名が status_code なのか status なのかをよく間違える、という問題がありました。$ rails g api_client を使用するとテンプレが作成されるので、そこからエラーハンドリングの定義を行うと間違えにくいのですが、後からエラーハンドリングを追加する時とかにやらかします。作者自身もたまにやらかしてました😇

# 正解
error_handling status_code: 400..499, raise: MyApiClient::ClientError

# 間違い
error_handling status: 400..499, raise: MyApiClient::ClientError

この PR の対応で間違ったオプションを指定すると以下のような例外が発生するようになりました。

RuntimeError:
  Specified an incorrect option: `status`
  You can use options that: [:response, :status_code, :json, :with, :raise, :block]

#176 Provides a syntax sugar of retry_on on error_handling

最初の PR でも出てきた retry_on ですが、 error_handling raise: MyApiClient::ApiLimitError でも同じ例外を指定していて DRY な感じじゃなかったり、retry_onerror_handling をそれぞれ定義してるとお互いの関連が実感しづらい、という不満がありました。

class ExampleApiClient < MyApiClient::Base
  endpoint 'https://example.com'

  retry_on MyApiClient::ApiLimitError, wait: 1.minute, attempts: 3
  error_handling json: { '$.errors.code': 20 }, raise: MyApiClient::ApiLimitError

  # GET https://example.com/users
  def get_users
    get 'users'
  end
end

この PR では retry というオプションを error_handling に追加しています。これにより、以下の 2 つのコードは等価になります。

retry_on MyApiClient::ApiLimitError, wait: 1.minute, attempts: 3
error_handling json: { '$.errors.code': 20 }, raise: MyApiClient::ApiLimitError
error_handling json: { '$.errors.code': 20 }, 
                          raise: MyApiClient::ApiLimitError, 
                          retry: { wait: 1.minute, attempts: 3 }

retry_on にオプションを指定する必要がなければ retry: true と書けば OK です。

error_handling json: { '$.errors.code': 20 }, 
                          raise: MyApiClient::ApiLimitError, 
                          retry: true

ただし、 retry オプションを使用する際は以下の点に注意が必要です。

  • error_handlingraise オプションの指定が必須となります。
  • Block を使った error_handling の定義は禁止されます。

私と gem

どーも、サトウリョウスケです。 金曜日に登壇した勉強会うっかり ブログ作るって言ってしまったので 10 年ぶりくらいにブログを復活させてみました。ブログのタイトルも 10 年前のタイトルと同じです(元ネタはファミコン時代のドラクエ IV)

勉強会の感想記事は近日中に書こうと思います✍️

この記事は Feedforce Advent Calendar 2019 の 15 日目です。

さて、最初の記事からいきなり会社のアドベントカレンダー記事になります🙏

adventar.org

昨日は Yutaka KAWAI さんの「コーヒーは科学である ~抽出器具による味の違い~ ペーパードリップ編」でした。

note.com

珈琲屋さんかな?ってくらい凄い記事でしたね☕️ 記事に出てきたドリッパーは全種類持ってるってヤバくないですか???(もちろん良い意味で)

ペーパードリップ編ってことは続編もあるのかな? この感じで記事が量産されたらそのうち書籍化されるかもしれません📚

本編

予告通り個人で gem を作る流れについて話そうと思うのですが、アドベントカレンダーから流れてくると非エンジニアの方もきっと読まれると思うので、あんまり技術的な話題にせずにフワッとした話でもしようかと思います。

そもそも gem ってなんだっけ?

そもそも gem っていうのは Ruby でできたライブラリの事でして、例えば僕が凄く便利なプログラムを書いて、それを gem として公開すれば、世界中の人が僕のイケてるプログラムを使えるようになる、というものです。まさに Win-Win しかない仕組み。gem は世界を救います。

一方で、プログラムってのは新機能が追加されたり、不具合が修正されたりして日々アップデートが繰り返されています。 「この機能にはバージョン 1.3 以降でないと使えません」とか「色々イケてない部分が多いからこの機能は廃止します」という変更もあるので、自分のプログラムは一体どのバージョンの gem を使っているのか、という話が物凄く重要だったりします。

gem にはどのバージョンを使っているのか(依存しているのか)という情報を管理する機能も備わっていますので、ある日突然新バージョンで挙動が変わっても「うちは一個前のバージョン使ってます!」という管理ができていれば動かなくなることはないのです。

とはいえ新バージョンでいきなりそんなトンデモ変更されたら困りますけども。

僕が初めて作った gem

自分が生まれて初めて作った gem はこの rubocop_challenger という gem です。

github.com

一般的に gem は他のプログラムと組み合わせて使う事が多いのですが、 rubocop_challenger は単体で動作するやつでして、実行したディレクトリ(フォルダ)にあるプログラムのソースコードを少しずつ綺麗に(人間にとって読みやすくしたり、書き方のルールを統一したり)してくれる、という gem です。

というと物凄い神 gem ですが、RuboCop というソースコードを解析してくれる便利な gem を内部で呼び出してプルリク(プルリクが何なのかはググってください)作ってくれる、という仕組みになっているので、「人間が毎日手作業でやらないといけなかった事を自動的にやる」というのが rubocop_challenger の提供する価値になります 🤖

詳しくはちょうど一年くらい前に会社のブログに書いたので、ご興味ありましたら是非。古い内容なので、現在のバージョンからは少しずれてますけど。

developer.feedforce.jp

ちなみにもうすぐ v2.0.0 をリリースする予定です。(pre バージョンですが、現時点でもすでに使えます)

ニッチな gem

初めて作ったのは rubocop_challenger という「人間が毎日手作業でやらないといけなかった事を自動的にやる」 gem でした。 完全に自分の会社のプロダクト用に作った gem でしたが、 Twitter などをエゴサしてると、ぼちぼち使って頂けているようです。 同じような悩みを抱えている人は世の中にはいるもんですね✨

とはいえ、 gem を公開したら世界中の人たちが使ってくれる、ということには中々ならないです。 一応頑張って英語で説明を書いたりはしていますが、個人の発信力には限界もありますし、何よりニッチです。

というか、個人が作る gem なんて大抵はニッチなものになります。 「あー、こんな gem あったらめっちゃ便利やん?」という gem は大抵世界のどこかの誰かが作ってます。 なので、今までにないような新しいgem を作ろうと思ったら大抵ニッチになります。

じゃあ gem を公開しても大して使ってもらえないし、あんまり意味ないじゃん、って思うかもしれませんが、意味無くはないんですよね👍

効能 1. スキルアップ

まず、めっちゃプログラムを書く勉強になります。

Ruby で Web 開発をしている人は大抵 Ruby on Rails を使って書いてると思います。ちなみに Ruby on Rails も gem です。 Ruby on Rails は凄くよく出来ているので、Web 開発の難しい部分を 9 割くらいの肩代わりしてくれます。

ところが、自分で一から gem を作ろうとすると、自分の力で解決しないといけないプログラム的な課題がめっちゃあります。 Web 開発はインターネット特有の課題が多いですが、 gem の開発には Web 開発以外の知識も色々要求されたりします。

何より、自分自身で仕様を一から考えないといけないので、普段の開発以上に意思決定量がめっちゃ多いです。 自分は rubocop_challenger 以外にもメンテナンスしている gem が 3 つほどありますが、これらの開発を通して日頃の Web 開発の品質も一段レベルが上がったな、と感じる事が多いです。

なので、gem の開発は自分自身の修行のためだと思ってやると良いかもしれません。誰かに使えてもらえたらラッキー、みたいな。

逆に使ってもらえる事をモチベーションにするとちょっと辛いかもしれないです。思った以上に流行らない。もっと流行れ!

効能 2. ポートフォリオ

gem を公開してるせいか、企業からのスカウトがめっちゃ来るようになります。

自分自身、会社の採用活動に関わる機会が多いのですが、例えばスカウト候補を探す際に候補者の GitHub は必ず見るようにしています。 その経験からですが自分で gem 書いて公開しているエンジニアは世の中の 1 割もいないんじゃないかな、って思っています。

自分も gem を作るようになったのはここ 1 ~ 2 年ですし、前職にいた頃だと普段の仕事の帰りが遅かったりもしたので家に帰ってから gem を作るような余裕もありませんでした。なので、本人の熱量や環境が整わないと gem の開発は難しいかもしれません。

しかしながら、自分も採用活動していて「お。この人すごいやん」ってなるのは GitHub や Qiita とかで何かしらアウトプットのある方なんですよね。 もちろんアウトプットが無くても実際会ってみたら凄かったって人は沢山いるんですが、採用活動だとその人の仕事でのアウトプットって見えないもんですから。。

別に gem じゃなくても良いんですが、自分自身のキャリア形成を意識するためのアウトプットの一環としてとても有用だと思います。

効能 3. 魔法のアイテム

自分が今年作った gem に my_api_client という gem がありまして、API Client を作るためのフレームワークなんですが、これはプロダクトのソースコードにも使っています。

自分が携わってるサービスは ソーシャルPLUS というものでして、企業の Web サービスとソーシャルログインプロバイダー( LINE とか Twitter とか)のハブになってるサービスなんですね。そのせいもあって、外部の Web API へリクエストするという処理が多く、毎回同じようなエラーハンドリングやリトライの処理を何度も書かないといけなくて大変だった訳です。

あと、エラーが発生した際に「ソーシャルPLUS」「ソーシャルPLUSを利用している企業」「ソーシャルログインプロバイダ」の誰が原因なのかを特定するための情報をログに残すとかも都度対応しないといけなくて超大変でした。

my_api_client はその辺の処理をすっきり簡単に書けるようにするための gem でして、すっきり簡単なもんだから、チームメイトが my_api_client を活用して自発的にガンガン課題を解決してくれる、という最高にホットな状況を産み出すことに一役買っております。

やっぱエンジニアも人間なので、普段の業務で忙しい中で改善活動も同時にやろうなんてモチベーションは普通は湧いてこない訳です。でも、これを使えばすっきり簡単だよ、っていう魔法のアイテムがあれば、みんなの重い腰を少しだけ軽くする事ができます。

my_api_client は自分の中でも割とよくできた gem なので、毎回こんな良い gem が作れる訳じゃないですけど、 gem を作ってチームの生産性が上がるってのはやっぱ魔法のアイテムだなぁって思うんです。gem は世界を救います!(2回目)

なお、 my_api_client については 銀座 Rails #10 で登壇したときの資料があるので貼っておきます。これも若干古いので、最新の仕様とは少し異なるかもしれません。 最新の仕様は こちら をご覧下さい。

効能 4. たのしい

最後はここに帰ってくるんですが、自分の gem を作るのはやっぱ楽しいのです。いきなり頭悪い文章になりました。いつから頭良い文章書いてると錯覚していた?

以前勉強会で「これまでどういうキャリアを意識してやってきましたか?」って若手のエンジニアから聞かれたのですが、自分みたいな 30 半ばのエンジニアって、エンジニアになった当初は今みたいにエンジニアが持て囃される時代でもなかったので、自分からエンジニアになろうって思った人は少なからず「ただプログラムが好きだった」っていう人が多いんじゃないですかね?わかんないですけど自分はそうでした。

自作の gem を作ってると「やっぱプログラム書くのって楽しい」ってのを思い出させてくれます。 自分は他にも趣味でバンドやったり絵を描いたりしていますが、プログラミングが子供の頃に好きだった工作に一番近いような気がしています。

中々プライベートで時間を作るのは難しいんですが、やはり楽しさが一番根底の原動力になっているのかもしれないですね。

まとまらないまとめ

会社のアドベントカレンダー向けだし、非エンジニアにも伝わるようなフワッとした文章にしようと思って書いてたら、途中から俺のポエムを書き殴ってただけになった気がしますが、役に立つかどうかは 2 の次にして、とりあえず gem 作ってみると楽しくスキルアップできるし、もしかしたら誰かの魔法のアイテムになってるかもしれないよ、というお話でした。

久々にゆる〜い文章書いてて自分的には楽しかったです 笑

さて、明日の Advent Calendar は?(CV. 加藤みどり

Feedforce Advent Calendar 2019、明日は上岡君が「野球についてor遠隔インターンについて」書いてくれるみたいです。

最近はインターンも遠隔で出来るんですね。野球も遠隔インターンも未経験のままおじさんになってしまったので自分には未知の領域です⚾️💨

乞うご期待!