rust-development
Obsidian LiveSync プロジェクトの Rust 開発を支援します。DDD 4層アーキテクチャ(domain/application/infrastructure/interfaces)に従った新規モジュール追加、axum ハンドラー・ルーティング実装、DomainError と thiserror を使用したエラーハンドリング、tokio-test・mockall・rstest によるテスト作成を行います。新機能追加、コード修正、アーキテクチャ設計、テスト実装を依頼されたときに使用してください。
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.
| name | rust-development |
|---|---|
| description | Obsidian 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. 新規モジュール追加の手順
ドメインモデルを定義(
domain/models.rs)#[derive(Debug, Clone, Serialize, Deserialize)] pub struct NewModel { pub id: String, // フィールド }トレイトを定義(
domain/services.rs)#[async_trait] pub trait NewRepository { async fn method(&self, param: &str) -> Result<NewModel, DomainError>; }アプリケーションサービスを実装(
application/services.rs)pub struct NewService { repo: Arc<dyn NewRepository + Send + Sync>, }インフラ層で実装(
infrastructure/)#[async_trait] impl NewRepository for ConcreteClient { async fn method(&self, param: &str) -> Result<NewModel, DomainError> { // 実装 } }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- LiveSyncServicelivesync-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