mcpField
R
rust-api-design

rust-api-design

github.com/full-stack-skills開発12117 回インストール5 週間前に更新
リポジトリ

rust-api-design について

Rust API Design は、Rust API Guidelines に基づいて Rust の公開 crate API を設計するスキルです。crate 作者が使いやすく予測可能で型安全なインターフェースを作り、将来の変更余地を確保するのに役立ちます。

主な機能

  1. 01命名と変換の規約
  2. 02型、トレイト、ジェネリクスの選択
  3. 03newtype と型安全な引数
  4. 04イテレーターとトレイトの設計
  5. 05API の将来の変更への備え

活用シーン

  1. 01公開 crate API のレビュー
  2. 02変換メソッドやアクセサーの設計
  3. 03曖昧な引数をドメイン型に置き換える
  4. 04将来の変更に備えた公開インターフェースの設計

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/full-stack-skills/rust-skills --skill rust-api-design

インストール後、Claude Code で/rust-api-designを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

agents/
openai.yaml
examples/
anti-patterns.md
examples/golden-api/
Cargo.lock
Cargo.toml
examples/golden-api/src/
lib.rs
references/
api-guidelines-checklist.md
future-proofing.md
naming-and-conversions.md

詳細

Rust API 設計(Rust API ガイドライン)

権威ある情報源:Rust API Guidelines — std、tokio、serde、bevy で採用されている、約 100 項目の C-* ルールからなる事実上の標準チェックリストです。各ルールの正式な文言については、完全なチェックリストを参照してください。

このスキルが対象とするのは、公開 Rust API の形状です。具体的には、型、トレイト、命名、変換、そして API の使いやすさ、扱いやすさ、破壊変更への耐性を保つ境界です。Cargo マニフェスト(rust-cargo-build)、ワークスペースの構成(rust-workspace)、クレート内の src/ 構成(rust-module-layout)、semver/公開(rust-semver)は対象外です。

対応範囲

✅ 得意分野
  1. API Guidelines の約 100 項目の C-* ルールを、公開クレートの具体的な設計判断に落とし込む
  2. std およびエコシステムの慣習に合わせて、型、関数、メソッド、フィーチャーに名前を付ける
  3. ジェネリクス、トレイトオブジェクト、具体型、newtype パターンから適切なものを選ぶ
  4. From/Into/TryFrom/AsRef/Borrow の変換を設計する(Deref によるポリモーフィズムは避ける)
  5. どの自動トレイト(Debug、Clone、Eq、Hash、Send、Sync)を導出するか選ぶ
  6. トレイトをシールし、#[non_exhaustive] を付け、構造体のフィールドを非公開にして、将来の拡張余地を確保する
  7. bool パラメーターを enum に、数値を newtype に、String を &str に置き換える
  8. イテレーター、Extend、Default、Display/FromStr を適切に設計する
  9. 公開 API での panic!/unwrap/expect を避け、代わりに Result を使う
⚠️ 前提条件
  1. Rust の所有権、トレイト、ライフタイム — rust-stable を参照
  2. モジュールの可視性と再エクスポート — rust-module-layout を参照
  3. [features] と optional = true を設定する Cargo マニフェスト — rust-cargo-build を参照
❌ 対象外
  1. semver のルールと公開ワークフロー → rust-semver
  2. Clippy の lint 設定 → rust-style-clippy
  3. doctest と rustdoc コメント → rust-documentation
  4. ワークスペースの構成 → rust-workspace

データのプライバシー

このスキルはユーザーデータを収集、保存、送信しません。


8 つの章(API Guidelines の各章に対応するセクション)

Guidelines は 11 章で構成されています。このスキルが対象とするのは、設計に関する 8 章です。ドキュメント、マクロ、必須事項は、それぞれ rust-documentation、rust-macros、rust-cargo-build が対象とします。

1. 命名(C-CASE、C-CONV、C-GETTER、C-NAMING)

