--- name: api-extend description: API拡張ワークフロー(ports → provider → handler → Mock → テスト → フロント型 → UI)を自動化 version: 1.0.0 tools: - Read - Write - Edit - Grep - Glob - Bash skill_type: workflow auto_invoke: false --- # API拡張ワークフロー ## 概要 新しいAPIフィールドやエンドポイントを追加する際の定型フローを自動化します。DDD(ドメイン駆動設計)とクリーンアーキテクチャの原則に従い、以下の8ステップを実行します: 1. **ports定義** - ドメイン層のインターフェース定義 2. **provider実装** - JRA-VAN APIプロバイダー実装 3. **Mock実装** - モックプロバイダー実装 4. **handler実装** - APIハンドラー(Lambda関数) 5. **テスト作成/更新** - TDDに基づくテストコード 6. **フロント型定義** - TypeScript型とマッピング関数 7. **APIクライアント** - フロントエンドAPIクライアント更新 8. **UI実装** - React コンポーネント更新 ## 入力形式 スキル呼び出し時に以下の情報を提供してください: ``` /api-extend 追加内容: - データクラス名: (例: TrainingData) - フィールド: - : - <説明> - : - <説明> - エンドポイント(新規の場合): GET /path/{param} - 既存エンドポイントへの追加(の場合): <エンドポイント名> ``` ## 実行プロセス ### ステップ1: ports定義(ドメイン層) **ファイル**: `main/backend/src/domain/ports/race_data_provider.py` **パターン**: ```python @dataclass(frozen=True) class : """<説明>.""" field_name: type field_name: type | None = None # オプショナルフィールド class RaceDataProvider(ABC): @abstractmethod def get_(self, ) -> : """<説明>.""" pass ``` **重要な規則**: - すべてのデータクラスは `@dataclass(frozen=True)` でイミュータブル - abstractmethodには必ず `pass` を記述 - 型ヒントは必須(`str | None` 形式) ### ステップ2: JRA-VAN Provider実装 **ファイル**: `main/backend/src/infrastructure/providers/jravan_race_data_provider.py` **パターン**: ```python def get_(self, ) -> : """<説明>.""" try: response = self._session.get( f"{self._base_url}/", params={"param": value} if needs_params else {}, timeout=self._timeout, ) if response.status_code == 404: return None # または [] response.raise_for_status() # リストの場合 data = response.json() return [self._to_(d) for d in data] # 単一オブジェクトの場合 return self._to_(response.json()) except requests.RequestException as e: logger.warning(f"Could not get : {e}") return None # または [] def _to_(self, data: dict) -> : """API レスポンスを に変換する.""" return ( field_name=data["field_name"], optional_field=data.get("optional_field"), ) ``` **重要な規則**: - 404エラーは `None` または空リスト `[]` を返す - その他のエラーは警告ログを出力してデフォルト値を返す - 必ず `_to_` 変換メソッドを実装 - `data.get("key")` でオプショナルフィールドを取得 ### ステップ3: Mock Provider実装 **ファイル**: `main/backend/src/infrastructure/providers/mock_race_data_provider.py` **パターン**: ```python def get_(self, ) -> : """<説明>.""" import random random.seed(_stable_hash(str()) % (2**32)) # データを生成 results = [] for i in range(): data = ( field_name=random.choice(self.SAMPLE_DATA), numeric_field=random.randint(min, max), optional_field=random.choice([...]) if needed else None, ) results.append(data) return results ``` **重要な規則**: - 必ず `_stable_hash()` でシード固定(再現可能なデータ生成) - クラス定数として `SAMPLE_DATA` を定義 - ランダム生成でもリアルなデータ分布を意識 ### ステップ4: API Handler実装 **ファイル**: `main/backend/src/api/handlers/races.py` **レスポンス構築パターン**: ```python def get_(event: dict, context: Any) -> dict: """<説明>. GET /?param=value Query/Path Parameters: param: 説明 Returns: レスポンス説明 """ # パラメータ取得 param = get_query_parameter(event, "param") # または param = get_path_parameter(event, "param") if not param: return bad_request_response("param is required") # ユースケース実行 provider = Dependencies.get_race_data_provider() result = provider.get_(param) if result is None: return not_found_response("") # レスポンス構築 response_data = [ { "field_name": item.field_name, "optional_field": item.optional_field, } for item in result ] return success_response({"data": response_data}) ``` **重要な規則**: - snake_case → camelCase 変換は不要(バックエンドはsnake_case統一) - 必ず入力バリデーション - エラーレスポンスは `bad_request_response()`, `not_found_response()` を使用 ### ステップ5: テスト作成/更新 **ファイル**: `main/backend/tests/` 配下の適切な場所 **テストパターン**: ```python def test_get_(): """<説明>.""" # Arrange provider = MockRaceDataProvider() # Act result = provider.get_() # Assert assert result is not None assert len(result) > 0 assert result[0].field_name == expected_value ``` **重要な規則**: - AAA パターン(Arrange, Act, Assert) - Mockプロバイダーを使用してテスト - 境界値・エラーケースもテスト ### ステップ6: フロントエンド型定義 **ファイル**: `main/frontend/src/types/index.ts` **パターン**: ```typescript // API形式(バックエンドレスポンス) export interface Api { field_name: string; optional_field?: number; } // フロントエンド表示用 export interface { fieldName: string; optionalField?: number; } // 変換関数 export function mapApiTo(api: Api): { return { fieldName: api.field_name, optionalField: api.optional_field, }; } ``` **重要な規則**: - API型は `Api` プレフィックス(snake_case) - フロントエンド型は camelCase - 必ずマッピング関数を提供 ### ステップ7: APIクライアント更新 **ファイル**: `main/frontend/src/api/client.ts` **パターン**: ```typescript async get(): Promise>> { const response = await this.requestResponse>( `/endpoint/${encodeURIComponent(param)}` ); if (!response.success || !response.data) { return { success: false, error: response.error }; } return { success: true, data: mapApiTo(response.data), }; } ``` **重要な規則**: - URLパラメータは `encodeURIComponent()` でエンコード - レスポンス型チェックを必ず実施 - エラーハンドリングを統一 ### ステップ8: UI実装ガイダンス **対象ファイル**: `main/frontend/src/pages/*Page.tsx`, `main/frontend/src/components/*` **提供内容**: - どのコンポーネントを更新すべきか - 新しいフィールドの表示例(Tailwind CSS) - 状態管理パターン(useState, useEffect) ## 出力形式 以下の形式で進捗を報告: ``` ✅ ステップ1: ports定義完了 - を追加 - get_メソッドを追加 ✅ ステップ2: JRA-VAN Provider実装完了 - get_を実装 - _to_変換メソッドを実装 ✅ ステップ3: Mock Provider実装完了 - サンプルデータ生成ロジックを実装 ✅ ステップ4: API Handler実装完了 - GET / エンドポイントを実装 ✅ ステップ5: テスト作成完了 - test_get_ を追加 ✅ ステップ6: フロントエンド型定義完了 - Api, 型を追加 - マッピング関数を追加 ✅ ステップ7: APIクライアント更新完了 - getメソッドを追加 ⏳ ステップ8: UI実装ガイダンス - 更新対象: - 表示例コードを提供 次のアクション: - [ ] テスト実行(pytest) - [ ] フロントエンドビルド確認(npm run build) - [ ] デプロイ前チェック(./scripts/pre-deploy-check.sh) ``` ## エラーハンドリング ### 頻出エラーと対処法 1. **ImportError: frozen dataclassのimport漏れ** - 対処: `from dataclasses import dataclass` を確認 2. **型エラー: Optional型の扱い** - 対処: `str | None` 形式を使用(`Optional[str]` は使わない) 3. **Mock更新漏れ** - 対処: 必ず Mock Provider も同時に更新 4. **フロントエンド型不整合** - 対処: `npm run typecheck` で確認 ## 使用例 ### 例1: 馬の血統情報を追加 ``` /api-extend 追加内容: - データクラス名: PedigreeData - フィールド: - horse_id: str - 馬ID - horse_name: str | None - 馬名 - sire_name: str | None - 父馬名 - dam_name: str | None - 母馬名 - broodmare_sire: str | None - 母父馬名 - エンドポイント: GET /horses/{horse_id}/pedigree ``` ### 例2: 既存APIに馬体重フィールドを追加 ``` /api-extend 追加内容: - データクラス名: WeightData - フィールド: - weight: int - 馬体重(kg) - weight_diff: int - 前走比増減 - 既存エンドポイントへの追加: GET /races/{race_id} のrunnersに追加 ``` ## 参照コード 実装時は以下のファイルを参照: - **ports定義**: `main/backend/src/domain/ports/race_data_provider.py` - **JRA-VAN Provider**: `main/backend/src/infrastructure/providers/jravan_race_data_provider.py` - **Mock Provider**: `main/backend/src/infrastructure/providers/mock_race_data_provider.py` - **API Handler**: `main/backend/src/api/handlers/races.py` - **フロント型**: `main/frontend/src/types/index.ts` - **APIクライアント**: `main/frontend/src/api/client.ts` ## 注意事項 - **TDD原則**: テストを先に書いてから実装(Red → Green → Refactor) - **イミュータブル**: すべてのデータクラスは `frozen=True` - **DDD原則**: ドメインロジックはポート・アダプターパターンを遵守 - **型安全性**: フロントエンドは必ず `npm run typecheck` で確認 - **git worktree**: 作業は必ず feature ブランチで(main直接push禁止)