前記事 でOCamlやってくぞ、と書いたけど結局Rustです。
Bluesky
Twitter代替の候補として噂される(?)分散型SNS Bluesky。 現状ではまだprivate betaということで招待コードが無いと使えないのだけど、先月運良くコードをいただくことができて使い始めてみている。
一通りのSNS的な動きはしているものの、まだまだ鋭意開発中ということで足りていない機能があったり 日々新機能が追加されたりと目紛しく動いている世界のようだ。
AT Protocolとエコシステムの現状 (〜2023/04)
AT Protocolについてはこちら。
日本語では以下の記事が詳しいでしょう。
- AT Protocol (BlueSky Social)仕様解説 ~ W3C DID仕様を添えて ~ - Qiita
- 開発視点から見る、新しい分散型SNS「Bluesky」とAT Protocolの可能性 | gihyo.jp
ざっくりとした理解では、新しい分散型SNSのために「AT Protocol」が開発されていて、いま使っているBlueskyというSNSもこのAT Protocolに従って実装されているSmall-worldの一つである、という感じなのかな。
「AT Protocol」は様々なテクノロジーの総称という感じで、その中の汎用通信レイヤーとして「XRPC」というものがある。これを使ってサーバーと通信する、ということらしい。 BlueskyはWeb UIも提供されているが、DevToolsを覗いてみる限りでは確かにすべてのデータ取得や更新などこのXPRCのリクエストによって行われているようだ。
主要なプロトコル実装としてTypeScriptで書かれた atproto
があり、そのGo版ということで indigo
が bluesky-social
organizationによって管理されている。
その他の非公式の言語実装やClient, Applicationなどが以下のrepositoryにまとめられている。
言語としてTypeScriptがメインであり それを使ってすべての現存機能が動かせるので、Webフロントエンドで完結する、ということでWeb Clientが多く作られているようだ。
Rust版の実装
上記で紹介されているライブラリでRustのものは、(2023/04時点で)以下の adenosine
のみ。
このrepository ownerはBlueskyの中の人ではあるが、公式としてGitHubのorganizationに入れられはしなかった、ようだ。
Lexiconとコード生成
前述したXRPCによるメッセージングは、すべて Lexicon というスキーマシステムによって型が定義されている。 JSON Schemaのように記述されるJSONによって、すべてのリクエストやレスポンス、その中のデータ型やフィールド名などが定義される。
ので、これがAPI通信の仕様のすべて、ということになる。
前述した atproto
や indigo
のAPIライブラリは、このLexicon schemaを示すJSONファイルからコード生成によって作成されているようだ。
この仕組みで作っていない限り、どうしてもclient libraryは最新のAPIに追従できない、ということになる。AT Protocolはまだまだすごい頻度で変更が加えられていて、lexicon schemaも毎日、とはいかないまでも毎週くらいのペースでは何らかの変更がある。
上述の adenosine
は、このコード生成によって作られたものではないため、実際既に最近追加されたAPIは使えないようだった。
その他にも reqwest
の blocking client に依存している、とか 結局内部のxprc clientには serde_json::Value
を渡していて何でも自由にできてしまう、など 微妙だなと思う点もあったので、自分でまったく別のRustライブラリを作ってみよう、ということになった。
(一応他にもそれっぽいlibraryやrepositoryを検索して先行事例を調査してみたりもしたが、ちゃんとそういったコード生成によって作られているものはなさそうだった。)
自作Rust版実装: ATrium
AT Protocolのための小さなライブラリが集まり青空を望む中庭、ということでATrium、という名前に。最初は atprs
とか雑な名前で書き始めていたけど ChatGPT に「いいかんじのプロジェクト名ないですかね」と相談したところ、ATriumという名前を提案してくれたので採用した。最近 YAPC::Kyoto の会場でも聞いた単語だったし丁度いいな、と。
Lexicon schema
まずは必要になるのはLexicon schemaのJSONを読み込むためのライブラリ。 これ自身のJSON Schemaなどが存在するわけではなく、実際のvalidationは zodを使って書かれている ので、これを必死に翻訳する形で。
このJSON自体もかなり柔軟性があり、色んな入力が有り得るので大変…。いちおう type
の tag を使って判別可能なようにはなっているので、それを使って enum
に振り分けることでどうにか serde
で正しく読み込めるようになった。
Option
なfieldが多く、一度 deserialize してから元のJSONに同等に戻せるようにすると記述が大変だったので、serde_with
の skip_serializing_none
を使った。
union
で新しい定義を作られるところが、単純に enum
に変換すると一段深くなってしまうしtagがずれても困るし…ということで仕方なくいちいちコピーしてしまっているが、もっといい方法あるだろうか。…macroを使えば良いのか?
コード生成
ともかくLexicon schemaを正しく読み込むことができれば、あとはそこからRustコードに落とし込んでいける。
細かいところはまだ実装できていないが、基本的な object
などはだいたい書けている、と思う。
Lexiconではdefsの名前は camelCase
だが Rustコードでは 型名は PascalCase
で field名やmodule名は snake_case
にしたい、などの変換が必要だったので、 heck
というライブラリを使用した。
正しくnamespaceをマッピングさせていれば ref
はそのまま定義済みの型として参照できるが、 union
はまぁ厄介で、それ用の enum
を追加で定義して Lexicon解析のとき同様に $type
tagで判別して振り分けるようにする必要があった。
とりあえず正しいコードとして生成されているかどうかは cargo build
で確認できて 通ればまぁ動くだろう、という判断ができて便利だった。
API設計
型の定義だけならまだラクだが、Lexiconには query
もしくは procedure
のXRPCリクエストの定義が含まれる。
これは関数の実装を提供することになるが、そのためにはHTTP requestを実行するためのclientが必要になる、がAPIライブラリとしてはそういったものは含めたくない、と考えた。
実はRustでHTTP clientをマトモに使ったことが無かったが、 hyper
だったり isahc
だったり surf
だったり、いろいろあるらしい。ライブラリでClientも含めることになると、どれを使うか判断する(もしくはすべてサポートするなどの)必要がでてきてしまう。
ので、それらは回避して HttpClient
というTraitを定義して、それを実装するものを外部から注入する形にした。
各XRPCリクエストは「HttpClient
をSupertraitとする、デフォルト実装を持つTrait」となり、外部で実装したClientがそれらの実装を宣言すれば各リクエストが使えるようになる、ということになる。
#[async_trait::async_trait] pub trait HttpClient { async fn send(&self, req: http::Request<Vec<u8>>) -> Result<http::Response<Vec<u8>>, Box<dyn Error>>; }
最初こういうのもライブラリとして存在しているのでは、と探して http-client
というのがあったので使おうとしたが、同梱されている HyperClient
を使ってみたら依存のtokio
のバージョンが古すぎて動かず、またこのTraitで使われている http-types
も良いけど http
を使ったほうがより汎用的で良いですよ、とChatGPTが教えてくれたので自前で簡単に定義することにした。
ともかく、この形にすることで、ライブラリのユーザーは一手間はかかるけど自分の好きなHTTP clientを使って各種XRPCリクエストを送ることができるようになっている。はず。
とりあえずこうして作った APIライブラリをまず atrium-api
という名前でpublishした。
docs.rs https://crates.io/crates/atrium-api
Lex
ちなみに Lexicon schema解析のための型定義だけのもので一つのライブラリとして切り出して作っていて、まだ publishはしていないけどやる予定。
上記の atrium-api
の設計が気に入らない、という人はこれを使って自分でコード生成を作って別のAPIライブラリを作ってくれれば良いと思います。
CLI
作成したAPIの動作実験も兼ねて、 mattn/bsky
のようなCLIも作ってみている。
これはこれで便利、なのかな? バイナリ配布だけしてみても良いかもしれない。
今後
まだまだ未完成で未実装のところも多いので埋めていく予定。 あとは本家のLexicon更新を検知して自動でコード生成してpushして publishまで…が自動化できても良さそうだけど そこまでは無理か? ちょっと調べてやってみたいところ。
あとは…折角Rustで動かせるようになるならTauriとか使ってGUIアプリまで作れると良いのかな… さすがにそんな余裕は無さそうだけども。
ともかくちゃんと使ってもらえるかもしれない(現時点で22 starsとかだけど)OSSっぽいものを初めて作った気がするので、まずは色々整備していきたいところ。