winbrew_models\shared/
error.rs

1//! Canonical error type for model parsing, validation, and contract checks.
2//!
3//! Use `ModelError` whenever a model cannot be parsed, validated, or mapped to
4//! a stable contract. The variants are intentionally narrow so higher layers can
5//! format user-facing diagnostics without guessing at the original failure.
6
7use thiserror::Error;
8
9#[derive(Debug, Error, Clone, PartialEq, Eq)]
10pub enum ModelError {
11    /// A required field was empty after trimming whitespace.
12    #[error("{field} cannot be empty")]
13    EmptyField { field: &'static str },
14    /// A URL failed parsing or used a non-HTTP scheme.
15    #[error("invalid url for {field}: {value}")]
16    InvalidUrl { field: &'static str, value: String },
17    /// A checksum field was blank or contained non-hexadecimal data.
18    #[error("invalid hash for {field}: {value}")]
19    InvalidHash { field: &'static str, value: String },
20    /// A semantic version string could not be parsed or normalized.
21    #[error("invalid version {value}: {reason}")]
22    InvalidVersion { value: String, reason: String },
23    /// A package id could not be parsed from `@winget` or `@scoop` syntax.
24    #[error("invalid package id {value}: {reason}")]
25    InvalidPackageId { value: String, reason: String },
26    /// An enum value was outside the accepted vocabulary for the field.
27    #[error("invalid {field}: {value}")]
28    InvalidEnumValue { field: &'static str, value: String },
29    /// A typed value did not match the source that the schema expected.
30    #[error("source mismatch for {field}: expected {expected}, got {actual}")]
31    SourceMismatch {
32        field: &'static str,
33        expected: String,
34        actual: String,
35    },
36    /// A higher-level invariant or contract was violated.
37    #[error("invalid contract for {field}: {reason}")]
38    InvalidContract { field: &'static str, reason: String },
39}
40
41impl ModelError {
42    /// Build an empty-field error for the given logical field name.
43    pub fn empty(field: &'static str) -> Self {
44        Self::EmptyField { field }
45    }
46
47    /// Build a URL validation error for the given logical field name.
48    pub fn invalid_url(field: &'static str, value: impl Into<String>) -> Self {
49        Self::InvalidUrl {
50            field,
51            value: value.into(),
52        }
53    }
54
55    /// Build a hash validation error for the given logical field name.
56    pub fn invalid_hash(field: &'static str, value: impl Into<String>) -> Self {
57        Self::InvalidHash {
58            field,
59            value: value.into(),
60        }
61    }
62
63    /// Build a version parsing error for the original value and reason.
64    pub fn invalid_version(value: impl Into<String>, reason: impl Into<String>) -> Self {
65        Self::InvalidVersion {
66            value: value.into(),
67            reason: reason.into(),
68        }
69    }
70
71    /// Build a package-id parsing error for the original value and reason.
72    pub fn invalid_package_id(value: impl Into<String>, reason: impl Into<String>) -> Self {
73        Self::InvalidPackageId {
74            value: value.into(),
75            reason: reason.into(),
76        }
77    }
78
79    /// Build a generic enum-vocabulary error for the given field.
80    pub fn invalid_enum_value(field: &'static str, value: impl Into<String>) -> Self {
81        Self::InvalidEnumValue {
82            field,
83            value: value.into(),
84        }
85    }
86
87    /// Build a source-mismatch error when a typed record disagrees with the schema.
88    pub fn source_mismatch(
89        field: &'static str,
90        expected: impl Into<String>,
91        actual: impl Into<String>,
92    ) -> Self {
93        Self::SourceMismatch {
94            field,
95            expected: expected.into(),
96            actual: actual.into(),
97        }
98    }
99
100    /// Build a contract error for a schema or invariant violation.
101    pub fn invalid_contract(field: &'static str, reason: impl Into<String>) -> Self {
102        Self::InvalidContract {
103            field,
104            reason: reason.into(),
105        }
106    }
107}