docs: update for new crate layout

This commit is contained in:
2026-10-25 22:58:46 +01:00
parent b76f5996da
commit 80072eb463
20 changed files with 148 additions and 106 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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");

View File

@@ -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() {

View File

@@ -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() {

View File

@@ -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;

View 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,
}
}

View File

@@ -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`.

View File

@@ -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

View File

@@ -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).

View File

@@ -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;

View File

@@ -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

View File

@@ -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.

View File

@@ -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.

View File

@@ -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,

View File

@@ -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).

View File

@@ -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).

View File

@@ -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`.