stygian_charon/vendor_classifier/
error.rs1use thiserror::Error;
9
10#[derive(Debug, Error)]
18pub enum VendorError {
19 #[error("vendor '{vendor_id}': field '{field}' has invalid value '{value}': {reason}")]
22 InvalidField {
23 vendor_id: String,
25 field: String,
27 value: String,
29 reason: String,
31 },
32
33 #[error("vendor '{vendor_id}': missing required field '{field}'")]
35 MissingField {
36 vendor_id: String,
38 field: String,
40 },
41
42 #[error("duplicate vendor id '{vendor_id}' in input bundle")]
44 DuplicateId {
45 vendor_id: String,
47 },
48
49 #[error("vendor '{vendor_id}' is not part of the supported taxonomy")]
53 UnknownVendorId {
54 vendor_id: String,
56 },
57
58 #[error("vendor TOML parse error: {0}")]
60 TomlParse(#[from] toml::de::Error),
61}
62
63impl VendorError {
64 #[must_use]
66 pub fn invalid_field(
67 vendor_id: impl Into<String>,
68 field: impl Into<String>,
69 value: impl std::fmt::Display,
70 reason: impl Into<String>,
71 ) -> Self {
72 Self::InvalidField {
73 vendor_id: vendor_id.into(),
74 field: field.into(),
75 value: value.to_string(),
76 reason: reason.into(),
77 }
78 }
79
80 #[must_use]
82 pub fn missing_field(vendor_id: impl Into<String>, field: impl Into<String>) -> Self {
83 Self::MissingField {
84 vendor_id: vendor_id.into(),
85 field: field.into(),
86 }
87 }
88
89 #[must_use]
91 pub fn field_path(&self) -> Option<&str> {
92 match self {
93 Self::InvalidField { field, .. } | Self::MissingField { field, .. } => Some(field),
94 _ => None,
95 }
96 }
97
98 #[must_use]
100 pub fn bad_value(&self) -> Option<&str> {
101 match self {
102 Self::InvalidField { value, .. } => Some(value),
103 _ => None,
104 }
105 }
106}
107
108#[cfg(test)]
109#[allow(
110 clippy::unwrap_used,
111 clippy::expect_used,
112 clippy::panic,
113 clippy::indexing_slicing
114)]
115mod tests {
116 use super::*;
117
118 #[test]
119 fn invalid_field_message_includes_field_and_value() {
120 let err =
121 VendorError::invalid_field("datadome", "signals[0].weight", "-1", "weight must be > 0");
122 let msg = err.to_string();
123 assert!(msg.contains("datadome"));
124 assert!(msg.contains("signals[0].weight"));
125 assert!(msg.contains("-1"));
126 assert!(msg.contains("weight must be > 0"));
127 assert_eq!(err.field_path(), Some("signals[0].weight"));
128 assert_eq!(err.bad_value(), Some("-1"));
129 }
130
131 #[test]
132 fn missing_field_message_includes_field() {
133 let err = VendorError::missing_field("cloudflare", "display_name");
134 let msg = err.to_string();
135 assert!(msg.contains("cloudflare"));
136 assert!(msg.contains("display_name"));
137 assert_eq!(err.field_path(), Some("display_name"));
138 assert_eq!(err.bad_value(), None);
139 }
140
141 #[test]
142 fn duplicate_id_does_not_report_field() {
143 let err = VendorError::DuplicateId {
144 vendor_id: "akamai".to_string(),
145 };
146 assert_eq!(err.field_path(), None);
147 assert_eq!(err.bad_value(), None);
148 assert!(err.to_string().contains("akamai"));
149 }
150}