rust-api-design について
Rust のコーディングエージェント向けに、crate の API 設計と互換性の責任を扱うスキルです。公開範囲、トレイト、ジェネリクス、依存型の公開、下流の呼び出し側に影響する変更を解説します。
主な機能
- 01公開 API の範囲を明確化
- 02具体型、ジェネリクス、`dyn` の比較
- 03トレイトと sealed trait の設計
- 04semver を破る API 変更の整理
- 05ドキュメントと互換性の確認
活用シーン
- 01Rust crate の公開 API レビュー
- 02拡張可能なトレイトと依存関係の設計
- 03リリース前の互換性評価
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ rewrite-rs/ skills --skill rust-api-designインストール後、Claude Code で/rust-api-designを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
DEPENDENCY-INJECTION.mdSEMVER.mdSURFACE.mdagents/openai.yaml詳細
Rust API 設計
クレートのルートから pub として公開するものはすべて、下流の呼び出し元に対する約束です。このスキルでは、呼び出し元から見え、依存されるもの、つまり公開 API の範囲、トレイトとジェネリクスの形、そしてどのような変更がその約束を破るのかを扱います。
公開 API は契約です
まず非公開にし、意図的に公開することを基本とします。pub fn が返すという理由だけで型を pub にしても、それは実装や公開フィールドも含めて API の一部です。pub(crate) を習慣にし、lib.rs から公開 API を再エクスポートして、モジュールパスは非公開にします。
// lib.rs
pub use crate::internal::parser::Parser; // the surface
mod internal; // the tree stays private
Parser にアクセスできるパスは一つだけです。モジュール名の変更、ファイルの移動、クレートの分割は、どれも破壊的変更にはなりません。外部から見えないからです。
公開 API では、次の3つの形を避けます。同じ項目を複数のパスで公開すること、公開シグネチャに Arc、Rc、Box、RefCell を含めること、そしてシグネチャに依存先の型を含めて、その依存関係まで semver 契約の一部にすることです。詳しくは SURFACE.md を参照してください。
依存関係を取り込む
段階を低い順に並べると、具体型はそのまま渡します。ラッパー構造体なら、シグネチャを変えずに内部を差し替えられます。ジェネリックパラメーターは静的ディスパッチを可能にしますが、それを保持するすべての型に影響が広がります。dyn Trait なら、その影響の広がりを止められますが、オブジェクト安全性が必要です。現在の段階で要件を表現できない場合にだけ次へ進みます。テストだけの必要性を理由にしてはいけません。ADR 0006 は /rust-testing の範囲です。各段階のコストは DEPENDENCY-INJECTION.md を参照してください。
ジェネリクスと dyn
| 要望 | 選択肢 | コスト |
|---|---|---|
| 単相化による速度、インライン化、仮想関数テーブルなし | impl Trait / <T: Trait> | コードの肥大化。呼び出し箇所ごとにインスタンス化されます |
| 異種コレクション、プラグインレジストリ、バイナリの縮小 | dyn Trait | 仮想関数テーブルによるディスパッチ。トレイトはオブジェクト安全である必要があります |
| 引数の位置、呼び出し側の利便性 | impl Trait 引数 | 呼び出し側はターボフィッシュ構文を使えません |
| 公開 API の戻り値の位置 | 名前付き型または impl Trait | impl Trait は型を隠します。これは意図的な選択であり、近道ではありません |
オブジェクト安全性で決まることが多いです。ジェネリックメソッド、値として Self を返すメソッド、またはオブジェクト経由で使う関連定数があると、dyn は使えません。異種レジストリは、列挙型またはトレイトオブジェクトなしではジェネリックにできません。
トレイト設計
トレイトとは、呼び出し元がジェネリックコードを書く際の基盤となる、共有動作の定義です。関連する関数をまとめるための名前空間ではありません。必須メソッドは少数にとどめ、残りにはデフォルト実装を用意します。必須メソッドを追加するたびに、将来のすべての実装者に対する約束が増えます。実装者ごとに妥当な選択肢が一つなら関連型(Iterator::Item)、複数ならジェネリックパラメーター(From<T>)を使います。包括的な実装にも注意してください。impl<T: Foo> Bar for T があると、Foo を満たさない型に対する後続の impl Bar for SomeType は追加できなくなります。この罠が表面化するのは包括的な実装を書いたときではなく、2つ目の実装を書いたときです。
シールドトレイト
シールドトレイトは、下流の実装者を破壊することなくクレートがメソッドを追加できるようにするためのものです。スーパー・トレイトを非公開モジュールに置くことで、このクレートだけが実装できるようにします。一方で、下流のコードから境界としてトレイト名を指定することはできます。記憶を頼りにこのパターンを再現すると間違えやすいため、ここに示します。
struct Widget;
mod sealed {
pub trait Sealed {}
}
pub trait WidgetBuilder: sealed::Sealed {
fn new() -> Self;
}
impl sealed::Sealed for Widget {}
impl WidgetBuilder for Widget {
fn new() -> Self {
Widget
}
}
破壊的変更になるもの、ならないもの
代表的な例を挙げます。公開列挙型へのバリアント追加は、網羅的なマッチを破壊します(列挙型が #[non_exhaustive] でない場合)。必須トレイトメソッドの追加は実装者を破壊しますが、デフォルト実装を追加しても破壊しません。#[non_exhaustive] でない公開構造体へのフィールド追加は、構造体リテラルによる構築を破壊します。引数型を狭めると呼び出し側を破壊し、戻り値型を広げると以前の型を前提にプログラムを書いた呼び出し側を破壊します。見落としやすいものも含む全表は SEMVER.md にあります。
対象外の項目
この API が返すエラー型の形は /rust-errors の範囲です。引数型を不変条件を持つニューメリック型にするべきかどうかは /type-driven-design の範囲です。公開関数を async にするべきか、またそれによってクレートに何が求められるかは /async-rust の範囲です。pub unsafe fn が正当化されるかどうか、およびその契約をどう文書化するかは /unsafe-rust の範囲です。
検証
cargo doc --no-deps # every public item documented and the links resolve
cargo clippy --all-targets # add -- -D warnings only if the repo has no lint config
cargo test
リポジトリで設定されている lint レベルで clippy を実行します。サーフェスチェックに合格させるために、missing_docs やほかの lint を強制的に有効にしてはいけません。代わりに提案してください。クレートが公開されている場合、または下流の利用者がいる場合は、エージェントが実行して確認する semver チェックは次のとおりです。
cargo semver-checks check-release # requires cargo-semver-checks; skip with a note if unavailable
ユーザー環境にツールを副作用としてインストールしてはいけません。cargo-semver-checks が見つからない場合は、その旨を伝え、SEMVER.md の表を参照してください。