HTTPieのような使い勝手でちょっとだけカスタマイズできるコマンドラインAPIクライアントを作った

最近、仕事のほうでAndroid Management APIってのをさわってて、お試しでAPIを叩くときの開発体験があんまり良くないなーと感じ、自分用にクライアントライブラリを書いてみた。

何が不満?

Android Management APIは、このとおり、本当に素直なREST APIだ。

developers.google.com

  • ベースURLは共通
  • 認証はサービスアカウントで認証トークンさえもらってきたら、あとはそれをAuthorizationヘッダに付けるだけ
  • device, enterprise, などのCRUDが本当にリソースフル
    • GET /enterprises/{enterpriseId}/devices/{deviceId} のようにIDパラメータを指定してリソースにアクセスできるし、更新はGETの代わりにPATCHを使うだけ、削除はDELETEを使うだけ。

GUIであれば、PostmanとかInsomniaのような便利なツールもあるが、コマンドラインで作業するときにはcurlやHTTPieに頼らざるを得ない。

そうなると、

  • ベースURLは毎度入力しないといけない
  • 認証トークンは3600秒でexpireするため、毎時間、トークン取得APIをわざわざ叩いて、わざわざAuthorizationヘッダにセットしないといけない
    • なんかの割り込みタスクで一時間が経つと、401が返る。あぁつらい
    • シェルのコマンドヒストリーから再実行すると基本的にトークン期限切れて401。あああああつらい
  • /enterprises/xxxxxxxxx/devices/yyyyyyyy のxxxxxxxの部分を変えてAPIを叩くときに、カーソルを移動させるのが面倒くさい

などなど、結構ストレス。とくに401エラー、お前は本当にストレスだ。

どうなったら嬉しい?

エンジニアたるもの、不便なものを「不便だなー」とストレス感じたままなのは良くない。便利にしよう。どんな物が欲しいかを定義してみる。

基本的にはHTTPieの使い勝手で、

パイプ入力でボディを送れる。

jo enterpriseDisplayName=ほげ | http PATCH /enterprises/xxxxxx

クエリパラメータやヘッダは、コマンドラインパラメータで指定可能。ただし! "Authorization: Bearer $TOKEN" みたいな " をいちいちつけなくて済むようにしたい

http GET /enterprises/xxxxxx pageSize=5 Authorization: Bearer xxxxxx

あとは、最大の不満ポイントの401はどう解決するか。

AndroidのOkHTTPなどはどうやっていたかを思い出すと、AuthenticatorとかInterceptorとかを使ってネットワークリクエストの前処理としてAuthorizationヘッダを付けたり後処理として401の際のトークンリフレッシュ&リトライをしたりしていた。 Authenticator - OkHttp - OkHttp

汎用的なAPIクライアントとしてコマンドラインオプションであれこれするよりは、専用のCLIコマンドをサクッと作れるような形にして、認証ロジックやログ出力などはHTTP Interceptorのようなものをプログラムで書いて埋め込めると良さそう。

イメージ的には、このくらいの記述で、

async function authIfNeeded(call, request) {
  const accessToken = await getCachedAccessToken() || await fetchAccessToken()
  request.headers['Authorization'] = `Bearer ${accessToken.token}`

  let response = await call(request)

  if (response.status == 401) {
    const accessToken = await refreshAccessToken(accessToken)
    request.headers['Authorization'] = `Bearer ${accessToken.token}`
    response = await call(request)
  }

  return response
}

const app = createClient('androidmanagement',
  baseURL: 'https://androidmanagement.googleapis.com/v1',
  interceptors: [
    authIfNeeded,
    logging,
  ]
)

app.handle(process.argv)

androidmanagement GET /enterprises/xxxxxx/devices/yyyyyy とかそういうのが叩けるイメージ。

実装言語は何がいいか?

なんとなく、自分以外の人にも使ってもらおうとすれば、npm installとか pip installみたいな操作自体をあまり要求したくはない。

シングルバイナリで渡せるのがベスト、妥協するにしても、npx androidmanagement GET /enterprises/.... のようにインストールしないでさっと使えるものが望ましい。

