docs: update for new crate layout
This commit is contained in:
@@ -47,13 +47,17 @@ Small files with a single concern. If a file grows past ~100-150 lines, it's pro
|
|||||||
|
|
||||||
### Tests
|
### Tests
|
||||||
|
|
||||||
Never in the same file as production code. Use cases keep their tests in a sibling `tests/` module, one file per use case:
|
Never in the same file as production code. Each crate keeps its tests in `crates/<crate>/tests/`, one file per unit under test, grouped by the same module as the production code. Cargo builds each top-level `tests/<group>.rs` as one integration-test binary; the group file lists its members with `#[path]`:
|
||||||
|
|
||||||
```
|
```
|
||||||
crates/application/src/diary/log_review.rs
|
crates/application/src/diary/log_review.rs
|
||||||
crates/application/src/diary/tests/log_review.rs
|
crates/application/tests/diary.rs # mod support; + #[path] list of the group
|
||||||
|
crates/application/tests/diary/log_review_test.rs
|
||||||
|
crates/application/tests/support/ # shared contexts and command builders
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Adapters follow the same layout (`crates/adapters/sqlite/tests/repositories/diary_repository_test.rs`). The only tests left beside source are small mapper tests in `crates/adapters/http-axum/src/**/tests.rs`, which need crate-private items; their shared fixtures live in `http-axum/src/test_support.rs`. Route smoke, API and handler tests run against the real composition and live in `crates/bootstrap/tests/`.
|
||||||
|
|
||||||
## Naming
|
## Naming
|
||||||
|
|
||||||
### Conventions
|
### Conventions
|
||||||
@@ -174,7 +178,7 @@ Errors propagate with `?` and bubble up to a single handler. No scattered panic
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Secrets (API keys, JWT secrets) and deployment settings live in environment variables, read once at startup into a typed config struct.
|
Secrets (API keys, JWT secrets) and deployment settings are read once at startup by the `config` crate into a typed `AppConfig`: built-in defaults, then `config.toml` (or the file named by `CONFIG_FILE`), then environment variables. Malformed values fail startup; empty values count as unset. Only the binaries load configuration; everything else receives typed config values.
|
||||||
|
|
||||||
Zero magic numbers or strings. Everything named, everything explained by its name.
|
Zero magic numbers or strings. Everything named, everything explained by its name.
|
||||||
|
|
||||||
@@ -207,7 +211,8 @@ When none of those can carry it, a short comment stating the reason (never resta
|
|||||||
- Domain crate: zero external dependencies beyond `thiserror`, `serde` and small value-type crates
|
- Domain crate: zero external dependencies beyond `thiserror`, `serde` and small value-type crates
|
||||||
- Application crate: depends only on domain
|
- Application crate: depends only on domain
|
||||||
- Adapters: free to pull in platform crates (`axum`, `sqlx`, `nats`, etc.)
|
- Adapters: free to pull in platform crates (`axum`, `sqlx`, `nats`, etc.)
|
||||||
- Bootstrap: wiring only
|
- Bootstrap: wiring only; the only crate that names adapters
|
||||||
|
- Binaries (`server`, `worker`): load config, call `bootstrap`, run — nothing else (`make check-binaries-are-thin`)
|
||||||
|
|
||||||
## Trait Objects for Ports
|
## Trait Objects for Ports
|
||||||
|
|
||||||
@@ -226,7 +231,7 @@ Prefer a closed enum over `dyn` only where exhaustiveness is the point — when
|
|||||||
|
|
||||||
### Test Helpers Are Gated
|
### Test Helpers Are Gated
|
||||||
|
|
||||||
`domain::testing` (in-memory fakes and noops) compiles only with the `test-helpers` feature, which crates enable in their `[dev-dependencies]`. Production builds must never contain fakes. `application::test_helpers` is `#[cfg(test)]`.
|
`domain::testing` (factories, the cascading `InMemoryStore`, noops and `Refusing*` fakes) compiles only with the `test-helpers` feature, which crates enable in their `[dev-dependencies]`. Production builds must never contain fakes. Build entities with the `domain::testing` factories instead of repeating struct literals in each test.
|
||||||
|
|
||||||
### Coverage Targets
|
### Coverage Targets
|
||||||
|
|
||||||
@@ -235,7 +240,7 @@ Prefer a closed enum over `dyn` only where exhaustiveness is the point — when
|
|||||||
| Domain | TDD (red-green-refactor) | 100% |
|
| Domain | TDD (red-green-refactor) | 100% |
|
||||||
| Application | TDD | High coverage |
|
| Application | TDD | High coverage |
|
||||||
| Adapters | Integration tests where practical | Best effort |
|
| Adapters | Integration tests where practical | Best effort |
|
||||||
| Bootstrap | No tests | — |
|
| Bootstrap | Route smoke, API and handler tests over the real wiring | Every route reachable |
|
||||||
|
|
||||||
### Fakes Must Not Be Kinder Than Production
|
### Fakes Must Not Be Kinder Than Production
|
||||||
|
|
||||||
|
|||||||
@@ -23,55 +23,61 @@ cd spa && bun install && bun run dev
|
|||||||
## Before submitting a PR
|
## Before submitting a PR
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make # runs fmt-check + clippy + test
|
make check # the full gate; plain `make` runs it too
|
||||||
```
|
```
|
||||||
|
|
||||||
Or individually:
|
`make check` runs, in order: `fmt-check`, `clippy` (workspace, all targets, `-D warnings`), `test`, `test-federation-off` (workspace tests with default features off), then the layering guards (`check-appcontext`, `check-handler-deps`, `check-handler-repos`, `check-http-adapter-deps`, `check-federation-off-social`, `check-sqlite-is-ap-free`, `check-binaries-are-thin`, `check-dockerfile-manifests`). CI runs the same steps.
|
||||||
|
|
||||||
|
For the SPA also run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo fmt --check
|
|
||||||
cargo clippy -- -D warnings
|
|
||||||
cargo test
|
|
||||||
cd spa && bunx tsc --noEmit
|
cd spa && bunx tsc --noEmit
|
||||||
```
|
```
|
||||||
|
|
||||||
All four must pass. PRs with clippy warnings or failing tests won't be merged.
|
PRs with a failing gate won't be merged.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
The project follows hexagonal (ports & adapters) architecture. See `architecture.mmd` for the full diagram.
|
The project follows hexagonal (ports & adapters) architecture. See `architecture.mmd` for the full diagram.
|
||||||
|
|
||||||
**Key rules:**
|
**Key rules:**
|
||||||
- Presentation handlers never touch repositories directly — all domain logic goes through use cases in the `application` crate
|
- HTTP handlers never touch repositories directly — all domain logic goes through use cases in the `application` crate
|
||||||
- Application use cases return raw domain data — URL formatting, date display, and view model assembly belong in presentation mappers (`crates/adapters/http-axum/src/mappers/`)
|
- Application use cases return raw domain data — URL formatting, date display, and view model assembly belong in the http-axum mappers (`crates/adapters/http-axum/src/html/mappers/` for the classic UI, the DTO conversions beside each handler under `api/`)
|
||||||
- Use cases called from presentation handlers take a `&FooDeps` struct (registered in `application::Deps`, built by `bootstrap::build_deps`) — never `&AppContext` itself, which `application` cannot even depend on. A few keep individual `Arc<dyn Trait>` params instead: `enrich_movie` and `request_enrichment` because they are called from adapters rather than handlers, and `diary::log_review` because its one extra dependency comes from `Services` rather than a repository. `wrapup::compute`, `import::cleanup` and `integrations::cleanup` also take individual params, but they are only ever called from jobs. See ADR-0007 for the exact list and why each is legitimate
|
- Use cases called from HTTP handlers take a `&FooDeps` struct (registered in `application::Deps`, built by `bootstrap`) — never `&AppContext` itself, which `application` cannot even depend on. A few keep individual `Arc<dyn Trait>` params instead: `enrich_movie` and `request_enrichment` because they are called from adapters rather than handlers, and `diary::log_review` because its one extra dependency comes from `Services` rather than a repository. `wrapup::compute`, `import::cleanup` and `integrations::cleanup` also take individual params, but they are only ever called from jobs. See ADR-0007 for the exact list and why each is legitimate
|
||||||
|
|
||||||
```
|
```
|
||||||
domain → pure types, traits (ports), zero deps
|
domain → pure types, traits (ports), zero deps
|
||||||
application → use cases, orchestration
|
application → use cases, orchestration
|
||||||
http-axum → Axum handlers, routes, OpenAPI
|
config → typed AppConfig: defaults → config.toml / CONFIG_FILE → env
|
||||||
worker → event consumer, background jobs
|
bootstrap → composition root, the only crate that names adapters
|
||||||
adapters/* → implements domain ports (sqlite, AP, etc.)
|
server, worker → thin binaries: load config, call bootstrap, run
|
||||||
|
adapters/* → implement domain ports (sqlite, activitypub, ...); http-axum is the HTTP driving adapter
|
||||||
spa/ → React SPA (TanStack Router + shadcn/ui)
|
spa/ → React SPA (TanStack Router + shadcn/ui)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Cargo features: `federation` (default; ActivityPub) and `nats`. `sqlite` and `sqlite-federation` remain as deprecated aliases so old build commands keep working.
|
||||||
|
|
||||||
### Adding a new feature
|
### Adding a new feature
|
||||||
|
|
||||||
1. **Domain first** — models in `domain/src/models/`, ports in `ports.rs`, events in `events.rs`
|
1. **Domain first** — models in `domain/src/models/`, ports in `domain/src/ports/`, events in `domain/src/events.rs`
|
||||||
2. **Adapters** — implement ports in the `sqlite*` adapters, add migration
|
2. **Adapters** — implement ports under `crates/adapters/sqlite/src/repositories/<aggregate>/`, add a migration
|
||||||
3. **Application** — use cases in `application/src/<domain>/`, wire into `context.rs`
|
3. **Application** — use cases in `application/src/<domain>/`, add the deps struct to `application/src/deps.rs`, build it in `bootstrap`
|
||||||
4. **API types** — DTOs in `api-types/src/`
|
4. **API types** — DTOs in `api-types/src/`
|
||||||
5. **Presentation** — handler file in `handlers/<domain>.rs`, routes in `routes.rs`
|
5. **HTTP** — JSON handlers in `crates/adapters/http-axum/src/api/<domain>.rs`, classic HTML handlers in `.../html/<domain>.rs` (templates in `html/templates/`), routes in `router.rs` / `api/mod.rs` / `html/mod.rs`
|
||||||
6. **SPA** — API client in `spa/src/lib/api/`, hook in `spa/src/hooks/`, components
|
6. **SPA** — API client in `spa/src/lib/api/`, hook in `spa/src/hooks/`, components
|
||||||
7. **Classic HTML** — Askama template + CSS in `static/style.css`
|
7. **Classic HTML** — Askama template + CSS in `static/style.css`
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
Tests live in `crates/<crate>/tests/`, never in the production file (see `CODE_STYLE.md`). Build fixtures with the `domain::testing` factories and use the cascading `InMemoryStore` and `Refusing*` fakes. Route smoke, API and handler tests over the real wiring are in `crates/bootstrap/tests/`.
|
||||||
|
|
||||||
### Database adapters
|
### Database adapters
|
||||||
|
|
||||||
SQLite is the only supported backend (see [ADR-0012](docs/adr/0012-sqlite-is-the-only-backend.md)). Add migrations under the sqlite adapter; SQLite uses `?` params and text datetimes.
|
SQLite is the only supported backend (see [ADR-0012](docs/adr/0012-sqlite-is-the-only-backend.md)). Add migrations under `crates/adapters/sqlite`; SQLite uses `?` params and text datetimes.
|
||||||
|
|
||||||
### Federation (ActivityPub)
|
### Federation (ActivityPub)
|
||||||
|
|
||||||
Federation is feature-gated (`#[cfg(feature = "federation")]`). If your feature should federate, add domain events, handle them in `activitypub/src/event_handler.rs`, and create an AP object + inbound handler.
|
Federation is feature-gated (`#[cfg(feature = "federation")]`). If your feature should federate, add domain events, handle them in `crates/adapters/activitypub/src/event_handler.rs`, and create an AP object + inbound handler.
|
||||||
|
|
||||||
## Code style
|
## Code style
|
||||||
|
|
||||||
@@ -83,5 +89,5 @@ Federation is feature-gated (`#[cfg(feature = "federation")]`). If your feature
|
|||||||
## Areas seeking help
|
## Areas seeking help
|
||||||
|
|
||||||
- **TUI** (`crates/tui`) — deprecated, needs a maintainer to bring it up to feature parity
|
- **TUI** (`crates/tui`) — deprecated, needs a maintainer to bring it up to feature parity
|
||||||
- **Tests** — the domain and application crates have 400+ unit tests; integration tests for the presentation layer are welcome
|
- **Tests** — the domain and application crates have 400+ tests; more route and handler tests in `crates/bootstrap/tests/` are welcome
|
||||||
- **Docs** — API usage examples, deployment guides
|
- **Docs** — API usage examples, deployment guides
|
||||||
|
|||||||
39
README.md
39
README.md
@@ -93,6 +93,7 @@ Hexagonal (Ports & Adapters) with Domain-Driven Design:
|
|||||||
api-types — shared REST API request/response DTOs (Serialize/Deserialize + utoipa schemas) + HtmlPageContext; used by http-axum and tui
|
api-types — shared REST API request/response DTOs (Serialize/Deserialize + utoipa schemas) + HtmlPageContext; used by http-axum and tui
|
||||||
config — typed configuration (defaults, config.toml, legacy env names); loaded once by the binaries
|
config — typed configuration (defaults, config.toml, legacy env names); loaded once by the binaries
|
||||||
bootstrap — composition root: the only crate that names adapters; builds the server router and the worker service
|
bootstrap — composition root: the only crate that names adapters; builds the server router and the worker service
|
||||||
|
event-payload — serde payloads for domain events, shared by the NATS and SQLite-queue adapters
|
||||||
domain — pure types and CQRS port traits (MovieCommand/MovieQuery, WatchEventCommand/WatchEventQuery, GoalCommand/GoalQuery, DiaryQuery, PersonCommand/PersonQuery, SearchCommand/SearchPort, SocialCommand/SocialQuery, ImageFetcher, RssFeedRenderer), no external deps except serde
|
domain — pure types and CQRS port traits (MovieCommand/MovieQuery, WatchEventCommand/WatchEventQuery, GoalCommand/GoalQuery, DiaryQuery, PersonCommand/PersonQuery, SearchCommand/SearchPort, SocialCommand/SocialQuery, ImageFetcher, RssFeedRenderer), no external deps except serde
|
||||||
application — use cases (commands + queries), business logic orchestration; handlers delegate here for all domain logic; modules: auth, diary, goals, import, integrations, movies, person, search, social, users, watchlist, wrapup
|
application — use cases (commands + queries), business logic orchestration; handlers delegate here for all domain logic; modules: auth, diary, goals, import, integrations, movies, person, search, social, users, watchlist, wrapup
|
||||||
server — thin HTTP binary (dotenv, config, bootstrap::build_server, axum::serve); features (federation, nats) forward to bootstrap
|
server — thin HTTP binary (dotenv, config, bootstrap::build_server, axum::serve); features (federation, nats) forward to bootstrap
|
||||||
@@ -100,7 +101,7 @@ worker — thin worker binary (bootstrap::build_worker, run until c
|
|||||||
adapters/
|
adapters/
|
||||||
http-axum — Axum HTTP adapter: JSON API, classic HTML UI (askama templates), OpenAPI spec, Swagger UI + Scalar, federation-gated routes; library crate, no binary of its own
|
http-axum — Axum HTTP adapter: JSON API, classic HTML UI (askama templates), OpenAPI spec, Swagger UI + Scalar, federation-gated routes; library crate, no binary of its own
|
||||||
auth — JWT issuance and validation (Argon2 passwords)
|
auth — JWT issuance and validation (Argon2 passwords)
|
||||||
sqlite — SQLite repositories, search (FTS5), social graph, durable event queue, connection factory + migrations
|
sqlite — SQLite repositories (one module per aggregate under repositories/), search (FTS5), social graph, durable event queue, connection factory + migrations; the only storage adapter
|
||||||
metadata — TMDB / OMDb HTTP client
|
metadata — TMDB / OMDb HTTP client
|
||||||
poster-fetcher — downloads poster images
|
poster-fetcher — downloads poster images
|
||||||
object-storage — stores images (posters + user avatars) on local filesystem or S3-compatible storage
|
object-storage — stores images (posters + user avatars) on local filesystem or S3-compatible storage
|
||||||
@@ -110,10 +111,10 @@ adapters/
|
|||||||
export — CSV and JSON diary serialization
|
export — CSV and JSON diary serialization
|
||||||
importer — CSV/TSV/JSON/XLSX parser and column mapper for bulk import
|
importer — CSV/TSV/JSON/XLSX parser and column mapper for bulk import
|
||||||
media-server — Jellyfin and Plex webhook payload parsers (MediaServerParser adapter; Plex requires Plex Pass)
|
media-server — Jellyfin and Plex webhook payload parsers (MediaServerParser adapter; Plex requires Plex Pass)
|
||||||
event-publisher — in-memory event channel (used in tests)
|
event-publisher — in-memory event channel
|
||||||
nats — NATS Core / JetStream event publisher and consumer
|
nats — NATS Core / JetStream event publisher and consumer
|
||||||
activitypub — ActivityPub federation adapter (follow, inbox/outbox, actor); delegates to k-ap for protocol internals
|
activitypub — ActivityPub federation adapter (follow, inbox/outbox, actor); delegates to k-ap for protocol internals
|
||||||
sqlite-federation — SQLite-backed federation repository
|
sqlite-federation — SQLite-backed federation repository (k-ap trait implementations; only built with the `federation` feature)
|
||||||
tui — terminal UI client (ratatui); shares api-types with http-axum for typed API access
|
tui — terminal UI client (ratatui); shares api-types with http-axum for typed API access
|
||||||
spa/ — React SPA (TanStack Router + shadcn/ui + Vite); served at /app/ by the backend
|
spa/ — React SPA (TanStack Router + shadcn/ui + Vite); served at /app/ by the backend
|
||||||
```
|
```
|
||||||
@@ -127,7 +128,13 @@ spa/ — React SPA (TanStack Router + shadcn/ui + Vite); served a
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Copy `.env.example` to `.env` and set the values below. Required fields must be set before the server will start.
|
Settings are layered, later sources winning:
|
||||||
|
|
||||||
|
1. built-in defaults
|
||||||
|
2. `config.toml` in the working directory, or the file named by `CONFIG_FILE` (a named file that does not exist is an error; the default `config.toml` is optional)
|
||||||
|
3. environment variables (including those from `.env`), under the names in the table below
|
||||||
|
|
||||||
|
Copy `.env.example` to `.env` and set the values you need. Malformed values (a non-numeric `PORT`, an unknown `EVENT_BUS_BACKEND`) fail startup with a message naming the setting. An empty value (`PORT=`) counts as unset, so the default applies. The variable names are unchanged from earlier releases; every one maps to a `section.field` key in `config.toml` (for example `PORT` is `server.port`, `JWT_SECRET` is `auth.jwt_secret`).
|
||||||
|
|
||||||
| Variable | Default | Required | Description |
|
| Variable | Default | Required | Description |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
@@ -155,6 +162,17 @@ Copy `.env.example` to `.env` and set the values below. Required fields must be
|
|||||||
| `EVENT_BUS_BACKEND` | `db` | No | `db` (default) or `nats` |
|
| `EVENT_BUS_BACKEND` | `db` | No | `db` (default) or `nats` |
|
||||||
| `NATS_URL` | — | NATS only | NATS connection URL (e.g. `nats://localhost:4222`) |
|
| `NATS_URL` | — | NATS only | NATS connection URL (e.g. `nats://localhost:4222`) |
|
||||||
|
|
||||||
|
Required fields must be set (in either source) before the server will start.
|
||||||
|
|
||||||
|
### Cargo features
|
||||||
|
|
||||||
|
| Feature | Default | Effect |
|
||||||
|
|---|---|---|
|
||||||
|
| `federation` | yes | ActivityPub federation (`activitypub`, `sqlite-federation`, federation routes) |
|
||||||
|
| `nats` | no | NATS event bus (`EVENT_BUS_BACKEND=nats`) |
|
||||||
|
|
||||||
|
Build without federation with `--no-default-features`. `sqlite` and `sqlite-federation` are deprecated aliases kept so old build commands still work.
|
||||||
|
|
||||||
The `worker` binary must run alongside `server` to process events:
|
The `worker` binary must run alongside `server` to process events:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -208,23 +226,26 @@ Supports review logging, bulk CSV import, and diary browsing.
|
|||||||
A `Makefile` wraps the most common dev commands:
|
A `Makefile` wraps the most common dev commands:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make # default: fmt-check + clippy + test (same order as CI)
|
make # make check: the full gate, same steps as CI
|
||||||
make fix # auto-apply fmt + clippy fixes
|
make fix # auto-apply fmt + clippy fixes
|
||||||
make fmt # apply rustfmt
|
make fmt # apply rustfmt
|
||||||
make clippy # clippy with -D warnings
|
make clippy # clippy with -D warnings
|
||||||
make test # cargo test
|
make test # cargo test --workspace
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`make check` runs fmt-check, clippy, tests (default features and `--no-default-features`), and the layering guards (see `CONTRIBUTING.md`).
|
||||||
|
|
||||||
## Test
|
## Test
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo test # full workspace (requires DATABASE_URL for sqlx offline checks)
|
cargo test --workspace # everything; needs no external services
|
||||||
cargo test -p application # business logic tests only — no database required
|
cargo test -p application # business logic tests only — in-memory fakes, no database
|
||||||
cargo test -p domain # domain model + value object tests
|
cargo test -p domain # domain model + value object tests
|
||||||
|
cargo test -p bootstrap # route smoke, API and handler tests over the real wiring
|
||||||
cargo llvm-cov -p application -p domain # line coverage report (requires cargo-llvm-cov)
|
cargo llvm-cov -p application -p domain # line coverage report (requires cargo-llvm-cov)
|
||||||
```
|
```
|
||||||
|
|
||||||
The `application` and `domain` crates have 400+ unit tests covering all use case modules (auth, diary, goals, import, integrations, movies, person, search, users, watchlist, wrapup) backed by in-memory fakes from `domain`'s `test-helpers` feature. These run without a database and are the fastest feedback loop for business logic changes.
|
Each crate keeps its tests in `crates/<crate>/tests/`. The `application` and `domain` crates have 400+ tests covering all use case modules (auth, diary, goals, import, integrations, movies, person, search, users, watchlist, wrapup) backed by the in-memory fakes and factories in `domain::testing` (the `test-helpers` feature). They are the fastest feedback loop for business logic changes.
|
||||||
|
|
||||||
## Docker
|
## Docker
|
||||||
|
|
||||||
|
|||||||
@@ -2,15 +2,19 @@
|
|||||||
title: Movies Diary — Hexagonal Architecture
|
title: Movies Diary — Hexagonal Architecture
|
||||||
---
|
---
|
||||||
graph TB
|
graph TB
|
||||||
subgraph Binaries["Binaries (Composition Root)"]
|
subgraph Binaries["Binaries (thin: config + bootstrap + run)"]
|
||||||
WEB["http-axum<br/><i>Axum web server + HTML templates</i><br/>Routes, Handlers, Mappers"]
|
SERVER["server<br/><i>load config, bootstrap::build_server, axum::serve</i>"]
|
||||||
WORKER["worker<br/><i>Event consumer</i><br/>Concurrent dispatch, graceful shutdown"]
|
WORKER["worker<br/><i>bootstrap::build_worker</i><br/>Concurrent dispatch, graceful shutdown"]
|
||||||
TUI["tui<br/><i>Terminal UI</i>"]
|
TUI["tui<br/><i>Terminal UI (deprecated)</i>"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Config["config"]
|
||||||
|
CFG["AppConfig<br/><i>defaults, config.toml / CONFIG_FILE,<br/>legacy env names</i>"]
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph Application["Application Layer"]
|
subgraph Application["Application Layer"]
|
||||||
direction TB
|
direction TB
|
||||||
CTX["AppContext<br/><i>Repositories + Services</i>"]
|
DEPS["Deps<br/><i>one narrow deps struct per use case</i>"]
|
||||||
APP_PORTS["ReviewLogger<br/><i>application-layer port</i>"]
|
APP_PORTS["ReviewLogger<br/><i>application-layer port</i>"]
|
||||||
subgraph UseCases["Use Cases"]
|
subgraph UseCases["Use Cases"]
|
||||||
UC_AUTH["auth<br/>login, register"]
|
UC_AUTH["auth<br/>login, register"]
|
||||||
@@ -33,7 +37,7 @@ graph TB
|
|||||||
EH_CLEANUP["SearchCleanupHandler"]
|
EH_CLEANUP["SearchCleanupHandler"]
|
||||||
EH_REINDEX["SearchReindexHandler"]
|
EH_REINDEX["SearchReindexHandler"]
|
||||||
EH_WRAPUP["WrapUpEventHandler"]
|
EH_WRAPUP["WrapUpEventHandler"]
|
||||||
EH_AP["ActivityPubEventHandler<br/><i>reviews, watchlist, goals,<br/>UserDeleted, UserAccountMoved</i>"]
|
EH_POSTER["PosterSyncHandler<br/><i>poster sync</i>"]
|
||||||
end
|
end
|
||||||
subgraph Jobs["Periodic Jobs"]
|
subgraph Jobs["Periodic Jobs"]
|
||||||
JOB_IMPORT["ImportSessionCleanup"]
|
JOB_IMPORT["ImportSessionCleanup"]
|
||||||
@@ -78,60 +82,66 @@ graph TB
|
|||||||
end
|
end
|
||||||
|
|
||||||
subgraph Bootstrap["bootstrap"]
|
subgraph Bootstrap["bootstrap"]
|
||||||
IW["build_server, build_worker,<br/>DbPool, EventBusBackend<br/><i>Composition root</i>"]
|
BS["build_server, build_worker<br/><i>Composition root: the only crate that names adapters</i>"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph EventPayload["event-payload"]
|
||||||
|
EP["Serde payloads for domain events"]
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph Adapters["Adapters (implement Port Traits)"]
|
subgraph Adapters["Adapters (implement Port Traits)"]
|
||||||
direction TB
|
direction TB
|
||||||
A_COMMON["adapter-common<br/><i>Shared row conversions,<br/>error mapping, date utils</i>"]
|
subgraph Http["HTTP driving adapter"]
|
||||||
|
A_HTTP["http-axum<br/><i>JSON API, classic HTML (askama),<br/>OpenAPI, federation-gated routes,<br/>one refusal shape</i>"]
|
||||||
|
end
|
||||||
subgraph Storage["Storage"]
|
subgraph Storage["Storage"]
|
||||||
A_SQLITE["sqlite<br/><i>SQLite repos</i>"]
|
A_SQLITE["sqlite<br/><i>repositories per aggregate, FTS5 search,<br/>social graph, durable event queue, migrations</i>"]
|
||||||
A_SQLITE_SEARCH["sqlite-search<br/><i>FTS5</i>"]
|
|
||||||
A_OBJ["object-storage<br/><i>S3 / filesystem</i>"]
|
A_OBJ["object-storage<br/><i>S3 / filesystem</i>"]
|
||||||
end
|
end
|
||||||
subgraph Messaging["Messaging"]
|
subgraph Messaging["Messaging"]
|
||||||
A_NATS["nats<br/><i>JetStream / Core</i>"]
|
A_NATS["nats<br/><i>JetStream / Core</i>"]
|
||||||
A_SQLITE_QUEUE["sqlite-event-queue<br/><i>Polling, dead-letter</i>"]
|
A_EVT_PUB["event-publisher<br/><i>In-memory channel</i>"]
|
||||||
A_EVT_PUB["event-publisher<br/><i>In-memory (tests)</i>"]
|
|
||||||
A_PAYLOAD["event-payload<br/><i>Serde (de)serialization</i>"]
|
|
||||||
end
|
end
|
||||||
subgraph External["External Services"]
|
subgraph External["External Services"]
|
||||||
A_METADATA["metadata<br/><i>TMDB search/details</i>"]
|
A_METADATA["metadata<br/><i>TMDB / OMDb search/details</i>"]
|
||||||
A_TMDB["tmdb-enrichment<br/><i>Credits, keywords, cast</i>"]
|
A_TMDB["tmdb-enrichment<br/><i>Credits, keywords, cast</i>"]
|
||||||
A_POSTER["poster-fetcher<br/><i>TMDB image download</i>"]
|
A_POSTER["poster-fetcher<br/><i>TMDB image download</i>"]
|
||||||
A_AUTH["auth<br/><i>JWT tokens</i>"]
|
A_AUTH["auth<br/><i>JWT tokens</i>"]
|
||||||
end
|
end
|
||||||
subgraph Federation["Federation (feature-gated)"]
|
subgraph Federation["Federation (feature-gated)"]
|
||||||
A_AP["activitypub<br/><i>k_ap library</i>"]
|
A_AP["activitypub<br/><i>k_ap library; ActivityPubEventHandler<br/>(reviews, watchlist, goals, UserDeleted, UserAccountMoved)</i>"]
|
||||||
A_SQLITE_FED["sqlite-federation"]
|
A_SQLITE_FED["sqlite-federation<br/><i>k_ap repositories</i>"]
|
||||||
end
|
end
|
||||||
subgraph Media["Media Processing"]
|
subgraph Media["Media Processing"]
|
||||||
A_IMG["image-converter<br/><i>AVIF/WebP</i>"]
|
A_IMG["image-converter<br/><i>AVIF/WebP</i>"]
|
||||||
A_POSTER_SYNC["poster-sync"]
|
|
||||||
end
|
end
|
||||||
subgraph Presentation["Presentation Helpers"]
|
subgraph Formats["Formats"]
|
||||||
A_RSS["rss<br/><i>Feed generation</i>"]
|
A_RSS["rss<br/><i>Feed generation</i>"]
|
||||||
A_EXPORT["export<br/><i>CSV/JSON diary</i>"]
|
A_EXPORT["export<br/><i>CSV/JSON diary</i>"]
|
||||||
A_IMPORT["importer<br/><i>CSV/JSON/XLSX parser</i>"]
|
A_IMPORT["importer<br/><i>CSV/JSON/XLSX parser</i>"]
|
||||||
end
|
A_MEDIA_SERVER["media-server<br/><i>Jellyfin + Plex webhook parsers</i>"]
|
||||||
subgraph Webhooks["Webhook Parsers"]
|
|
||||||
A_JELLYFIN["jellyfin"]
|
|
||||||
A_PLEX["plex"]
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
%% Dependency arrows
|
%% Dependency arrows
|
||||||
WEB -->|"uses"| Application
|
SERVER -->|"loads"| Config
|
||||||
WEB -->|"maps to"| ApiTypes
|
SERVER -->|"calls"| Bootstrap
|
||||||
WORKER -->|"uses"| Application
|
WORKER -->|"loads"| Config
|
||||||
|
WORKER -->|"calls"| Bootstrap
|
||||||
|
Bootstrap -->|"wires"| Adapters
|
||||||
|
Bootstrap -->|"builds deps for"| Application
|
||||||
|
A_HTTP -->|"uses"| Application
|
||||||
|
A_HTTP -->|"maps to"| ApiTypes
|
||||||
|
TUI -->|"typed API access"| ApiTypes
|
||||||
|
|
||||||
Application -->|"depends on"| Domain
|
Application -->|"depends on"| Domain
|
||||||
UC_WRAPUP -->|"delegates to"| DS_WRAPUP
|
UC_WRAPUP -->|"delegates to"| DS_WRAPUP
|
||||||
|
|
||||||
Adapters -.->|"implements"| Ports
|
Adapters -.->|"implements"| Ports
|
||||||
|
Messaging -.->|"serialises with"| EventPayload
|
||||||
|
|
||||||
%% Key data flows
|
%% Key data flows
|
||||||
WEB ===|"HTTP"| DTO
|
A_HTTP ===|"HTTP"| DTO
|
||||||
WORKER ===|"Events"| EVENTS
|
WORKER ===|"Events"| EVENTS
|
||||||
|
|
||||||
classDef domain fill:#1a1a2e,stroke:#e94560,color:#fff
|
classDef domain fill:#1a1a2e,stroke:#e94560,color:#fff
|
||||||
@@ -145,4 +155,6 @@ graph TB
|
|||||||
class Application app
|
class Application app
|
||||||
class Adapters adapter
|
class Adapters adapter
|
||||||
class Binaries binary
|
class Binaries binary
|
||||||
|
class Config app
|
||||||
|
class Bootstrap binary
|
||||||
class ApiTypes api
|
class ApiTypes api
|
||||||
|
|||||||
@@ -1,15 +1,7 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::test_support::local_actor;
|
||||||
use domain::value_objects::{InstanceIdentity, SocialActor, SocialIdentity, UserId};
|
use domain::value_objects::{InstanceIdentity, SocialActor, SocialIdentity, UserId};
|
||||||
|
|
||||||
fn local_actor(uid: &UserId) -> SocialActor {
|
|
||||||
SocialActor {
|
|
||||||
identity: SocialIdentity::Local(uid.clone()),
|
|
||||||
handle: "@gabriel@md.example".into(),
|
|
||||||
display_name: None,
|
|
||||||
avatar_url: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn local_actor_dto_url_round_trips_back_to_the_same_identity() {
|
fn local_actor_dto_url_round_trips_back_to_the_same_identity() {
|
||||||
let instance = InstanceIdentity::new("https://md.example");
|
let instance = InstanceIdentity::new("https://md.example");
|
||||||
|
|||||||
@@ -1,14 +1,6 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use domain::value_objects::{InstanceIdentity, SocialActor, SocialIdentity, UserId};
|
use crate::test_support::local_actor;
|
||||||
|
use domain::value_objects::{InstanceIdentity, SocialIdentity, UserId};
|
||||||
fn local_actor(uid: &UserId) -> SocialActor {
|
|
||||||
SocialActor {
|
|
||||||
identity: SocialIdentity::Local(uid.clone()),
|
|
||||||
handle: "@gabriel@md.example".into(),
|
|
||||||
display_name: None,
|
|
||||||
avatar_url: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn local_blocked_dto_url_round_trips_back_to_the_same_identity() {
|
fn local_blocked_dto_url_round_trips_back_to_the_same_identity() {
|
||||||
|
|||||||
@@ -1,14 +1,6 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use domain::value_objects::{InstanceIdentity, SocialActor, SocialIdentity, UserId};
|
use crate::test_support::local_actor;
|
||||||
|
use domain::value_objects::{InstanceIdentity, SocialIdentity, UserId};
|
||||||
fn local_actor(uid: &UserId) -> SocialActor {
|
|
||||||
SocialActor {
|
|
||||||
identity: SocialIdentity::Local(uid.clone()),
|
|
||||||
handle: "@gabriel@md.example".into(),
|
|
||||||
display_name: None,
|
|
||||||
avatar_url: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn local_template_url_round_trips_back_to_the_same_identity() {
|
fn local_template_url_round_trips_back_to_the_same_identity() {
|
||||||
|
|||||||
@@ -8,3 +8,5 @@ pub mod openapi;
|
|||||||
pub mod router;
|
pub mod router;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
mod support;
|
mod support;
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test_support;
|
||||||
|
|||||||
12
crates/adapters/http-axum/src/test_support.rs
Normal file
12
crates/adapters/http-axum/src/test_support.rs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
//! Fixtures shared by the in-crate mapper tests.
|
||||||
|
|
||||||
|
use domain::value_objects::{SocialActor, SocialIdentity, UserId};
|
||||||
|
|
||||||
|
pub(crate) fn local_actor(uid: &UserId) -> SocialActor {
|
||||||
|
SocialActor {
|
||||||
|
identity: SocialIdentity::Local(uid.clone()),
|
||||||
|
handle: "@gabriel@md.example".into(),
|
||||||
|
display_name: None,
|
||||||
|
avatar_url: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -150,7 +150,7 @@ pub struct WrapupGroup {
|
|||||||
pub list_wrapups: ListWrapUpsDeps,
|
pub list_wrapups: ListWrapUpsDeps,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Every deps struct a handler can need, built once by the composition root.
|
/// Every deps struct a handler can need, built once by `bootstrap`.
|
||||||
/// Use cases still receive only their own narrow struct — nothing takes `&Deps`.
|
/// Use cases still receive only their own narrow struct — nothing takes `&Deps`.
|
||||||
///
|
///
|
||||||
/// `bootstrap::build_deps` is called only from `bootstrap::build_server`.
|
/// `bootstrap::build_deps` is called only from `bootstrap::build_server`.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//! Absorbs `handlers/import.rs::api_apply_profile`'s three-step orchestration:
|
//! Absorbs `http-axum/src/api/import/sessions.rs::api_apply_profile`'s three-step orchestration:
|
||||||
//! apply the saved profile's field mappings onto the session, reload the
|
//! apply the saved profile's field mappings onto the session, reload the
|
||||||
//! session to read back the mappings `apply_profile` just wrote, then run
|
//! session to read back the mappings `apply_profile` just wrote, then run
|
||||||
//! `apply_mapping` to regenerate `row_results` from them. All three steps used
|
//! `apply_mapping` to regenerate `row_results` from them. All three steps used
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//! Backs `handlers/import.rs::api_get_session` — a plain state query, not a
|
//! Backs `http-axum/src/api/import/sessions.rs::api_get_session` — a plain state query, not a
|
||||||
//! redirect-driving gate (the API has nothing to redirect to; a missing
|
//! redirect-driving gate (the API has nothing to redirect to; a missing
|
||||||
//! session is just a 404).
|
//! session is just a 404).
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ pub mod event_handler;
|
|||||||
pub mod get_movie_profile;
|
pub mod get_movie_profile;
|
||||||
pub mod get_movies;
|
pub mod get_movies;
|
||||||
pub mod merge_duplicates;
|
pub mod merge_duplicates;
|
||||||
pub mod poster_sync;
|
pub mod poster_sync_handler;
|
||||||
pub mod queries;
|
pub mod queries;
|
||||||
pub mod reindex_search;
|
pub mod reindex_search;
|
||||||
pub mod request_enrichment;
|
pub mod request_enrichment;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
//! The federated half of the former `get_profile` (ADR-0004's closed wart).
|
//! The federated half of the former `get_profile` (ADR-0004's closed wart).
|
||||||
//! `build_federated_profile_response` (`handlers/users.rs`) calls this with a
|
//! `build_federated_profile_response` (`http-axum/src/api/user_profile.rs`) calls this with a
|
||||||
//! synthetic `user_id` that has no row in the local `users` table — a federated
|
//! synthetic `user_id` that has no row in the local `users` table — a federated
|
||||||
//! actor's identity is built entirely from the resolved `FederatedProfile`, so
|
//! actor's identity is built entirely from the resolved `FederatedProfile`, so
|
||||||
//! this type carries no `identity` field at all: the old `get_profile` computed
|
//! this type carries no `identity` field at all: the old `get_profile` computed
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
//! Backs page-chrome data: the nav bar's email/admin state and pending-follow
|
//! Backs page-chrome data: the nav bar's email/admin state and pending-follow
|
||||||
//! badge. Absorbs `handlers/helpers.rs::build_page_context`'s inline user lookup
|
//! badge. Absorbs `html/helpers.rs::build_page_context`'s inline user lookup
|
||||||
//! and pending-follower count.
|
//! and pending-follower count.
|
||||||
//!
|
//!
|
||||||
//! **Error policy (deliberately no internal tolerance):** this function
|
//! **Error policy (deliberately no internal tolerance):** this function
|
||||||
//! propagates every error from `user` and `follow_graph` with `?`. It does not
|
//! propagates every error from `user` and `follow_graph` with `?`. It does not
|
||||||
//! itself decide to degrade — that's a presentation-layer policy
|
//! itself decide to degrade — that's a http-adapter policy
|
||||||
//! (`build_page_context` logs a warning and falls back to a zeroed `PageViewer`
|
//! (`build_page_context` logs a warning and falls back to a zeroed `PageViewer`
|
||||||
//! on `Err`). Page chrome degrading is a rendering decision, not a domain one.
|
//! on `Err`). Page chrome degrading is a rendering decision, not a domain one.
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//! Absorbs `handlers/users.rs::get_user_by_username`'s
|
//! Absorbs `http-axum/src/html/users.rs::get_user_by_username`'s
|
||||||
//! `repos.user.find_by_username()` call. `None` means "no such username" — the
|
//! `repos.user.find_by_username()` call. `None` means "no such username" — the
|
||||||
//! handler turns that into a 404, same as it always has; this is not an error
|
//! handler turns that into a 404, same as it always has; this is not an error
|
||||||
//! condition.
|
//! condition.
|
||||||
|
|||||||
@@ -184,15 +184,17 @@ async fn assemble(
|
|||||||
// ── Event handlers ────────────────────────────────────────────────────────
|
// ── Event handlers ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
let handlers: Vec<Arc<dyn EventHandler>> = {
|
let handlers: Vec<Arc<dyn EventHandler>> = {
|
||||||
let poster = Arc::new(application::movies::poster_sync::PosterSyncHandler::new(
|
let poster = Arc::new(
|
||||||
Arc::clone(&movie_command),
|
application::movies::poster_sync_handler::PosterSyncHandler::new(
|
||||||
Arc::clone(&movie_query),
|
Arc::clone(&movie_command),
|
||||||
Arc::clone(&metadata),
|
Arc::clone(&movie_query),
|
||||||
Arc::clone(&poster_fetcher),
|
Arc::clone(&metadata),
|
||||||
Arc::clone(&object_storage),
|
Arc::clone(&poster_fetcher),
|
||||||
Arc::clone(&event_publisher),
|
Arc::clone(&object_storage),
|
||||||
3,
|
Arc::clone(&event_publisher),
|
||||||
)) as Arc<dyn EventHandler>;
|
3,
|
||||||
|
),
|
||||||
|
) as Arc<dyn EventHandler>;
|
||||||
|
|
||||||
let cleanup = Arc::new(object_storage::ImageCleanupHandler::new(Arc::clone(
|
let cleanup = Arc::new(object_storage::ImageCleanupHandler::new(Arc::clone(
|
||||||
&object_storage,
|
&object_storage,
|
||||||
|
|||||||
@@ -146,3 +146,5 @@ naming the exact file and line, before reverting.
|
|||||||
- `CONTRIBUTING.md`'s architecture rules were updated in this same commit to stop describing use cases
|
- `CONTRIBUTING.md`'s architecture rules were updated in this same commit to stop describing use cases
|
||||||
as taking `&AppContext` (a claim already false before this plan) and instead describe the
|
as taking `&AppContext` (a claim already false before this plan) and instead describe the
|
||||||
`&FooDeps`-struct convention this ADR documents.
|
`&FooDeps`-struct convention this ADR documents.
|
||||||
|
|
||||||
|
Now at `crates/adapters/http-axum/src` (was `crates/presentation/src`); see [ADR-0014](0014-http-adapter-and-one-refusal-shape.md).
|
||||||
|
|||||||
@@ -263,3 +263,5 @@ recorded under Known follow-ups below, which is also pre-existing.
|
|||||||
---
|
---
|
||||||
|
|
||||||
Superseded in part by [ADR-0012](0012-sqlite-is-the-only-backend.md): the postgres backends, crates and features mentioned above no longer exist; SQLite is the only backend.
|
Superseded in part by [ADR-0012](0012-sqlite-is-the-only-backend.md): the postgres backends, crates and features mentioned above no longer exist; SQLite is the only backend.
|
||||||
|
|
||||||
|
Now `make check-http-adapter-deps` over `crates/adapters/http-axum/Cargo.toml` (was `check-presentation-adapters`); see [ADR-0014](0014-http-adapter-and-one-refusal-shape.md).
|
||||||
|
|||||||
@@ -369,3 +369,5 @@ still fails with the same two pre-existing errors. This branch neither fixed nor
|
|||||||
---
|
---
|
||||||
|
|
||||||
Superseded in part by [ADR-0012](0012-sqlite-is-the-only-backend.md): the postgres backends, crates and features mentioned above no longer exist; SQLite is the only backend.
|
Superseded in part by [ADR-0012](0012-sqlite-is-the-only-backend.md): the postgres backends, crates and features mentioned above no longer exist; SQLite is the only backend.
|
||||||
|
|
||||||
|
Now `make check-sqlite-is-ap-free` (was `check-social-crates-are-ap-free`); the sqlite social crates merged into `crates/adapters/sqlite`.
|
||||||
|
|||||||
Reference in New Issue
Block a user