refactor: deps cleanup, split openapi, extract api-types crate
This commit is contained in:
@@ -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 }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -8,5 +8,4 @@ domain = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
@@ -7,5 +7,4 @@ edition = "2024"
|
||||
domain = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -20,5 +20,3 @@ tracing = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true }
|
||||
|
||||
@@ -5,6 +5,5 @@ edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
rss-feed = { package = "rss", version = "2" }
|
||||
chrono = { workspace = true }
|
||||
domain = { workspace = true }
|
||||
application = { workspace = true }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -6,7 +6,6 @@ edition = "2024"
|
||||
[dependencies]
|
||||
askama = { version = "0.16.0" }
|
||||
|
||||
serde = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
|
||||
|
||||
9
crates/api-types/Cargo.toml
Normal file
9
crates/api-types/Cargo.toml
Normal file
@@ -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"] }
|
||||
23
crates/api-types/src/auth.rs
Normal file
23
crates/api-types/src/auth.rs
Normal file
@@ -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,
|
||||
}
|
||||
7
crates/api-types/src/common.rs
Normal file
7
crates/api-types/src/common.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Default)]
|
||||
pub struct PaginationQueryParams {
|
||||
pub limit: Option<u32>,
|
||||
pub offset: Option<u32>,
|
||||
}
|
||||
78
crates/api-types/src/diary.rs
Normal file
78
crates/api-types/src/diary.rs
Normal file
@@ -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<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub manual_title: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub manual_release_year: Option<u16>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub manual_director: Option<String>,
|
||||
pub rating: u8,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub comment: Option<String>,
|
||||
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<DiaryEntryDto>,
|
||||
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<u32>,
|
||||
pub offset: Option<u32>,
|
||||
pub sort_by: Option<String>,
|
||||
pub movie_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, utoipa::IntoParams)]
|
||||
#[into_params(parameter_in = Query)]
|
||||
pub struct ActivityFeedQueryParams {
|
||||
pub limit: Option<u32>,
|
||||
pub offset: Option<u32>,
|
||||
}
|
||||
|
||||
#[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<FeedEntryDto>,
|
||||
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()
|
||||
}
|
||||
47
crates/api-types/src/import.rs
Normal file
47
crates/api-types/src/import.rs
Normal file
@@ -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<String>,
|
||||
pub sample_rows: Vec<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct SessionStateResponse {
|
||||
pub session_id: String,
|
||||
pub columns: Vec<String>,
|
||||
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<f64>,
|
||||
/// For watched_at fields: strftime format hint (e.g. "%d/%m/%Y")
|
||||
pub date_format: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct ApplyMappingRequest {
|
||||
pub mappings: Vec<ApiFieldMapping>,
|
||||
}
|
||||
|
||||
#[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<usize>,
|
||||
}
|
||||
|
||||
#[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,
|
||||
}
|
||||
15
crates/api-types/src/lib.rs
Normal file
15
crates/api-types/src/lib.rs
Normal file
@@ -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::*;
|
||||
58
crates/api-types/src/movies.rs
Normal file
58
crates/api-types/src/movies.rs
Normal file
@@ -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<String>,
|
||||
pub poster_path: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct ReviewDto {
|
||||
pub id: Uuid,
|
||||
pub rating: u8,
|
||||
pub comment: Option<String>,
|
||||
pub watched_at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct ReviewHistoryResponse {
|
||||
pub movie: MovieDto,
|
||||
pub viewings: Vec<ReviewDto>,
|
||||
pub trend: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct MovieStatsDto {
|
||||
pub total_count: u64,
|
||||
pub avg_rating: Option<f64>,
|
||||
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<String>,
|
||||
pub watched_at: String,
|
||||
pub is_federated: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct SocialFeedResponse {
|
||||
pub items: Vec<SocialReviewDto>,
|
||||
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,
|
||||
}
|
||||
44
crates/api-types/src/social.rs
Normal file
44
crates/api-types/src/social.rs
Normal file
@@ -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<String>,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct ActorListResponse {
|
||||
pub actors: Vec<RemoteActorDto>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct BlockedDomainResponse {
|
||||
pub domain: String,
|
||||
pub reason: Option<String>,
|
||||
pub blocked_at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct AddBlockedDomainRequest {
|
||||
pub domain: String,
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct BlockedActorResponse {
|
||||
pub url: String,
|
||||
pub handle: String,
|
||||
pub display_name: Option<String>,
|
||||
pub avatar_url: Option<String>,
|
||||
}
|
||||
85
crates/api-types/src/users.rs
Normal file
85
crates/api-types/src/users.rs
Normal file
@@ -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<f64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct UsersResponse {
|
||||
pub users: Vec<UserSummaryDto>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, utoipa::IntoParams)]
|
||||
#[into_params(parameter_in = Query)]
|
||||
pub struct UserProfileQueryParams {
|
||||
/// One of: `recent` (default), `ratings`, `history`, `trends`
|
||||
pub view: Option<String>,
|
||||
pub limit: Option<u32>,
|
||||
pub offset: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct UserStatsDto {
|
||||
pub total_movies: i64,
|
||||
pub avg_rating: Option<f64>,
|
||||
pub favorite_director: Option<String>,
|
||||
pub most_active_month: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct MonthActivityDto {
|
||||
pub year_month: String,
|
||||
pub month_label: String,
|
||||
pub count: i64,
|
||||
pub entries: Vec<DiaryEntryDto>,
|
||||
}
|
||||
|
||||
#[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<MonthlyRatingDto>,
|
||||
pub top_directors: Vec<DirectorStatDto>,
|
||||
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<DiaryResponse>,
|
||||
/// Populated for view=history
|
||||
pub history: Option<Vec<MonthActivityDto>>,
|
||||
/// Populated for view=trends
|
||||
pub trends: Option<UserTrendsDto>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct ProfileResponse {
|
||||
pub username: String,
|
||||
pub bio: Option<String>,
|
||||
pub avatar_url: Option<String>,
|
||||
}
|
||||
@@ -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"] }
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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<Option<T>, D::Error>
|
||||
use api_types::{DiaryQueryParams, LogReviewRequest};
|
||||
|
||||
pub fn empty_string_as_none<'de, D, T>(de: D) -> Result<Option<T>, 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<u32>,
|
||||
pub offset: Option<u32>,
|
||||
pub sort_by: Option<String>,
|
||||
pub movie_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct LogReviewForm {
|
||||
#[serde(default, deserialize_with = "empty_string_as_none")]
|
||||
@@ -67,7 +60,7 @@ pub struct ErrorQuery {
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[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<String>,
|
||||
pub manual_title: Option<String>,
|
||||
pub manual_release_year: Option<u16>,
|
||||
pub manual_director: Option<String>,
|
||||
pub rating: u8,
|
||||
pub comment: Option<String>,
|
||||
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<String>,
|
||||
pub poster_path: Option<String>,
|
||||
#[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<String>,
|
||||
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<String>,
|
||||
#[serde(rename = "_csrf", default)]
|
||||
pub csrf_token: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, utoipa::ToSchema)]
|
||||
pub struct DiaryResponse {
|
||||
pub items: Vec<DiaryEntryDto>,
|
||||
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<ReviewDto>,
|
||||
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<String>,
|
||||
pub limit: Option<u32>,
|
||||
pub offset: Option<u32>,
|
||||
pub error: Option<String>,
|
||||
#[serde(default)]
|
||||
pub sort_by: String,
|
||||
#[serde(default)]
|
||||
pub search: String,
|
||||
}
|
||||
|
||||
pub struct LogReviewData {
|
||||
@@ -239,283 +218,22 @@ impl LogReviewData {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DiaryQueryParams> 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<String>,
|
||||
#[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<String>,
|
||||
pub limit: Option<u32>,
|
||||
pub offset: Option<u32>,
|
||||
pub error: Option<String>,
|
||||
#[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<u32>,
|
||||
pub offset: Option<u32>,
|
||||
}
|
||||
|
||||
#[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<FeedEntryDto>,
|
||||
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<f64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, utoipa::ToSchema)]
|
||||
pub struct UsersResponse {
|
||||
pub users: Vec<UserSummaryDto>,
|
||||
}
|
||||
|
||||
// ── User profile ───────────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Deserialize, utoipa::IntoParams)]
|
||||
#[into_params(parameter_in = Query)]
|
||||
pub struct UserProfileQueryParams {
|
||||
/// One of: `recent` (default), `ratings`, `history`, `trends`
|
||||
pub view: Option<String>,
|
||||
pub limit: Option<u32>,
|
||||
pub offset: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, utoipa::ToSchema)]
|
||||
pub struct UserStatsDto {
|
||||
pub total_movies: i64,
|
||||
pub avg_rating: Option<f64>,
|
||||
pub favorite_director: Option<String>,
|
||||
pub most_active_month: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, utoipa::ToSchema)]
|
||||
pub struct MonthActivityDto {
|
||||
pub year_month: String,
|
||||
pub month_label: String,
|
||||
pub count: i64,
|
||||
pub entries: Vec<DiaryEntryDto>,
|
||||
}
|
||||
|
||||
#[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<MonthlyRatingDto>,
|
||||
pub top_directors: Vec<DirectorStatDto>,
|
||||
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<DiaryResponse>,
|
||||
/// Populated for view=history
|
||||
pub history: Option<Vec<MonthActivityDto>>,
|
||||
/// Populated for view=trends
|
||||
pub trends: Option<UserTrendsDto>,
|
||||
}
|
||||
|
||||
#[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<String>,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, utoipa::ToSchema)]
|
||||
pub struct ActorListResponse {
|
||||
pub actors: Vec<RemoteActorDto>,
|
||||
}
|
||||
|
||||
#[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<u32>,
|
||||
pub offset: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, utoipa::ToSchema)]
|
||||
pub struct ProfileResponse {
|
||||
pub username: String,
|
||||
pub bio: Option<String>,
|
||||
pub avatar_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, utoipa::ToSchema)]
|
||||
pub struct MovieStatsDto {
|
||||
pub total_count: u64,
|
||||
pub avg_rating: Option<f64>,
|
||||
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<String>,
|
||||
pub watched_at: String,
|
||||
pub is_federated: bool,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, utoipa::ToSchema)]
|
||||
pub struct SocialFeedResponse {
|
||||
pub items: Vec<SocialReviewDto>,
|
||||
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<String>,
|
||||
pub blocked_at: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, utoipa::ToSchema)]
|
||||
pub struct AddBlockedDomainRequest {
|
||||
pub domain: String,
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, utoipa::ToSchema)]
|
||||
pub struct BlockedActorResponse {
|
||||
pub url: String,
|
||||
pub handle: String,
|
||||
pub display_name: Option<String>,
|
||||
pub avatar_url: Option<String>,
|
||||
}
|
||||
|
||||
#[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)
|
||||
@@ -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<AppState>,
|
||||
Query(params): Query<DiaryQueryParams>,
|
||||
) -> Result<Json<DiaryResponse>, 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<crate::dtos::BlockedDomainResponse>),
|
||||
(status = 200, body = Vec<BlockedDomainResponse>),
|
||||
(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<crate::dtos::BlockedDomainResponse> = domains
|
||||
let response: Vec<BlockedDomainResponse> = 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<AppState>,
|
||||
_admin: crate::extractors::AdminUser,
|
||||
axum::Json(body): axum::Json<crate::dtos::AddBlockedDomainRequest>,
|
||||
axum::Json(body): axum::Json<AddBlockedDomainRequest>,
|
||||
) -> 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<crate::dtos::BlockedActorResponse>),
|
||||
(status = 200, body = Vec<BlockedActorResponse>),
|
||||
(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<crate::dtos::BlockedActorResponse> = actors
|
||||
let response: Vec<BlockedActorResponse> = actors
|
||||
.into_iter()
|
||||
.map(|a| crate::dtos::BlockedActorResponse {
|
||||
.map(|a| BlockedActorResponse {
|
||||
url: a.url,
|
||||
handle: a.handle,
|
||||
display_name: a.display_name,
|
||||
|
||||
@@ -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<CsrfToken>,
|
||||
Path(review_id): Path<Uuid>,
|
||||
Form(form): Form<crate::dtos::DeleteRedirectForm>,
|
||||
Form(form): Form<crate::forms::DeleteRedirectForm>,
|
||||
) -> 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<AppState>,
|
||||
RequiredCookieUser(user_id): RequiredCookieUser,
|
||||
Query(params): Query<crate::dtos::ExportQueryParams>,
|
||||
Query(params): Query<api_types::ExportQueryParams>,
|
||||
) -> 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<AppState>,
|
||||
Path(profile_user_uuid): Path<Uuid>,
|
||||
headers: axum::http::HeaderMap,
|
||||
Query(params): Query<crate::dtos::ProfileQueryParams>,
|
||||
Query(params): Query<crate::forms::ProfileQueryParams>,
|
||||
Extension(csrf): Extension<CsrfToken>,
|
||||
) -> 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<AppState>,
|
||||
Path(profile_user_uuid): Path<Uuid>,
|
||||
Query(params): Query<crate::dtos::ErrorQuery>,
|
||||
Query(params): Query<crate::forms::ErrorQuery>,
|
||||
Extension(csrf): Extension<CsrfToken>,
|
||||
) -> 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<AppState>,
|
||||
Path(profile_user_uuid): Path<Uuid>,
|
||||
Query(params): Query<crate::dtos::ErrorQuery>,
|
||||
Query(params): Query<crate::forms::ErrorQuery>,
|
||||
Extension(csrf): Extension<CsrfToken>,
|
||||
) -> 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<AppState>,
|
||||
Path(movie_id): Path<uuid::Uuid>,
|
||||
Query(params): Query<crate::dtos::PaginationQueryParams>,
|
||||
Query(params): Query<api_types::PaginationQueryParams>,
|
||||
Extension(csrf): Extension<CsrfToken>,
|
||||
) -> impl IntoResponse {
|
||||
let ctx = build_page_context(&state, user_id, csrf.0).await;
|
||||
|
||||
@@ -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<String>,
|
||||
pub sample_rows: Vec<Vec<String>>,
|
||||
}
|
||||
|
||||
#[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<String>,
|
||||
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<f64>,
|
||||
/// For watched_at fields: strftime format hint (e.g. "%d/%m/%Y")
|
||||
pub date_format: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, utoipa::ToSchema)]
|
||||
pub struct ApplyMappingRequest {
|
||||
pub mappings: Vec<ApiFieldMapping>,
|
||||
}
|
||||
|
||||
#[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<usize>,
|
||||
}
|
||||
|
||||
#[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,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
pub mod csrf;
|
||||
pub mod dtos;
|
||||
pub mod forms;
|
||||
pub mod errors;
|
||||
pub mod extractors;
|
||||
pub mod handlers;
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
12
crates/presentation/src/openapi/auth.rs
Normal file
12
crates/presentation/src/openapi/auth.rs
Normal file
@@ -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;
|
||||
22
crates/presentation/src/openapi/diary.rs
Normal file
22
crates/presentation/src/openapi/diary.rs
Normal file
@@ -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;
|
||||
27
crates/presentation/src/openapi/import.rs
Normal file
27
crates/presentation/src/openapi/import.rs
Normal file
@@ -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;
|
||||
51
crates/presentation/src/openapi/mod.rs
Normal file
51
crates/presentation/src/openapi/mod.rs
Normal file
@@ -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))
|
||||
}
|
||||
27
crates/presentation/src/openapi/movies.rs
Normal file
27
crates/presentation/src/openapi/movies.rs
Normal file
@@ -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;
|
||||
38
crates/presentation/src/openapi/social.rs
Normal file
38
crates/presentation/src/openapi/social.rs
Normal file
@@ -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;
|
||||
20
crates/presentation/src/openapi/users.rs
Normal file
20
crates/presentation/src/openapi/users.rs
Normal file
@@ -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;
|
||||
@@ -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"
|
||||
|
||||
@@ -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<Command> {
|
||||
#[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(),
|
||||
|
||||
@@ -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<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub manual_title: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub manual_release_year: Option<u16>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub manual_director: Option<String>,
|
||||
pub rating: u8,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub comment: Option<String>,
|
||||
pub watched_at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct DiaryResponse {
|
||||
pub items: Vec<DiaryEntryDto>,
|
||||
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<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct ReviewDto {
|
||||
pub id: Uuid,
|
||||
pub rating: u8,
|
||||
pub comment: Option<String>,
|
||||
pub watched_at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct ReviewHistoryResponse {
|
||||
pub movie: MovieDto,
|
||||
pub viewings: Vec<ReviewDto>,
|
||||
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<String>,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct ActorListResponse {
|
||||
pub actors: Vec<RemoteActorDto>,
|
||||
}
|
||||
|
||||
// ── Error ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
||||
@@ -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 }
|
||||
|
||||
Reference in New Issue
Block a user