C-CASE — 大文字・小文字の慣習
項目ケース例
型(struct/enum/trait)、モジュール、クレートUpperCamelCaseHttpClient、tokio
関数、メソッド、ローカル変数、フィールドsnake_casesend_request
定数、static 変数SCREAMING_SNAKE_CASEMAX_RETRIES
ジェネリック型1 文字の UpperCamelCase、または短い単語T、K、V、Req
ライフタイム短い 'a/'b、または説明的な 'src'src
フィーチャーkebab-case(Cargo が強制)serde-json

場当たり的な略語は避けてください。BufferedReader は std の規約に準拠していますが、BufRdr は準拠していません。

C-CONV — 変換メソッドの命名
変換接頭辞借用するか例
低コスト、借用as_はい(&self → &T)as_slice、as_bytes
低コスト、所有to_(確保なし)/into_(self を消費)場合によるto_vec、into_bytes
高コスト、新しい値を返すto_いいえ(&self → T)to_lowercase
消費するinto_いいえ(self → T)into_string、into_iter
失敗する可能性があるTryFrom/TryInto—u32::try_from(byte)

要件:(1) as_X という名前で所有値の X を返すメソッドは誤りです。to_X に変更してください。(2) 可変借用に対応する逆向きの名前は as_X_mut/X_mut です。

C-GETTER — アクセサーの命名
// ✅ C-GETTER compliant
pub struct Buffer { data: Vec<u8> }
impl Buffer {
    pub fn data(&self) -> &[u8] { &self.data }       // no get_ prefix
    pub fn data_mut(&mut self) -> &mut [u8] { &mut self.data }
    pub fn len(&self) -> usize { self.data.len() }   // not get_len
    pub fn is_empty(&self) -> bool { self.data.is_empty() }
}

get_ が許容される例外:Cell::get、Map::get(実際の検索を行う場合)。

2. 相互運用性(C-COMMON-TRAITS、C-CONVERT、C-ITER、C-SERDE)

C-COMMON-TRAITS — 明らかなトレイトを導出する

すべての公開型について、Debug、Clone、PartialEq、Eq、PartialOrd、Ord、Hash、Defaultを導出すべきか検討します。

トレイトデフォルト例外
Debugはいシークレットの場合は、値を隠す手動のDebug実装にします
Clone安価ならはい複製にコストがかかる場合は、省略してその旨を文書化します
Copy16バイト以下かつ損失がない場合のみ—
PartialEq/Eq全順序が成立する場合ははいf64はEqにできません
HashEqの場合ははいEqとの整合性が必要です
Default自然な空またはゼロの値がある場合ははい—
C-CONVERT — From/Into/AsRef/Borrowを使い、Derefによる多相性は避ける
// ✅ Impl From<T> for U; Into comes for free
impl From<ErrorKind> for Error { fn from(k: ErrorKind) -> Self { Error::Kind(k) } }

// ✅ AsRef for borrowed views
impl AsRef<str> for Name { fn as_ref(&self) -> &str { &self.0 } }

// ✅ Borrow<str> if Eq/Hash should agree with str
impl Borrow<str> for Name { fn borrow(&self) -> &str { &self.0 } }

多相性のためにDerefを使うのは避けます:Derefはスマートポインター(Box、Rc、Arc、Stringなど。標準ライブラリの先例を参照)向けです。Derefを使ってMyClientをHttpClientに透過させると、メソッドが暗黙に追加され、&selfの解決が壊れ、deref多相性のアンチパターンになります。AsRef、明示的なメソッド、またはコンポジションを使います。

C-ITER — イテレーターの設計
// ✅ Provide IntoIterator for &T, &mut T, T when sensible
impl<'a> IntoIterator for &'a Grid {
    type Item = &'a Cell;
    type IntoIter = std::slice::Iter<'a, Cell>;
    fn into_iter(self) -> Self::IntoIter { self.cells.iter() }
}

