全体処理フロー(ユースケース手順)
0) 起動〜前処理
- CLI 引数を解釈(
-i <yaml>必須) - ロギング初期化(ログは stderr / stdout とは分離)
- 入力ファイル存在チェック(無ければ入力エラーで即時終了)
1) YAML 読み込み(入力→ドメイン)
- YAML を読み込み、ドメインモデル(Book/Chapter/Page)へ変換
- ここでトリム等の正規化(前後 trim)は 変換時に実施(以後は正規化済みを前提にする)
2) ドメイン検証(API を叩く前に止める)
- 必須チェック・型チェック・重複チェックなど(要件にある “順序が意味を持つ” を前提に、順序自体は保持)
- 検証 NG は 入力エラー として即時終了(API は一切呼ばない)
3) 事前取得(同一性判定に必要な最小情報だけ)
- 対象 Book の既存構造(Book直下 pages / chapters / chapter配下 pages)を API で取得
- 取得するのは同一性判定に必要な最小フィールド(例: id, name, parent 等)に限定
4) Book 直下ページ処理(YAMLの順序通り)
-
YAML の
pagesを上から順に処理 -
各ページについて:
- 同一性判定(既存があるか)
- あれば SKIP
- 無ければ CREATE → OK
5) Chapter 処理(YAMLの順序通り)
-
YAML の
chaptersを上から順に処理 -
各 Chapter について:
- 同一性判定(既存があるか)
- 無ければ CREATE → OK
- あれば SKIP
- 以後、その Chapter コンテナ(id)を確定させる
6) Chapter 配下ページ処理(章ごと、YAMLの順序通り)
-
各 Chapter の
pagesを上から順に処理 -
各ページについて:
- 同一性判定
- あれば SKIP
- 無ければ CREATE → OK
7) 終了
- 全処理が成功/スキップで完了したら終了コード 0
3.2 エラー発生時の即時終了ルール
即時終了の原則を先に固定しておくと、実装がブレない。
A. 入力エラー(YAML/検証)は「即時終了・部分実行なし」
- YAML パース不能
- 必須項目不足
- 型不正
- 重複や同一性ルール上の矛盾(設計で決めた NG 条件)
- → 終了コード:入力エラー(例: 2)、stdout には最後に ERROR を出す(下記参照)
B. API 実行エラーは「即時終了」
- 途中まで作ってしまっても “破壊操作なし・再実行で復旧” が前提なので、基本は即時終了でよい
- → 終了コード:実行エラー(例: 1)
C. 例外(想定外)は「即時終了」
- → 実行エラー(1)扱いでよい(内部ログにスタックトレース)
3.3 API リトライ適用範囲(どこでやるかも含めて固定)
リトライの配置
- BookStackClient(インフラ層)に集約 ユースケース層は「API を呼ぶ/呼ばない」の判断だけを持つ。
リトライ対象(推奨)
-
ネットワーク系(タイムアウト、接続失敗、DNS 等)
-
5xx(サーバエラー)
-
429(レート制限)※
Retry-Afterがあれば尊重 -
GET/POST とも対象にできるが、POST は冪等性が崩れやすいので 注意事項を仕様に書く:
- “作成 API が二重作成を起こし得る” 場合:POST は原則リトライしない か、あるいは「作成直後に同一性再チェックして二重作成を抑止」などの方針を明文化(どちらかに寄せて決め打つ)
リトライ非対象(推奨)
- 4xx(400/401/403/404/422 など)※入力や権限や存在の問題なので再試行しても改善しない
- JSON decode 失敗などのアプリ側バグ疑い
3.4 標準出力イベント(OK / SKIP / ERROR)の定義
stdout は「機械/人が追える最小ログ」に限定して、ログ本体は logger 側へ。
1行1イベント(推奨フォーマット例)
OK <kind> <path> <id?> <name>SKIP <kind> <path> <id?> <name> (reason=<...>)ERROR <phase> <message>
ここで:
<kind>:BOOK_PAGE | CHAPTER | CHAPTER_PAGE<path>: 例book/pages[03]chapters[01]chapters[01]/pages[02]<id?>: 作成した/既存の id を出せるなら出す(後追いデバッグが楽)
ERROR 出力のタイミング
- 入力エラー:検証で落ちた時点で ERROR 1行出して終了
- API エラー:リトライ尽きた時点で ERROR 1行出して終了
3.5 「どのモジュールにどの機能を持たせるか」案(責務分割)
実装前提の分割案。名前は例で、責務境界が重要。責務境界と依存方向を固定するのが目的。
(client は外界、domain は純粋、usecase が意思決定、という軸を崩さない)
📁 想定ファイル構成(案)
.venv/
.vscode/
bookstack_skeleton/
__init__.py
main.py
loader.py
domain.py
validator.py
usecase/
__init__.py
apply.py
identity.py
client.py
reporter.py
docs/
tests/
test_main.py
test_loader.py
test_domain.py
test_validator.py
test_usecase_apply.py
test_identity.py
test_client.py
test_reporter.py
create_skeleton.py
usecase.apply.pyは Python 的に不自然になりやすいため、usecase/apply.pyに分割するcreate_skeleton.pyは CLI エントリ(薄いラッパ)で、実処理はbookstack_skeleton.mainに寄せる
bookstack_skeleton.main(エントリポイント)
- CLI 引数の解釈
ログ初期化ロギング初期化(stdout と分離)ユースケース呼び出し入力ロード → 検証 → ユースケース呼び出しの全体制御- 例外整形・終了コード返却
bookstack_skeleton.loader
- YAML の読み込み
ドメインへ変換(正規化:trimYAML等)→ ドメインへの変換- 正規化(例:前後 trim)は ここで完了し、以後は「正規化済み」を前提とする
- 「YAML構造→ドメイン」の責務のみ(
検証は別)検証はvalidatorに分離)
bookstack_skeleton.domain(dataclass 群)
-
BookSpec / ChapterSpec / PageSpec等(命名は任意) -
API や YAML を意識しない純粋なデータ構造
-
ドメイン不変条件(
軽いバリデーションはここでも可。ただし“入力エラー一覧”を出したいなら軽いバリデーションはここでも可)- ただし「入力エラーを複数件まとめて提示」したい場合は
validatorに寄せる方が実務的)に寄せる
- ただし「入力エラーを複数件まとめて提示」したい場合は
-
前提:
loaderが正規化済みの値を渡す(domain 側で trim はしない)
bookstack_skeleton.validator
- ドメイン検証(必須・型・重複・規約)
- エラーは「複数件まとめて」返せる形が望ましい(
ユーザーの修正コストが下がる)ユーザー修正コスト削減) - 前提:ここで落ちた場合は 入力エラーとして API を叩かずに終了(main の責務)
bookstack_skeleton.usecase.apply(ユースケース本体)
- 処理順の固定(
Book直下→Chapter→Book直下 → Chapter → Chapter配下) 同一性判定の呼び出し事前取得(同一性判定に必要な最小情報)と、その結果の保持- 同一性判定(
identity)の呼び出し - “作る/
作らない”作らない(SKIP/CREATE)” の意思決定 - stdout イベントの発行(
Reporterreporter経由でも良い)経由)
bookstack_skeleton.identity
- 同一性判定ルール(Book直下ページ / Chapter / Chapter配下ページ)
- “SKIP 条件”
をここで一元化(=を一元化(判定結果にreasonを持たせる) clientは呼ばない(外界依存を持たない)。必要な既存情報はusecaseが渡す
bookstack_skeleton.client(BookStackClient)
- BookStack API 呼び出しの薄いラッパ
- リトライ(範囲・回数・待機)をここに集約
- レート制限対応(必要なら
Retry-After等)もここ - 取得最適化(「
最小フィールドだけ取る」など取得最適化もここ同一性判定に必要な最小フィールドだけ取る」)もここ - 業務ルール(同一性、SKIP、処理順)は一切持たない
bookstack_skeleton.reporter(任意)stdout 出力)
- stdout のイベント整形(
OK/SKIP/OK / SKIP / ERROR の1行化) main/表示フォーマットをここに固定し、main/usecaseから分離したい場合のみを汚さない- 将来の拡張(quiet、JSON、CI向け)もここで吸収できるようにする