//! Shared vocabulary for the admin bulk-action endpoint (`POST //! /web/cases/bulk`). //! //! The wire shape is `application/x-www-form-urlencoded` with an //! `action=` field and one or more `case_id=` repetitions. //! Keeping the set of accepted tokens in a single enum lets the server //! handler match exhaustively and lets tests build request bodies //! without string literals that can silently drift from the handler's //! accepted values. use std::fmt; use std::str::FromStr; use serde::{Deserialize, Serialize}; /// Which operation a bulk POST applies to every selected case. /// /// Serialized as lowercase `"close" | "analyze" | "reset"`. The server /// handler [`BulkAction::from_str`]-parses the raw form value so an /// unknown token produces a domain error ("Unbekannte Aktion") instead /// of a serde/extractor error — preserving the HTTP contract across /// refactors. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum BulkAction { /// Mark each case closed (write `.closed` marker). Close, /// Queue each case for LLM analysis. Analyze, /// Drop derived artefacts (oneliner, document, analysis input) and /// revert `.m4a.failed` to `.m4a` so transcription self-heal retries. Reset, } impl BulkAction { /// Lowercase on-the-wire token. Intended for test request bodies /// and redirect targets; the server's own deserializer goes through /// [`FromStr`] to preserve the exact error shape. pub const fn as_str(self) -> &'static str { match self { BulkAction::Close => "close", BulkAction::Analyze => "analyze", BulkAction::Reset => "reset", } } } impl fmt::Display for BulkAction { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.as_str()) } } /// Returned by [`BulkAction::from_str`] on an unknown token. Carries /// the offending input so the caller can log it without re-capturing. #[derive(Debug, Clone, PartialEq, Eq)] pub struct UnknownBulkAction(pub String); impl fmt::Display for UnknownBulkAction { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "unknown bulk action: {}", self.0) } } impl std::error::Error for UnknownBulkAction {} impl FromStr for BulkAction { type Err = UnknownBulkAction; fn from_str(s: &str) -> Result { match s { "close" => Ok(BulkAction::Close), "analyze" => Ok(BulkAction::Analyze), "reset" => Ok(BulkAction::Reset), other => Err(UnknownBulkAction(other.to_string())), } } } #[cfg(test)] mod tests { use super::*; #[test] fn round_trip_lowercase_tokens() { for v in [BulkAction::Close, BulkAction::Analyze, BulkAction::Reset] { assert_eq!(v, v.as_str().parse().unwrap()); } } #[test] fn wire_tokens_are_stable() { assert_eq!(BulkAction::Close.as_str(), "close"); assert_eq!(BulkAction::Analyze.as_str(), "analyze"); assert_eq!(BulkAction::Reset.as_str(), "reset"); } #[test] fn unknown_token_reports_input() { let err = "foo".parse::().unwrap_err(); assert_eq!(err.0, "foo"); } #[test] fn serde_matches_wire() { let json = serde_json::to_string(&BulkAction::Analyze).unwrap(); assert_eq!(json, "\"analyze\""); let back: BulkAction = serde_json::from_str("\"reset\"").unwrap(); assert_eq!(back, BulkAction::Reset); } }