impl Grid {
    pub fn iter(&self) -> impl Iterator<Item = &Cell> { /* */ }
    pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut Cell> { /* */ }
}

impl Extend<Cell> for Grid { /* */ }

ルール:iter()は借用し、iter_mut()は可変借用し、into_iter()は消費します。イテレーション用メソッドからVecを返してはいけません。impl Iteratorを返します。

C-SERDE — シリアライゼーションの相互運用
  • 型内では一貫して#[serde(rename_all = "kebab-case")]を使います
  • #[non_exhaustive]の列挙型では、未知のバリアントとの前方互換性のために#[serde(other)]が必要です
  • 下流のクレートがオプトアウトできるよう、機能(serde = ["dep:serde"])を通じてserdeを再エクスポートします

3. 予測可能性(C-INTUITIVE、C-CONST、C-COMMON)

C-INTUITIVE — 名前でセマンティクスを表す

呼び出し側がreadの動作を知るためにソースを読む必要があるなら、そのAPIは適切ではありません。予想外の動作は避けます。EOFでパニックするreadメソッドは欠陥です。

C-CONST — 可能な場合はconst fnにする
pub const fn new(value: u32) -> Self { Self(value) }
const fn is_power_of_two(n: u32) -> bool { n != 0 && n & (n - 1) == 0 }

const fnにできる標準ライブラリのAPIはすべて候補になります。これにより、呼び出し側でconst MAX: UserId = UserId::new(1000);を使えるようになります。

C-COMMON — Defaultとビルダーで適切なデフォルト値を提供する
impl Default for Config {
    fn default() -> Self {
        Config { retry_count: 3, timeout: Duration::from_secs(30) }
    }
}

// Builder for complex construction
let client = Client::builder().with_retry(3).with_timeout(Duration::from_secs(10)).build()?;

4. 柔軟性(C-OVERLOAD、C-GENERIC、C-NEWTYPE、C-EXT)

C-GENERIC — 入力はジェネリックにし、出力は具体型にする
// ✅ Generic over AsRef<str> — caller passes &str, String, Cow
pub fn parse(input: impl AsRef<str>) -> Result<Foo> { /* */ }

// ❌ Concrete &str — forces caller to borrow
pub fn parse(input: &str) -> Result<Foo> { /* */ }

トレードオフ:ジェネリックが増えるほど、コンパイル時間が長くなり、診断が難しくなります。入力型(AsRef<str>、IntoIteratorなど)はジェネリックにし、出力型は具体型にします。

C-NEWTYPE — プリミティブ型をラップして誤用を防ぐ
// ✅ Newtypes around raw primitives
pub struct UserId(pub u64);
pub struct Email(String);   // private inner — can't be constructed unsafely

fn delete_user(id: UserId) { /* */ }   // can't accidentally pass a PostId

// ❌ Plain primitives — confusion and argument-order bugs
fn delete_user(id: u64) { /* */ }

ゼロコスト(基になる型にコンパイルされます)で、引数の順序の誤りを防ぎ、メソッド(UserId::is_anonymous()など)を追加できます。

C-EXT — 拡張トレイトにはExtサフィックスを付ける
pub trait StringExt { fn slugify(&self) -> String; }
impl StringExt for str { fn slugify(&self) -> String { /* */ } }

// Caller opts in:
use my_crate::StringExt;
"Hello World".slugify();

別のクレートからString/Vec/HttpRequestに直接メソッドを追加してはいけません。Extトレイトを使います。

5. 型安全性(C-BOOL、C-NONZERO、C-STR、C-SIGNED、C-BITFLAG、C-WRAPPER、C-INTERVAL)

権威ある資料:API Guidelines — Type Safety。標準的な表現については、references/api-guidelines-checklist.mdを参照してください。

