mcpField
R
rust-implement

rust-implement

github.com/pproenca開発218243 回インストール11 週間前に更新
リポジトリ

rust-implement について

設計、実装、簡素化、検証の段階に分けて進める Rust 開発者向けスキルです。型設計、エラー処理、所有権、非同期処理、リソース解放の指針を扱います。

主な機能

  1. 01実装前の型と関数シグネチャ設計
  2. 02エラー処理と所有権の指針
  3. 03非同期処理とリソース解放の規則
  4. 04実装を簡素化するレビュー
  5. 05Lint と手動チェック

活用シーン

  1. 01コーディング前の Rust モジュール設計
  2. 02エラー処理と非同期クリーンアップのレビュー
  3. 03実装規約に沿ったコード確認

インストール

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

$npx skills add https://github.com/pproenca/dot-skills --skill rust-implement

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

同梱ファイル

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

AGENTS.md
metadata.json
scripts/
lint.sh

詳細

Rust 実装の規律

コードは段階的に書きます。熟練者でも、完璧なコードを一度で生み出すわけではありません。設計し、実装し、レビューし、簡潔にします。作成するすべてのモジュールで、このプロセスに従ってください。


パス 1:型とシグネチャを設計する

実装を書く前に、型定義と関数シグネチャだけを書きます。本文もロジックも書きません。

次に進む前に、以下の質問を自問してください。

  • enum の状態で無効な組み合わせを表現できてしまいませんか?できる場合は、無効な状態を表現できないように構造を見直してください。
  • すべてのパラメーターは、それ自体で意味が分かる名前になっていますか?bool パラメーターは enum に置き換えてください(変換 2)。
  • シリアライズまたは比較するすべての構造体で、HashMap ではなく BTreeMap を使っていますか(変換 3)?
  • 設定用の構造体は Default を derive していますか(変換 6)?
  • String パラメーターの順序を呼び出し元が取り違える可能性はありませんか?newtype を使ってください。
  • 一度に有効な組み合わせが 1 つだけの Option<T> フィールドを 2 つ以上持つ構造体はありませんか?enum に変換してください(変換 8)。
  • パラメーターを 4 つ以上取る関数はありませんか?引数用の構造体に置き換えてください(変換 9)。

型が適切になるまで、パス 2 に進まないでください。型がアーキテクチャを形作ります。


パス 2:実装する

関数の本文を記述します。各関数を書く際は、次のルールを適用してください。

  • すべての ? に .context("failed to [verb] [noun]") を付けます。例外はありません(変換 1)。
  • ライブラリコードでは、エラー enum に thiserror を使います。アプリケーションコードでは、anyhow を使います。
  • 関数が借用する場合と割り当てる場合があるときは、Cow<str> を使います(変換 7)。
  • 戻り値が 2 つ以上の関数では、タプルではなく名前付き構造体を返します(変換 5)。
  • match アームはすべて網羅します。ワイルドカードの _ => は使いません(変換 4)。
フィールドの引き回し

実行箇所まで渡す必要がある新しいフィールドを追加するときは、コードを書く前に、値が通る経路全体をたどり、途中にあるすべてのレイヤーを明示してください。各段階で、その値を受け渡すモジュールと構造体/関数を示します。途中のレイヤーを飛ばすと、実行時にフィールドが気付かないまま失われます。

Drop/終了処理の精度
  • .await の前にロックガードを解放してください。await 中に MutexGuard を保持すると、エグゼキューターをブロックします。
  • Drop 実装では stdin を明示的に閉じてください。短時間待機してから、必要に応じて強制終了します。
  • 親より長く存続してはならない子プロセスには、kill_on_drop(true) を使います。
  • クリーンアップの順序を決定論的にします。リスナーをクリア -> タスクを排出 -> リソースを破棄。
防御的なシリアライズ
  • SQLite の書き込みトランザクションでは BEGIN ではなく BEGIN IMMEDIATE を使います(SQLITE_BUSY_SNAPSHOT を防ぎます)。
  • リトライを追加するのではなく、ロックの取得を順序付けます。
  • 冪等な挿入では、読み取ってから書き込むのではなく ON CONFLICT DO NOTHING を使います。
孤立イベントの処理

存在しなくなったエンティティ(終了したエージェント、閉じたスレッド)に対するイベントが届いた場合は、明示的に処理してください。黙って破棄してはいけません。警告をログに記録し、古い状態をクリーンアップします。孤立イベントでパニックを起こすのは、常に誤りです。

所有権の判断フロー