ということで、候補としてはTypeScriptとDartをあげた。(というか両方作ったww)

JSON APIを最も雑に扱えるのがJavaScriptシリアライズ/デシリアライズがほとんどいらないで、オブジェクトプロパティアクセスができる)なのだけど、フレームワークを型/型ヒントのない言語で実装するのは流石に気が引けたので、あまり好きじゃないけどTypeScript。

あとは、シングルバイナリにコンパイルできるけどGoほどきつくないDart(個人の好みが大きいw)。

まずはTypeScriptで作ってみた

github.com

github.com

Android Management API以外にも対応できるよう、coreとAndroid Management APIクライアントは分けた。

感想としては、非常にいまいち。

  • TypeScriptがnpmとの親和性悪そう
    • 一度JSにバラしてライブラリ公開したものを、再度TypeScriptから読み込む、みたいなのが若干モヤッとする
    • npxでGitHubリポジトリを指定するには、TypeScriptじゃだめでJavaScriptGitHub上にGit管理下に置く必要がある
  • GET with bodyができない
    • Android Management APIでは困らないが、いずれElasticsearchのクライアント作りたいなと思ったときに困る

コードを書いているときのTypeScriptはそんなに不満はなかったが、いざnpmに出すときに、TypeScriptをそのままアップできないのが非常にモヤッとした。自分がTypeScriptそんなに好きじゃないという要因は大いにありそうだが。

Dartでも書いてみた

Dartはばっちり型のある言語なので、JSのコードをそのまま持っていくと、データクラスだらけになるため、ほんの少しだけオブジェクト指向っぽくしている部分はある。でもほとんどJSのコードそのまま。

github.com

Dartは大好きなのだが、実際書いてみると、そこまで生産性の高い言語じゃないなーーという気分にもなった。Kotlinのようなdata classはやっぱりほしい。

ネーミングが...

TypeScriptのほうは @zatsu/androidmanagement, Dartのほうは rakuda

ぜんぜん一貫性もなく、名前を聞いてもあまり印象には残らない。うーむ、、自分ネーミングセンスなさすぎ。。。orz

まとめ

自分のためのプロダクト開発で、TypeScriptとDartを少しだけ学んだ。以上。

(ぜんぜんまとまっていないw)

入力フォームや設定画面をつくるときのデータ構造アンチパターン

「なんで?」と聞かれるとうまく言語化できてない部分が多いけど、経験的に不吉な香りがするやつ。

デフォルトがtrueの選択肢

f:id:YusukeIwaki:20211112020238p:plain

デフォルトtrueの選択肢は、falseとNULLを厳密に区別しないといけなくなる。 item.value || true なんて書いた瞬間にバグる

  • ユーザがもともと全部チェックを明示的に外していた場合には、次回以降のフォーム描画は初期表示がunchecked
  • でも、 選択肢を1個増やすことになった場合には 、その増えた選択肢の項目は初期表示がchecked

falseとNULLを気をつけて区別したら、もちろん実装できなくはない。ただ、JavaScriptRubyなど、多くの言語ではfalseとNULLを区別しないほうが条件分岐が楽に書けるはずだ。

if item.value.nil?
  true
else
  item.value
end

こんな条件文、いちいち書きたい??俺はいやだ。

可能な限り、文言などで工夫して、デフォルトfalseで設計したい。

nullableなBoolean

これもnullとfalseの区別という観点だが、「ユーザが何も設定していない状態」がnullで、「ユーザが明示的に設定をオフにした状態」がfalse みたいになっていると、ややこしい。

たとえば(こんなシステムはないとは思うが)

  • ?in_stock=true で在庫がある商品を絞り込み
  • ?in_stock=false で在庫切れ商品を絞り込み
  • in_stockパラメータがない場合には絞り込まない

のようなシステム。in stock をチェックボックスで実装すると、在庫切れ商品で絞り込めない。

Booleanは確実にNonNullにしよう。

現在の状態を表すモデルと、更新フォームの値を保持するモデルが同じ

更新フォームと現在の状態を表すモデルは、Railsなどのフレームワークを使うと、同じモデルで済ませてしまうことが多い。

