From aadd876994ee3b76ad52303c0255887f1360e7fc Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Thu, 14 May 2026 17:04:42 +0200 Subject: [PATCH] feat: camelCase JSON responses, isFollowedByViewer, customCss, GET /users/me/following-list --- crates/api-types/src/requests.rs | 7 +++++ crates/api-types/src/responses.rs | 12 ++++++++ crates/presentation/src/handlers/auth.rs | 2 ++ crates/presentation/src/handlers/users.rs | 35 +++++++++++++++++++++-- crates/presentation/src/routes.rs | 11 ++++++- 5 files changed, 63 insertions(+), 4 deletions(-) diff --git a/crates/api-types/src/requests.rs b/crates/api-types/src/requests.rs index 50248c6..5f9c0d9 100644 --- a/crates/api-types/src/requests.rs +++ b/crates/api-types/src/requests.rs @@ -2,6 +2,7 @@ use serde::Deserialize; use uuid::Uuid; #[derive(Deserialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] pub struct RegisterRequest { /// Username (1-32 chars, alphanumeric + underscore) pub username: String, @@ -10,12 +11,14 @@ pub struct RegisterRequest { } #[derive(Deserialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] pub struct LoginRequest { pub email: String, pub password: String, } #[derive(Deserialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] pub struct CreateThoughtRequest { /// Up to 128 characters pub content: String, @@ -27,11 +30,13 @@ pub struct CreateThoughtRequest { } #[derive(Deserialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] pub struct EditThoughtRequest { pub content: String, } #[derive(Deserialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] pub struct UpdateProfileRequest { pub display_name: Option, pub bio: Option, @@ -41,12 +46,14 @@ pub struct UpdateProfileRequest { } #[derive(Deserialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] pub struct SetTopFriendsRequest { /// Ordered list of user UUIDs, max 8 pub friend_ids: Vec, } #[derive(Deserialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] pub struct CreateApiKeyRequest { pub name: String, } diff --git a/crates/api-types/src/responses.rs b/crates/api-types/src/responses.rs index 17168d5..6e69a14 100644 --- a/crates/api-types/src/responses.rs +++ b/crates/api-types/src/responses.rs @@ -3,12 +3,14 @@ use serde::Serialize; use uuid::Uuid; #[derive(Serialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] pub struct AuthResponse { pub token: String, pub user: UserResponse, } #[derive(Serialize, Clone, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] pub struct UserResponse { pub id: Uuid, pub username: String, @@ -16,15 +18,20 @@ pub struct UserResponse { pub bio: Option, pub avatar_url: Option, pub header_url: Option, + pub custom_css: Option, pub local: bool, + pub is_followed_by_viewer: bool, + #[serde(rename = "joinedAt")] pub created_at: DateTime, } #[derive(Serialize, Clone, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] pub struct ThoughtResponse { pub id: Uuid, pub content: String, pub author: UserResponse, + #[serde(rename = "replyToId")] pub in_reply_to_id: Option, pub visibility: String, pub content_warning: Option, @@ -39,6 +46,7 @@ pub struct ThoughtResponse { } #[derive(Serialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] pub struct PagedResponse { pub items: Vec, pub total: i64, @@ -47,6 +55,7 @@ pub struct PagedResponse { } #[derive(Serialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] pub struct ApiKeyResponse { pub id: Uuid, pub name: String, @@ -54,6 +63,7 @@ pub struct ApiKeyResponse { } #[derive(Serialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] pub struct NotificationResponse { pub id: Uuid, pub notification_type: String, @@ -64,11 +74,13 @@ pub struct NotificationResponse { } #[derive(Serialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] pub struct ErrorResponse { pub error: String, } #[derive(Serialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] pub struct CreatedApiKeyResponse { pub id: Uuid, pub name: String, diff --git a/crates/presentation/src/handlers/auth.rs b/crates/presentation/src/handlers/auth.rs index d3dfeab..400df36 100644 --- a/crates/presentation/src/handlers/auth.rs +++ b/crates/presentation/src/handlers/auth.rs @@ -14,7 +14,9 @@ pub fn to_user_response(u: &domain::models::user::User) -> UserResponse { bio: u.bio.clone(), avatar_url: u.avatar_url.clone(), header_url: u.header_url.clone(), + custom_css: u.custom_css.clone(), local: u.local, + is_followed_by_viewer: false, created_at: u.created_at, } } diff --git a/crates/presentation/src/handlers/users.rs b/crates/presentation/src/handlers/users.rs index 13393f3..38b098f 100644 --- a/crates/presentation/src/handlers/users.rs +++ b/crates/presentation/src/handlers/users.rs @@ -1,8 +1,11 @@ use crate::{ - errors::ApiError, extractors::AuthUser, handlers::auth::to_user_response, state::AppState, + errors::ApiError, + extractors::{AuthUser, OptionalAuthUser}, + handlers::auth::to_user_response, + state::AppState, }; use api_types::{ - requests::UpdateProfileRequest, + requests::{PaginationQuery, UpdateProfileRequest}, responses::{ErrorResponse, UserResponse}, }; use application::use_cases::feed::list_users; @@ -24,9 +27,17 @@ use axum::{ pub async fn get_user( State(s): State, Path(username): Path, + OptionalAuthUser(viewer): OptionalAuthUser, ) -> Result, ApiError> { let user = get_user_by_username(&*s.users, &username).await?; - Ok(Json(to_user_response(&user))) + let is_followed = if let Some(viewer_id) = viewer { + s.follows.find(&viewer_id, &user.id).await?.is_some() + } else { + false + }; + let mut resp = to_user_response(&user); + resp.is_followed_by_viewer = is_followed; + Ok(Json(resp)) } #[utoipa::path( @@ -81,6 +92,24 @@ pub async fn get_me( Ok(Json(to_user_response(&user))) } +pub async fn get_me_following_list( + State(s): State, + AuthUser(uid): AuthUser, + Query(q): Query, +) -> Result, ApiError> { + use application::use_cases::feed::get_following; + use domain::models::feed::PageParams; + let page = PageParams { + page: q.page(), + per_page: q.per_page(), + }; + let result = get_following(&*s.follows, &uid, page).await?; + Ok(Json(serde_json::json!({ + "total": result.total, + "items": result.items.iter().map(to_user_response).collect::>(), + }))) +} + pub async fn get_users( State(s): State, Query(params): Query>, diff --git a/crates/presentation/src/routes.rs b/crates/presentation/src/routes.rs index ea81184..4772256 100644 --- a/crates/presentation/src/routes.rs +++ b/crates/presentation/src/routes.rs @@ -14,7 +14,16 @@ pub fn router() -> Router { // users — static paths before parameterised .route("/users", get(users::get_users)) .route("/users/count", get(users::get_user_count)) - .route("/users/me", get(users::get_me).patch(users::patch_profile)) + .route( + "/users/me", + get(users::get_me) + .patch(users::patch_profile) + .put(users::patch_profile), + ) + .route( + "/users/me/following-list", + get(users::get_me_following_list), + ) .route("/users/me/top-friends", put(social::put_top_friends)) .route( "/users/{username}/top-friends",