winbrew_models\shared/
validation.rs

1//! Shared validation helpers for model invariants.
2//!
3//! Validation in this crate is intentionally lightweight: types implement the
4//! `Validate` trait when they can check their own invariants, and helper
5//! functions provide reusable checks for common string-based contracts.
6
7use super::error::ModelError;
8
9/// A model type that can verify its own invariants.
10pub trait Validate {
11    fn validate(&self) -> Result<(), ModelError>;
12}
13
14/// Reject values that are empty after trimming whitespace.
15pub fn ensure_non_empty(field: &'static str, value: &str) -> Result<(), ModelError> {
16    if value.trim().is_empty() {
17        Err(ModelError::empty(field))
18    } else {
19        Ok(())
20    }
21}
22
23/// Accept only `http` and `https` URLs.
24pub fn ensure_http_url(field: &'static str, value: &str) -> Result<(), ModelError> {
25    let parsed = url::Url::parse(value)
26        .map_err(|err| ModelError::invalid_url(field, format!("{value} ({err})")))?;
27
28    match parsed.scheme() {
29        "http" | "https" => Ok(()),
30        other => Err(ModelError::invalid_url(
31            field,
32            format!("{value} (unsupported scheme {other})"),
33        )),
34    }
35}
36
37/// Accept hexadecimal hashes with or without a known algorithm prefix.
38pub fn ensure_hash(field: &'static str, value: &str) -> Result<(), ModelError> {
39    let normalized = value.trim();
40    if normalized.is_empty() {
41        return Err(ModelError::invalid_hash(field, value));
42    }
43
44    let candidate = normalized
45        .strip_prefix("sha256:")
46        .or_else(|| normalized.strip_prefix("sha1:"))
47        .or_else(|| normalized.strip_prefix("md5:"))
48        .or_else(|| normalized.strip_prefix("sha512:"))
49        .unwrap_or(normalized);
50
51    if candidate.chars().all(|ch| ch.is_ascii_hexdigit()) {
52        Ok(())
53    } else {
54        Err(ModelError::invalid_hash(field, value))
55    }
56}