C-BOOL — boolパラメーターを列挙型に置き換える
// ✅ Enum — caller intent is explicit at call site
pub enum Trim { Whitespace, None }
pub fn parse(input: &str, trim: Trim) -> Result<Foo> { /* */ }
parse("  x  ", Trim::Whitespace);

// ❌ Bool — caller must remember what true means
pub fn parse(input: &str, trim: bool) -> Result<Foo> { /* */ }
parse("  x  ", true);   // true = ??

boolパラメーターが2つあると複雑になります。f(true, false, true)では意味が分かりません。引数を取る列挙型を最低限使います。機械的に強制するには、clippy.tomlでmax-fn-params-bools = 1とmax-struct-bools = 1を設定します。

C-NONZERO — ゼロが無効な場合はNonZeroUsizeを使う
use std::num::NonZeroUsize;

// ✅ NonZeroUsize encodes "≥ 1" in the type
pub fn chunk_size(&self) -> NonZeroUsize { /* */ }

ニッチ最適化が有効になります。Option<NonZeroU32>はu32と同じサイズです。NonZeroU8/NonZeroU16/NonZeroU32/NonZeroU64/NonZeroUsizeと、NonZeroI*の各バリアントを使います。

C-STR — &Stringではなく&strを、&Vec<T>ではなく&[T]を使う
// ✅ Borrow slices for inputs
pub fn process(data: &[u8], name: &str) { /* */ }

// ❌ Forces caller to have owned collections
pub fn process(data: &Vec<u8>, name: &String) { /* */ }
C-SIGNED — 値が負にならない場合は符号なし型を優先する
// ✅ u64 — semantically "count" can't be negative
pub struct Counter { count: u64 }

// ❌ i64 — implies negative values are valid (they aren't)
pub struct Counter { count: i64 }

特別な範囲(例:Ageの0..=150)の場合は、検証するコンストラクターを備えたニュータイプを使い、型システムによって無効な値を防ぎます。

C-BITFLAG — フラグの集合にはbitflags!マクロを使う
use bitflags::bitflags;

bitflags! {
    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
    pub struct Permissions: u32 {
        const READ = 0b001;
        const WRITE = 0b010;
        const EXECUTE = 0b100;
    }
}

let p = Permissions::READ | Permissions::WRITE;   // type-checked composition
assert!(p.contains(Permissions::READ));            // built-in methods

フラグの集合に生のu32を使うのは避けます。型安全性が失われ、contains/insert/remove/intersectsも使えなくなります。

C-WRAPPER — プリミティブ型に意味のあるセマンティクスを持たせるにはニュータイプを使う
pub struct UserId(pub u64);
pub struct AccountId(pub u64);
pub struct OrderId(pub u64);

// Compiler rejects wrong-id bugs:
fn transfer(from: AccountId, to: AccountId, amount: Cents) { /* */ }
// transfer(UserId(1), UserId(2), Cents(100))  ← compile error

実行時にはゼロコスト(基になる型にコンパイルされます)。デフォルトでは#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]を使います。

C-INTERVAL — 範囲は緩いペアではなく型として表現する
// ✅ Dedicated range type with validating constructor
pub struct ChunkRange { start: u32, end: u32 }   // invariant: end >= start

impl ChunkRange {
    pub fn new(start: u32, end: u32) -> Result<Self, RangeError> {
        if end < start { return Err(RangeError::Inverted); }
        Ok(Self { start, end })
    }
    pub fn contains(&self, x: u32) -> bool { self.start <= x && x <= self.end }
}

// ❌ Loose pair — caller might pass end < start
pub fn process_chunk(start: u32, end: u32) { /* */ }

標準の範囲には、RangeInclusive/Range/RangeTo を使用します。ページネーションや時間ウィンドウなど、ドメイン固有の範囲は新しい型でラップします。

C-COMMENT-HIDDEN — #[doc(hidden)] は公開 API から除外しません
// ❌ Hides from rustdoc but is still semver-relevant
#[doc(hidden)]
pub mod unstable { /* */ }   // downstream can still `use crate::unstable::Foo;`

