skp_validator_core/
context.rs

1//! Validation context for runtime validation state.
2//!
3//! The [`ValidationContext`] provides:
4//! - Access to field values during validation
5//! - Metadata storage for cross-field validation
6//! - Configuration options for validation behavior
7
8use std::any::Any;
9use std::collections::HashMap;
10use std::sync::Arc;
11
12#[cfg(feature = "serde")]
13use serde_json::Value as JsonValue;
14
15/// Context for validation operations.
16///
17/// Provides runtime state during validation, including:
18/// - Field values for cross-field validation
19/// - Metadata storage
20/// - Locale settings for i18n
21///
22/// # Example
23///
24/// ```rust
25/// use skp_validator_core::ValidationContext;
26///
27/// let ctx = ValidationContext::new()
28///     .with_locale("en")
29///     .with_meta("request_id", "123");
30/// ```
31#[derive(Debug, Clone, Default)]
32pub struct ValidationContext {
33    /// Field values for cross-field validation (JSON values)
34    #[cfg(feature = "serde")]
35    field_values: HashMap<String, JsonValue>,
36
37    /// Field values without serde (using string representation)
38    #[cfg(not(feature = "serde"))]
39    field_values: HashMap<String, String>,
40
41    /// Arbitrary metadata
42    metadata: HashMap<String, String>,
43
44    /// Locale for error messages (default: "en")
45    locale: String,
46
47    /// Whether to collect all errors or fail fast
48    fail_fast: bool,
49
50    /// Custom data (type-erased)
51    custom_data: Option<Arc<dyn Any + Send + Sync>>,
52}
53
54impl ValidationContext {
55    /// Create a new empty validation context
56    pub fn new() -> Self {
57        Self {
58            field_values: HashMap::new(),
59            metadata: HashMap::new(),
60            locale: "en".to_string(),
61            fail_fast: false,
62            custom_data: None,
63        }
64    }
65
66    /// Set the locale for error messages
67    pub fn with_locale(mut self, locale: impl Into<String>) -> Self {
68        self.locale = locale.into();
69        self
70    }
71
72    /// Set fail-fast mode (stop on first error)
73    pub fn with_fail_fast(mut self, fail_fast: bool) -> Self {
74        self.fail_fast = fail_fast;
75        self
76    }
77
78    /// Add metadata
79    pub fn with_meta(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
80        self.metadata.insert(key.into(), value.into());
81        self
82    }
83
84    /// Set custom data (type-erased)
85    pub fn with_custom_data<T: Any + Send + Sync>(mut self, data: T) -> Self {
86        self.custom_data = Some(Arc::new(data));
87        self
88    }
89
90    /// Get the locale
91    pub fn locale(&self) -> &str {
92        &self.locale
93    }
94
95    /// Check if fail-fast mode is enabled
96    pub fn is_fail_fast(&self) -> bool {
97        self.fail_fast
98    }
99
100    /// Get metadata value
101    pub fn get_meta(&self, key: &str) -> Option<&str> {
102        self.metadata.get(key).map(|s| s.as_str())
103    }
104
105    /// Get custom data by type
106    pub fn get_custom_data<T: Any + Send + Sync>(&self) -> Option<&T> {
107        self.custom_data
108            .as_ref()
109            .and_then(|d| d.downcast_ref::<T>())
110    }
111
112    // === Field value access (with serde feature) ===
113
114    /// Create context from a JSON value (requires serde feature)
115    #[cfg(feature = "serde")]
116    pub fn from_json(json: &JsonValue) -> Self {
117        let mut field_values = HashMap::new();
118
119        if let Some(obj) = json.as_object() {
120            for (key, value) in obj {
121                field_values.insert(key.clone(), value.clone());
122            }
123        }
124
125        Self {
126            field_values,
127            metadata: HashMap::new(),
128            locale: "en".to_string(),
129            fail_fast: false,
130            custom_data: None,
131        }
132    }
133
134    /// Create context from a serializable object (requires serde feature)
135    #[cfg(feature = "serde")]
136    pub fn from_serde<T: serde::Serialize>(data: &T) -> Result<Self, serde_json::Error> {
137        let json = serde_json::to_value(data)?;
138        Ok(Self::from_json(&json))
139    }
140
141    /// Get a field value as JSON (requires serde feature)
142    #[cfg(feature = "serde")]
143    pub fn get_field(&self, name: &str) -> Option<&JsonValue> {
144        self.field_values.get(name)
145    }
146
147    /// Set a field value (requires serde feature)
148    #[cfg(feature = "serde")]
149    pub fn set_field(&mut self, name: impl Into<String>, value: JsonValue) {
150        self.field_values.insert(name.into(), value);
151    }
152
153    /// Set a field value (builder pattern, requires serde feature)
154    #[cfg(feature = "serde")]
155    pub fn with_field(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
156        self.field_values.insert(name.into(), JsonValue::String(value.into()));
157        self
158    }
159
160    /// Get a field value as string (requires serde feature)
161    #[cfg(feature = "serde")]
162    pub fn get_string(&self, name: &str) -> Option<&str> {
163        self.get_field(name)?.as_str()
164    }
165
166    /// Get a field value as i64 (requires serde feature)
167    #[cfg(feature = "serde")]
168    pub fn get_i64(&self, name: &str) -> Option<i64> {
169        self.get_field(name)?.as_i64()
170    }
171
172    /// Get a field value as f64 (requires serde feature)
173    #[cfg(feature = "serde")]
174    pub fn get_f64(&self, name: &str) -> Option<f64> {
175        self.get_field(name)?.as_f64()
176    }
177
178    /// Get a field value as bool (requires serde feature)
179    #[cfg(feature = "serde")]
180    pub fn get_bool(&self, name: &str) -> Option<bool> {
181        self.get_field(name)?.as_bool()
182    }
183
184    /// Check if a field exists and is not null/empty (requires serde feature)
185    #[cfg(feature = "serde")]
186    pub fn has_value(&self, name: &str) -> bool {
187        if let Some(value) = self.get_field(name) {
188            !value.is_null()
189                && match value {
190                    JsonValue::String(s) => !s.trim().is_empty(),
191                    JsonValue::Array(arr) => !arr.is_empty(),
192                    JsonValue::Object(obj) => !obj.is_empty(),
193                    _ => true,
194                }
195        } else {
196            false
197        }
198    }
199
200    /// Check if a field is empty or null (requires serde feature)
201    #[cfg(feature = "serde")]
202    pub fn is_empty(&self, name: &str) -> bool {
203        !self.has_value(name)
204    }
205
206    /// Get all field names
207    pub fn field_names(&self) -> impl Iterator<Item = &String> {
208        self.field_values.keys()
209    }
210
211    // === Field value access (without serde feature) ===
212
213    /// Set a field value as string (no serde)
214    #[cfg(not(feature = "serde"))]
215    pub fn set_field(&mut self, name: impl Into<String>, value: impl Into<String>) {
216        self.field_values.insert(name.into(), value.into());
217    }
218
219    /// Get a field value as string (no serde)
220    #[cfg(not(feature = "serde"))]
221    pub fn get_string(&self, name: &str) -> Option<&str> {
222        self.field_values.get(name).map(|s| s.as_str())
223    }
224
225    /// Check if a field exists (no serde)
226    #[cfg(not(feature = "serde"))]
227    pub fn has_value(&self, name: &str) -> bool {
228        self.field_values.get(name).map(|s| !s.is_empty()).unwrap_or(false)
229    }
230}
231
232/// Builder for ValidationContext with custom context type
233pub struct ValidationContextBuilder<C> {
234    context: ValidationContext,
235    custom: Option<C>,
236}
237
238impl<C: Any + Send + Sync> ValidationContextBuilder<C> {
239    /// Create a new builder
240    pub fn new() -> Self {
241        Self {
242            context: ValidationContext::new(),
243            custom: None,
244        }
245    }
246
247    /// Set the locale
248    pub fn locale(mut self, locale: impl Into<String>) -> Self {
249        self.context.locale = locale.into();
250        self
251    }
252
253    /// Set fail-fast mode
254    pub fn fail_fast(mut self, fail_fast: bool) -> Self {
255        self.context.fail_fast = fail_fast;
256        self
257    }
258
259    /// Add metadata
260    pub fn meta(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
261        self.context.metadata.insert(key.into(), value.into());
262        self
263    }
264
265    /// Set custom context data
266    pub fn custom(mut self, custom: C) -> Self {
267        self.custom = Some(custom);
268        self
269    }
270
271    /// Build the context
272    pub fn build(mut self) -> ValidationContext {
273        if let Some(custom) = self.custom {
274            self.context.custom_data = Some(Arc::new(custom));
275        }
276        self.context
277    }
278}
279
280impl<C: Any + Send + Sync> Default for ValidationContextBuilder<C> {
281    fn default() -> Self {
282        Self::new()
283    }
284}
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289
290    #[test]
291    fn test_context_creation() {
292        let ctx = ValidationContext::new()
293            .with_locale("hi")
294            .with_meta("request_id", "123");
295
296        assert_eq!(ctx.locale(), "hi");
297        assert_eq!(ctx.get_meta("request_id"), Some("123"));
298    }
299
300    #[test]
301    fn test_custom_data() {
302        #[derive(Debug, Clone)]
303        struct MyContext {
304            user_id: u64,
305        }
306
307        let ctx = ValidationContext::new().with_custom_data(MyContext { user_id: 42 });
308
309        let my_ctx = ctx.get_custom_data::<MyContext>().unwrap();
310        assert_eq!(my_ctx.user_id, 42);
311    }
312
313    #[cfg(feature = "serde")]
314    #[test]
315    fn test_from_json() {
316        let json = serde_json::json!({
317            "name": "John",
318            "age": 30,
319            "active": true
320        });
321
322        let ctx = ValidationContext::from_json(&json);
323
324        assert_eq!(ctx.get_string("name"), Some("John"));
325        assert_eq!(ctx.get_i64("age"), Some(30));
326        assert_eq!(ctx.get_bool("active"), Some(true));
327    }
328
329    #[cfg(feature = "serde")]
330    #[test]
331    fn test_has_value() {
332        let json = serde_json::json!({
333            "name": "John",
334            "empty": "",
335            "null": null
336        });
337
338        let ctx = ValidationContext::from_json(&json);
339
340        assert!(ctx.has_value("name"));
341        assert!(!ctx.has_value("empty"));
342        assert!(!ctx.has_value("null"));
343        assert!(!ctx.has_value("missing"));
344    }
345}