siderite_validation/
context.rs1use crate::error::{FieldError, LocationItem, ValidationError};
5use std::any::{Any, TypeId};
6use std::borrow::Cow;
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
11pub enum Extra {
12 #[default]
14 Ignore,
15 Forbid,
19 Allow,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
29pub struct ModelConfig {
30 pub strict: bool,
32 pub extra: Extra,
34 pub str_strip_whitespace: bool,
36 pub str_to_lower: bool,
38 pub str_to_upper: bool,
40}
41
42impl ModelConfig {
43 pub const DEFAULT: Self = Self {
45 strict: false,
46 extra: Extra::Ignore,
47 str_strip_whitespace: false,
48 str_to_lower: false,
49 str_to_upper: false,
50 };
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
57pub enum InputKind {
58 #[default]
60 Json,
61 Text,
63}
64
65#[derive(Default)]
67pub struct ValidationContext {
68 config: ModelConfig,
69 strict_override: Option<bool>,
70 input: InputKind,
71 location: Vec<LocationItem>,
72 errors: ValidationError,
73 data: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
74}
75
76impl std::fmt::Debug for ValidationContext {
77 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78 f.debug_struct("ValidationContext")
79 .field("config", &self.config)
80 .field("strict_override", &self.strict_override)
81 .field("input", &self.input)
82 .field("location", &self.location)
83 .field("errors", &self.errors)
84 .finish_non_exhaustive()
85 }
86}
87
88impl ValidationContext {
89 pub fn new() -> Self {
91 Self::default()
92 }
93
94 pub fn for_text() -> Self {
96 Self {
97 input: InputKind::Text,
98 ..Self::default()
99 }
100 }
101
102 #[must_use]
105 pub fn with_strict(mut self, strict: bool) -> Self {
106 self.strict_override = Some(strict);
107 self
108 }
109
110 #[must_use]
112 pub fn at_root(mut self, item: impl Into<LocationItem>) -> Self {
113 self.location.push(item.into());
114 self
115 }
116
117 #[must_use]
119 pub fn with_data<T: Any + Send + Sync>(mut self, value: T) -> Self {
120 self.data.insert(TypeId::of::<T>(), Box::new(value));
121 self
122 }
123
124 pub fn data<T: Any + Send + Sync>(&self) -> Option<&T> {
126 self.data.get(&TypeId::of::<T>())?.downcast_ref()
127 }
128
129 pub fn config(&self) -> &ModelConfig {
131 &self.config
132 }
133
134 pub fn allows_text_coercion(&self) -> bool {
136 self.input == InputKind::Text || !self.is_strict()
137 }
138
139 pub fn is_strict(&self) -> bool {
141 self.strict_override.unwrap_or(self.config.strict)
142 }
143
144 pub fn input_kind(&self) -> InputKind {
146 self.input
147 }
148
149 pub fn with_config<R>(&mut self, config: ModelConfig, f: impl FnOnce(&mut Self) -> R) -> R {
151 let previous = std::mem::replace(&mut self.config, config);
152 let out = f(self);
153 self.config = previous;
154 out
155 }
156
157 pub fn at<R>(&mut self, item: impl Into<LocationItem>, f: impl FnOnce(&mut Self) -> R) -> R {
159 self.location.push(item.into());
160 let out = f(self);
161 self.location.pop();
162 out
163 }
164
165 pub fn location(&self) -> &[LocationItem] {
167 &self.location
168 }
169
170 pub fn error(&mut self, code: impl Into<Cow<'static, str>>, message: impl Into<String>) {
172 self.push(FieldError::new(code, message));
173 }
174
175 pub fn push(&mut self, mut error: FieldError) {
177 let mut location = self.location.clone();
178 location.append(&mut error.location);
179 error.location = location;
180 self.errors.push(error);
181 }
182
183 pub fn check(&mut self, result: Result<(), FieldError>) {
185 if let Err(error) = result {
186 self.push(error);
187 }
188 }
189
190 pub fn has_errors(&self) -> bool {
192 !self.errors.is_empty()
193 }
194
195 pub fn error_count(&self) -> usize {
197 self.errors.errors.len()
198 }
199
200 pub fn take_errors(&mut self) -> ValidationError {
202 std::mem::take(&mut self.errors)
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 #[test]
211 fn locations_nest_and_restore() {
212 let mut ctx = ValidationContext::new().at_root("body");
213 ctx.at("user", |ctx| ctx.at(0_usize, |ctx| ctx.error("x", "bad")));
214 ctx.error("y", "top");
215 let errors = ctx.take_errors().errors;
216 assert_eq!(
217 errors[0].location,
218 vec!["body".into(), "user".into(), LocationItem::index(0)]
219 );
220 assert_eq!(errors[1].location, vec![LocationItem::key("body")]);
221 }
222
223 #[test]
224 fn strictness_override_and_text_input() {
225 let strict = ModelConfig {
226 strict: true,
227 ..ModelConfig::DEFAULT
228 };
229 let mut ctx = ValidationContext::new();
230 ctx.with_config(strict, |ctx| {
231 assert!(ctx.is_strict());
232 assert!(!ctx.allows_text_coercion());
233 });
234 assert!(!ctx.is_strict());
235 let mut text = ValidationContext::for_text().with_strict(true);
236 assert!(text.allows_text_coercion());
237 text.with_config(ModelConfig::DEFAULT, |ctx| assert!(ctx.is_strict()));
238 }
239
240 #[test]
241 fn user_data_round_trips() {
242 let ctx = ValidationContext::new().with_data(42_u32);
243 assert_eq!(ctx.data::<u32>(), Some(&42));
244 assert!(ctx.data::<i64>().is_none());
245 }
246}