これだと、以下のように値を更新しない場合のバリデーションで分岐が必要になる。新規レコード作成時だけバリデーション、など。

ユーザー名
_Yusuke Iwaki___________

パスワード(変更しない場合には空白のままでOK)
________________________

あとは、RailsであればActiveRecord::Dirtyで多少の回避はできるが、ユーザが値を変更していないのに更新通知を出してしまうなどの事故も起きやすくなる。

更新フォームは、素直にフォームオブジェクトを使うようにしよう。

ほかにも...

他にも思いついたものはいろいろあった気がするが、いざ書いてみるとなかなか思い出せない。

思い出したらそのうち書く。

Kaigi on Rails 2021で「Railsのシステムテスト解剖学」の話をした

10/22(金)に、Kaigi on Rails 2021に登壇した。登壇者としての回想日記。

kaigionrails.org


登壇のきっかけ

なんだかんだで1年半くらいRuby向けにブラウザの自動操作ライブラリを作っている - YusukeIwakiのブログ

この記事を書いていた頃、実はRubyKaigi 2021にCFPを出していたのだけど、結果はrejected。

ブラウザの自動操作にSelenium以外の選択肢を増やしたいという感じの内容だったのだけど、まーRuby的な面白さはなにもないし、もしかすると「それはRailsの界隈で話せばいいんでは」と解釈されたのかもしれない。ともかく、RubyKaigi 2021のCFPはrejected。

ただ、自分の中ではそれなりに頑張っていた内容だったので、さすがにどこかで供養しておかねばと思った。

で、その週にたまたま参加していた Fukuoka.rbの夜会で、大倉さんがKaigi on RailsのCFP大募集されていて、自分の中で「RubyKaigiのCFPを、少しRailsの文脈を厚めにしてKaigi on RailsにCFP出してみようかなー」となった。

登壇テーマの "システムテスト解剖学" を決めるまで

実は最初はこんな真面目なテーマではなく、「まだCapybaraで消耗してるの?」みたいなタイトルを考えていたw

「6月に銀座Rails#34で発表したときには、PlaywrightベースのCapybaraドライバの話をしていたじゃないか」と言われたらそのとおり。

RailsのSystem specから 🎭Playwrightを使う - Speaker Deck

けど、実は自分は今のCapybaraがあまり好きではない。

実際にKaigi on Railsの登壇の中でも話したのだけど、 Capybara DSLを使うことで、Playwrightの旨味であるauto-waitingなどの特性が何も使えなくなってしまう からだ。

あと、Capybaraは開発体制にも大きな問題があって、GitHub上のteamcapybaraという組織には、T.Wapoleさん1人だけしかいない。メンテナー1人でCapybaraという超巨大なOSSを世話するのが大変なのはわかる。なんだけど、issueやPRに対してあまり友好的ではなくて、どちらかというと上からな対応ばかりしていて、個人的にはそこがあまり好みじゃない。このOSSはこのままだと未来があまり明るくない気がしている。

それでも、Capybaraは本当にソースコードリーディングが大変なので、少しでも改善したいなと思い、不要なコードを削除するPull Requestを出したりもしたんだけど、案の定、改善を受け入れる気が全くなさそうだった。

CapybaraもRSpecのように coreとDSLとそれ以外のGem分割をするなど必要と自分は考えていたのだけど、取っ掛かりの小さい改善すらも全く理解されなかったので、もうCapybaraにコントリビューションする気はない。

で、"システムテスト解剖学" はどこから?

さて、ものっすごく脱線してしまったがw 少し本題に戻そう。

Railsシステムテストの世の中の使われ方を見ていると、わりとみんなCapybaraでハマっていることがわかった。テストが不安定だとか、最初のテストでWebpackコンパイルタイムアウトするとか。

Capybaraの将来があるかどうかは一旦おいといて、 こんなソースコードリーディングが難しいOSSをみんな自分で読まないといけないのはつらすぎる。Capybaraの内部構造を体系的に説明してみんなが理解すれば、Railsシステムテストはもっと合理的に活用できるはずだ。

