Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions docs/explanation/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# アーキテクチャ概要

## レイヤー構造

nene2-python はクリーンアーキテクチャに基づいており、依存関係は外から内へ向かいます。

```
┌─────────────────────────────────────────────┐
│ HTTP Handler (FastAPI router) │
│ parse request → call use-case → response │
├─────────────────────────────────────────────┤
│ UseCase │
│ ビジネスロジック。HTTP・DB を知らない │
├─────────────────────────────────────────────┤
│ RepositoryInterface (ABC) │
│ ドメインが必要とする操作の契約 │
├─────────────────────────────────────────────┤
│ ConcreteRepository │
│ SQLAlchemy / InMemory 実装 │
└─────────────────────────────────────────────┘
```

## 各レイヤーの責務

### HTTP Handler

- **唯一の責務**: リクエストを解析し UseCase を呼び、レスポンスを返す
- Pydantic `BaseModel` でリクエストボディを検証(HTTP 境界のみ)
- ドメインロジックを持たない
- `make_xxx_router()` ファクトリ関数がルーターを返す

```python
@router.post("", status_code=201)
async def create_note(body: CreateNoteBody) -> JSONResponse:
note = create_use_case.execute(CreateNoteInput(title=body.title, body=body.body))
return JSONResponse({"id": note.id, "title": note.title, "body": note.body}, status_code=201)
```

### UseCase

- **唯一の責務**: ビジネスルールを実装する
- `execute(input_: XxxInput) -> XxxOutput` の単一メソッド
- `import fastapi`, `import sqlalchemy` を持たない
- 他の UseCase を呼ばない(オーケストレーションは上位層)
- `InMemoryRepository` でテスト可能

### RepositoryInterface

- ABC で契約を定義
- UseCase は Interface のみに依存する(具象クラスを知らない)
- InMemory 実装と SQLAlchemy 実装が同じ Interface を実装する

### ConcreteRepository

- SQLAlchemy Core(ORM なし)でパラメータ化クエリを実行
- `SqlAlchemyQueryExecutor` でクエリを抽象化
- テーブルスキーマは `src/example/schema.py` で一元管理

## ミドルウェアスタック

リクエストは外側から内側に向かって処理されます:

```
BearerTokenMiddleware 認証 (Bearer Token)
ApiKeyAuthMiddleware 認証 (API Key)
CORSMiddleware CORS
ThrottleMiddleware レートリミット
RequestSizeLimitMiddleware ペイロードサイズ制限
RequestLoggingMiddleware リクエストロギング
RequestIdMiddleware リクエスト ID 付与
SecurityHeadersMiddleware セキュリティヘッダー付与
ErrorHandlerMiddleware 例外 → RFC 9457 Problem Details 変換
```

## DI パターン

FastAPI の `Depends` は HTTP 境界のみで使用します。UseCase とリポジトリはコンストラクタインジェクションで接続します。

```python
# app.py — ワイヤリング
note_repo = SqlAlchemyNoteRepository(executor)
app.include_router(make_note_router(
list_use_case=ListNotesUseCase(note_repo),
create_use_case=CreateNoteUseCase(note_repo),
...
))
```

## ドメインパッケージ構造

```
src/example/<domain>/
__init__.py
entity.py — @dataclass(frozen=True, slots=True)
repository.py — ABC + InMemory 実装
exceptions.py — XxxNotFoundException + ExceptionHandler
use_case.py — 5 UseCase + Input/Output DTO
handler.py — FastAPI router
sqlalchemy_repository.py — SQL バックエンド
```
68 changes: 68 additions & 0 deletions docs/explanation/design-philosophy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# 設計思想と PHP NENE2 との対応

## NENE2 の設計原則

nene2-python は PHP 版 NENE2 と同一の設計思想を持ちます。

### API First

JSON API と OpenAPI 契約を中心に据えます。DB 設計より先に API の形を定義し、スキーマを `uv run export-openapi` で生成します。

### 薄い HTTP 層

HTTP Handler はビジネスロジックを持ちません。**parse → use-case → response** の 3 ステップのみ。ドメインルールは UseCase に集約されます。

