#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParamType {
Json,
Query,
Path,
Form,
Header,
Cookie,
Body,
}
impl std::fmt::Display for ParamType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ParamType::Json => write!(f, "Json"),
ParamType::Query => write!(f, "Query"),
ParamType::Path => write!(f, "Path"),
ParamType::Form => write!(f, "Form"),
ParamType::Header => write!(f, "Header"),
ParamType::Cookie => write!(f, "Cookie"),
ParamType::Body => write!(f, "Body"),
}
}
}
#[derive(Debug, Clone)]
pub struct ParamErrorContext {
pub param_type: ParamType,
pub field_name: Option<String>,
pub message: String,
pub source_message: Option<String>,
pub raw_value: Option<String>,
pub expected_type: Option<String>,
}
impl ParamErrorContext {
pub fn new(param_type: ParamType, message: impl Into<String>) -> Self {
Self {
param_type,
field_name: None,
message: message.into(),
source_message: None,
raw_value: None,
expected_type: None,
}
}
pub fn with_field(mut self, field: impl Into<String>) -> Self {
self.field_name = Some(field.into());
self
}
pub fn with_source(mut self, source: Box<dyn std::error::Error + Send + Sync>) -> Self {
self.source_message = Some(source.to_string());
self
}
pub fn with_raw_value(mut self, value: impl Into<String>) -> Self {
let value = value.into();
if value.len() > 500 {
let truncation_point = value
.char_indices()
.map(|(idx, _)| idx)
.take_while(|&idx| idx <= 500)
.last()
.unwrap_or(0);
self.raw_value = Some(format!("{}...[truncated]", &value[..truncation_point]));
} else {
self.raw_value = Some(value);
}
self
}
pub fn with_expected_type<T>(mut self) -> Self {
self.expected_type = Some(std::any::type_name::<T>().to_string());
self
}
pub fn format_error(&self) -> String {
let mut parts = vec![format!("{} parameter extraction failed", self.param_type)];
if let Some(ref field) = self.field_name {
parts.push(format!("field: '{}'", field));
}
parts.push(format!("error: {}", self.message));
if let Some(ref expected) = self.expected_type {
parts.push(format!("expected type: {}", expected));
}
parts.join(", ")
}
pub fn format_multiline(&self, include_raw_value: bool) -> String {
let mut lines = vec![
format!(" {} parameter extraction failed", self.param_type),
format!(" Error: {}", self.message),
];
if let Some(ref field) = self.field_name {
lines.push(format!(" Field: {}", field));
}
if let Some(ref expected) = self.expected_type {
lines.push(format!(" Expected type: {}", expected));
}
if include_raw_value && let Some(ref raw) = self.raw_value {
lines.push(format!(" Received: {}", raw));
}
lines.join("\n")
}
}
pub fn extract_field_from_serde_error(err: &serde_json::Error) -> Option<String> {
let msg = err.to_string();
if let Some(start) = msg.find("missing field `") {
let rest = &msg[start + 15..];
if let Some(end) = rest.find('`') {
return Some(rest[..end].to_string());
}
}
if let Some(start) = msg.find("unknown field `") {
let rest = &msg[start + 15..];
if let Some(end) = rest.find('`') {
return Some(rest[..end].to_string());
}
}
if let Some(start) = msg.find("duplicate field `") {
let rest = &msg[start + 17..];
if let Some(end) = rest.find('`') {
return Some(rest[..end].to_string());
}
}
None
}
pub fn extract_field_from_urlencoded_error(err: &serde_urlencoded::de::Error) -> Option<String> {
let msg = err.to_string();
if let Some(start) = msg.find("missing field `") {
let rest = &msg[start + 15..];
if let Some(end) = rest.find('`') {
return Some(rest[..end].to_string());
}
}
None
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest]
fn with_raw_value_does_not_panic_on_multibyte_utf8() {
let japanese_str: String = "あ".repeat(200);
assert!(japanese_str.len() > 500);
let ctx = ParamErrorContext::new(ParamType::Json, "test").with_raw_value(japanese_str);
let raw = ctx.raw_value.unwrap();
assert!(raw.ends_with("...[truncated]"));
}
#[rstest]
fn with_raw_value_does_not_panic_on_emoji() {
let emoji_str: String = "\u{1F600}".repeat(150);
assert!(emoji_str.len() > 500);
let ctx = ParamErrorContext::new(ParamType::Query, "test").with_raw_value(emoji_str);
let raw = ctx.raw_value.unwrap();
assert!(raw.ends_with("...[truncated]"));
assert!(raw.is_char_boundary(0));
}
#[rstest]
fn with_raw_value_does_not_truncate_short_strings() {
let short = "hello world";
let ctx = ParamErrorContext::new(ParamType::Path, "test").with_raw_value(short);
assert_eq!(ctx.raw_value.unwrap(), "hello world");
}
#[rstest]
fn with_raw_value_handles_mixed_multibyte_ascii() {
let mixed: String = "a".repeat(498) + "ああ"; assert!(mixed.len() > 500);
let ctx = ParamErrorContext::new(ParamType::Form, "test").with_raw_value(mixed);
let raw = ctx.raw_value.unwrap();
assert!(raw.ends_with("...[truncated]"));
}
#[rstest]
fn with_raw_value_preserves_exactly_500_byte_string() {
let exact = "x".repeat(500);
assert_eq!(exact.len(), 500);
let ctx = ParamErrorContext::new(ParamType::Header, "test").with_raw_value(exact.clone());
assert_eq!(ctx.raw_value.unwrap(), exact);
}
}