shuttle.rs を使用して数値化した行動のロギング API を開発およびデプロイする

statscribe API とは

shuttle.rs を使用して日々の行動を数値化して記録する API を開発しました。体重/体温/歩数といった個人的な数値から、売上/プロジェクトの KPI といったチームで共有したい数値を蓄積して記録した上で、一連のデータを取得することができます。

日々の積み重ねは長期的に個人に大きな変化をもたらします。より望ましい自分となるために、この習慣化を手助けすることが私の個人事業の目標です。習慣化の第一歩として、積み重ねたい行動を記録することはそれ自体が継続を促すという点で価値があります。

開発アルファ版なので認証機能のかわりに自由に発行できるトークンで個人を識別します。また、活動の種類をタグで識別します。クライアントは未実装なため、xh + jq を使用していますが、実装の折には取得した数値を (e)chart.js もしくは R などを使用して可視化することを検討しています。

xhs の作成

xh に対して xhs というシンボリックリンクを張ることで HTTPS アクセスが可能になります

➜ echo $(dirname $(which xh))
/Users/kyagi/.cargo/bin

➜ (cd $(dirname $(which xh)) && ln -s ./xh ./xhs)

➜ ls $(dirname $(which xh))/{xh,xhs}
/Users/kyagi/.cargo/bin/xh  /Users/kyagi/.cargo/bin/xhs

API リクエストとレスポンス

➜ xhs get -b statscribe.shuttleapp.rs/token | jq -cr .
{"token":"feab32fd-cd48-462c-90d8-80544bc3cc7a"}

➜ xhs post -b statscribe.shuttleapp.rs/stats stat:=77.4 tag=scale token=feab32fd-cd48-462c-90d8-80544bc3cc7a | jq -cr .
{"id":3,"stat":77.4,"tag":"scale","datetime":"2024-07-01T08:54:27.989593+09:00"}

➜ xhs post -b statscribe.shuttleapp.rs/stats stat:=77.1 tag=scale token=feab32fd-cd48-462c-90d8-80544bc3cc7a | jq -cr .
{"id":4,"stat":77.1,"tag":"scale","datetime":"2024-07-01T08:54:36.051251+09:00"}

➜ xhs post -b statscribe.shuttleapp.rs/stats stat:=76.5 tag=scale token=feab32fd-cd48-462c-90d8-80544bc3cc7a | jq -cr .
"feab32fd-cd48-462c-90d8-80544bc3cc7a"}'
{"id":5,"stat":76.5,"tag":"scale","datetime":"2024-07-01T08:54:44.826053+09:00"}

➜ xhs post -b statscribe.shuttleapp.rs/stats stat:=4129 tag=pedometer token=feab32fd-cd48-462c-90d8-80544bc3cc7a | jq -cr .
{"id":6,"stat":4129.0,"tag":"pedometer","datetime":"2024-07-01T08:59:57.811817+09:00"}

➜ xhs get -b statscribe.shuttleapp.rs/stats/feab32fd-cd48-462c-90d8-80544bc3cc7a/all | jq -cr '.[]'
{"id":3,"stat":77.4,"tag":"scale","datetime":"2024-07-01T08:54:27.989593+09:00"}
{"id":4,"stat":77.1,"tag":"sclae","datetime":"2024-07-01T08:54:36.051251+09:00"}
{"id":5,"stat":76.5,"tag":"scale","datetime":"2024-07-01T08:54:44.826053+09:00"}
{"id":6,"stat":4129,"tag":"pedometer","datetime":"2024-07-01T08:59:57.811817+09:00"}

➜ xhs get -b statscribe.shuttleapp.rs/stats/feab32fd-cd48-462c-90d8-80544bc3cc7a/all | jq -cr '.[] | select(.tag == "scale")'
{"id":3,"stat":77.4,"tag":"scale","datetime":"2024-07-01T08:54:27.989593+09:00"}
{"id":4,"stat":77.1,"tag":"scale","datetime":"2024-07-01T08:54:36.051251+09:00"}
{"id":5,"stat":76.5,"tag":"scale","datetime":"2024-07-01T08:54:44.826053+09:00"}

