rust-development

ystk-kai's avatarfrom ystk-kai

Obsidian LiveSync プロジェクトの Rust 開発を支援します。DDD 4層アーキテクチャ(domain/application/infrastructure/interfaces)に従った新規モジュール追加、axum ハンドラー・ルーティング実装、DomainError と thiserror を使用したエラーハンドリング、tokio-test・mockall・rstest によるテスト作成を行います。新機能追加、コード修正、アーキテクチャ設計、テスト実装を依頼されたときに使用してください。

⭐0stars🔀0forks📁View on GitHub🕐Updated Dec 19, 2025

When & Why to Use This Skill

This Claude skill accelerates Rust development for the Obsidian LiveSync project by enforcing a strict DDD (Domain-Driven Design) 4-layer architecture. It streamlines the implementation of axum handlers, domain models, and infrastructure components while ensuring robust error handling with thiserror and comprehensive testing using mockall and rstest. It is designed to maintain high code quality and architectural consistency across the codebase.

Use Cases

  • Scaffolding new modules by generating domain models, repository traits, and application services following the 4-layer architecture.
  • Implementing and routing new HTTP endpoints using the axum framework, including state management and request handling.
  • Standardizing error handling by defining custom DomainError variants and implementing IntoResponse for automated API error mapping.
  • Creating unit and integration tests with mockall and tokio-test to validate business logic and infrastructure interactions.
namerust-development
descriptionObsidian LiveSync プロジェクトの Rust 開発を支援します。DDD 4層アーキテクチャ(domain/application/infrastructure/interfaces)に従った新規モジュール追加、axum ハンドラー・ルーティング実装、DomainError と thiserror を使用したエラーハンドリング、tokio-test・mockall・rstest によるテスト作成を行います。新機能追加、コード修正、アーキテクチャ設計、テスト実装を依頼されたときに使用してください。

Rust Development for Obsidian LiveSync

Overview

このスキルは、Obsidian LiveSync プロジェクトの Rust 開発における標準的なパターンと規約に従った実装を支援します。

プロジェクトは DDD(Domain-Driven Design)4層アーキテクチャを採用しています:

livesync-proxy/src/
├── domain/           # ドメイン層: ビジネスロジックの中核
│   ├── models.rs     # CouchDbDocument, DomainError
│   └── services.rs   # CouchDbRepository トレイト
├── application/      # アプリケーション層: ユースケース
│   └── services.rs   # LiveSyncService
├── infrastructure/   # インフラ層: 外部サービス接続
│   ├── config.rs     # AppConfig
│   └── couchdb.rs    # CouchDbClient(674行)
└── interfaces/       # インターフェース層: HTTP エンドポイント
    └── web/
        ├── server.rs    # ルーター設定(340行)
        ├── handlers.rs  # リクエストハンドラ(163行)
        ├── health.rs    # ヘルスチェック
        └── metrics.rs   # Prometheus メトリクス

Instructions

1. DDD 4層の責務と依存方向

依存方向のルール: 外側の層は内側の層に依存できるが、逆は不可。

interfaces → application → domain ← infrastructure
層 責務 依存可能
domain ビジネスモデル、トレイト定義 なし(最も内側)
application ユースケースの実装 domain のみ
infrastructure 外部サービス接続 domain のみ
interfaces HTTP エンドポイント application, domain

2. 新規モジュール追加の手順

  1. ドメインモデルを定義(domain/models.rs)

    #[derive(Debug, Clone, Serialize, Deserialize)]
    pub struct NewModel {
        pub id: String,
        // フィールド
    }
    
  2. トレイトを定義(domain/services.rs)

    #[async_trait]
    pub trait NewRepository {
        async fn method(&self, param: &str) -> Result<NewModel, DomainError>;
    }
    
  3. アプリケーションサービスを実装(application/services.rs)

    pub struct NewService {
        repo: Arc<dyn NewRepository + Send + Sync>,
    }
    
  4. インフラ層で実装(infrastructure/)

    #[async_trait]
    impl NewRepository for ConcreteClient {
        async fn method(&self, param: &str) -> Result<NewModel, DomainError> {
            // 実装
        }
    }
    
  5. lib.rs にモジュールを追加

    pub mod new_module;
    

3. axum ハンドラー・ルーティング追加

ハンドラーの基本パターン(interfaces/web/handlers.rs参照):

use axum::{
    extract::State,
    http::{Request, StatusCode},
    response::{IntoResponse, Response},
    Json,
};
use std::sync::Arc;
use crate::interfaces::web::server::AppState;

pub async fn new_handler(
    State(state): State<Arc<AppState>>,
    req: Request<Body>,
) -> impl IntoResponse {
    // 処理
    Json(serde_json::json!({"status": "ok"}))
}

ルーティング追加(interfaces/web/server.rs):

let app = Router::new()
    .route("/new-endpoint", get(new_handler))
    .route("/new-endpoint/{id}", any(new_handler_with_param))
    // 既存のルート
    .with_state(app_state);

4. エラーハンドリング

DomainError の定義(domain/models.rs):