データへの共有アクセスが必要な場合は、次のフローを順番にたどってください。

  1. 参照を渡せますか? &T を使います。コストも複雑さもありません。
  2. 所有されている場合と借用されている場合がありますか? Cow<'_, str> を使います。借用時はゼロコストです。
  3. 同期境界をまたいで複数の所有者がいますか? Arc<T> を使います。x.clone() より Arc::clone(&x) を優先します。
  4. 複数の所有者がいて、変更も必要ですか? Arc<Mutex<T>> または Arc<RwLock<T>> を使います。読み取りが書き込みを大幅に上回る場合は RwLock を選びます。
  5. 起動した処理を待たずに実行しますか? 所有データを使う move クロージャーを使います。

すべての .clone() は判断の機会です。「このクローンは本当に必要ですか。それとも借用するように構成し直せますか」と問いかけてください。

エラーの方針
コンテキストツール理由
アプリケーションコード(main、CLI、テスト)anyhow::Result + .context()詳細なエラーチェーンを得られ、定型コードが不要です
ライブラリコード(他のクレートから利用されるクレート)thiserror の列挙型型付けされ、パターンマッチでき、呼び出し側でバリアントに応じて処理できます

エラー列挙型の設計:ユーザー向けメッセージでは、内部で何が問題になったかではなく、ユーザーが何をすべきかを伝えます。大きなペイロードは Box<T> にします。is_retryable() では各バリアントを明示的に分類し、ワイルドカードは使いません。

非同期処理に関する判断

Box::pin を使う場合: 薄い非同期ラッパーが大きな呼び出し先の future をステートマシンにインライン化し、スタックに負荷がかかるときです。内部呼び出しをラップします:Box::pin(self.inner_method(args)).await。

必要なものチャネル理由
1つの応答を返すoneshot値を1つだけ受け取り、その後終了します
イベントのストリームmpsc複数の送信側と単一の受信側に対応します
最新の値のみwatch受信側は常に最新の値を確認できます
全員へのブロードキャストbroadcastすべての受信側がすべてのメッセージを受け取ります

シャットダウン:階層的なシャットダウンには CancellationToken を使います。順序が必要な非同期クリーンアップで Drop に依存してはいけません。.await ポイントをまたいで MutexGuard を保持してはいけません。


パス3:簡素化する

コードを追加するのではなく、削除するつもりでレビューしてください。

次の3つの診断用の質問を確認します:

  1. 呼び出しチェーンを通じて渡されるだけのパラメーターはありませんか? 削除し、アンビエントアクセスを使います。
  2. 使われていないフィールドや関数はありませんか? 削除します。
  3. 正当化できない抽象化はありませんか?(一度しか使わないヘルパーや、何も追加しないラッパーなど)インライン化します。

「配線し直すのではなく、書き直す」原則: バグが関数の内部ロジックにある場合は、明示的なチェックを使って本体を書き直します。たまたま現時点で正しい結果を生成する既存APIに委譲してはいけません。明示的に記述したほうが監査しやすく、上流の変更にも耐えられます。

タスクに厳密に必要な量を超えてコードを追加したなら、何かが間違っています。削りましょう。


パス4:検証する

同梱の lint スクリプトをコードに対して実行します:

bash ${SKILL_DIR}/scripts/lint.sh <your-file.rs>

すべての ERROR を修正します。すべての WARNING を確認します。続いて、次の手動チェックリストを実行します:

[ ] Every ? has .context("failed to [verb] [noun]")
[ ] No unwrap() outside #[cfg(test)]
[ ] BTreeMap where output is serialized or compared
[ ] No bool parameters -- use enums
[ ] Match arms exhaustive -- no _ => wildcards
[ ] Config structs derive Default
[ ] No single-use helper functions -- inline if called once
[ ] Module under 500 lines (excluding tests)
[ ] No unnecessary .clone() -- prefer borrowing
[ ] Return structs, not tuples, for multi-value returns
[ ] Cow<str> where allocation is conditional
[ ] serde attrs present: rename_all, default, deny_unknown_fields as needed
[ ] No struct with 2+ Option<T> fields that should be an enum
[ ] No function with 4+ parameters (use args struct)
[ ] Lock guards dropped before any .await
[ ] Events for dead/missing entities handled explicitly

コードを提示する前に、すべての違反を修正してください。


クイックリファレンス:9つの変換

各変換では、初稿から本番品質への正確な差分を示します。

1. すべての ? 演算子に .context() を付ける

変更前:

let content = std::fs::read_to_string(path)?;
let config: Config = toml::from_str(&content)?;

変更後:

let content = std::fs::read_to_string(path)
    .context("failed to read config file")?;
let config: Config = toml::from_str(&content)
    .context("failed to parse TOML config")?;

パターン:"failed to [verb] [noun]"。上流のエラーはそれ自体を説明します。何の操作が失敗したのかを示すのが、あなたの役目です。