実装時に苦労したところ

実は Rust で API を作成するのは 2 回目です。前回は actix-web を使用して Zero To Production In Rust を読みながら AWS EC2 のコスト計算 API を開発しました。今回は axum を使用しましたが、作りとしては似ているところが多いような気がしました。ただ以下のような根本的な Rust 力のなさを痛感することになりました。

  • chrono, chrono_tz を使用した時間処理に手間取りました。DB へは Utc で保存し表示する時だけローカルタイムにしたいのですが、正しい方法がわからず、とりあえずは Tokyo.from_utc_datetime で JST にしています。
  • データベースには UTC で保存し、表示する時だけローカルタイムにしたかったので前者用に Stat 後者用に StatView とそれぞれの型を用意しました。これに作成時(HTTP POST 時) の id 未発行の StatNew もあわせると同じような型が 3 つも存在することになりました。そして、レスポンス時に Stat から StatView に手動で変換しているというイケてないことをしています。ここは From and Into にリファクタリング予定です。
  • cargo build でエラーになった時にそのエラーがコード依存なのかライブラリ依存なのかわかりにくいことがありました。エラーメッセージを正しく理解して何が原因なのかを把握するためには Rust の言語仕様への理解を理論として深めつつ、実践での経験値を積んでいく必要がありそうです。また、crate の公式ドキュメントも一通りさらいつつ (trait の implementor や axum_core::response::ItoResponse が trait として他の型ではどう実装されているのか(Imprementations on Foreign Types)、また同じクレート内ではどう実装されているのか(Implementors > In axum_core::response > structs) などを追う必要がありました(それでも十分に理解できたとは言い難いです)
  • cargo の知識が不足していたため cargo build で出てきたエラーメッセージに対して Cargo.toml でどう指定すればいいのかわからず、ググった結果を雰囲気で指定してうまくいくまで試すような行き当たりばったりの試行が多くなってしまいました(Rust の chrono::NaiveDateTime 型を PostgreSQL での TIMESTAMP 型として格納したい場合など)。features はデフォルトでオフ ということさえ知りませんでした...

shuttle.rs + axum の開発体験

今回はじめて axum および shuttle.rs が提供しているライブラリ(crate)の shuttle-axum, shuttle-runtime, shuttle-shared-db を使用しました。 shuttle.rs の操作が cargo のサブコマンドとして組み込まれており、開発からデプロイまでの一連の作業をサポートしてくれます。また Github で公開している サンプルプログラム集 も参考になります。

開発用のプラットフォームとしてローカル環境での実行からデプロイまで一通り必要なコマンドは揃っています(例: cargo shuttle run cargo shuttle deploy cargo shuttle resource) 。cargo に組み込まれた shuttle.rs プラットフォームへの操作を通して shuttle.rs を Rust での Web アプリケーション開発におけるエコシステムとして使用することができます。結果的に sqlx を [shuttle.rs]が提供している共用 PostgreSQL を操作するために使用したり axum もより shuttle.rs に合わせた形で使うことになり、積極的にこの敷かれたレールに載ってしまう方が開発効率がよいと感じました。

shuttle.rs のブログでは OpenAIAmazon Bedrock との連携、さらには Clerk を使用した認証機能の例も紹介されており、全体的にフットワークが軽く新進気鋭な雰囲気を感じることができます。個人的には分散 sqlite3 である Turso と連携できるところが気になっています。

次の一手

テストの実装と postman を使用したテストに着手します。また認証処理(JWT or Bearer Auth or Clerk) も必要になります。curl + jq では手間となる部分を吸収したクライアントも作成するとより簡単に使えるようになるのかなと考えています。token 保存や tag のタイポ防止、R と連携した合計や平均値の自動出力、(e)chart.js を利用したグラフ化などを通して習慣化をエンパワーメントできればと考えています。

参考情報