// ✅ For actually-unstable items, gate behind a feature
#[cfg(feature = "unstable")]
pub mod unstable { /* */ }

#[doc(hidden)] は cargo doc で非表示にするだけです。semver や安定性を管理するには、フィーチャーフラグまたはモジュールのプライバシーを使用します。

所有権を持つ入力には、String/Vec または impl Into<String>/impl IntoIterator を受け取ります。

6. 依存性(C-PANIC、C-UNWRAP、C-TRANSMUTE)

C-PANIC — 到達不能な不変条件に限りパニックを許容する
// ✅ Documented, unreachable from public API
pub fn lookup(&self, id: UserId) -> &User {
    self.table.get(&id).expect("internal: index invariant broken")
}

// ❌ Panics on user input
pub fn divide(a: f64, b: f64) -> f64 {
    if b == 0.0 { panic!("division by zero") }
    a / b
}

// ✅ Return Result for fallible operations
pub fn divide(a: f64, b: f64) -> Result<f64, DivError> { /* */ }

許容されるケース:プログラマーが不変条件に違反した場合、または関数が失敗しないことを文書化している場合です。許容されないケース:入力に起因する失敗です。

C-UNWRAP — 公開パスでの unwrap/expect を禁止する
// ❌ Library code
pub fn parse(input: &str) -> Config {
    serde_json::from_str(input).unwrap()   // panics on bad input
}

// ✅ Propagate
pub fn parse(input: &str) -> Result<Config, ParseError> {
    Ok(serde_json::from_str(input)?)
}

unwrap はテスト、const コンテキスト(? を使えない場合)、および到達不能であることが証明されたコードでは使用できます。診断情報のため、裸の unwrap() より expect("reason") を使用します。

C-TRANSMUTE — 型の再解釈に std::mem::transmute を使わない

transmute は型システムを迂回してビットを再解釈します。代わりに From/Into/TryFrom、bytemuck::cast/zerocopy(検証済みのバイトキャスト)、u64::from_ne_bytes/to_ne_bytes、または拡張変換には as を使用します。transmute を使おうとしている場合は、rust-unsafe-ffi に切り替えてください。

7. デバッグのしやすさ(C-DEBUG)

すべての公開型に Debug を実装します。Debug がないとデバッグの妨げになり、Debug で情報を過剰に公開すると秘密情報が漏えいします。

#[derive(Debug)]
pub struct Client { /* */ }

// ✅ Manual Debug for secrets
impl std::fmt::Debug for Password {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str("Password(***)")
    }
}

8. 将来への備え(C-SEALED、C-STRUCT-FIELD、C-NON-EXHAUSTIVE)

C-SEALED — トレイトをシールして外部からの実装を防ぐ
mod private { pub trait Sealed {} }

pub trait Serializer: private::Sealed {
    fn serialize(&self, v: &impl Serialize);
}

pub struct JsonSerializer;
impl private::Sealed for JsonSerializer {}
impl Serializer for JsonSerializer { /* */ }

シールしないと、誰でも impl MyTrait for TheirType を記述できるため、メソッドを追加できなくなります(追加すると破壊的変更になります)。references/future-proofing.md を参照してください。

C-STRUCT-FIELD — 公開構造体フィールドは永久に安定した API となる
// ✅ Private fields, public constructor — adding fields is non-breaking
pub struct Config { retry_count: u32, timeout: Duration }
impl Config {
    pub fn new(retry: u32, timeout: Duration) -> Self { Self { retry_count: retry, timeout } }
    pub fn builder() -> ConfigBuilder { /* */ }
}

// ❌ Pub fields + struct literal — adding a field breaks callers
let c = Config { retry_count: 3, timeout: Duration::from_secs(30) };

ルール:pub フィールド=永久に安定。プライベートフィールド=変更可能。

