Files
doctate/doctate-common/src/bulk.rs
T
Brummel 813fb896ff refactor: consolidate bulk actions, URL assembly, and case-artefact filenames
Pulls three pattern groups into shared helpers so a rename or wire-format
tweak edits one file instead of 10+:

- BulkAction enum in doctate-common replaces the "close"/"analyze"/"reset"
  string literals that were duplicated between the bulk handler and 3
  attack/CSRF test files. FromStr preserves the exact "Unbekannte Aktion"
  error shape; the handler match is now exhaustive over the enum.
- join_url helper in doctate-common absorbs 7 identical
  trim_end_matches+format! call sites across client-core, client-desktop,
  server/transcribe (ollama, whisper), and server/analyze (llm).
- server/tests/common/artefacts.rs re-exports ONELINER_FILENAME
  (doctate-common), DOCUMENT_FILE + ANALYSIS_INPUT_FILE
  (doctate-server::analyze), and CLOSE_MARKER (doctate-server::paths) so
  test files reference the canonical name instead of inlining literals.

Also lifts client-desktop local duplication:
- paths.rs: project_path helper collapses 4 identical ProjectDirs chains
- main.rs: or_die helper replaces 4 eprintln!+exit(1) blocks
- app.rs: named RecordingContext struct replaces (Uuid, String) tuple
  at 3 sites around the ffmpeg-flush finalization path

Verification: 397 tests pass (baseline was 389; +8 for new unit tests on
BulkAction and join_url), 0 failed, 4 ignored (unchanged). clippy clean.
2026-04-22 12:09:46 +02:00

112 lines
3.5 KiB
Rust

//! Shared vocabulary for the admin bulk-action endpoint (`POST
//! /web/cases/bulk`).
//!
//! The wire shape is `application/x-www-form-urlencoded` with an
//! `action=<token>` field and one or more `case_id=<uuid>` 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<Self, Self::Err> {
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::<BulkAction>().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);
}
}