#[derive(Debug, thiserror::Error)]
pub enum DomainError {
    #[error("Invalid message format: {0}")]
    InvalidMessage(String),

    #[error("Authentication error: {0}")]
    AuthError(String),

    #[error("CouchDB error: {0}")]
    CouchDbError(String),

    #[error("HTTP proxy error: {0}")]
    HttpProxyError(String),
}

IntoResponse トレイトの実装(axum 0.8 ベストプラクティス):

impl IntoResponse for DomainError {
    fn into_response(self) -> Response {
        let (status, message) = match &self {
            DomainError::InvalidMessage(_) => (StatusCode::BAD_REQUEST, self.to_string()),
            DomainError::AuthError(_) => (StatusCode::UNAUTHORIZED, self.to_string()),
            DomainError::CouchDbError(_) => (StatusCode::BAD_GATEWAY, self.to_string()),
            DomainError::HttpProxyError(_) => (StatusCode::BAD_GATEWAY, self.to_string()),
        };
        (status, Json(json!({"error": message}))).into_response()
    }
}

ハンドラーでの Result 型返却(推奨パターン):

// IntoResponse 実装により ? 演算子が使用可能
pub async fn handler(State(state): State<Arc<AppState>>) -> Result<Json<Value>, DomainError> {
    let result = state.service.do_something().await?;
    Ok(Json(result))
}

ミドルウェアエラーハンドリング(タイムアウト等):

use tower::timeout::TimeoutLayer;
use axum::error_handling::HandleErrorLayer;

let app = Router::new()
    .route("/api/slow", get(slow_handler))
    .layer(
        ServiceBuilder::new()
            .layer(HandleErrorLayer::new(|err: BoxError| async move {
                if err.is::<tower::timeout::error::Elapsed>() {
                    (StatusCode::REQUEST_TIMEOUT, "Request timed out".to_string())
                } else {
                    (StatusCode::INTERNAL_SERVER_ERROR, format!("Error: {}", err))
                }
            }))
            .layer(TimeoutLayer::new(Duration::from_secs(30)))
    );

エラーの伝播(map_err パターン):

// 外部ライブラリのエラーを DomainError に変換
let data = serde_json::from_str(&body)
    .map_err(|e| DomainError::InvalidMessage(format!("JSON parse error: {}", e)))?;

// reqwest エラーの変換
let response = client.get(&url).send().await
    .map_err(|e| DomainError::CouchDbError(e.to_string()))?;

5. テスト作成

テストファイルの場所: livesync-proxy/tests/

使用ライブラリ:

  • tokio-test: 非同期テスト
  • mockall: モック作成
  • rstest: パラメータ化テスト

モック作成パターン(tests/couchdb_repository_test.rs参照):

use mockall::mock;
use async_trait::async_trait;

mock! {
    pub CouchDbMock {}

    #[async_trait]
    impl CouchDbRepository for CouchDbMock {
        async fn get_document(&self, db_name: &str, doc_id: &str)
            -> Result<CouchDbDocument, DomainError>;
        // 他のメソッド
    }
}

#[tokio::test]
async fn test_example() {
    let mut mock = MockCouchDbMock::new();
    mock.expect_get_document()
        .returning(|_, _| Ok(CouchDbDocument { ... }));

    // テスト
}

インメモリ実装パターン:

struct InMemoryRepo {
    data: Mutex<HashMap<String, Model>>,
}

#[async_trait]
impl Repository for InMemoryRepo {
    // 実装
}

Examples

新しいエンドポイントの追加

// 1. handlers.rs に追加
pub async fn new_feature_handler(
    State(state): State<Arc<AppState>>,
) -> impl IntoResponse {
    match state.livesync_service.new_feature().await {
        Ok(result) => Json(result).into_response(),
        Err(e) => {
            (StatusCode::INTERNAL_SERVER_ERROR,
             Json(json!({"error": e.to_string()}))).into_response()
        }
    }
}

// 2. server.rs のルーターに追加
.route("/api/new-feature", get(new_feature_handler))

ドメインエラーの追加

// domain/models.rs
#[derive(Debug, thiserror::Error)]
pub enum DomainError {
    // 既存のエラー...

    #[error("Validation error: {0}")]
    ValidationError(String),
}

Reference

主要ファイル

  • livesync-proxy/src/domain/models.rs - ドメインモデル
  • livesync-proxy/src/domain/services.rs - トレイト定義
  • livesync-proxy/src/application/services.rs - LiveSyncService
  • livesync-proxy/src/interfaces/web/server.rs - ルーター
  • livesync-proxy/src/interfaces/web/handlers.rs - ハンドラー

コマンド

cd livesync-proxy
cargo build              # 開発ビルド
cargo build --release    # リリースビルド
cargo test --verbose     # 全テスト実行
cargo fmt --all -- --check  # フォーマットチェック
cargo clippy -- -D warnings # リンター

依存クレート(Cargo.toml)

  • axum 0.8.4, tokio 1.45, reqwest 0.12
  • thiserror 2.0, anyhow 1.0
  • tokio-test 0.4, mockall 0.13, rstest 0.25