### AI-readable

明示的なディレクトリ構造、小さなクラス(150 行以下)、型付き境界により、LLM がコードベースを正確に理解・操作できます。

### Security First

セキュリティは後付けではなく設計の出発点です。
- Pydantic による HTTP 境界の全入力検証
- パラメータ化クエリのみ(SQLインジェクション防止)
- `secrets.compare_digest` によるタイミング安全な比較
- セキュリティヘッダーをミドルウェアで付与

### LLM Delivery Ready

UseCase は HTTP・DB から独立しているため、MCP ツールとして直接再利用できます。`src/example/mcp.py` はその実証です。

## PHP NENE2 との対応表

| PHP 版 | Python 版 | 備考 |
|---|---|---|
| `readonly class` | `@dataclass(frozen=True, slots=True)` | 不変 Value Object |
| `ValidationException` + `ValidationError` | 同名クラス (`nene2.validation`) | 422 + Problem Details |
| `PaginationQueryParser` | `nene2.http.PaginationQueryParser` | クエリパラメータ解析 |
| `PaginationResponse` | `nene2.http.PaginationResponse` | ページネーションレスポンス |
| `ProblemDetailsResponseFactory` | `nene2.http.problem_details_response()` | RFC 9457 |
| `ErrorHandlerMiddleware` | `nene2.middleware.ErrorHandlerMiddleware` | 全例外をキャッチ |
| `PHPStan level 8` | `mypy --strict` | 最高レベルの型チェック |
| `PHP-CS-Fixer` | `ruff format` | コードフォーマット |
| `UseCaseInterface` | `nene2.use_case.UseCaseProtocol[I, O]` | 構造的サブタイピング |

## Python 3.12+ 固有の選択

| 用途 | 選択 | 理由 |
|---|---|---|
| 型エイリアス | `type X = list[str]` | PEP 695 — 新構文 |
| ジェネリクス | `class Foo[T]` | PEP 695 — TypeVar 不要 |
| 不変 VO | `dataclass(frozen=True, slots=True)` | メモリ効率 + 不変性 |
| HTTP 検証 | Pydantic v2 BaseModel | 高速 + 型安全 |
| SQL | SQLAlchemy Core | ORM なしで SQL を直接制御 |
| ロギング | structlog | JSON / Console の両対応 |
| MCP | mcp (Anthropic SDK) | FastMCP ラッパー |

## ADR 一覧

設計の個別決定は ADR に記録されています:

- [ADR-0001: ツールチェーン](../adr/0001-toolchain.md)
- [ADR-0002: クリーンアーキテクチャ](../adr/0002-clean-architecture.md)
- [ADR-0003: セキュリティファースト](../adr/0003-security-first.md)
- [ADR-0004: AI ファースト設計](../adr/0004-ai-first-design.md)
- [ADR-0005: ロギング](../adr/0005-logging.md)
- [ADR-0006: レートリミット](../adr/0006-rate-limiting.md)
- [ADR-0009: MCP 設計](../adr/0009-mcp-design.md)
- [ADR-0010: AsyncUseCase パターン](../adr/0010-async-use-case.md)
87 changes: 87 additions & 0 deletions docs/how-to/add-new-domain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# 新しいドメインを追加する

既存の Note・Tag・Comment ドメインと同じパターンで新しいドメインを追加するチェックリストです。

## チェックリスト

### 1. ドメインパッケージを作成する

```bash
mkdir -p src/example/<domain>
touch src/example/<domain>/__init__.py
```

### 2. 各ファイルを作成する

| ファイル | 内容 |
|---|---|
| `entity.py` | `@dataclass(frozen=True, slots=True)` でエンティティを定義 |
| `repository.py` | `XxxRepositoryInterface(ABC)` + `InMemoryXxxRepository` |
| `exceptions.py` | `XxxNotFoundException` + `XxxNotFoundExceptionHandler` |
| `use_case.py` | 5 UseCase (List / Get / Create / Update / Delete) + Input/Output DTO |
| `handler.py` | `make_xxx_router()` — parse → use-case → response |
| `sqlalchemy_repository.py` | SQL バックエンド実装 |