C-NON-EXHAUSTIVE — 拡張予定の列挙型に指定する
#[non_exhaustive]
pub enum Error { Io(io::Error), Parse(ParseError) }

// Downstream must use a wildcard arm:
match err {
    Error::Io(e) => /* */,
    Error::Parse(e) => /* */,
    _ => /* unknown */,
}

拡張する予定のエラー/イベント/ステータス列挙型と、フィールドを追加する可能性がある公開フィールド付き構造体に使用します。


ワークフロー

  1. 公開 API を洗い出す — cargo doc --open を実行し、すべての pub 項目を列挙します。これが契約です。
  2. 命名規則を適用する(セクション 1)— 大文字小文字、変換プレフィックス(as_/to_/into_)を修正し、get_ を削除します。
  3. 相互運用性の規則を適用する(セクション 2)— Debug/Clone などを導出し、From/AsRef を追加して、イテレーターを設計します。
  4. 予測可能性と柔軟性を適用する(セクション 3~4)— Default、const fn、プリミティブ型の新しい型、Ext トレイトを使用します。
  5. 型安全性を適用する(セクション 5)— bool を列挙型に置き換え、NonZero* を使用し、&String より &str を選びます。
  6. 依存性の規則を適用する(セクション 6)— 公開パスから unwrap/panic! を削除し、Result を返します。
  7. 将来への備えを適用する(セクション 8)— 拡張可能なトレイトをシールし、拡張予定の列挙型に #[non_exhaustive] を指定し、構造体フィールドをプライベートにします。
  8. 引き継ぐ — semver のチェック → rust-semver、ドキュメントテスト → rust-documentation、lint 設定 → rust-style-clippy。

判断の早見表

質問回答
Option と Result のどちらを使うべきですか?「存在しない場合がある」なら Option、「失敗する可能性がある」なら Result
トレイトと列挙型のどちらを使うべきですか?オープン(他の人が実装を追加する)ならトレイト、クローズド(バリアントを自分で管理する)なら列挙型
ジェネリック型と具体型のどちらを使うべきですか?入力にはジェネリック型、出力には具体型
フィールドを pub にすべきですか?既定ではいいえ。構造体リテラルでの構築を意図している場合に限ります
&str と impl AsRef<str> のどちらを使うべきですか?柔軟性を求めるなら AsRef<str>、シンプルさを求めるなら &str
Copy を導出すべきですか?小さい(≤16 バイト)かつビット単位のコピーが正しい場合に限ります
このトレイトをシールすべきですか?サードパーティーによる実装の追加が必要な場合(まれです)を除き、はい
#[non_exhaustive] を指定すべきですか?エラー/イベント列挙型にははい。拡張する可能性のある公開フィールド付き構造体にもはい

アンチパターンカタログ

  1. Deref によるポリモーフィズム — メソッドを「継承」するために Deref を使います。コンポジションを使ってください。
  2. get_X アクセサー — get_ を削除します。fn data(&self) -> &[u8]。
  3. bool パラメーター — fn f(x: bool, y: bool)。2 つの列挙型を使ってください。
  4. 公開 API での unwrap — パニックが伝播します。Result を使ってください。
  5. 入力での String/Vec — 所有権を強制します。&str/&[T] を使ってください。
  6. 封印されていない拡張トレイト — 将来メソッドを追加できなくなります。封印してください。
  7. エラーに #[non_exhaustive] がない — バリアントの追加で下流のコードが壊れます。
  8. Newtype がない — fn transfer(amount: u64, from: u64, to: u64) — 引数の順序を間違えやすくなります。
  9. 生成された Debug による秘密情報の漏えい — #[derive(Debug)] struct ApiKey(String)。独自の実装で値をマスキングしてください。
  10. キャストに transmute を使う — From/as/from_ne_bytes を使ってください。

それぞれのリファクタリング前後の例については、examples/anti-patterns.md を参照してください。

リソース

上流のソース

コメント