Files
Mangalord/backend/src/domain/reaction.rs
MechaCat02 258a536254 feat(reactions): per-user like/dislike on mangas
Add a private per-user reaction resource: a manga_reactions table (one row
per user+manga, like/dislike, cascades) and endpoints PUT/DELETE
/mangas/:id/reaction plus GET /me/reactions/:manga_id, wired on the existing
per-user seam (mirrors bookmarks/read_progress). Unknown manga → 404 via the
FK-violation mapping; an invalid reaction value → 422. Reactions are never
exposed publicly — this is a taste signal for the upcoming content-based
recommendations. Integration tests cover set/toggle/clear/read, per-user
isolation, and the 404/422/401 paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 16:54:11 +02:00

41 lines
1.2 KiB
Rust

use serde::{Deserialize, Serialize};
use uuid::Uuid;
/// A user's private taste signal on a manga. Stored as text in
/// `manga_reactions.reaction` (CHECK-constrained), so we map to/from a
/// `&str` at the repo layer rather than deriving a Postgres enum type.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Reaction {
Like,
Dislike,
}
impl Reaction {
pub fn as_str(self) -> &'static str {
match self {
Reaction::Like => "like",
Reaction::Dislike => "dislike",
}
}
/// Parse a stored/inbound value. Returns `None` for anything outside the
/// closed vocabulary (the DB CHECK guarantees stored rows are valid; this
/// also guards the inbound API body).
pub fn parse(s: &str) -> Option<Reaction> {
match s {
"like" => Some(Reaction::Like),
"dislike" => Some(Reaction::Dislike),
_ => None,
}
}
}
/// Response shape for `GET /me/reactions/:manga_id` — the current user's
/// reaction on one manga, or `null` when they haven't reacted.
#[derive(Debug, Clone, Serialize)]
pub struct MangaReaction {
pub manga_id: Uuid,
pub reaction: Option<Reaction>,
}