そんなわけで、「まだCapybaraで消耗してるの?」だとアンチCapybaraっぽさが際立ってしまうしCapybaraの内部構造にフォーカスしてる感がゼロなので、いろいろオブラートに包んで「システムテスト解剖学」というタイトルに決めた。

ネーミング重要。重要なので結構こだわった。

コアメッセージとストーリー構成を決める

"システムテスト解剖学" としてまず伝えたいことは何かを考えた。

まだCapybaraで消耗してるの?のタイトルのときから一貫して「Capybaraを思考停止で使わないでくれ」ということは伝えたかったのだが、「じゃあどうすればいいのか?」を同時に伝えないと単なる批判で終わってしまい、聴衆には何も刺さらない。

昨年の動画を参考にする

ソースコードリーディングが大変なところのイロハを説明する発表は、去年にもあった。

Viewがレンダリングされるまでの技術とその理解 www.youtube.com

この発表はどういうメッセージを主軸にしているか見てみたところ、「RailsがなくなってもRailsがやっていたことを知っていれば自分たちでなんとかできる」のようなことを言っていた。

 

 これだ!

  「Capybaraがなくなっても、Capybaraがやっていたことを知っていれば、自分たちでシステムテストをなんとかできる」

2つのメッセージを組み合わせる

  • Capybaraを思考停止で使わないでくれ
  • Capybaraがなくなっても、Capybaraがやっていたことを知っていれば、自分たちでシステムテストをなんとかできる

は全然違うことを言っている。これを単純にマージしたら「Capybaraを思考停止で使わずに、中身を知っておいて、いざCapybaraが開発停止してもどうにかできるようにしようよ」のようになるだろう。

だがしかし、前提として、世の中のRailsエンジニアの大半はそもそもCapybaraがそこにあるから使えばいいと思っているし、他に選択肢がないと思っている。

"システムテスト解剖学"というキャッチーなタイトルを付けておきながら、一番伝えたいことが「Capybaraを思考停止で使わずに、中身を知っておいて、いざCapybaraが開発停止してもどうにかできるようにしようよ」だとしたらあまりにもギャップが大きいし、「Capybaraを使わない意味がわからない」と反感を買うだけだ。

f:id:YusukeIwaki:20211029230207p:plain

もう少し深堀り。

  • Railsエンジニアは、そもそもなんでシステムテストを使うのか。実際使ってみたらどうなのか。
  • 一番伝えたい内容は、そもそもどういう経緯でそこに至ったのか

f:id:YusukeIwaki:20211029225954p:plain

次に、 どこまでが事実でどこからが解釈かを整理 (余談だが、これはクラウドワークス時代に教わった考え方で、未だにこの考え方は大事にしているw)

f:id:YusukeIwaki:20211029230653p:plain

事実だけに着目してみると「RailsはE2Eテストをカンタンに導入できる仕組みを用意した」ゆえに「不安定なテスト」が生み出される、というストーリーがうっっっすらと浮かび上がってくる。

これを逆説的にした「RailsはE2Eテストは便利だけど、技術選定して使っていこうね!」というのをメッセージの主軸と進めればよさそう。

こうして、システムテスト解剖学で伝えるべきコアメッセージがようやく決まった。

ストーリー構成

コアメッセージは決まったので、あとはとにかく

思い当たるところをメモで書き留めて → つなげてみて → 削ぎ落として

の繰り返しだ。

「思い当たるところをメモ」は、移動中に突然メモしたくなったときにはGoogle Keep、思考整理しつつやるときはiPadの標準のメモ(Apple Pencilが使えるから)と使い分けた。iPhoneユーザだったらこんな使い分けは要らないのだろうけど、自分はAndroid(Galaxy Note)愛用者なのでGoogle Keepを使わざるを得なかった。

iPadでのApple Pencil手書きメモは結構重宝して、こんなかんじで書いたり消したりで、全部で12ページくらい。

f:id:YusukeIwaki:20211030104004p:plain

f:id:YusukeIwaki:20211030104201p:plain

結果、スライドの枚数が120枚を超える超大作になってしまったw (後悔はしていない)

speakerdeck.com

