81 lines
2.3 KiB
Rust
81 lines
2.3 KiB
Rust
use std::fmt;
|
|
use std::str::FromStr;
|
|
|
|
use axum::extract::{FromRequestParts, Path};
|
|
use axum::http::request::Parts;
|
|
use serde::{Deserialize, Deserializer};
|
|
use uuid::Uuid;
|
|
|
|
use crate::error::AppError;
|
|
|
|
/// Typed wrapper around `uuid::Uuid` for case identifiers. Enforces the
|
|
/// "parse, don't validate" idiom: any `CaseId` in code has already passed
|
|
/// UUID validation at the request boundary.
|
|
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
|
pub struct CaseId(pub Uuid);
|
|
|
|
impl fmt::Display for CaseId {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
fmt::Display::fmt(&self.0, f)
|
|
}
|
|
}
|
|
|
|
impl FromStr for CaseId {
|
|
type Err = uuid::Error;
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
Uuid::parse_str(s).map(CaseId)
|
|
}
|
|
}
|
|
|
|
impl<'de> Deserialize<'de> for CaseId {
|
|
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
|
let s = String::deserialize(deserializer)?;
|
|
CaseId::from_str(&s).map_err(serde::de::Error::custom)
|
|
}
|
|
}
|
|
|
|
/// Axum Path-extractor that validates the `{case_id}` URL segment and
|
|
/// returns `AppError::BadRequest("Invalid case_id")` on failure — matching
|
|
/// the legacy error body byte-for-byte.
|
|
pub struct CaseIdPath(pub CaseId);
|
|
|
|
impl<S> FromRequestParts<S> for CaseIdPath
|
|
where
|
|
S: Send + Sync,
|
|
{
|
|
type Rejection = AppError;
|
|
|
|
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
|
let Path(s) = Path::<String>::from_request_parts(parts, state)
|
|
.await
|
|
.map_err(|_| AppError::BadRequest("Invalid case_id".into()))?;
|
|
let id =
|
|
CaseId::from_str(&s).map_err(|_| AppError::BadRequest("Invalid case_id".into()))?;
|
|
Ok(CaseIdPath(id))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn from_str_accepts_valid_uuid() {
|
|
let id = CaseId::from_str("11111111-1111-1111-1111-111111111111").unwrap();
|
|
assert_eq!(id.0.to_string(), "11111111-1111-1111-1111-111111111111");
|
|
}
|
|
|
|
#[test]
|
|
fn from_str_rejects_non_uuid() {
|
|
assert!(CaseId::from_str("not-a-uuid").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn display_matches_uuid_display() {
|
|
let raw = "22222222-2222-2222-2222-222222222222";
|
|
let id = CaseId::from_str(raw).unwrap();
|
|
assert_eq!(id.to_string(), raw);
|
|
}
|
|
}
|