1use std::fmt::Display;
27
28pub trait Validate {
36 type Error: std::error::Error + Send + Sync + 'static;
37
38 fn validate(&self) -> Result<(), Self::Error>;
39}
40
41#[derive(Debug, thiserror::Error)]
45#[error("config field `{field}` invalid: {message}")]
46pub struct ValidationError {
47 pub field: &'static str,
48 pub message: String,
49}
50
51impl ValidationError {
52 pub fn new(field: &'static str, message: impl Into<String>) -> Self {
53 Self {
54 field,
55 message: message.into(),
56 }
57 }
58}
59
60pub fn ensure_range<T>(value: T, min: T, max: T, field: &'static str) -> Result<(), ValidationError>
63where
64 T: PartialOrd + Display + Copy,
65{
66 if value < min || value > max {
67 return Err(ValidationError::new(
68 field,
69 format!("value {value} not in {min}..={max}"),
70 ));
71 }
72 Ok(())
73}
74
75pub fn ensure_non_zero<T>(value: T, field: &'static str) -> Result<(), ValidationError>
79where
80 T: PartialEq + Default + Display,
81{
82 if value == T::default() {
83 return Err(ValidationError::new(field, "must not be zero"));
84 }
85 Ok(())
86}
87
88pub fn ensure_one_of<T>(
91 value: &T,
92 allowed: &[T],
93 field: &'static str,
94) -> Result<(), ValidationError>
95where
96 T: PartialEq + Display,
97{
98 if !allowed.iter().any(|a| a == value) {
99 let listed: Vec<String> = allowed.iter().map(|x| format!("\"{x}\"")).collect();
100 return Err(ValidationError::new(
101 field,
102 format!("value \"{value}\" not in [{}]", listed.join(", ")),
103 ));
104 }
105 Ok(())
106}
107
108pub fn ensure_non_empty_str(value: &str, field: &'static str) -> Result<(), ValidationError> {
110 if value.is_empty() {
111 return Err(ValidationError::new(field, "must not be empty"));
112 }
113 Ok(())
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 #[test]
121 fn ensure_range_inclusive_boundaries_ok() {
122 assert!(ensure_range(1u8, 1, 16, "tab_width").is_ok());
123 assert!(ensure_range(16u8, 1, 16, "tab_width").is_ok());
124 assert!(ensure_range(8u8, 1, 16, "tab_width").is_ok());
125 }
126
127 #[test]
128 fn ensure_range_below_min_errs() {
129 let err = ensure_range(0u8, 1, 16, "tab_width").unwrap_err();
130 assert_eq!(err.field, "tab_width");
131 assert!(err.message.contains("0"));
132 assert!(err.message.contains("1..=16"));
133 }
134
135 #[test]
136 fn ensure_range_above_max_errs() {
137 let err = ensure_range(64u8, 1, 16, "tab_width").unwrap_err();
138 assert_eq!(err.field, "tab_width");
139 assert!(err.message.contains("64"));
140 }
141
142 #[test]
143 fn ensure_non_zero_rejects_zero() {
144 assert!(ensure_non_zero(0u32, "x").is_err());
145 assert!(ensure_non_zero(0i64, "x").is_err());
146 }
147
148 #[test]
149 fn ensure_non_zero_accepts_nonzero() {
150 assert!(ensure_non_zero(1u32, "x").is_ok());
151 assert!(ensure_non_zero(42u64, "x").is_ok());
152 }
153
154 #[test]
155 fn ensure_one_of_finds_match() {
156 let allowed = ["dark".to_string(), "light".to_string()];
157 assert!(ensure_one_of(&"dark".to_string(), &allowed, "theme").is_ok());
158 }
159
160 #[test]
161 fn ensure_one_of_rejects_unknown() {
162 let allowed = ["dark".to_string(), "light".to_string()];
163 let err = ensure_one_of(&"solarized".to_string(), &allowed, "theme").unwrap_err();
164 assert_eq!(err.field, "theme");
165 assert!(err.message.contains("solarized"));
166 assert!(err.message.contains("dark"));
167 assert!(err.message.contains("light"));
168 }
169
170 #[test]
171 fn ensure_non_empty_str_works() {
172 assert!(ensure_non_empty_str("x", "name").is_ok());
173 let err = ensure_non_empty_str("", "name").unwrap_err();
174 assert_eq!(err.field, "name");
175 }
176
177 #[test]
178 fn validation_error_display_includes_field_and_message() {
179 let err = ValidationError::new("editor.tab_width", "value 0 not in 1..=16");
180 let s = err.to_string();
181 assert!(s.contains("editor.tab_width"));
182 assert!(s.contains("value 0 not in 1..=16"));
183 }
184}