Files
huskies/server/src/validation/error.rs
T

128 lines
4.3 KiB
Rust
Raw Normal View History

2026-05-14 12:53:14 +00:00
//! Typed validation error enum returned from all MCP write-tool input validation.
use serde::{Deserialize, Serialize};
use std::fmt;
/// Structured error from input validation.
///
/// Each variant carries exactly the data a caller needs to act on the error.
/// Serialises to serde's default externally-tagged form, e.g.
/// `{"FieldTooLong":{"field":"description","max":200,"actual":287}}`.
/// Callers can pattern-match on the JSON tag without parsing prose.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ValidationError {
/// A required field was absent from the input.
FieldMissing { field: String },
/// A field value is empty (or whitespace-only) after trimming.
EmptyAfterTrim { field: String },
/// A field value exceeds the maximum allowed length.
FieldTooLong {
field: String,
max: usize,
actual: usize,
},
/// A field value contains a character outside the allowed set.
InvalidCharacter {
field: String,
ch: char,
position: usize,
},
/// A field value contains a tool-call grammar fragment that must be rejected.
AntiGrammarToken { field: String, token: String },
/// A numeric field value is outside its allowed range.
OutOfRange {
field: String,
min: i64,
max: i64,
actual: i64,
},
/// A list field has fewer items than the minimum.
TooFewItems {
field: String,
min: usize,
actual: usize,
},
/// A list field has more items than the maximum.
TooManyItems {
field: String,
max: usize,
actual: usize,
},
/// A field value is not valid UTF-8.
InvalidUtf8 { field: String },
/// A `depends_on` entry references the same item being created or updated.
SelfReference { field: String },
}
impl fmt::Display for ValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::FieldMissing { field } => {
write!(f, "field '{field}' is required but was not provided")
}
Self::EmptyAfterTrim { field } => {
write!(f, "field '{field}' must not be empty or whitespace-only")
}
Self::FieldTooLong { field, max, actual } => {
write!(f, "field '{field}' is too long ({actual} chars, max {max})")
}
Self::InvalidCharacter {
field,
ch,
position,
} => {
write!(
f,
"field '{field}' contains invalid character {ch:?} at position {position}"
)
}
Self::AntiGrammarToken { field, token } => {
write!(
f,
"field '{field}' contains a tool-call grammar fragment: {token:?}"
)
}
Self::OutOfRange {
field,
min,
max,
actual,
} => {
write!(
f,
"field '{field}' value {actual} is out of allowed range [{min}, {max}]"
)
}
Self::TooFewItems { field, min, actual } => {
write!(
f,
"field '{field}' has too few items ({actual}; minimum {min})"
)
}
Self::TooManyItems { field, max, actual } => {
write!(
f,
"field '{field}' has too many items ({actual}; maximum {max})"
)
}
Self::InvalidUtf8 { field } => {
write!(f, "field '{field}' contains invalid UTF-8")
}
Self::SelfReference { field } => {
write!(
f,
"field '{field}' contains a self-reference (depends on itself)"
)
}
}
}
}
/// Serialise a slice of validation errors as a pretty-printed JSON string.
///
/// Used to turn `Vec<ValidationError>` into the `Err(String)` value returned by
/// MCP tool handlers.
pub fn format_errors_as_json(errors: &[ValidationError]) -> String {
serde_json::to_string_pretty(errors).unwrap_or_else(|_| format!("{errors:?}"))
}