2. ブール値ではなく列挙型を使う

変更前:

fn create_sandbox(network: bool, writable: bool) -> Sandbox {

変更後:

fn create_sandbox(network: NetworkMode, access: AccessLevel) -> Sandbox {

foo(true, false) では意味が伝わりません。列挙型に置き換えると、呼び出し箇所を NetworkMode::Restricted, AccessLevel::ReadOnly のように記述できます。APIを変更できない場合は、意味の分かりにくいリテラルの前に /*param_name*/ コメントを追加します。

3. シリアライズまたは比較するデータには BTreeMap を使う

変更前:HashMap<String, Rule> -- 変更後:BTreeMap<String, Rule>

HashMap のイテレーション順序はランダムなため、差分が不必要に増え、スナップショットテストが不安定になります。データをシリアライズする場合、テストで比較する場合、またはユーザーに表示する場合は BTreeMap を使います。

4. ワイルドカードを使わず、網羅的な match を書く

変更前:

match decision {
    PolicyDecision::Allow => true,
    _ => false,
}

変更後:

match decision {
    PolicyDecision::Allow => true,
    PolicyDecision::Block { .. } => false,
    PolicyDecision::Rewrite { .. } => false,
    PolicyDecision::Ask { .. } => false,
}

新しいバリアントが追加されると、コンパイラーが更新の必要なすべての match を指摘します。ワイルドカードはこれを隠してしまいます。

5. タプルではなく構造体を返す

変更前:fn build_command() -> (Vec<String>, Vec<OwnedFd>) -- 呼び出し側は result.0 と記述します 変更後:fn build_command() -> CommandOutput -- 呼び出し側は output.args、output.preserved_fds と記述します

名前付きフィールドは、それ自体で内容を説明します。2つ以上の値を返す関数には構造体を定義します。

6. 設定用構造体に Default を導出する
#[derive(Default)]
pub struct ServerConfig {
    pub timeout_secs: u64,
    pub max_retries: u32,
    pub bind_addr: String,
}
let config = ServerConfig { timeout_secs: 30, ..Default::default() };

呼び出し側が必要な項目だけを上書きできるよう、Default を導出します。ゼロ以外のデフォルト値が必要な場合は、Default を手動で実装します。

7. 所有権が条件によって変わる場合は Cow<str> を使う

変更前:

fn normalize_path(input: &str) -> String {
    if needs_normalization(input) { input.replace('\\', "/") }
    else { input.to_string() }  // allocates even when unchanged
}

変更後:

fn normalize_path(input: &str) -> Cow<'_, str> {
    if needs_normalization(input) { Cow::Owned(input.replace('\\', "/")) }
    else { Cow::Borrowed(input) }  // zero-cost when unchanged
}

一部のパスでのみ割り当てが必要な場合、Cow<str> を使うと不要な無条件の割り当てを避けられます。

8. 列挙型による非交和

変更前:

struct Permissions {
    profile_name: Option<String>,       // only for named profiles
    sandbox_policy: Option<SandboxPolicy>, // only for legacy
    file_paths: Option<Vec<PathBuf>>,   // only when sandbox_policy is set
}

変更後:

enum Permissions {
    Named { profile_name: String },
    Legacy { sandbox_policy: SandboxPolicy, file_paths: Vec<PathBuf> },
}

構造体に、特定の組み合わせでのみ有効な Option<T> フィールドがあると、型レベルで無効な状態を許容してしまいます。各バリアントに関連するデータだけを持たせる列挙型に変換します。診断の目安:is_some()/is_none() ガードを使う Option<T> フィールドが2つ以上ある場合は、列挙型にするべきです。

9. 長いパラメーターリストを構造体にする

変更前:

fn spawn_process(
    cmd: &str,
    args: &[String],
    env: &BTreeMap<String, String>,
    cwd: &Path,
    stdin_policy: StdinPolicy,
    sandbox: SandboxPolicy,
    timeout: Duration,
) -> Result<Child> {

変更後:

struct SpawnArgs<'a> {
    cmd: &'a str,
    args: &'a [String],
    env: &'a BTreeMap<String, String>,
    cwd: &'a Path,
    stdin_policy: StdinPolicy,
    sandbox: SandboxPolicy,
    timeout: Duration,
}

fn spawn_process(args: &SpawnArgs<'_>) -> Result<Child> {

パラメーターが4つ以上になると、呼び出し箇所が読みづらくなり、順序を間違えやすくなります。引数用の構造体を使うと、呼び出し箇所で各フィールドに名前を付けられ、将来パラメーターを追加しても互換性を損ないません。

コメント