登壇準備

登壇といっても、今回はオンライン開催のため、ビデオを予めYouTubeにアップロードするという選択肢が選べた。ので、それをありがたく活用させてもらった。

ビデオを撮影するにあたり1つ問題があって、我が家はすぐ横を電車が通る。しかも頻度が5〜8分に1本なので、30分の動画を撮影する最中に最低でも4回は電車の騒音が入ってしまう。

どうしたかというと、コールセンター用のヘッドセットを中古でポチった。(んで、ビデオ撮影が終わったらメルカリで売ったw)

www.jabra.jp

これは中古でもかなり高くて2万以上した。ただ、それ相応の性能はあって、実際に動画を見てもらえればわかると思うけど、本当にノイズが何も入っていない。Jabra Engage 65はマジで神機

神機なので、ビデオ撮影が終わっても日頃の在宅業務でもずっと使っていこうかと思ったんだけど、iPadでは気軽に使えない(あくまで電話機やPCと接続するためのヘッドセットである)ことがわかったので、泣く泣くメルカリで出品。元値が高いので、そこそこのお値段では売れた。

完全に余談だけど、いまはJabra Evolve2 65というヘッドセットを使っている。Jabra Engage 65ほどのノイズ除去力は残念ながらないけど、電話会議くらいであれば問題ない感じ。次もし何かの機会でビデオ撮影が必要になったら、またJabra Engage 65を中古でポチるかもしれないw

DEMOの用意

自分はそこまで会話能力が高い方ではないので、30分間しゃべりつづけるだけだと聞いてる人が暇してしまう。

最初は安直に

  • Capybaraなしで自動操作する
  • CapybaraとPuppeteerを協調的に使って自動操作する

などをデモで動かすことを考えたんだけど、「あぁ自動操作してるねー」にしか見えなくてやめたw

そもそも発表をじっくり聞く人はCapybaraでの自動テストをもう知ってるだろうから、今回はあえてニッチなActiveRecordのコネクション共有のところを実感できるデモをすることにした。

  • 同一プロセスでテストランナーとRailsサーバーが動くときに限って、テストデータがブラウザから見える
  • Pumaマルチプロセスモードだとダメ
  • rails cしてもテストデータは見えない

あたりをデモで見せるためのコンテンツを実際に作った。

github.com

完全に余談だけど、初めてReact Hooks使ってコードを書いたけど、めっちゃ直感的で書きやすくて良かった。

使われなかったデモコンテンツたち

スライドを作る上では役に立ったけど、デモには登場しなかった子たちを一応まとめて葬っておく。

Capybaraを単体でいろいろいじって自動操作するサンプル

github.com

ActiveRecord::TestFixtures(コネクション共有の内部実装)の挙動を実感するためのサンプル

github.com

CapybaraなしでRailsシステムテストをするサンプル

github.com

本番

動画は提出済みだったので、発表時間帯はTwitterを眺めていたw あとは、発表後にQ&Aセッションがあったけど、誰も来なかったので、playwright-ruby-clientのコードを書いていたw

ライブコーディングみたいなのをrebako上でして待っていればよかったかなーとか今になって思いついたが、後悔先に立たず。

仕事とか私用とかで、残念ながらKaigi on Rails 2021はあまり参加できなかったのだけど、

このあたりの発表は自分の中でだいぶグッときた。他の発表もふくめて、動画公開されたら順に見ていこうと思う。

(・・・とおもったら、ブログ書くのがおそすぎて、もう公開されちゃってた...w Kaigi on Railsの方々の仕事の速さには脱帽です)

まとめ

「まだCapybaraで消耗してるの?」に始まり、ストーリーを練って、下調べもめっちゃして、登壇駆動で ものすごく勉強した気がする。

システムテストというニッチな領域の話だったので、一部のメンバーにだけすごく刺さる感じでストーリーを練ったのは、Twitterの反応を見る限り狙い通りできたかなと思う。

初学者から熟練者まで集うKaigi on Railsという場で登壇する機会をいただけて、本当に良かった。主催者の皆様には本当に感謝です。ありがとうございました。


www.youtube.com