refactor: conform standards docs to doc-forge wiki/lib layout; generic build.py; static kind links
This commit is contained in:
89
standards/src/lib/03_composition_root.md
Normal file
89
standards/src/lib/03_composition_root.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# Composition Root
|
||||
|
||||
## Summary
|
||||
|
||||
All wiring happens in exactly one module. It builds a **flat resource
|
||||
registry** and exposes it to the HTTP layer and to scripts.
|
||||
|
||||
---
|
||||
|
||||
## The resource registry
|
||||
|
||||
`init.py` returns a plain `dict[str, Any]`. Keys follow two conventions:
|
||||
|
||||
- `<resource>_repo` — a repository adapter for a resource.
|
||||
- `<resource>_svc` — a service for a resource.
|
||||
|
||||
Plus named infrastructure adaptors:
|
||||
|
||||
- `http_cache_store` — response cache backend.
|
||||
- `progress_bus` — progress/event bus.
|
||||
- `<vendor>_<kind>` — fetchers, clients, stores.
|
||||
|
||||
The API stores the dict on `app.state.resources` during lifespan startup.
|
||||
Handlers read from it; nothing imports concrete adapters.
|
||||
|
||||
## Shape
|
||||
|
||||
```python
|
||||
_resources: dict = {}
|
||||
_initialized = False
|
||||
|
||||
async def init(settings=None) -> dict:
|
||||
global _initialized
|
||||
if _initialized:
|
||||
return _resources
|
||||
|
||||
await setup_database(uri=settings.mongo_uri, db_name=settings.mongo_db)
|
||||
|
||||
# 1. leaf repositories
|
||||
user_repo = create_user_repo()
|
||||
|
||||
# 2. dependent repositories (dependencies passed by keyword)
|
||||
wallet_repo = create_wallet_repo(user_repo=user_repo)
|
||||
|
||||
# 3. services (build via the DI library, injecting the repo)
|
||||
_resources["user_repo"] = user_repo
|
||||
_resources["wallet_repo"] = wallet_repo
|
||||
_resources["user_svc"] = get_user_service(user_repo)
|
||||
_resources["wallet_svc"] = get_wallet_service(wallet_repo)
|
||||
|
||||
# 4. infrastructure adaptors (optional)
|
||||
_resources["http_cache_store"] = HttpCacheStore(...)
|
||||
_resources["progress_bus"] = ProgressEventBus()
|
||||
|
||||
_initialized = True
|
||||
return _resources
|
||||
|
||||
async def shutdown() -> None:
|
||||
global _initialized
|
||||
await close_stores(_resources)
|
||||
await shutdown_database()
|
||||
_resources.clear()
|
||||
_initialized = False
|
||||
```
|
||||
|
||||
## Typed accessors
|
||||
|
||||
Expose a `get_svc(name)` helper with one `@overload` per key so scripts get
|
||||
typing without importing concrete classes:
|
||||
|
||||
```python
|
||||
@overload
|
||||
def get_svc(name: Literal["user_svc"]) -> UserService: ...
|
||||
@overload
|
||||
def get_svc(name: Literal["wallet_svc"]) -> WalletService: ...
|
||||
|
||||
def get_svc(name: str):
|
||||
return _resources[name]
|
||||
```
|
||||
|
||||
Also expose `get_db()` (raw database handle) and pure helpers such as
|
||||
`compute_fingerprint(payload)` (canonical hash used for deduplication).
|
||||
|
||||
## Rules
|
||||
|
||||
- `init()` is **idempotent**.
|
||||
- The composition root is the only place that imports concrete adapters.
|
||||
- Handlers never call `init()`; they read `request.app.state.resources`.
|
||||
- Scripts call `init()` / `shutdown()` around their work.
|
||||
Reference in New Issue
Block a user