Major refactoring: HTTP client unification and project restructuring
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 13m53s

## HTTP Client Refactoring
- Create unified HttpClient module (src/http_client.rs)
- Refactor 24 files to use shared HTTP client
- Replace .unwrap() with proper error handling
- Eliminate code duplication in HTTP requests

## Project Restructuring
- Rename package: ai → aibot
- Add dual binary support: aibot (main) + ai (compatibility alias)
- Migrate config directory: ~/.config/ai/ → ~/.config/syui/ai/bot/
- Implement backward compatibility with automatic migration

## Testing Infrastructure
- Add unit tests for HttpClient
- Create test infrastructure with cargo-make
- Add test commands: test, test-quick, test-verbose

## Documentation
- Complete migration guide with step-by-step instructions
- Updated development guide with new structure
- HTTP client API reference documentation
- Comprehensive refactoring summary

## Files Changed
- Modified: 24 source files (HTTP client integration)
- Added: src/http_client.rs, src/alias.rs, src/tests/
- Added: 5 documentation files in docs/
- Added: migration setup script

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-06-06 23:47:12 +09:00
parent 998777d46a
commit a17d2c9d66
45 changed files with 1871 additions and 593 deletions

334
docs/http-client-api.md Normal file
View File

@ -0,0 +1,334 @@
# HttpClient API リファレンス
## 概要
`HttpClient`は、AT Protocol APIへの統一されたHTTPクライアントインターフェースです。認証、エラーハンドリング、リクエスト管理を自動化します。
## 基本的な使用方法
```rust
use crate::http_client::HttpClient;
let client = HttpClient::new();
// または
let client = HttpClient::default();
```
## APIメソッド
### 認証付きリクエスト
#### get_with_auth
AT Protocolの認証が必要なGETリクエストを実行します。
```rust
pub async fn get_with_auth(&self, url: &str) -> Result<String, Error>
```
**パラメータ:**
- `url`: リクエスト先のURL
**戻り値:**
- `Ok(String)`: レスポンスボディ(文字列)
- `Err(Error)`: リクエストエラー
**使用例:**
```rust
let client = HttpClient::new();
let url = "https://bsky.social/xrpc/app.bsky.actor.getProfile?actor=user.bsky.social";
match client.get_with_auth(&url).await {
Ok(response) => println!("Response: {}", response),
Err(e) => eprintln!("Error: {}", e),
}
```
#### post_json_with_auth
AT Protocolの認証が必要なPOSTリクエストJSONを実行します。
```rust
pub async fn post_json_with_auth<T: Serialize>(&self, url: &str, json: &T) -> Result<String, Error>
```
**パラメータ:**
- `url`: リクエスト先のURL
- `json`: シリアライズ可能なJSONデータ
**戻り値:**
- `Ok(String)`: レスポンスボディ(文字列)
- `Err(Error)`: リクエストエラー
**使用例:**
```rust
use serde_json::json;
let client = HttpClient::new();
let url = "https://bsky.social/xrpc/com.atproto.repo.createRecord";
let payload = json!({
"repo": "user.bsky.social",
"collection": "app.bsky.feed.post",
"record": {
"text": "Hello, World!",
"createdAt": "2025-01-01T00:00:00Z"
}
});
match client.post_json_with_auth(&url, &payload).await {
Ok(response) => println!("Post created: {}", response),
Err(e) => eprintln!("Error: {}", e),
}
```
#### delete_with_auth
AT Protocolの認証が必要なDELETEリクエストを実行します。
```rust
pub async fn delete_with_auth(&self, url: &str) -> Result<String, Error>
```
**パラメータ:**
- `url`: リクエスト先のURL
**戻り値:**
- `Ok(String)`: レスポンスボディ(文字列)
- `Err(Error)`: リクエストエラー
**使用例:**
```rust
let client = HttpClient::new();
let url = "https://bsky.social/xrpc/com.atproto.repo.deleteRecord";
match client.delete_with_auth(&url).await {
Ok(response) => println!("Deleted: {}", response),
Err(e) => eprintln!("Error: {}", e),
}
```
### 認証なしリクエスト
#### get
認証なしのGETリクエストを実行します。
```rust
pub async fn get(&self, url: &str) -> Result<String, Error>
```
**使用例:**
```rust
let client = HttpClient::new();
let url = "https://public-api.example.com/data";
match client.get(&url).await {
Ok(response) => println!("Response: {}", response),
Err(e) => eprintln!("Error: {}", e),
}
```
#### post_json
認証なしのPOSTリクエストJSONを実行します。ログイン処理などで使用。
```rust
pub async fn post_json<T: Serialize>(&self, url: &str, json: &T) -> Result<String, Error>
```
**使用例:**
```rust
use serde_json::json;
let client = HttpClient::new();
let url = "https://bsky.social/xrpc/com.atproto.session.create";
let credentials = json!({
"identifier": "user.bsky.social",
"password": "password"
});
match client.post_json(&url, &credentials).await {
Ok(response) => println!("Login successful: {}", response),
Err(e) => eprintln!("Login failed: {}", e),
}
```
### カスタムヘッダー付きリクエスト
#### post_with_headers
カスタムヘッダーを指定したPOSTリクエストを実行します。
```rust
pub async fn post_with_headers<T: Serialize>(
&self,
url: &str,
json: &T,
headers: Vec<(&str, &str)>
) -> Result<String, Error>
```
**パラメータ:**
- `url`: リクエスト先のURL
- `json`: シリアライズ可能なJSONデータ
- `headers`: ヘッダーのキーと値のペアのベクター
**使用例:**
```rust
use serde_json::json;
let client = HttpClient::new();
let url = "https://bsky.social/xrpc/com.atproto.session.refresh";
let refresh_token = "refresh_token_value";
let auth_header = format!("Bearer {}", refresh_token);
let headers = vec![("Authorization", auth_header.as_str())];
let empty_json = json!({});
match client.post_with_headers(&url, &empty_json, headers).await {
Ok(response) => println!("Token refreshed: {}", response),
Err(e) => eprintln!("Refresh failed: {}", e),
}
```
## 内部実装詳細
### 認証処理
認証付きメソッドは自動的に以下を実行します:
1. `data_refresh(&"access")`でアクセストークンを取得
2. `Authorization: Bearer {token}`ヘッダーを自動追加
3. リクエストを実行
### エラーハンドリング
- ネットワークエラー
- HTTPステータスエラー
- JSON解析エラー
- タイムアウトエラー
すべて`reqwest::Error`として統一されて返されます。
## 使用上の注意
### 1. トークン管理
- アクセストークンは自動的に取得されます
- トークンの有効期限切れは呼び出し元で処理する必要があります
- リフレッシュトークンは`post_with_headers`で手動設定
### 2. エラー処理パターン
```rust
// 推奨パターン
match client.get_with_auth(&url).await {
Ok(response) => {
// 成功処理
response
},
Err(e) => {
eprintln!("API call failed: {}", e);
"err".to_string() // 既存コードとの互換性
}
}
```
### 3. パフォーマンス
- `HttpClient`の作成は軽量な操作です
- 内部でrequwestクライアントを再利用しています
- 複数のリクエストで同じインスタンスを使い回すことも可能
### 4. デバッグ
リクエスト/レスポンスの詳細をログ出力したい場合:
```rust
// HttpClientモジュール内で適宜printlnを追加
println!("Request URL: {}", url);
println!("Response: {}", response);
```
## 実装例新しいAPI呼び出しモジュール
```rust
use crate::http_client::HttpClient;
use crate::{data_toml, url};
use serde_json::json;
use iso8601_timestamp::Timestamp;
pub async fn create_custom_record(data: String) -> String {
let did = data_toml(&"did");
let handle = data_toml(&"handle");
let url = url(&"record_create");
let timestamp = Timestamp::now_utc().to_string();
let payload = json!({
"repo": handle,
"did": did,
"collection": "app.bsky.custom.record",
"record": {
"data": data,
"createdAt": timestamp
}
});
let client = HttpClient::new();
match client.post_json_with_auth(&url, &payload).await {
Ok(response) => response,
Err(e) => {
eprintln!("Error creating custom record: {}", e);
"err".to_string()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_create_custom_record() {
let result = create_custom_record("test data".to_string()).await;
assert_ne!(result, "err");
}
}
```
## 移行ガイド
### 既存コードからの移行
#### Before (旧実装)
```rust
extern crate reqwest;
use crate::data_refresh;
pub async fn old_request() -> String {
let token = data_refresh(&"access");
let client = reqwest::Client::new();
let res = client
.post(url)
.json(&data)
.header("Authorization", "Bearer ".to_owned() + &token)
.send()
.await
.unwrap()
.text()
.await
.unwrap();
res
}
```
#### After (新実装)
```rust
use crate::http_client::HttpClient;
pub async fn new_request() -> String {
let client = HttpClient::new();
match client.post_json_with_auth(&url, &data).await {
Ok(response) => response,
Err(e) => {
eprintln!("Error in request: {}", e);
"err".to_string()
}
}
}
```
### チェックリスト
- [ ] `extern crate reqwest;`を削除
- [ ] `use crate::http_client::HttpClient;`を追加
- [ ] `data_refresh(&"access")`の手動呼び出しを削除
- [ ] `reqwest::Client::new()``HttpClient::new()`に置換
- [ ] `.unwrap()`を適切な`match`文に置換
- [ ] エラーメッセージの追加