128 lines
4.3 KiB
Rust
128 lines
4.3 KiB
Rust
//! 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:?}"))
|
|
}
|