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>
41 lines
1.2 KiB
Rust
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>,
|
|
}
|