From 99ce81efe517d0f2498516c123ea409ac0fea72c Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Tue, 12 May 2026 11:54:00 +0200 Subject: [PATCH] refactor: deps cleanup, split openapi, extract api-types crate --- Cargo.lock | 52 +-- Cargo.toml | 6 +- Dockerfile | 2 +- README.md | 8 +- crates/adapters/activitypub-base/Cargo.toml | 2 - crates/adapters/activitypub/Cargo.toml | 1 - crates/adapters/event-payload/Cargo.toml | 1 - crates/adapters/event-publisher/Cargo.toml | 1 - crates/adapters/importer/Cargo.toml | 1 - crates/adapters/nats/Cargo.toml | 2 - .../adapters/postgres-event-queue/Cargo.toml | 3 - .../adapters/postgres-federation/Cargo.toml | 2 - crates/adapters/rss/Cargo.toml | 1 - crates/adapters/sqlite-event-queue/Cargo.toml | 3 - crates/adapters/template-askama/Cargo.toml | 1 - crates/api-types/Cargo.toml | 9 + crates/api-types/src/auth.rs | 23 + crates/api-types/src/common.rs | 7 + crates/api-types/src/diary.rs | 78 ++++ crates/api-types/src/import.rs | 47 ++ crates/api-types/src/lib.rs | 15 + crates/api-types/src/movies.rs | 58 +++ crates/api-types/src/social.rs | 44 ++ crates/api-types/src/users.rs | 85 ++++ crates/doc/Cargo.toml | 13 - crates/doc/src/lib.rs | 16 - crates/domain/Cargo.toml | 1 - crates/presentation/Cargo.toml | 5 +- crates/presentation/src/{dtos.rs => forms.rs} | 406 +++--------------- crates/presentation/src/handlers/api.rs | 41 +- crates/presentation/src/handlers/html.rs | 18 +- crates/presentation/src/handlers/import.rs | 52 +-- crates/presentation/src/lib.rs | 2 +- crates/presentation/src/main.rs | 6 +- crates/presentation/src/openapi.rs | 188 -------- crates/presentation/src/openapi/auth.rs | 12 + crates/presentation/src/openapi/diary.rs | 22 + crates/presentation/src/openapi/import.rs | 27 ++ crates/presentation/src/openapi/mod.rs | 51 +++ crates/presentation/src/openapi/movies.rs | 27 ++ crates/presentation/src/openapi/social.rs | 38 ++ crates/presentation/src/openapi/users.rs | 20 + crates/tui/Cargo.toml | 3 +- crates/tui/src/app.rs | 5 +- crates/tui/src/client.rs | 91 +--- crates/worker/Cargo.toml | 7 - 46 files changed, 695 insertions(+), 808 deletions(-) create mode 100644 crates/api-types/Cargo.toml create mode 100644 crates/api-types/src/auth.rs create mode 100644 crates/api-types/src/common.rs create mode 100644 crates/api-types/src/diary.rs create mode 100644 crates/api-types/src/import.rs create mode 100644 crates/api-types/src/lib.rs create mode 100644 crates/api-types/src/movies.rs create mode 100644 crates/api-types/src/social.rs create mode 100644 crates/api-types/src/users.rs delete mode 100644 crates/doc/Cargo.toml delete mode 100644 crates/doc/src/lib.rs rename crates/presentation/src/{dtos.rs => forms.rs} (54%) delete mode 100644 crates/presentation/src/openapi.rs create mode 100644 crates/presentation/src/openapi/auth.rs create mode 100644 crates/presentation/src/openapi/diary.rs create mode 100644 crates/presentation/src/openapi/import.rs create mode 100644 crates/presentation/src/openapi/mod.rs create mode 100644 crates/presentation/src/openapi/movies.rs create mode 100644 crates/presentation/src/openapi/social.rs create mode 100644 crates/presentation/src/openapi/users.rs diff --git a/Cargo.lock b/Cargo.lock index 44e40a7..fca9271 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15,7 +15,6 @@ dependencies = [ "domain", "serde", "serde_json", - "tokio", "tracing", "url", "uuid", @@ -31,10 +30,8 @@ dependencies = [ "axum", "chrono", "enum_delegate", - "reqwest 0.13.3", "serde", "serde_json", - "thiserror 2.0.18", "tokio", "tracing", "url", @@ -288,6 +285,15 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "api-types" +version = "0.1.0" +dependencies = [ + "serde", + "utoipa", + "uuid", +] + [[package]] name = "apple-native-keyring-store" version = "1.0.0" @@ -1443,17 +1449,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "doc" -version = "0.1.0" -dependencies = [ - "axum", - "tracing", - "utoipa", - "utoipa-scalar", - "utoipa-swagger-ui", -] - [[package]] name = "document-features" version = "0.2.12" @@ -1467,7 +1462,6 @@ dependencies = [ name = "domain" version = "0.1.0" dependencies = [ - "anyhow", "async-trait", "chrono", "email_address", @@ -1655,7 +1649,6 @@ dependencies = [ name = "event-payload" version = "0.1.0" dependencies = [ - "anyhow", "chrono", "domain", "serde", @@ -1671,7 +1664,6 @@ dependencies = [ "domain", "futures", "tokio", - "tracing", ] [[package]] @@ -2427,7 +2419,6 @@ dependencies = [ "csv", "domain", "serde_json", - "thiserror 2.0.18", ] [[package]] @@ -2913,9 +2904,7 @@ dependencies = [ "domain", "event-payload", "futures", - "serde", "serde_json", - "thiserror 2.0.18", "tokio", "tracing", "uuid", @@ -3470,16 +3459,13 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", - "chrono", "domain", "event-payload", "futures", - "serde", "serde_json", "sqlx", "tokio", "tracing", - "uuid", ] [[package]] @@ -3493,7 +3479,6 @@ dependencies = [ "chrono", "domain", "sqlx", - "tokio", "tracing", "uuid", ] @@ -3528,13 +3513,13 @@ version = "0.1.0" dependencies = [ "activitypub", "anyhow", + "api-types", "application", "async-trait", "auth", "axum", "axum-governor", "chrono", - "doc", "domain", "dotenvy", "export", @@ -3557,13 +3542,14 @@ dependencies = [ "sqlite-federation", "sqlx", "template-askama", - "thiserror 2.0.18", "tokio", "tower", "tower-http", "tracing", "tracing-subscriber", "utoipa", + "utoipa-scalar", + "utoipa-swagger-ui", "uuid", ] @@ -4057,7 +4043,6 @@ name = "rss" version = "0.1.0" dependencies = [ "application", - "chrono", "domain", "rss 2.0.12", ] @@ -4625,16 +4610,13 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", - "chrono", "domain", "event-payload", "futures", - "serde", "serde_json", "sqlx", "tokio", "tracing", - "uuid", ] [[package]] @@ -4997,7 +4979,6 @@ dependencies = [ "askama", "chrono", "domain", - "serde", "uuid", ] @@ -5424,6 +5405,7 @@ name = "tui" version = "0.1.0" dependencies = [ "anyhow", + "api-types", "apple-native-keyring-store", "csv", "directories", @@ -5432,7 +5414,6 @@ dependencies = [ "reqwest 0.13.3", "serde", "serde_json", - "tempfile", "thiserror 2.0.18", "tokio", "uuid", @@ -6333,13 +6314,10 @@ dependencies = [ "activitypub", "anyhow", "application", - "async-trait", "auth", - "chrono", "domain", "dotenvy", "export", - "futures", "image-storage", "importer", "metadata", @@ -6349,17 +6327,13 @@ dependencies = [ "postgres", "postgres-event-queue", "postgres-federation", - "serde", - "serde_json", "sqlite", "sqlite-event-queue", "sqlite-federation", "sqlx", - "thiserror 2.0.18", "tokio", "tracing", "tracing-subscriber", - "uuid", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 20d9ab5..6b6593b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,18 +19,18 @@ members = [ "crates/adapters/export", "crates/adapters/event-payload", "crates/adapters/nats", + "crates/api-types", "crates/application", "crates/domain", "crates/presentation", "crates/tui", - "crates/doc", "crates/worker", "crates/adapters/importer", ] resolver = "2" [workspace.dependencies] -tokio = { version = "1.0", features = ["full"] } +tokio = { version = "1.0", features = ["macros", "net", "rt", "rt-multi-thread", "sync", "time"] } futures = "0.3" dotenvy = "0.15" serde = { version = "1.0", features = ["derive"] } @@ -53,6 +53,7 @@ object_store = { version = "0.11", features = ["aws"] } axum = { version = "0.8.8", features = ["macros", "multipart"] } csv = "1" +api-types = { path = "crates/api-types" } domain = { path = "crates/domain" } application = { path = "crates/application" } presentation = { path = "crates/presentation" } @@ -71,7 +72,6 @@ postgres-federation = { path = "crates/adapters/postgres-federation" } template-askama = { path = "crates/adapters/template-askama" } activitypub = { path = "crates/adapters/activitypub" } activitypub-base = { path = "crates/adapters/activitypub-base" } -doc = { path = "crates/doc" } event-payload = { path = "crates/adapters/event-payload" } nats = { path = "crates/adapters/nats" } sqlite-event-queue = { path = "crates/adapters/sqlite-event-queue" } diff --git a/Dockerfile b/Dockerfile index e327ff8..0f8ab41 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,10 +27,10 @@ COPY crates/adapters/postgres/Cargo.toml crates/adapters/postgres/Car COPY crates/adapters/postgres-federation/Cargo.toml crates/adapters/postgres-federation/Cargo.toml COPY crates/adapters/postgres-event-queue/Cargo.toml crates/adapters/postgres-event-queue/Cargo.toml COPY crates/adapters/template-askama/Cargo.toml crates/adapters/template-askama/Cargo.toml +COPY crates/api-types/Cargo.toml crates/api-types/Cargo.toml COPY crates/application/Cargo.toml crates/application/Cargo.toml COPY crates/domain/Cargo.toml crates/domain/Cargo.toml COPY crates/presentation/Cargo.toml crates/presentation/Cargo.toml -COPY crates/doc/Cargo.toml crates/doc/Cargo.toml COPY crates/tui/Cargo.toml crates/tui/Cargo.toml COPY crates/worker/Cargo.toml crates/worker/Cargo.toml diff --git a/README.md b/README.md index ca36459..914cdde 100644 --- a/README.md +++ b/README.md @@ -24,9 +24,10 @@ A self-hosted, server-side rendered movie logging system with a full REST API. B Hexagonal (Ports & Adapters) with Domain-Driven Design: ``` +api-types — shared REST API request/response DTOs (Serialize/Deserialize + utoipa schemas); used by presentation and tui domain — pure types and trait definitions, no external deps application — use cases / business logic orchestration -presentation — Axum HTTP router, composition root for the HTTP process +presentation — Axum HTTP router, OpenAPI spec assembly, Swagger UI + Scalar serving, composition root for the HTTP process worker — standalone worker binary (event consumer, poster sync, federation) adapters/ auth — JWT issuance and validation (Argon2 passwords) @@ -44,13 +45,12 @@ adapters/ sqlite-event-queue — durable polling event queue backed by SQLite postgres-event-queue — durable polling event queue backed by PostgreSQL nats — NATS Core / JetStream event publisher and consumer - event-publisher — in-memory event channel (tests only) + event-publisher — in-memory event channel (used in tests) activitypub — ActivityPub federation wiring (follow, inbox/outbox, actor) activitypub-base — core ActivityPub protocol types and service sqlite-federation — SQLite-backed federation repository postgres-federation — PostgreSQL-backed federation repository -doc — OpenAPI spec assembly and Swagger UI / Scalar serving -tui — terminal UI client (ratatui) +tui — terminal UI client (ratatui); shares api-types with presentation for typed API access ``` ## Prerequisites diff --git a/crates/adapters/activitypub-base/Cargo.toml b/crates/adapters/activitypub-base/Cargo.toml index c83befd..bf104fa 100644 --- a/crates/adapters/activitypub-base/Cargo.toml +++ b/crates/adapters/activitypub-base/Cargo.toml @@ -7,10 +7,8 @@ edition = "2024" tokio = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } -reqwest = { workspace = true } uuid = { workspace = true } chrono = { workspace = true } -thiserror = { workspace = true } anyhow = { workspace = true } tracing = { workspace = true } async-trait = { workspace = true } diff --git a/crates/adapters/activitypub/Cargo.toml b/crates/adapters/activitypub/Cargo.toml index 533586f..efcf64f 100644 --- a/crates/adapters/activitypub/Cargo.toml +++ b/crates/adapters/activitypub/Cargo.toml @@ -7,7 +7,6 @@ edition = "2024" activitypub-base = { workspace = true } domain = { workspace = true } axum = { workspace = true } -tokio = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } uuid = { workspace = true } diff --git a/crates/adapters/event-payload/Cargo.toml b/crates/adapters/event-payload/Cargo.toml index 8c62c0a..f06e536 100644 --- a/crates/adapters/event-payload/Cargo.toml +++ b/crates/adapters/event-payload/Cargo.toml @@ -8,5 +8,4 @@ domain = { workspace = true } serde = { workspace = true } chrono = { workspace = true } uuid = { workspace = true } -anyhow = { workspace = true } serde_json = { workspace = true } diff --git a/crates/adapters/event-publisher/Cargo.toml b/crates/adapters/event-publisher/Cargo.toml index bf60be1..33f40e1 100644 --- a/crates/adapters/event-publisher/Cargo.toml +++ b/crates/adapters/event-publisher/Cargo.toml @@ -7,5 +7,4 @@ edition = "2024" domain = { workspace = true } async-trait = { workspace = true } tokio = { workspace = true } -tracing = { workspace = true } futures = { workspace = true } diff --git a/crates/adapters/importer/Cargo.toml b/crates/adapters/importer/Cargo.toml index 1f0c8ba..3c6ed3d 100644 --- a/crates/adapters/importer/Cargo.toml +++ b/crates/adapters/importer/Cargo.toml @@ -9,6 +9,5 @@ xlsx = ["dep:calamine"] [dependencies] domain = { workspace = true } serde_json = { workspace = true } -thiserror = { workspace = true } csv = { workspace = true } calamine = { version = "0.26", optional = true } diff --git a/crates/adapters/nats/Cargo.toml b/crates/adapters/nats/Cargo.toml index 086d788..0e5fd46 100644 --- a/crates/adapters/nats/Cargo.toml +++ b/crates/adapters/nats/Cargo.toml @@ -10,9 +10,7 @@ domain = { workspace = true } event-payload = { workspace = true } async-trait = { workspace = true } anyhow = { workspace = true } -thiserror = { workspace = true } tracing = { workspace = true } -serde = { workspace = true } serde_json = { workspace = true } uuid = { workspace = true } chrono = { workspace = true } diff --git a/crates/adapters/postgres-event-queue/Cargo.toml b/crates/adapters/postgres-event-queue/Cargo.toml index d1904c2..e9cd111 100644 --- a/crates/adapters/postgres-event-queue/Cargo.toml +++ b/crates/adapters/postgres-event-queue/Cargo.toml @@ -9,10 +9,7 @@ domain = { workspace = true } event-payload = { workspace = true } anyhow = { workspace = true } async-trait = { workspace = true } -serde = { workspace = true } serde_json = { workspace = true } tokio = { workspace = true } futures = { workspace = true } tracing = { workspace = true } -chrono = { workspace = true } -uuid = { workspace = true } diff --git a/crates/adapters/postgres-federation/Cargo.toml b/crates/adapters/postgres-federation/Cargo.toml index 3b8f524..f0d2d11 100644 --- a/crates/adapters/postgres-federation/Cargo.toml +++ b/crates/adapters/postgres-federation/Cargo.toml @@ -20,5 +20,3 @@ tracing = { workspace = true } async-trait = { workspace = true } anyhow = { workspace = true } -[dev-dependencies] -tokio = { workspace = true } diff --git a/crates/adapters/rss/Cargo.toml b/crates/adapters/rss/Cargo.toml index 1f69056..18502c4 100644 --- a/crates/adapters/rss/Cargo.toml +++ b/crates/adapters/rss/Cargo.toml @@ -5,6 +5,5 @@ edition = "2024" [dependencies] rss-feed = { package = "rss", version = "2" } -chrono = { workspace = true } domain = { workspace = true } application = { workspace = true } diff --git a/crates/adapters/sqlite-event-queue/Cargo.toml b/crates/adapters/sqlite-event-queue/Cargo.toml index a0b20d4..30604ac 100644 --- a/crates/adapters/sqlite-event-queue/Cargo.toml +++ b/crates/adapters/sqlite-event-queue/Cargo.toml @@ -9,10 +9,7 @@ domain = { workspace = true } event-payload = { workspace = true } anyhow = { workspace = true } async-trait = { workspace = true } -serde = { workspace = true } serde_json = { workspace = true } tokio = { workspace = true } futures = { workspace = true } tracing = { workspace = true } -chrono = { workspace = true } -uuid = { workspace = true } diff --git a/crates/adapters/template-askama/Cargo.toml b/crates/adapters/template-askama/Cargo.toml index 1769697..312505f 100644 --- a/crates/adapters/template-askama/Cargo.toml +++ b/crates/adapters/template-askama/Cargo.toml @@ -6,7 +6,6 @@ edition = "2024" [dependencies] askama = { version = "0.16.0" } -serde = { workspace = true } chrono = { workspace = true } uuid = { workspace = true } diff --git a/crates/api-types/Cargo.toml b/crates/api-types/Cargo.toml new file mode 100644 index 0000000..bf8d7ab --- /dev/null +++ b/crates/api-types/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "api-types" +version = "0.1.0" +edition = "2024" + +[dependencies] +serde = { workspace = true } +uuid = { workspace = true } +utoipa = { version = "5.5.0", features = ["axum_extras", "uuid"] } diff --git a/crates/api-types/src/auth.rs b/crates/api-types/src/auth.rs new file mode 100644 index 0000000..2f13a4a --- /dev/null +++ b/crates/api-types/src/auth.rs @@ -0,0 +1,23 @@ +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct LoginRequest { + pub email: String, + pub password: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct LoginResponse { + pub token: String, + pub user_id: Uuid, + pub email: String, + pub expires_at: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct RegisterRequest { + pub email: String, + pub username: String, + pub password: String, +} diff --git a/crates/api-types/src/common.rs b/crates/api-types/src/common.rs new file mode 100644 index 0000000..cae9bbb --- /dev/null +++ b/crates/api-types/src/common.rs @@ -0,0 +1,7 @@ +use serde::Deserialize; + +#[derive(Debug, Clone, Deserialize, Default)] +pub struct PaginationQueryParams { + pub limit: Option, + pub offset: Option, +} diff --git a/crates/api-types/src/diary.rs b/crates/api-types/src/diary.rs new file mode 100644 index 0000000..a3fbaf1 --- /dev/null +++ b/crates/api-types/src/diary.rs @@ -0,0 +1,78 @@ +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::movies::{MovieDto, ReviewDto}; + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct LogReviewRequest { + #[serde(skip_serializing_if = "Option::is_none")] + pub external_metadata_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub manual_title: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub manual_release_year: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub manual_director: Option, + pub rating: u8, + #[serde(skip_serializing_if = "Option::is_none")] + pub comment: Option, + pub watched_at: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct DiaryEntryDto { + pub movie: MovieDto, + pub review: ReviewDto, +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct DiaryResponse { + pub items: Vec, + pub total_count: u64, + pub limit: u32, + pub offset: u32, +} + +#[derive(Debug, Clone, Deserialize, utoipa::IntoParams)] +#[into_params(parameter_in = Query)] +pub struct DiaryQueryParams { + pub limit: Option, + pub offset: Option, + pub sort_by: Option, + pub movie_id: Option, +} + +#[derive(Debug, Clone, Deserialize, utoipa::IntoParams)] +#[into_params(parameter_in = Query)] +pub struct ActivityFeedQueryParams { + pub limit: Option, + pub offset: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct FeedEntryDto { + pub movie: MovieDto, + pub review: ReviewDto, + pub user_email: String, + pub user_display_name: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct ActivityFeedResponse { + pub items: Vec, + pub total_count: u64, + pub limit: u32, + pub offset: u32, +} + +#[derive(Debug, Clone, Deserialize, utoipa::IntoParams)] +#[into_params(parameter_in = Query)] +pub struct ExportQueryParams { + /// Output format: `csv` (default) or `json` + #[serde(default = "default_export_format")] + pub format: String, +} + +fn default_export_format() -> String { + "csv".to_string() +} diff --git a/crates/api-types/src/import.rs b/crates/api-types/src/import.rs new file mode 100644 index 0000000..8cd797a --- /dev/null +++ b/crates/api-types/src/import.rs @@ -0,0 +1,47 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct SessionCreatedResponse { + pub session_id: String, + pub columns: Vec, + pub sample_rows: Vec>, +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct SessionStateResponse { + pub session_id: String, + pub columns: Vec, + pub has_mappings: bool, + pub row_count: usize, +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct ApiFieldMapping { + /// Column name in the source file + pub source_column: String, + /// Domain field: title | release_year | director | rating | watched_at | comment | external_metadata_id + pub domain_field: String, + /// For rating fields: multiply raw value by this factor (e.g. 0.5 for 10-point → 5-point scale) + pub rating_scale: Option, + /// For watched_at fields: strftime format hint (e.g. "%d/%m/%Y") + pub date_format: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct ApplyMappingRequest { + pub mappings: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct ConfirmRequest { + /// Indices (0-based) of rows from the mapping preview to import + pub confirmed_indices: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct SaveProfileRequest { + /// Session UUID whose current field_mappings to save + pub session_id: String, + /// Human-readable profile name (e.g. "Letterboxd") + pub name: String, +} diff --git a/crates/api-types/src/lib.rs b/crates/api-types/src/lib.rs new file mode 100644 index 0000000..2d9823b --- /dev/null +++ b/crates/api-types/src/lib.rs @@ -0,0 +1,15 @@ +pub mod auth; +pub mod common; +pub mod diary; +pub mod import; +pub mod movies; +pub mod social; +pub mod users; + +pub use auth::*; +pub use common::*; +pub use diary::*; +pub use import::*; +pub use movies::*; +pub use social::*; +pub use users::*; diff --git a/crates/api-types/src/movies.rs b/crates/api-types/src/movies.rs new file mode 100644 index 0000000..18d6ab9 --- /dev/null +++ b/crates/api-types/src/movies.rs @@ -0,0 +1,58 @@ +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct MovieDto { + pub id: Uuid, + pub title: String, + pub release_year: u16, + pub director: Option, + pub poster_path: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct ReviewDto { + pub id: Uuid, + pub rating: u8, + pub comment: Option, + pub watched_at: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct ReviewHistoryResponse { + pub movie: MovieDto, + pub viewings: Vec, + pub trend: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct MovieStatsDto { + pub total_count: u64, + pub avg_rating: Option, + pub federated_count: u64, + pub rating_histogram: [u64; 5], +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct SocialReviewDto { + pub user_display: String, + pub rating: u8, + pub comment: Option, + pub watched_at: String, + pub is_federated: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct SocialFeedResponse { + pub items: Vec, + pub total_count: u64, + pub limit: u32, + pub offset: u32, +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct MovieDetailResponse { + pub movie: MovieDto, + pub stats: MovieStatsDto, + pub reviews: SocialFeedResponse, +} diff --git a/crates/api-types/src/social.rs b/crates/api-types/src/social.rs new file mode 100644 index 0000000..1a90aaa --- /dev/null +++ b/crates/api-types/src/social.rs @@ -0,0 +1,44 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct FollowRequest { + pub handle: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct ActorUrlRequest { + pub actor_url: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct RemoteActorDto { + pub handle: String, + pub display_name: Option, + pub url: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct ActorListResponse { + pub actors: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct BlockedDomainResponse { + pub domain: String, + pub reason: Option, + pub blocked_at: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct AddBlockedDomainRequest { + pub domain: String, + pub reason: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct BlockedActorResponse { + pub url: String, + pub handle: String, + pub display_name: Option, + pub avatar_url: Option, +} diff --git a/crates/api-types/src/users.rs b/crates/api-types/src/users.rs new file mode 100644 index 0000000..41ad018 --- /dev/null +++ b/crates/api-types/src/users.rs @@ -0,0 +1,85 @@ +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::diary::{DiaryEntryDto, DiaryResponse}; + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct UserSummaryDto { + pub id: Uuid, + pub email: String, + pub total_movies: i64, + pub avg_rating: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct UsersResponse { + pub users: Vec, +} + +#[derive(Debug, Clone, Deserialize, utoipa::IntoParams)] +#[into_params(parameter_in = Query)] +pub struct UserProfileQueryParams { + /// One of: `recent` (default), `ratings`, `history`, `trends` + pub view: Option, + pub limit: Option, + pub offset: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct UserStatsDto { + pub total_movies: i64, + pub avg_rating: Option, + pub favorite_director: Option, + pub most_active_month: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct MonthActivityDto { + pub year_month: String, + pub month_label: String, + pub count: i64, + pub entries: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct MonthlyRatingDto { + pub year_month: String, + pub month_label: String, + pub avg_rating: f64, + pub count: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct DirectorStatDto { + pub director: String, + pub count: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct UserTrendsDto { + pub monthly_ratings: Vec, + pub top_directors: Vec, + pub max_director_count: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct UserProfileResponse { + pub user_id: Uuid, + pub username: String, + pub stats: UserStatsDto, + pub following_count: usize, + pub followers_count: usize, + /// Populated for view=recent and view=ratings + pub entries: Option, + /// Populated for view=history + pub history: Option>, + /// Populated for view=trends + pub trends: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct ProfileResponse { + pub username: String, + pub bio: Option, + pub avatar_url: Option, +} diff --git a/crates/doc/Cargo.toml b/crates/doc/Cargo.toml deleted file mode 100644 index 1cd6d13..0000000 --- a/crates/doc/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "doc" -version = "0.1.0" -edition = "2024" - -[dependencies] -axum = { workspace = true } -tracing = { workspace = true } -utoipa = { version = "5.5.0", features = ["axum_extras"] } -utoipa-scalar = { version = "0.3.0", features = [ - "axum", -], default-features = false } -utoipa-swagger-ui = { version = "9.0.2", features = ["axum", "vendored"] } diff --git a/crates/doc/src/lib.rs b/crates/doc/src/lib.rs deleted file mode 100644 index 249d970..0000000 --- a/crates/doc/src/lib.rs +++ /dev/null @@ -1,16 +0,0 @@ -use axum::Router; -use utoipa::openapi::OpenApi; -use utoipa_scalar::{Scalar, Servable}; -use utoipa_swagger_ui::SwaggerUi; - -pub trait ApiDocExt { - fn with_api_doc(self, spec: OpenApi) -> Self; -} - -impl ApiDocExt for Router { - fn with_api_doc(self, spec: OpenApi) -> Self { - tracing::info!("API docs at /docs (Swagger) and /scalar"); - self.merge(SwaggerUi::new("/docs").url("/openapi.json", spec.clone())) - .merge(Scalar::with_url("/scalar", spec)) - } -} diff --git a/crates/domain/Cargo.toml b/crates/domain/Cargo.toml index 9093117..2936c93 100644 --- a/crates/domain/Cargo.toml +++ b/crates/domain/Cargo.toml @@ -7,7 +7,6 @@ edition = "2024" uuid = { workspace = true } chrono = { workspace = true } async-trait = { workspace = true } -anyhow = { workspace = true } thiserror = { workspace = true } futures = { workspace = true } diff --git a/crates/presentation/Cargo.toml b/crates/presentation/Cargo.toml index 9966a50..42f881a 100644 --- a/crates/presentation/Cargo.toml +++ b/crates/presentation/Cargo.toml @@ -33,7 +33,6 @@ axum = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } anyhow = { workspace = true } -thiserror = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } tokio = { workspace = true } @@ -42,6 +41,7 @@ uuid = { workspace = true } chrono = { workspace = true } async-trait = { workspace = true } +api-types = { workspace = true } domain = { workspace = true } application = { workspace = true } auth = { workspace = true } @@ -52,10 +52,11 @@ template-askama = { workspace = true } nats = { workspace = true, optional = true } rss = { workspace = true } export = { workspace = true } -doc = { workspace = true } importer = { workspace = true } sqlx = { workspace = true } utoipa = { version = "5.5.0", features = ["axum_extras", "uuid"] } +utoipa-scalar = { version = "0.3.0", features = ["axum"], default-features = false } +utoipa-swagger-ui = { version = "9.0.2", features = ["axum", "vendored"] } # Optional — database backends sqlite = { workspace = true, optional = true } diff --git a/crates/presentation/src/dtos.rs b/crates/presentation/src/forms.rs similarity index 54% rename from crates/presentation/src/dtos.rs rename to crates/presentation/src/forms.rs index b364f25..1f36673 100644 --- a/crates/presentation/src/dtos.rs +++ b/crates/presentation/src/forms.rs @@ -1,11 +1,13 @@ use chrono::NaiveDateTime; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; use uuid::Uuid; use application::{commands::LogReviewCommand, queries::GetDiaryQuery}; use domain::{errors::DomainError, models::SortDirection}; -fn empty_string_as_none<'de, D, T>(de: D) -> Result, D::Error> +use api_types::{DiaryQueryParams, LogReviewRequest}; + +pub fn empty_string_as_none<'de, D, T>(de: D) -> Result, D::Error> where D: serde::Deserializer<'de>, T: std::str::FromStr, @@ -18,15 +20,6 @@ where } } -#[derive(Deserialize, utoipa::IntoParams)] -#[into_params(parameter_in = Query)] -pub struct DiaryQueryParams { - pub limit: Option, - pub offset: Option, - pub sort_by: Option, - pub movie_id: Option, -} - #[derive(Deserialize)] pub struct LogReviewForm { #[serde(default, deserialize_with = "empty_string_as_none")] @@ -67,7 +60,7 @@ pub struct ErrorQuery { pub error: Option, } -#[derive(serde::Deserialize, Default)] +#[derive(Deserialize, Default)] pub struct FeedQueryParams { #[serde(default)] pub filter: String, @@ -87,74 +80,60 @@ pub struct DeleteRedirectForm { pub csrf_token: String, } -#[derive(Deserialize, utoipa::ToSchema)] -pub struct LogReviewRequest { - pub external_metadata_id: Option, - pub manual_title: Option, - pub manual_release_year: Option, - pub manual_director: Option, - pub rating: u8, - pub comment: Option, - pub watched_at: String, +#[derive(Deserialize)] +pub struct FollowForm { + pub handle: String, + #[serde(rename = "_csrf", default)] + pub csrf_token: String, } -#[derive(Serialize, utoipa::ToSchema)] -pub struct MovieDto { - pub id: Uuid, - pub title: String, - pub release_year: u16, - pub director: Option, - pub poster_path: Option, +#[derive(Deserialize)] +pub struct UnfollowForm { + pub actor_url: String, + #[serde(rename = "_csrf", default)] + pub csrf_token: String, } -#[derive(Serialize, utoipa::ToSchema)] -pub struct ReviewDto { - pub id: Uuid, - pub rating: u8, - pub comment: Option, - pub watched_at: String, +#[derive(Deserialize)] +pub struct FollowerActionForm { + pub actor_url: String, + #[serde(rename = "_csrf", default)] + pub csrf_token: String, } -#[derive(Serialize, utoipa::ToSchema)] -pub struct DiaryEntryDto { - pub movie: MovieDto, - pub review: ReviewDto, +#[derive(Deserialize)] +pub struct BlockDomainForm { + pub domain: String, + #[serde(default)] + pub reason: Option, + #[serde(rename = "_csrf", default)] + pub csrf_token: String, } -#[derive(Serialize, utoipa::ToSchema)] -pub struct DiaryResponse { - pub items: Vec, - pub total_count: u64, - pub limit: u32, - pub offset: u32, +#[derive(Deserialize)] +pub struct RemoveDomainForm { + pub domain: String, + #[serde(rename = "_csrf", default)] + pub csrf_token: String, } -#[derive(Serialize, utoipa::ToSchema)] -pub struct ReviewHistoryResponse { - pub movie: MovieDto, - pub viewings: Vec, - pub trend: String, +#[derive(Deserialize)] +pub struct ActorUrlForm { + pub actor_url: String, + #[serde(rename = "_csrf", default)] + pub csrf_token: String, } -#[derive(Deserialize, utoipa::ToSchema)] -pub struct LoginRequest { - pub email: String, - pub password: String, -} - -#[derive(Serialize, utoipa::ToSchema)] -pub struct LoginResponse { - pub token: String, - pub user_id: Uuid, - pub email: String, - pub expires_at: String, -} - -#[derive(Deserialize, utoipa::ToSchema)] -pub struct RegisterRequest { - pub email: String, - pub username: String, - pub password: String, +#[derive(Deserialize, Default)] +pub struct ProfileQueryParams { + pub view: Option, + pub limit: Option, + pub offset: Option, + pub error: Option, + #[serde(default)] + pub sort_by: String, + #[serde(default)] + pub search: String, } pub struct LogReviewData { @@ -239,283 +218,22 @@ impl LogReviewData { } } -impl From for GetDiaryQuery { - fn from(p: DiaryQueryParams) -> Self { - GetDiaryQuery { - limit: p.limit, - offset: p.offset, - sort_by: p.sort_by.as_deref().map(|s| { - if s == "asc" { - SortDirection::Ascending - } else { - SortDirection::Descending - } - }), - movie_id: p.movie_id, - user_id: None, - } +pub fn to_diary_query(p: DiaryQueryParams) -> GetDiaryQuery { + GetDiaryQuery { + limit: p.limit, + offset: p.offset, + sort_by: p.sort_by.as_deref().map(|s| { + if s == "asc" { + SortDirection::Ascending + } else { + SortDirection::Descending + } + }), + movie_id: p.movie_id, + user_id: None, } } -#[derive(Deserialize)] -pub struct FollowForm { - pub handle: String, - #[serde(rename = "_csrf", default)] - pub csrf_token: String, -} - -#[derive(Deserialize)] -pub struct UnfollowForm { - pub actor_url: String, - #[serde(rename = "_csrf", default)] - pub csrf_token: String, -} - -#[derive(Deserialize)] -pub struct FollowerActionForm { - pub actor_url: String, - #[serde(rename = "_csrf", default)] - pub csrf_token: String, -} - -#[derive(Deserialize)] -pub struct BlockDomainForm { - pub domain: String, - #[serde(default)] - pub reason: Option, - #[serde(rename = "_csrf", default)] - pub csrf_token: String, -} - -#[derive(Deserialize)] -pub struct RemoveDomainForm { - pub domain: String, - #[serde(rename = "_csrf", default)] - pub csrf_token: String, -} - -#[derive(Deserialize)] -pub struct ActorUrlForm { - pub actor_url: String, - #[serde(rename = "_csrf", default)] - pub csrf_token: String, -} - -#[derive(serde::Deserialize, Default)] -pub struct ProfileQueryParams { - pub view: Option, - pub limit: Option, - pub offset: Option, - pub error: Option, - #[serde(default)] - pub sort_by: String, - #[serde(default)] - pub search: String, -} - -// ── Activity feed ───────────────────────────────────────────────────────────── - -#[derive(Deserialize, utoipa::IntoParams)] -#[into_params(parameter_in = Query)] -pub struct ActivityFeedQueryParams { - pub limit: Option, - pub offset: Option, -} - -#[derive(Serialize, utoipa::ToSchema)] -pub struct FeedEntryDto { - pub movie: MovieDto, - pub review: ReviewDto, - pub user_email: String, - pub user_display_name: String, -} - -#[derive(Serialize, utoipa::ToSchema)] -pub struct ActivityFeedResponse { - pub items: Vec, - pub total_count: u64, - pub limit: u32, - pub offset: u32, -} - -// ── Users ────────────────────────────────────────────────────────────────────── - -#[derive(Serialize, utoipa::ToSchema)] -pub struct UserSummaryDto { - pub id: Uuid, - pub email: String, - pub total_movies: i64, - pub avg_rating: Option, -} - -#[derive(Serialize, utoipa::ToSchema)] -pub struct UsersResponse { - pub users: Vec, -} - -// ── User profile ─────────────────────────────────────────────────────────────── - -#[derive(Deserialize, utoipa::IntoParams)] -#[into_params(parameter_in = Query)] -pub struct UserProfileQueryParams { - /// One of: `recent` (default), `ratings`, `history`, `trends` - pub view: Option, - pub limit: Option, - pub offset: Option, -} - -#[derive(Serialize, utoipa::ToSchema)] -pub struct UserStatsDto { - pub total_movies: i64, - pub avg_rating: Option, - pub favorite_director: Option, - pub most_active_month: Option, -} - -#[derive(Serialize, utoipa::ToSchema)] -pub struct MonthActivityDto { - pub year_month: String, - pub month_label: String, - pub count: i64, - pub entries: Vec, -} - -#[derive(Serialize, utoipa::ToSchema)] -pub struct MonthlyRatingDto { - pub year_month: String, - pub month_label: String, - pub avg_rating: f64, - pub count: i64, -} - -#[derive(Serialize, utoipa::ToSchema)] -pub struct DirectorStatDto { - pub director: String, - pub count: i64, -} - -#[derive(Serialize, utoipa::ToSchema)] -pub struct UserTrendsDto { - pub monthly_ratings: Vec, - pub top_directors: Vec, - pub max_director_count: i64, -} - -#[derive(Serialize, utoipa::ToSchema)] -pub struct UserProfileResponse { - pub user_id: Uuid, - pub username: String, - pub stats: UserStatsDto, - pub following_count: usize, - pub followers_count: usize, - /// Populated for view=recent and view=ratings - pub entries: Option, - /// Populated for view=history - pub history: Option>, - /// Populated for view=trends - pub trends: Option, -} - -#[derive(Deserialize, utoipa::ToSchema)] -pub struct FollowRequest { - pub handle: String, -} - -#[derive(Deserialize, utoipa::ToSchema)] -pub struct ActorUrlRequest { - pub actor_url: String, -} - -#[derive(Serialize, utoipa::ToSchema)] -pub struct RemoteActorDto { - pub handle: String, - pub display_name: Option, - pub url: String, -} - -#[derive(Serialize, utoipa::ToSchema)] -pub struct ActorListResponse { - pub actors: Vec, -} - -#[derive(serde::Deserialize, utoipa::IntoParams)] -#[into_params(parameter_in = Query)] -pub struct ExportQueryParams { - /// Output format: `csv` (default) or `json` - #[serde(default = "default_export_format")] - pub format: String, -} - -fn default_export_format() -> String { - "csv".to_string() -} - -#[derive(serde::Deserialize, Default)] -pub struct PaginationQueryParams { - pub limit: Option, - pub offset: Option, -} - -#[derive(serde::Serialize, utoipa::ToSchema)] -pub struct ProfileResponse { - pub username: String, - pub bio: Option, - pub avatar_url: Option, -} - -#[derive(serde::Serialize, utoipa::ToSchema)] -pub struct MovieStatsDto { - pub total_count: u64, - pub avg_rating: Option, - pub federated_count: u64, - pub rating_histogram: [u64; 5], -} - -#[derive(serde::Serialize, utoipa::ToSchema)] -pub struct SocialReviewDto { - pub user_display: String, - pub rating: u8, - pub comment: Option, - pub watched_at: String, - pub is_federated: bool, -} - -#[derive(serde::Serialize, utoipa::ToSchema)] -pub struct SocialFeedResponse { - pub items: Vec, - pub total_count: u64, - pub limit: u32, - pub offset: u32, -} - -#[derive(serde::Serialize, utoipa::ToSchema)] -pub struct MovieDetailResponse { - pub movie: MovieDto, - pub stats: MovieStatsDto, - pub reviews: SocialFeedResponse, -} - -#[derive(serde::Serialize, utoipa::ToSchema)] -pub struct BlockedDomainResponse { - pub domain: String, - pub reason: Option, - pub blocked_at: String, -} - -#[derive(serde::Deserialize, utoipa::ToSchema)] -pub struct AddBlockedDomainRequest { - pub domain: String, - pub reason: Option, -} - -#[derive(serde::Serialize, utoipa::ToSchema)] -pub struct BlockedActorResponse { - pub url: String, - pub handle: String, - pub display_name: Option, - pub avatar_url: Option, -} - #[cfg(test)] mod tests { use super::*; @@ -602,7 +320,7 @@ mod tests { offset: None, movie_id: None, }; - let query = GetDiaryQuery::from(params); + let query = to_diary_query(params); assert!(matches!( query.sort_by, Some(domain::models::SortDirection::Ascending) @@ -617,7 +335,7 @@ mod tests { offset: None, movie_id: None, }; - let query = GetDiaryQuery::from(params); + let query = to_diary_query(params); assert!(matches!( query.sort_by, Some(domain::models::SortDirection::Descending) diff --git a/crates/presentation/src/handlers/api.rs b/crates/presentation/src/handlers/api.rs index 8e5dccf..07f37fb 100644 --- a/crates/presentation/src/handlers/api.rs +++ b/crates/presentation/src/handlers/api.rs @@ -31,19 +31,22 @@ use domain::{ }; #[cfg(feature = "federation")] -use crate::dtos::{ActorListResponse, ActorUrlRequest, FollowRequest, RemoteActorDto}; +use api_types::{ + ActorListResponse, ActorUrlRequest, AddBlockedDomainRequest, BlockedActorResponse, + BlockedDomainResponse, FollowRequest, RemoteActorDto, +}; +use api_types::{ + ActivityFeedQueryParams, ActivityFeedResponse, DiaryEntryDto, DiaryQueryParams, DiaryResponse, + DirectorStatDto, ExportQueryParams, FeedEntryDto, LogReviewRequest, LoginRequest, LoginResponse, + MonthActivityDto, MonthlyRatingDto, MovieDetailResponse, MovieDto, MovieStatsDto, + PaginationQueryParams, ProfileResponse, RegisterRequest, ReviewDto, ReviewHistoryResponse, + SocialFeedResponse, SocialReviewDto, UserProfileQueryParams, UserProfileResponse, UserStatsDto, + UserSummaryDto, UserTrendsDto, UsersResponse, +}; use crate::{ - dtos::{ - ActivityFeedQueryParams, ActivityFeedResponse, DiaryEntryDto, DiaryQueryParams, - DiaryResponse, DirectorStatDto, ExportQueryParams, FeedEntryDto, LogReviewData, - LogReviewRequest, LoginRequest, LoginResponse, MonthActivityDto, MonthlyRatingDto, - MovieDetailResponse, MovieDto, MovieStatsDto, PaginationQueryParams, ProfileResponse, - RegisterRequest, ReviewDto, ReviewHistoryResponse, SocialFeedResponse, SocialReviewDto, - UserProfileQueryParams, UserProfileResponse, UserStatsDto, UserSummaryDto, UserTrendsDto, - UsersResponse, - }, errors::ApiError, extractors::AuthenticatedUser, + forms::{to_diary_query, LogReviewData}, state::AppState, }; @@ -60,7 +63,7 @@ pub async fn get_diary( State(state): State, Query(params): Query, ) -> Result, ApiError> { - let page = get_diary::execute(&state.app_ctx, params.into()).await?; + let page = get_diary::execute(&state.app_ctx, to_diary_query(params)).await?; Ok(Json(DiaryResponse { items: page.items.iter().map(entry_to_dto).collect(), @@ -415,7 +418,7 @@ fn entry_to_dto(entry: &DiaryEntry) -> DiaryEntryDto { #[utoipa::path( get, path = "/api/v1/admin/blocked-domains", responses( - (status = 200, body = Vec), + (status = 200, body = Vec), (status = 401, description = "Unauthorized"), (status = 403, description = "Forbidden — admin only"), ), @@ -427,9 +430,9 @@ pub async fn get_blocked_domains_admin( ) -> impl IntoResponse { match state.ap_service.get_blocked_domains().await { Ok(domains) => { - let response: Vec = domains + let response: Vec = domains .into_iter() - .map(|d| crate::dtos::BlockedDomainResponse { + .map(|d| BlockedDomainResponse { domain: d.domain, reason: d.reason, blocked_at: d.blocked_at, @@ -444,7 +447,7 @@ pub async fn get_blocked_domains_admin( #[cfg(feature = "federation")] #[utoipa::path( post, path = "/api/v1/admin/blocked-domains", - request_body = crate::dtos::AddBlockedDomainRequest, + request_body = AddBlockedDomainRequest, responses( (status = 201, description = "Domain blocked"), (status = 401, description = "Unauthorized"), @@ -455,7 +458,7 @@ pub async fn get_blocked_domains_admin( pub async fn add_blocked_domain_admin( State(state): State, _admin: crate::extractors::AdminUser, - axum::Json(body): axum::Json, + axum::Json(body): axum::Json, ) -> impl IntoResponse { match state.ap_service.add_blocked_domain(&body.domain, body.reason.as_deref()).await { Ok(()) => StatusCode::CREATED.into_response(), @@ -531,7 +534,7 @@ pub async fn unblock_actor_api( #[utoipa::path( get, path = "/api/v1/social/blocked", responses( - (status = 200, body = Vec), + (status = 200, body = Vec), (status = 401, description = "Unauthorized"), ), security(("bearer_auth" = [])) @@ -542,9 +545,9 @@ pub async fn get_blocked_actors_api( ) -> impl IntoResponse { match state.ap_service.get_blocked_actors(user.0.value()).await { Ok(actors) => { - let response: Vec = actors + let response: Vec = actors .into_iter() - .map(|a| crate::dtos::BlockedActorResponse { + .map(|a| BlockedActorResponse { url: a.url, handle: a.handle, display_name: a.display_name, diff --git a/crates/presentation/src/handlers/html.rs b/crates/presentation/src/handlers/html.rs index a512347..f71192f 100644 --- a/crates/presentation/src/handlers/html.rs +++ b/crates/presentation/src/handlers/html.rs @@ -30,12 +30,10 @@ use domain::models::ExportFormat; use domain::{errors::DomainError, value_objects::UserId}; #[cfg(feature = "federation")] -use crate::dtos::{ActorUrlForm, BlockDomainForm, FollowForm, FollowerActionForm, RemoveDomainForm, UnfollowForm}; +use crate::forms::{ActorUrlForm, BlockDomainForm, FollowForm, FollowerActionForm, RemoveDomainForm, UnfollowForm}; use crate::{ csrf::CsrfToken, - dtos::{ - ErrorQuery, FeedQueryParams, LogReviewData, LogReviewForm, LoginForm, RegisterForm, - }, + forms::{ErrorQuery, FeedQueryParams, LogReviewData, LogReviewForm, LoginForm, RegisterForm}, extractors::{AdminUser, OptionalCookieUser, RequiredCookieUser}, state::AppState, }; @@ -280,7 +278,7 @@ pub async fn post_delete_review( RequiredCookieUser(user_id): RequiredCookieUser, Extension(csrf): Extension, Path(review_id): Path, - Form(form): Form, + Form(form): Form, ) -> impl IntoResponse { if crate::csrf::mismatch(&csrf, &form.csrf_token) { return StatusCode::FORBIDDEN.into_response(); @@ -311,7 +309,7 @@ pub async fn post_delete_review( pub async fn get_export( State(state): State, RequiredCookieUser(user_id): RequiredCookieUser, - Query(params): Query, + Query(params): Query, ) -> impl IntoResponse { let format = match params.format.as_str() { "csv" => ExportFormat::Csv, @@ -504,7 +502,7 @@ pub async fn get_user_profile( State(state): State, Path(profile_user_uuid): Path, headers: axum::http::HeaderMap, - Query(params): Query, + Query(params): Query, Extension(csrf): Extension, ) -> impl IntoResponse { // Content negotiation: AP clients request application/activity+json @@ -800,7 +798,7 @@ pub async fn get_following_page( RequiredCookieUser(user_id): RequiredCookieUser, State(state): State, Path(profile_user_uuid): Path, - Query(params): Query, + Query(params): Query, Extension(csrf): Extension, ) -> impl IntoResponse { if user_id.value() != profile_user_uuid { @@ -850,7 +848,7 @@ pub async fn get_followers_page( RequiredCookieUser(user_id): RequiredCookieUser, State(state): State, Path(profile_user_uuid): Path, - Query(params): Query, + Query(params): Query, Extension(csrf): Extension, ) -> impl IntoResponse { if user_id.value() != profile_user_uuid { @@ -935,7 +933,7 @@ pub async fn get_movie_detail( OptionalCookieUser(user_id): OptionalCookieUser, State(state): State, Path(movie_id): Path, - Query(params): Query, + Query(params): Query, Extension(csrf): Extension, ) -> impl IntoResponse { let ctx = build_page_context(&state, user_id, csrf.0).await; diff --git a/crates/presentation/src/handlers/import.rs b/crates/presentation/src/handlers/import.rs index 0b429e1..a27abad 100644 --- a/crates/presentation/src/handlers/import.rs +++ b/crates/presentation/src/handlers/import.rs @@ -4,7 +4,11 @@ use axum::{ http::StatusCode, response::{Html, IntoResponse, Redirect}, }; -use serde::{Deserialize, Serialize}; +use api_types::{ + ApplyMappingRequest, ConfirmRequest, SaveProfileRequest, SessionCreatedResponse, + SessionStateResponse, +}; +use serde::Deserialize; use std::collections::HashMap; use application::{ @@ -465,13 +469,6 @@ pub async fn get_import_done( // ── REST API handlers ────────────────────────────────────────────────────── -#[derive(Serialize, utoipa::ToSchema)] -pub struct SessionCreatedResponse { - pub session_id: String, - pub columns: Vec, - pub sample_rows: Vec>, -} - #[utoipa::path( post, path = "/api/v1/import/sessions", request_body(content_type = "multipart/form-data", description = "file (binary) + format (csv|json|xlsx)"), @@ -544,14 +541,6 @@ pub async fn api_post_session( } } -#[derive(Serialize, utoipa::ToSchema)] -pub struct SessionStateResponse { - pub session_id: String, - pub columns: Vec, - pub has_mappings: bool, - pub row_count: usize, -} - #[utoipa::path( get, path = "/api/v1/import/sessions/{id}", params(("id" = String, Path, description = "Import session UUID")), @@ -607,23 +596,6 @@ pub async fn api_get_session( } } -#[derive(Deserialize, utoipa::ToSchema)] -pub struct ApiFieldMapping { - /// Column name in the source file - pub source_column: String, - /// Domain field: title | release_year | director | rating | watched_at | comment | external_metadata_id - pub domain_field: String, - /// For rating fields: multiply raw value by this factor (e.g. 0.5 for 10-point → 5-point scale) - pub rating_scale: Option, - /// For watched_at fields: strftime format hint (e.g. "%d/%m/%Y") - pub date_format: Option, -} - -#[derive(Deserialize, utoipa::ToSchema)] -pub struct ApplyMappingRequest { - pub mappings: Vec, -} - #[utoipa::path( put, path = "/api/v1/import/sessions/{id}/mapping", params(("id" = String, Path, description = "Import session UUID")), @@ -692,12 +664,6 @@ pub async fn api_put_mapping( } } -#[derive(Deserialize, utoipa::ToSchema)] -pub struct ConfirmRequest { - /// Indices (0-based) of rows from the mapping preview to import - pub confirmed_indices: Vec, -} - #[utoipa::path( post, path = "/api/v1/import/sessions/{id}/confirm", params(("id" = String, Path, description = "Import session UUID")), @@ -776,14 +742,6 @@ pub async fn api_get_profiles( } } -#[derive(Deserialize, utoipa::ToSchema)] -pub struct SaveProfileRequest { - /// Session UUID whose current field_mappings to save - pub session_id: String, - /// Human-readable profile name (e.g. "Letterboxd") - pub name: String, -} - #[utoipa::path( post, path = "/api/v1/import/profiles", request_body = SaveProfileRequest, diff --git a/crates/presentation/src/lib.rs b/crates/presentation/src/lib.rs index 5be283f..4928585 100644 --- a/crates/presentation/src/lib.rs +++ b/crates/presentation/src/lib.rs @@ -1,5 +1,5 @@ pub mod csrf; -pub mod dtos; +pub mod forms; pub mod errors; pub mod extractors; pub mod handlers; diff --git a/crates/presentation/src/main.rs b/crates/presentation/src/main.rs index 8023ddc..f4be111 100644 --- a/crates/presentation/src/main.rs +++ b/crates/presentation/src/main.rs @@ -11,9 +11,7 @@ use importer::ImporterDocumentParser; use rss::RssAdapter; use template_askama::AskamaHtmlRenderer; -use doc::ApiDocExt; -use presentation::{openapi::ApiDoc, routes, state::AppState}; -use utoipa::OpenApi as _; +use presentation::{openapi, routes, state::AppState}; use domain::ports::{DiaryExporter, DocumentParser, EventPublisher, ImportProfileRepository, ImportSessionRepository}; @@ -29,7 +27,7 @@ async fn main() -> anyhow::Result<()> { .await .context("Failed to wire dependencies")?; - let app = routes::build_router(state, ap_router).with_api_doc(ApiDoc::openapi()); + let app = openapi::serve(routes::build_router(state, ap_router)); let host = std::env::var("HOST").unwrap_or_else(|_| "0.0.0.0".to_string()); let port = std::env::var("PORT").unwrap_or_else(|_| "3000".to_string()); diff --git a/crates/presentation/src/openapi.rs b/crates/presentation/src/openapi.rs deleted file mode 100644 index d94961c..0000000 --- a/crates/presentation/src/openapi.rs +++ /dev/null @@ -1,188 +0,0 @@ -use utoipa::{ - Modify, OpenApi, - openapi::security::{Http, HttpAuthScheme, SecurityScheme}, -}; - -use crate::dtos::{ - ActivityFeedResponse, DiaryEntryDto, DiaryResponse, - DirectorStatDto, FeedEntryDto, LoginRequest, LoginResponse, LogReviewRequest, - MonthActivityDto, MonthlyRatingDto, MovieDetailResponse, MovieDto, MovieStatsDto, - ProfileResponse, RegisterRequest, ReviewDto, ReviewHistoryResponse, SocialFeedResponse, - SocialReviewDto, UserProfileResponse, UserStatsDto, UserSummaryDto, UserTrendsDto, UsersResponse, -}; -use crate::handlers::import::{ - ApiFieldMapping, ApplyMappingRequest, ConfirmRequest, SaveProfileRequest, - SessionCreatedResponse, SessionStateResponse, -}; -#[cfg(feature = "federation")] -use crate::dtos::{ - ActorListResponse, ActorUrlRequest, BlockedActorResponse, BlockedDomainResponse, - AddBlockedDomainRequest, FollowRequest, RemoteActorDto, -}; - -struct SecurityAddon; - -impl Modify for SecurityAddon { - fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) { - let components = openapi.components.get_or_insert_with(Default::default); - components.add_security_scheme( - "bearer_auth", - SecurityScheme::Http(Http::new(HttpAuthScheme::Bearer)), - ); - } -} - -#[cfg(not(feature = "federation"))] -#[derive(OpenApi)] -#[openapi( - info( - title = "Movies Diary API", - version = "1.0.0", - description = "REST API for Movies Diary. Authenticate with `POST /api/v1/auth/login` to get a Bearer token." - ), - paths( - crate::handlers::api::get_diary, - crate::handlers::api::get_review_history, - crate::handlers::api::get_movie_detail, - crate::handlers::api::post_review, - crate::handlers::api::delete_review, - crate::handlers::api::sync_poster, - crate::handlers::api::login, - crate::handlers::api::register, - crate::handlers::api::export_diary, - crate::handlers::api::get_activity_feed, - crate::handlers::api::list_users, - crate::handlers::api::get_user_profile, - crate::handlers::import::api_post_session, - crate::handlers::import::api_get_session, - crate::handlers::import::api_put_mapping, - crate::handlers::import::api_post_confirm, - crate::handlers::import::api_get_profiles, - crate::handlers::import::api_post_profile, - crate::handlers::import::api_delete_profile, - crate::handlers::api::get_profile, - crate::handlers::api::update_profile_handler, - ), - components(schemas( - DiaryResponse, - DiaryEntryDto, - MovieDto, - ReviewDto, - LogReviewRequest, - LoginRequest, - LoginResponse, - RegisterRequest, - ReviewHistoryResponse, - MovieDetailResponse, - MovieStatsDto, - SocialFeedResponse, - SocialReviewDto, - ActivityFeedResponse, - FeedEntryDto, - UsersResponse, - UserSummaryDto, - UserProfileResponse, - UserStatsDto, - MonthActivityDto, - MonthlyRatingDto, - DirectorStatDto, - UserTrendsDto, - ProfileResponse, - SessionCreatedResponse, - SessionStateResponse, - ApiFieldMapping, - ApplyMappingRequest, - ConfirmRequest, - SaveProfileRequest, - )), - modifiers(&SecurityAddon), -)] -pub struct ApiDoc; - -#[cfg(feature = "federation")] -#[derive(OpenApi)] -#[openapi( - info( - title = "Movies Diary API", - version = "1.0.0", - description = "REST API for Movies Diary. Authenticate with `POST /api/v1/auth/login` to get a Bearer token." - ), - paths( - crate::handlers::api::get_diary, - crate::handlers::api::get_review_history, - crate::handlers::api::get_movie_detail, - crate::handlers::api::post_review, - crate::handlers::api::delete_review, - crate::handlers::api::sync_poster, - crate::handlers::api::login, - crate::handlers::api::register, - crate::handlers::api::export_diary, - crate::handlers::api::get_activity_feed, - crate::handlers::api::list_users, - crate::handlers::api::get_user_profile, - crate::handlers::api::get_following, - crate::handlers::api::get_followers, - crate::handlers::api::get_pending_followers, - crate::handlers::api::follow, - crate::handlers::api::unfollow, - crate::handlers::api::accept_follower, - crate::handlers::api::reject_follower, - crate::handlers::api::remove_follower, - crate::handlers::api::get_profile, - crate::handlers::api::update_profile_handler, - crate::handlers::api::get_blocked_domains_admin, - crate::handlers::api::add_blocked_domain_admin, - crate::handlers::api::remove_blocked_domain_admin, - crate::handlers::api::block_actor_api, - crate::handlers::api::unblock_actor_api, - crate::handlers::api::get_blocked_actors_api, - crate::handlers::import::api_post_session, - crate::handlers::import::api_get_session, - crate::handlers::import::api_put_mapping, - crate::handlers::import::api_post_confirm, - crate::handlers::import::api_get_profiles, - crate::handlers::import::api_post_profile, - crate::handlers::import::api_delete_profile, - ), - components(schemas( - DiaryResponse, - DiaryEntryDto, - MovieDto, - ReviewDto, - LogReviewRequest, - LoginRequest, - LoginResponse, - RegisterRequest, - ReviewHistoryResponse, - MovieDetailResponse, - MovieStatsDto, - SocialFeedResponse, - SocialReviewDto, - ActorListResponse, - RemoteActorDto, - FollowRequest, - ActorUrlRequest, - ProfileResponse, - BlockedDomainResponse, - AddBlockedDomainRequest, - BlockedActorResponse, - ActivityFeedResponse, - FeedEntryDto, - UsersResponse, - UserSummaryDto, - UserProfileResponse, - UserStatsDto, - MonthActivityDto, - MonthlyRatingDto, - DirectorStatDto, - UserTrendsDto, - SessionCreatedResponse, - SessionStateResponse, - ApiFieldMapping, - ApplyMappingRequest, - ConfirmRequest, - SaveProfileRequest, - )), - modifiers(&SecurityAddon), -)] -pub struct ApiDoc; diff --git a/crates/presentation/src/openapi/auth.rs b/crates/presentation/src/openapi/auth.rs new file mode 100644 index 0000000..253892f --- /dev/null +++ b/crates/presentation/src/openapi/auth.rs @@ -0,0 +1,12 @@ +use api_types::{LoginRequest, LoginResponse, RegisterRequest}; +use utoipa::OpenApi; + +#[derive(OpenApi)] +#[openapi( + paths( + crate::handlers::api::login, + crate::handlers::api::register, + ), + components(schemas(LoginRequest, LoginResponse, RegisterRequest)), +)] +pub struct AuthDoc; diff --git a/crates/presentation/src/openapi/diary.rs b/crates/presentation/src/openapi/diary.rs new file mode 100644 index 0000000..cc1ab29 --- /dev/null +++ b/crates/presentation/src/openapi/diary.rs @@ -0,0 +1,22 @@ +use api_types::{ActivityFeedResponse, DiaryEntryDto, DiaryResponse, FeedEntryDto, LogReviewRequest, ReviewDto}; +use utoipa::OpenApi; + +#[derive(OpenApi)] +#[openapi( + paths( + crate::handlers::api::get_diary, + crate::handlers::api::post_review, + crate::handlers::api::delete_review, + crate::handlers::api::export_diary, + crate::handlers::api::get_activity_feed, + ), + components(schemas( + DiaryResponse, + DiaryEntryDto, + ReviewDto, + LogReviewRequest, + ActivityFeedResponse, + FeedEntryDto, + )), +)] +pub struct DiaryDoc; diff --git a/crates/presentation/src/openapi/import.rs b/crates/presentation/src/openapi/import.rs new file mode 100644 index 0000000..a6a03ff --- /dev/null +++ b/crates/presentation/src/openapi/import.rs @@ -0,0 +1,27 @@ +use api_types::{ + ApiFieldMapping, ApplyMappingRequest, ConfirmRequest, SaveProfileRequest, + SessionCreatedResponse, SessionStateResponse, +}; +use utoipa::OpenApi; + +#[derive(OpenApi)] +#[openapi( + paths( + crate::handlers::import::api_post_session, + crate::handlers::import::api_get_session, + crate::handlers::import::api_put_mapping, + crate::handlers::import::api_post_confirm, + crate::handlers::import::api_get_profiles, + crate::handlers::import::api_post_profile, + crate::handlers::import::api_delete_profile, + ), + components(schemas( + SessionCreatedResponse, + SessionStateResponse, + ApiFieldMapping, + ApplyMappingRequest, + ConfirmRequest, + SaveProfileRequest, + )), +)] +pub struct ImportDoc; diff --git a/crates/presentation/src/openapi/mod.rs b/crates/presentation/src/openapi/mod.rs new file mode 100644 index 0000000..7807edb --- /dev/null +++ b/crates/presentation/src/openapi/mod.rs @@ -0,0 +1,51 @@ +mod auth; +mod diary; +mod import; +mod movies; +mod social; +mod users; + +use axum::Router; +use utoipa::{ + Modify, OpenApi, + openapi::security::{Http, HttpAuthScheme, SecurityScheme}, +}; +use utoipa_scalar::{Scalar, Servable}; +use utoipa_swagger_ui::SwaggerUi; + +struct SecurityAddon; + +impl Modify for SecurityAddon { + fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) { + let components = openapi.components.get_or_insert_with(Default::default); + components.add_security_scheme( + "bearer_auth", + SecurityScheme::Http(Http::new(HttpAuthScheme::Bearer)), + ); + } +} + +fn build() -> utoipa::openapi::OpenApi { + let mut api = auth::AuthDoc::openapi(); + api.info = utoipa::openapi::InfoBuilder::new() + .title("Movies Diary API") + .version("1.0.0") + .description(Some("REST API for Movies Diary. Authenticate with `POST /api/v1/auth/login` to get a Bearer token.")) + .build(); + api.merge(diary::DiaryDoc::openapi()); + api.merge(movies::MoviesDoc::openapi()); + api.merge(users::UsersDoc::openapi()); + api.merge(import::ImportDoc::openapi()); + #[cfg(feature = "federation")] + api.merge(social::SocialDoc::openapi()); + SecurityAddon.modify(&mut api); + api +} + +pub fn serve(router: Router) -> Router { + tracing::info!("API docs at /docs (Swagger) and /scalar"); + let spec = build(); + router + .merge(SwaggerUi::new("/docs").url("/openapi.json", spec.clone())) + .merge(Scalar::with_url("/scalar", spec)) +} diff --git a/crates/presentation/src/openapi/movies.rs b/crates/presentation/src/openapi/movies.rs new file mode 100644 index 0000000..e3e41f7 --- /dev/null +++ b/crates/presentation/src/openapi/movies.rs @@ -0,0 +1,27 @@ +use api_types::{ + DirectorStatDto, MonthActivityDto, MonthlyRatingDto, MovieDetailResponse, MovieDto, + MovieStatsDto, ReviewHistoryResponse, SocialFeedResponse, SocialReviewDto, UserTrendsDto, +}; +use utoipa::OpenApi; + +#[derive(OpenApi)] +#[openapi( + paths( + crate::handlers::api::get_movie_detail, + crate::handlers::api::get_review_history, + crate::handlers::api::sync_poster, + ), + components(schemas( + MovieDto, + MovieDetailResponse, + MovieStatsDto, + ReviewHistoryResponse, + SocialFeedResponse, + SocialReviewDto, + MonthActivityDto, + MonthlyRatingDto, + DirectorStatDto, + UserTrendsDto, + )), +)] +pub struct MoviesDoc; diff --git a/crates/presentation/src/openapi/social.rs b/crates/presentation/src/openapi/social.rs new file mode 100644 index 0000000..04f8d50 --- /dev/null +++ b/crates/presentation/src/openapi/social.rs @@ -0,0 +1,38 @@ +#[cfg(feature = "federation")] +use api_types::{ + ActorListResponse, ActorUrlRequest, AddBlockedDomainRequest, BlockedActorResponse, + BlockedDomainResponse, FollowRequest, RemoteActorDto, +}; +#[cfg(feature = "federation")] +use utoipa::OpenApi; + +#[cfg(feature = "federation")] +#[derive(OpenApi)] +#[openapi( + paths( + crate::handlers::api::get_following, + crate::handlers::api::get_followers, + crate::handlers::api::get_pending_followers, + crate::handlers::api::follow, + crate::handlers::api::unfollow, + crate::handlers::api::accept_follower, + crate::handlers::api::reject_follower, + crate::handlers::api::remove_follower, + crate::handlers::api::get_blocked_domains_admin, + crate::handlers::api::add_blocked_domain_admin, + crate::handlers::api::remove_blocked_domain_admin, + crate::handlers::api::block_actor_api, + crate::handlers::api::unblock_actor_api, + crate::handlers::api::get_blocked_actors_api, + ), + components(schemas( + ActorListResponse, + RemoteActorDto, + FollowRequest, + ActorUrlRequest, + BlockedDomainResponse, + AddBlockedDomainRequest, + BlockedActorResponse, + )), +)] +pub struct SocialDoc; diff --git a/crates/presentation/src/openapi/users.rs b/crates/presentation/src/openapi/users.rs new file mode 100644 index 0000000..cc1d2ef --- /dev/null +++ b/crates/presentation/src/openapi/users.rs @@ -0,0 +1,20 @@ +use api_types::{ProfileResponse, UserProfileResponse, UserStatsDto, UserSummaryDto, UsersResponse}; +use utoipa::OpenApi; + +#[derive(OpenApi)] +#[openapi( + paths( + crate::handlers::api::list_users, + crate::handlers::api::get_user_profile, + crate::handlers::api::get_profile, + crate::handlers::api::update_profile_handler, + ), + components(schemas( + UsersResponse, + UserSummaryDto, + UserProfileResponse, + UserStatsDto, + ProfileResponse, + )), +)] +pub struct UsersDoc; diff --git a/crates/tui/Cargo.toml b/crates/tui/Cargo.toml index e9759fd..1afb8dc 100644 --- a/crates/tui/Cargo.toml +++ b/crates/tui/Cargo.toml @@ -24,6 +24,7 @@ apple-native-keyring-store = { version = "1.0.0", optional = true, features = [ zbus-secret-service-keyring-store = { version = "1.0.0", optional = true } windows-native-keyring-store = { version = "1.0.0", optional = true } +api-types = { workspace = true } reqwest = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } @@ -32,5 +33,3 @@ anyhow = { workspace = true } uuid = { workspace = true } thiserror = { workspace = true } -[dev-dependencies] -tempfile = "3" diff --git a/crates/tui/src/app.rs b/crates/tui/src/app.rs index b44b18d..6437f1f 100644 --- a/crates/tui/src/app.rs +++ b/crates/tui/src/app.rs @@ -1,4 +1,4 @@ -use crate::client::{DiaryEntryDto, LogReviewRequest, ReviewHistoryResponse}; +use api_types::{DiaryEntryDto, LogReviewRequest, ReviewHistoryResponse}; use crate::config::Config; use uuid::Uuid; @@ -995,7 +995,7 @@ pub fn update(app: &mut App, action: Action) -> Vec { #[cfg(test)] mod tests { use super::*; - use crate::client::{DiaryEntryDto, MovieDto, ReviewDto}; + use api_types::{DiaryEntryDto, MovieDto, ReviewDto}; use uuid::Uuid; fn setup_app() -> App { @@ -1038,6 +1038,7 @@ mod tests { title: "The Matrix".into(), release_year: 1999, director: None, + poster_path: None, }, review: ReviewDto { id: Uuid::new_v4(), diff --git a/crates/tui/src/client.rs b/crates/tui/src/client.rs index ba1246d..51fe170 100644 --- a/crates/tui/src/client.rs +++ b/crates/tui/src/client.rs @@ -1,92 +1,9 @@ -use serde::{Deserialize, Serialize}; +use api_types::{ + ActorListResponse, ActorUrlRequest, DiaryResponse, FollowRequest, LogReviewRequest, + LoginRequest, LoginResponse, ReviewHistoryResponse, +}; use uuid::Uuid; -// ── DTOs (mirror backend dtos.rs exactly) ──────────────────────────────────── - -#[derive(Debug, Clone, Serialize)] -pub struct LoginRequest { - pub email: String, - pub password: String, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct LoginResponse { - pub token: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct LogReviewRequest { - #[serde(skip_serializing_if = "Option::is_none")] - pub external_metadata_id: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub manual_title: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub manual_release_year: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub manual_director: Option, - pub rating: u8, - #[serde(skip_serializing_if = "Option::is_none")] - pub comment: Option, - pub watched_at: String, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct DiaryResponse { - pub items: Vec, - pub total_count: u64, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct DiaryEntryDto { - pub movie: MovieDto, - pub review: ReviewDto, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct MovieDto { - pub id: Uuid, - pub title: String, - pub release_year: u16, - pub director: Option, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct ReviewDto { - pub id: Uuid, - pub rating: u8, - pub comment: Option, - pub watched_at: String, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct ReviewHistoryResponse { - pub movie: MovieDto, - pub viewings: Vec, - pub trend: String, -} - -#[derive(Debug, Clone, Serialize)] -pub struct FollowRequest { - pub handle: String, -} - -#[derive(Debug, Clone, Serialize)] -pub struct ActorUrlRequest { - pub actor_url: String, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct RemoteActorDto { - pub handle: String, - pub display_name: Option, - pub url: String, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct ActorListResponse { - pub actors: Vec, -} - // ── Error ───────────────────────────────────────────────────────────────────── #[derive(Debug, thiserror::Error)] diff --git a/crates/worker/Cargo.toml b/crates/worker/Cargo.toml index a7595f4..78b9f54 100644 --- a/crates/worker/Cargo.toml +++ b/crates/worker/Cargo.toml @@ -17,16 +17,9 @@ domain = { workspace = true } application = { workspace = true } tokio = { workspace = true } anyhow = { workspace = true } -thiserror = { workspace = true } -chrono = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } -futures = { workspace = true } dotenvy = { workspace = true } -uuid = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -async-trait = { workspace = true } auth = { workspace = true } metadata = { workspace = true } poster-fetcher = { workspace = true }