Skip to main content

vld/
error.rs

1use std::fmt;
2
3/// A segment in a validation error path.
4#[derive(Debug, Clone)]
5#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
6#[cfg_attr(feature = "deserialize", derive(serde::Deserialize))]
7pub enum PathSegment {
8    /// Object field name.
9    Field(String),
10    /// Array index.
11    Index(usize),
12}
13
14impl fmt::Display for PathSegment {
15    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
16        match self {
17            PathSegment::Field(name) => write!(f, ".{}", name),
18            PathSegment::Index(idx) => write!(f, "[{}]", idx),
19        }
20    }
21}
22
23/// Type of string validation that failed.
24#[derive(Debug, Clone)]
25#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
26#[cfg_attr(feature = "deserialize", derive(serde::Deserialize))]
27pub enum StringValidation {
28    Email,
29    Url,
30    UrlStrict,
31    Uri,
32    Uuid,
33    UuidV1,
34    UuidV4,
35    UuidV7,
36    Ip,
37    Slug,
38    Color,
39    Currency,
40    CountryCode,
41    Locale,
42    Cron,
43    Regex,
44    StartsWith,
45    EndsWith,
46    Ipv4,
47    Ipv6,
48    Cidr,
49    Mac,
50    Hex,
51    CreditCard,
52    Phone,
53    PhoneE164,
54    Semver,
55    SemverFull,
56    Jwt,
57    Ascii,
58    Alpha,
59    Alphanumeric,
60    Lowercase,
61    Uppercase,
62    Base64,
63    IsoDate,
64    IsoDatetime,
65    IsoTime,
66    Hostname,
67    Cuid2,
68    Ulid,
69    Nanoid,
70    Emoji,
71}
72
73/// Validation issue code — describes what went wrong.
74#[derive(Debug, Clone)]
75#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
76#[cfg_attr(feature = "deserialize", derive(serde::Deserialize))]
77pub enum IssueCode {
78    InvalidType { expected: String, received: String },
79    TooSmall { minimum: f64, inclusive: bool },
80    TooBig { maximum: f64, inclusive: bool },
81    InvalidString { validation: StringValidation },
82    NotInt,
83    NotFinite,
84    MissingField,
85    UnrecognizedField,
86    IoError,
87    ParseError,
88    Custom { code: String },
89}
90
91/// A single validation issue with path, message and received value.
92#[derive(Debug, Clone)]
93#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
94#[cfg_attr(feature = "deserialize", derive(serde::Deserialize))]
95pub struct ValidationIssue {
96    pub code: IssueCode,
97    pub message: String,
98    pub path: Vec<PathSegment>,
99    /// The value that was received (if available).
100    pub received: Option<serde_json::Value>,
101}
102
103/// Collection of validation errors.
104///
105/// Errors are accumulated (not short-circuited), so all issues are reported at once.
106#[derive(Debug, Clone)]
107#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
108#[cfg_attr(feature = "deserialize", derive(serde::Deserialize))]
109pub struct VldError {
110    pub issues: Vec<ValidationIssue>,
111}
112
113impl VldError {
114    /// Create an empty error container.
115    pub fn new() -> Self {
116        Self { issues: vec![] }
117    }
118
119    /// Create an error with a single issue (no received value).
120    pub fn single(code: IssueCode, message: impl Into<String>) -> Self {
121        Self {
122            issues: vec![ValidationIssue {
123                code,
124                message: message.into(),
125                path: vec![],
126                received: None,
127            }],
128        }
129    }
130
131    /// Create an error with a single issue and the received value.
132    pub fn single_with_value(
133        code: IssueCode,
134        message: impl Into<String>,
135        received: &serde_json::Value,
136    ) -> Self {
137        Self {
138            issues: vec![ValidationIssue {
139                code,
140                message: message.into(),
141                path: vec![],
142                received: Some(truncate_value(received)),
143            }],
144        }
145    }
146
147    /// Prepend a path segment to all issues (used for nested objects/arrays).
148    pub fn with_prefix(mut self, segment: PathSegment) -> Self {
149        for issue in &mut self.issues {
150            issue.path.insert(0, segment.clone());
151        }
152        self
153    }
154
155    /// Merge another error's issues into this one.
156    pub fn merge(mut self, other: VldError) -> Self {
157        self.issues.extend(other.issues);
158        self
159    }
160
161    /// Check if there are no issues.
162    pub fn is_empty(&self) -> bool {
163        self.issues.is_empty()
164    }
165
166    /// Push a single issue (no received value).
167    pub fn push(&mut self, code: IssueCode, message: impl Into<String>) {
168        self.issues.push(ValidationIssue {
169            code,
170            message: message.into(),
171            path: vec![],
172            received: None,
173        });
174    }
175
176    /// Push a single issue with the received value.
177    pub fn push_with_value(
178        &mut self,
179        code: IssueCode,
180        message: impl Into<String>,
181        received: &serde_json::Value,
182    ) {
183        self.issues.push(ValidationIssue {
184            code,
185            message: message.into(),
186            path: vec![],
187            received: Some(truncate_value(received)),
188        });
189    }
190}
191
192/// Fluent builder for constructing a single [`ValidationIssue`].
193///
194/// Obtained via [`VldError::issue()`]. Push it into a `VldError` with
195/// [`.finish()`](IssueBuilder::finish).
196///
197/// # Example
198/// ```
199/// use vld::prelude::*;
200///
201/// let mut errors = VldError::new();
202/// errors
203///     .issue(IssueCode::Custom { code: "password_weak".into() })
204///     .message("Password is too weak")
205///     .path_field("password")
206///     .received(&serde_json::json!("123"))
207///     .finish();
208/// assert_eq!(errors.issues.len(), 1);
209/// assert_eq!(errors.issues[0].message, "Password is too weak");
210/// ```
211pub struct IssueBuilder<'a> {
212    errors: &'a mut VldError,
213    code: IssueCode,
214    message: Option<String>,
215    path: Vec<PathSegment>,
216    received: Option<serde_json::Value>,
217}
218
219impl<'a> IssueBuilder<'a> {
220    /// Set the error message.
221    pub fn message(mut self, msg: impl Into<String>) -> Self {
222        self.message = Some(msg.into());
223        self
224    }
225
226    /// Add a field path segment.
227    pub fn path_field(mut self, name: impl Into<String>) -> Self {
228        self.path.push(PathSegment::Field(name.into()));
229        self
230    }
231
232    /// Add an index path segment.
233    pub fn path_index(mut self, idx: usize) -> Self {
234        self.path.push(PathSegment::Index(idx));
235        self
236    }
237
238    /// Attach the received value.
239    pub fn received(mut self, value: &serde_json::Value) -> Self {
240        self.received = Some(truncate_value(value));
241        self
242    }
243
244    /// Finish building and push the issue into the parent `VldError`.
245    pub fn finish(self) {
246        let msg = self
247            .message
248            .unwrap_or_else(|| format!("Validation error: {}", self.code.key()));
249        self.errors.issues.push(ValidationIssue {
250            code: self.code,
251            message: msg,
252            path: self.path,
253            received: self.received,
254        });
255    }
256}
257
258impl VldError {
259    /// Start building a new issue with the fluent API.
260    ///
261    /// Call `.message(...)`, `.path_field(...)`, `.received(...)` etc., then
262    /// `.finish()` to push the issue.
263    ///
264    /// # Example
265    /// ```
266    /// use vld::prelude::*;
267    ///
268    /// let mut errors = VldError::new();
269    /// errors
270    ///     .issue(IssueCode::Custom { code: "my_check".into() })
271    ///     .message("something went wrong")
272    ///     .path_field("field_name")
273    ///     .finish();
274    /// ```
275    pub fn issue(&mut self, code: IssueCode) -> IssueBuilder<'_> {
276        IssueBuilder {
277            errors: self,
278            code,
279            message: None,
280            path: vec![],
281            received: None,
282        }
283    }
284}
285
286impl Default for VldError {
287    fn default() -> Self {
288        Self::new()
289    }
290}
291
292impl fmt::Display for VldError {
293    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
294        for (i, issue) in self.issues.iter().enumerate() {
295            if i > 0 {
296                writeln!(f)?;
297            }
298            if !issue.path.is_empty() {
299                let path_str: String = issue.path.iter().map(|p| p.to_string()).collect();
300                write!(f, "{}: ", path_str)?;
301            }
302            write!(f, "{}", issue.message)?;
303            if let Some(val) = &issue.received {
304                write!(f, ", received {}", format_value_short(val))?;
305            }
306        }
307        Ok(())
308    }
309}
310
311impl std::error::Error for VldError {}
312
313impl IssueCode {
314    /// Stable string key for this error code. Useful for i18n and error mapping.
315    pub fn key(&self) -> &str {
316        match self {
317            IssueCode::InvalidType { .. } => "invalid_type",
318            IssueCode::TooSmall { .. } => "too_small",
319            IssueCode::TooBig { .. } => "too_big",
320            IssueCode::InvalidString { .. } => "invalid_string",
321            IssueCode::NotInt => "not_int",
322            IssueCode::NotFinite => "not_finite",
323            IssueCode::MissingField => "missing_field",
324            IssueCode::UnrecognizedField => "unrecognized_field",
325            IssueCode::IoError => "io_error",
326            IssueCode::ParseError => "parse_error",
327            IssueCode::Custom { code } => code,
328        }
329    }
330
331    /// Extract key-value parameters from this error code for message formatting.
332    ///
333    /// Useful for i18n: format templates like `"Must be at least {minimum}"`.
334    pub fn params(&self) -> Vec<(&str, String)> {
335        match self {
336            IssueCode::InvalidType { expected, received } => {
337                vec![
338                    ("expected", expected.clone()),
339                    ("received", received.clone()),
340                ]
341            }
342            IssueCode::TooSmall { minimum, inclusive } => {
343                vec![
344                    ("minimum", minimum.to_string()),
345                    ("inclusive", inclusive.to_string()),
346                ]
347            }
348            IssueCode::TooBig { maximum, inclusive } => {
349                vec![
350                    ("maximum", maximum.to_string()),
351                    ("inclusive", inclusive.to_string()),
352                ]
353            }
354            IssueCode::InvalidString { validation } => {
355                vec![("validation", format!("{:?}", validation))]
356            }
357            _ => vec![],
358        }
359    }
360}
361
362/// Returns the JSON type name for a value.
363#[doc(hidden)]
364pub fn value_type_name(value: &serde_json::Value) -> String {
365    match value {
366        serde_json::Value::Null => "null",
367        serde_json::Value::Bool(_) => "boolean",
368        serde_json::Value::Number(_) => "number",
369        serde_json::Value::String(_) => "string",
370        serde_json::Value::Array(_) => "array",
371        serde_json::Value::Object(_) => "object",
372    }
373    .to_string()
374}
375
376/// Format a JSON value for display in errors (short form).
377pub fn format_value_short(value: &serde_json::Value) -> String {
378    match value {
379        serde_json::Value::Null => "null".to_string(),
380        serde_json::Value::Bool(b) => b.to_string(),
381        serde_json::Value::Number(n) => n.to_string(),
382        serde_json::Value::String(s) => {
383            let char_count = s.chars().count();
384            if char_count > 50 {
385                let prefix: String = s.chars().take(47).collect();
386                format!("\"{}...\"", prefix)
387            } else {
388                format!("\"{}\"", s)
389            }
390        }
391        serde_json::Value::Array(arr) => format!("Array(len={})", arr.len()),
392        serde_json::Value::Object(obj) => format!("Object(keys={})", obj.len()),
393    }
394}
395
396/// Result of validating a single field.
397///
398/// Returned by `validate_fields()` generated by the [`schema!`](crate::schema!) macro.
399#[derive(Debug, Clone)]
400#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
401#[cfg_attr(feature = "deserialize", derive(serde::Deserialize))]
402pub struct FieldResult {
403    /// Field name.
404    pub name: String,
405    /// Raw input value (before validation).
406    pub input: serde_json::Value,
407    /// `Ok(json_value)` if the field is valid (output serialized to JSON),
408    /// `Err(error)` if validation failed.
409    pub result: Result<serde_json::Value, VldError>,
410}
411
412impl FieldResult {
413    /// Whether this field passed validation.
414    pub fn is_ok(&self) -> bool {
415        self.result.is_ok()
416    }
417
418    /// Whether this field failed validation.
419    pub fn is_err(&self) -> bool {
420        self.result.is_err()
421    }
422}
423
424impl std::fmt::Display for FieldResult {
425    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
426        match &self.result {
427            Ok(v) => write!(f, "✔ {}: {}", self.name, format_value_short(v)),
428            Err(e) => {
429                let received = format_value_short(&self.input);
430                for (i, issue) in e.issues.iter().enumerate() {
431                    if i > 0 {
432                        writeln!(f)?;
433                    }
434                    write!(
435                        f,
436                        "✖ {}: {} (received: {})",
437                        self.name, issue.message, received
438                    )?;
439                }
440                Ok(())
441            }
442        }
443    }
444}
445
446/// Result of lenient parsing: contains the (possibly partial) struct and
447/// per-field diagnostics.
448///
449/// Created by `parse_lenient()` / `parse_lenient_value()` generated by
450/// [`impl_validate_fields!`](crate::impl_validate_fields).
451///
452/// The struct is always constructed — invalid fields fall back to
453/// `Default::default()`. You can inspect which fields passed/failed via
454/// [`fields()`](Self::fields), and save the result to a file at any time
455/// via `save_to_file()` (requires the `serialize` feature).
456#[derive(Debug)]
457pub struct ParseResult<T> {
458    /// The constructed struct (invalid fields use `Default`).
459    pub value: T,
460    /// Per-field validation diagnostics.
461    field_results: Vec<FieldResult>,
462}
463
464impl<T> ParseResult<T> {
465    /// Create a new parse result.
466    pub fn new(value: T, field_results: Vec<FieldResult>) -> Self {
467        Self {
468            value,
469            field_results,
470        }
471    }
472
473    /// All per-field results.
474    pub fn fields(&self) -> &[FieldResult] {
475        &self.field_results
476    }
477
478    /// Get a specific field's result by name.
479    ///
480    /// Returns `None` if no field with that name exists.
481    pub fn field(&self, name: &str) -> Option<&FieldResult> {
482        self.field_results.iter().find(|f| f.name == name)
483    }
484
485    /// Only the fields that passed validation.
486    pub fn valid_fields(&self) -> Vec<&FieldResult> {
487        self.field_results.iter().filter(|f| f.is_ok()).collect()
488    }
489
490    /// Only the fields that failed validation.
491    pub fn error_fields(&self) -> Vec<&FieldResult> {
492        self.field_results.iter().filter(|f| f.is_err()).collect()
493    }
494
495    /// Whether all fields passed validation.
496    pub fn is_valid(&self) -> bool {
497        self.field_results.iter().all(|f| f.is_ok())
498    }
499
500    /// Whether at least one field failed.
501    pub fn has_errors(&self) -> bool {
502        self.field_results.iter().any(|f| f.is_err())
503    }
504
505    /// Number of valid fields.
506    pub fn valid_count(&self) -> usize {
507        self.field_results.iter().filter(|f| f.is_ok()).count()
508    }
509
510    /// Number of invalid fields.
511    pub fn error_count(&self) -> usize {
512        self.field_results.iter().filter(|f| f.is_err()).count()
513    }
514
515    /// Consume and return the inner struct.
516    pub fn into_value(self) -> T {
517        self.value
518    }
519
520    /// Consume and return both the struct and field results.
521    pub fn into_parts(self) -> (T, Vec<FieldResult>) {
522        (self.value, self.field_results)
523    }
524}
525
526#[cfg(feature = "serialize")]
527impl<T: serde::Serialize> ParseResult<T> {
528    /// Serialize the struct to a JSON file.
529    ///
530    /// Requires the `serialize` and `std` features.
531    #[cfg(feature = "std")]
532    pub fn save_to_file(&self, path: &std::path::Path) -> std::io::Result<()> {
533        let json = serde_json::to_string_pretty(&self.value)
534            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
535        if let Some(parent) = path.parent() {
536            if !parent.as_os_str().is_empty() {
537                std::fs::create_dir_all(parent)?;
538            }
539        }
540        std::fs::write(path, json)
541    }
542
543    /// Serialize the struct to a JSON string.
544    ///
545    /// Requires the `serialize` feature.
546    pub fn to_json_string(&self) -> Result<String, serde_json::Error> {
547        serde_json::to_string_pretty(&self.value)
548    }
549
550    /// Serialize the struct to a `serde_json::Value`.
551    ///
552    /// Requires the `serialize` feature.
553    pub fn to_json_value(&self) -> Result<serde_json::Value, serde_json::Error> {
554        serde_json::to_value(&self.value)
555    }
556}
557
558impl<T: std::fmt::Debug> std::fmt::Display for ParseResult<T> {
559    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
560        writeln!(
561            f,
562            "ParseResult ({} valid, {} errors):",
563            self.valid_count(),
564            self.error_count()
565        )?;
566        for field in &self.field_results {
567            writeln!(f, "  {}", field)?;
568        }
569        Ok(())
570    }
571}
572
573/// Truncate large values to avoid storing huge payloads in errors.
574fn truncate_value(value: &serde_json::Value) -> serde_json::Value {
575    match value {
576        serde_json::Value::String(s) if s.len() > 100 => {
577            serde_json::Value::String(format!("{}...", &s[..97]))
578        }
579        serde_json::Value::Array(arr) if arr.len() > 5 => {
580            let mut truncated: Vec<serde_json::Value> = arr[..5].to_vec();
581            truncated.push(serde_json::Value::String(format!(
582                "... ({} more)",
583                arr.len() - 5
584            )));
585            serde_json::Value::Array(truncated)
586        }
587        _ => value.clone(),
588    }
589}