reinhardt_core/exception/
param_error.rs1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum ParamType {
11 Json,
13 Query,
15 Path,
17 Form,
19 Header,
21 Cookie,
23 Body,
25}
26
27impl std::fmt::Display for ParamType {
28 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29 match self {
30 ParamType::Json => write!(f, "Json"),
31 ParamType::Query => write!(f, "Query"),
32 ParamType::Path => write!(f, "Path"),
33 ParamType::Form => write!(f, "Form"),
34 ParamType::Header => write!(f, "Header"),
35 ParamType::Cookie => write!(f, "Cookie"),
36 ParamType::Body => write!(f, "Body"),
37 }
38 }
39}
40
41#[derive(Debug, Clone)]
43pub struct ParamErrorContext {
44 pub param_type: ParamType,
46 pub field_name: Option<String>,
48 pub message: String,
50 pub source_message: Option<String>,
52 pub raw_value: Option<String>,
54 pub expected_type: Option<String>,
56}
57
58impl ParamErrorContext {
59 pub fn new(param_type: ParamType, message: impl Into<String>) -> Self {
61 Self {
62 param_type,
63 field_name: None,
64 message: message.into(),
65 source_message: None,
66 raw_value: None,
67 expected_type: None,
68 }
69 }
70
71 pub fn with_field(mut self, field: impl Into<String>) -> Self {
73 self.field_name = Some(field.into());
74 self
75 }
76
77 pub fn with_source(mut self, source: Box<dyn std::error::Error + Send + Sync>) -> Self {
79 self.source_message = Some(source.to_string());
80 self
81 }
82
83 pub fn with_raw_value(mut self, value: impl Into<String>) -> Self {
85 let value = value.into();
86 if value.len() > 500 {
90 let truncation_point = value
91 .char_indices()
92 .map(|(idx, _)| idx)
93 .take_while(|&idx| idx <= 500)
94 .last()
95 .unwrap_or(0);
96 self.raw_value = Some(format!("{}...[truncated]", &value[..truncation_point]));
97 } else {
98 self.raw_value = Some(value);
99 }
100 self
101 }
102
103 pub fn with_expected_type<T>(mut self) -> Self {
105 self.expected_type = Some(std::any::type_name::<T>().to_string());
106 self
107 }
108
109 pub fn format_error(&self) -> String {
111 let mut parts = vec![format!("{} parameter extraction failed", self.param_type)];
112
113 if let Some(ref field) = self.field_name {
114 parts.push(format!("field: '{}'", field));
115 }
116
117 parts.push(format!("error: {}", self.message));
118
119 if let Some(ref expected) = self.expected_type {
120 parts.push(format!("expected type: {}", expected));
121 }
122
123 parts.join(", ")
124 }
125
126 pub fn format_multiline(&self, include_raw_value: bool) -> String {
128 let mut lines = vec![
129 format!(" {} parameter extraction failed", self.param_type),
130 format!(" Error: {}", self.message),
131 ];
132
133 if let Some(ref field) = self.field_name {
134 lines.push(format!(" Field: {}", field));
135 }
136
137 if let Some(ref expected) = self.expected_type {
138 lines.push(format!(" Expected type: {}", expected));
139 }
140
141 if include_raw_value && let Some(ref raw) = self.raw_value {
142 lines.push(format!(" Received: {}", raw));
143 }
144
145 lines.join("\n")
146 }
147}
148
149pub fn extract_field_from_serde_error(err: &serde_json::Error) -> Option<String> {
151 let msg = err.to_string();
152
153 if let Some(start) = msg.find("missing field `") {
155 let rest = &msg[start + 15..];
156 if let Some(end) = rest.find('`') {
157 return Some(rest[..end].to_string());
158 }
159 }
160
161 if let Some(start) = msg.find("unknown field `") {
163 let rest = &msg[start + 15..];
164 if let Some(end) = rest.find('`') {
165 return Some(rest[..end].to_string());
166 }
167 }
168
169 if let Some(start) = msg.find("duplicate field `") {
171 let rest = &msg[start + 17..];
172 if let Some(end) = rest.find('`') {
173 return Some(rest[..end].to_string());
174 }
175 }
176
177 None
178}
179
180pub fn extract_field_from_urlencoded_error(err: &serde_urlencoded::de::Error) -> Option<String> {
182 let msg = err.to_string();
183
184 if let Some(start) = msg.find("missing field `") {
186 let rest = &msg[start + 15..];
187 if let Some(end) = rest.find('`') {
188 return Some(rest[..end].to_string());
189 }
190 }
191
192 None
193}
194
195#[cfg(test)]
196mod tests {
197 use rstest::rstest;
198
199 use super::*;
200
201 #[rstest]
202 fn with_raw_value_does_not_panic_on_multibyte_utf8() {
203 let japanese_str: String = "あ".repeat(200);
206 assert!(japanese_str.len() > 500);
207
208 let ctx = ParamErrorContext::new(ParamType::Json, "test").with_raw_value(japanese_str);
210
211 let raw = ctx.raw_value.unwrap();
213 assert!(raw.ends_with("...[truncated]"));
214 }
215
216 #[rstest]
217 fn with_raw_value_does_not_panic_on_emoji() {
218 let emoji_str: String = "\u{1F600}".repeat(150);
220 assert!(emoji_str.len() > 500);
221
222 let ctx = ParamErrorContext::new(ParamType::Query, "test").with_raw_value(emoji_str);
224
225 let raw = ctx.raw_value.unwrap();
227 assert!(raw.ends_with("...[truncated]"));
228 assert!(raw.is_char_boundary(0));
230 }
231
232 #[rstest]
233 fn with_raw_value_does_not_truncate_short_strings() {
234 let short = "hello world";
236
237 let ctx = ParamErrorContext::new(ParamType::Path, "test").with_raw_value(short);
239
240 assert_eq!(ctx.raw_value.unwrap(), "hello world");
242 }
243
244 #[rstest]
245 fn with_raw_value_handles_mixed_multibyte_ascii() {
246 let mixed: String = "a".repeat(498) + "ああ"; assert!(mixed.len() > 500);
249
250 let ctx = ParamErrorContext::new(ParamType::Form, "test").with_raw_value(mixed);
252
253 let raw = ctx.raw_value.unwrap();
255 assert!(raw.ends_with("...[truncated]"));
256 }
257
258 #[rstest]
259 fn with_raw_value_preserves_exactly_500_byte_string() {
260 let exact = "x".repeat(500);
262 assert_eq!(exact.len(), 500);
263
264 let ctx = ParamErrorContext::new(ParamType::Header, "test").with_raw_value(exact.clone());
266
267 assert_eq!(ctx.raw_value.unwrap(), exact);
269 }
270}