### 3. schema.py にテーブルを追加する

`src/example/schema.py` の `ensure_schema()` にテーブル定義を追加します。

```python
executor.write(
"CREATE TABLE IF NOT EXISTS your_domain ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"name TEXT NOT NULL,"
"created_at DATETIME DEFAULT CURRENT_TIMESTAMP"
")"
)
```

### 4. app.py に組み込む

`src/example/app.py` の `_build_repositories()` と `create_app()` を更新します。

```python
# _build_repositories() の戻り値に追加
your_repo = SqlAlchemyYourRepository(executor)

# create_app() でルーターを登録
app.include_router(make_your_router(
list_use_case=ListYourUseCase(your_repo),
...
))
```

### 5. テストを書く

```
tests/example/<domain>/
__init__.py
test_<domain>_use_case.py # UseCase 単体テスト(DB なし)
test_<domain>_repository.py # Repository 契約テスト(InMemory + SQLAlchemy)
test_<domain>_http.py # HTTP 統合テスト(TestClient)
```

### 6. MCP ツールに追加する(任意)

`src/example/mcp.py` の `create_mcp_server()` に UseCase を登録します。

### 7. 全チェックを通過させる

```bash
uv run pytest && \
uv run mypy src/ && \
uv run ruff check src/ tests/ && \
uv run ruff format --check src/ tests/
```

## 命名規則

- エンティティクラス: `PascalCase` (`Note`, `Tag`, `Comment`)
- UseCase 入力 DTO: `XxxInput` (`CreateNoteInput`)
- 例外: `XxxNotFoundException`
- ハンドラーファクトリ: `make_xxx_router()`

## 参考実装

- `src/example/note/` — 基本的な CRUD ドメイン
- `src/example/comment/` — 外部キー (note_id) を持つネストドメイン
84 changes: 84 additions & 0 deletions docs/how-to/configure-auth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# 認証を設定する

nene2-python は Bearer Token 認証と API Key 認証の 2 種類をサポートしています。
どちらもミドルウェアとして実装されており、環境変数で有効化できます。

## Bearer Token 認証

### 有効化する

`.env` ファイルに以下を追加します:

```dotenv
BEARER_TOKEN_ENABLED=true
BEARER_TOKENS=token1,token2,token3
```

### 動作

- `Authorization: Bearer <token>` ヘッダーが必須になります
- トークンは `secrets.compare_digest` でタイミング安全な比較を行います
- 無効なトークンは `401 Unauthorized` を返します

### curl での利用

```bash
curl -H "Authorization: Bearer token1" http://localhost:8080/notes
```

## API Key 認証

### 有効化する

```dotenv
API_KEY_ENABLED=true
API_KEYS=key1,key2
```

### 動作

- `X-Api-Key: <key>` ヘッダーが必須になります
- 無効なキーは `401 Unauthorized` を返します

### curl での利用

```bash
curl -H "X-Api-Key: key1" http://localhost:8080/notes
```

## 両方を有効化する場合

Bearer Token と API Key を同時に有効化すると、リクエストは両方の認証を通過する必要があります。
通常は どちらか一方を使います。

## テスト時に認証を無効化する

```python
from nene2.config import AppSettings
from fastapi.testclient import TestClient
from example.app import create_app

client = TestClient(create_app(AppSettings(bearer_token_enabled=False)))
```

## カスタム TokenVerifier を実装する

`TokenVerifierProtocol` を実装することで、JWT や外部サービスによる検証を追加できます。

```python
from nene2.auth.interfaces import TokenVerifierProtocol
import jwt

class JwtTokenVerifier:
def __init__(self, secret: str) -> None:
self._secret = secret

def verify(self, token: str) -> bool:
try:
jwt.decode(token, self._secret, algorithms=["HS256"])
return True
except jwt.InvalidTokenError:
return False
```

`create_app()` の `verifier` 引数に渡すか、`BearerTokenMiddleware` を直接使います。
Loading
Loading