1use crate::debug_log_module;
7use crate::validation::{ValidationError, ValidationErrorDetail};
8use serde_json::{Value, json};
9use std::collections::HashMap;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum ParameterSource {
14 Query,
15 Path,
16 Header,
17 Cookie,
18}
19
20impl ParameterSource {
21 fn from_str(s: &str) -> Option<Self> {
22 match s {
23 "query" => Some(Self::Query),
24 "path" => Some(Self::Path),
25 "header" => Some(Self::Header),
26 "cookie" => Some(Self::Cookie),
27 _ => None,
28 }
29 }
30}
31
32#[derive(Debug, Clone)]
34struct ParameterDef {
35 name: String,
36 source: ParameterSource,
37 expected_type: Option<String>,
38 format: Option<String>,
39 required: bool,
40}
41
42#[derive(Clone, Debug)]
44pub struct ParameterValidator {
45 schema: Value,
46 parameter_defs: Vec<ParameterDef>,
47}
48
49impl ParameterValidator {
50 pub fn new(schema: Value) -> Result<Self, String> {
55 let parameter_defs = Self::extract_parameter_defs(&schema)?;
56
57 Ok(Self { schema, parameter_defs })
58 }
59
60 fn extract_parameter_defs(schema: &Value) -> Result<Vec<ParameterDef>, String> {
62 let mut defs = Vec::new();
63
64 let properties = schema
65 .get("properties")
66 .and_then(|p| p.as_object())
67 .cloned()
68 .unwrap_or_default();
69
70 let required_list = schema
71 .get("required")
72 .and_then(|r| r.as_array())
73 .map(|arr| arr.iter().filter_map(|v| v.as_str()).collect::<Vec<_>>())
74 .unwrap_or_default();
75
76 for (name, prop) in properties {
77 let source_str = prop.get("source").and_then(|s| s.as_str()).ok_or_else(|| {
78 anyhow::anyhow!("Invalid parameter schema")
79 .context(format!("Parameter '{}' missing required 'source' field", name))
80 .to_string()
81 })?;
82
83 let source = ParameterSource::from_str(source_str).ok_or_else(|| {
84 anyhow::anyhow!("Invalid parameter schema")
85 .context(format!(
86 "Invalid source '{}' for parameter '{}' (expected: query, path, header, or cookie)",
87 source_str, name
88 ))
89 .to_string()
90 })?;
91
92 let expected_type = prop.get("type").and_then(|t| t.as_str()).map(String::from);
93 let format = prop.get("format").and_then(|f| f.as_str()).map(String::from);
94
95 let is_optional = prop.get("optional").and_then(|v| v.as_bool()).unwrap_or(false);
96 let required = required_list.contains(&name.as_str()) && !is_optional;
97
98 defs.push(ParameterDef {
99 name: name.clone(),
100 source,
101 expected_type,
102 format,
103 required,
104 });
105 }
106
107 Ok(defs)
108 }
109
110 pub fn schema(&self) -> &Value {
112 &self.schema
113 }
114
115 pub fn validate_and_extract(
122 &self,
123 query_params: &Value,
124 raw_query_params: &HashMap<String, Vec<String>>,
125 path_params: &HashMap<String, String>,
126 headers: &HashMap<String, String>,
127 cookies: &HashMap<String, String>,
128 ) -> Result<Value, ValidationError> {
129 tracing::debug!(
130 "validate_and_extract called with query_params: {:?}, path_params: {:?}, headers: {} items, cookies: {} items",
131 query_params,
132 path_params,
133 headers.len(),
134 cookies.len()
135 );
136 tracing::debug!("parameter_defs count: {}", self.parameter_defs.len());
137
138 let mut params_map = serde_json::Map::new();
139 let mut errors = Vec::new();
140 let mut raw_values_map: HashMap<String, String> = HashMap::new();
141
142 for param_def in &self.parameter_defs {
143 tracing::debug!(
144 "Processing param: {:?}, source: {:?}, required: {}, expected_type: {:?}",
145 param_def.name,
146 param_def.source,
147 param_def.required,
148 param_def.expected_type
149 );
150
151 if param_def.source == ParameterSource::Query && param_def.expected_type.as_deref() == Some("array") {
152 let query_value = query_params.get(¶m_def.name);
153
154 if param_def.required && query_value.is_none() {
155 errors.push(ValidationErrorDetail {
156 error_type: "missing".to_string(),
157 loc: vec!["query".to_string(), param_def.name.clone()],
158 msg: "Field required".to_string(),
159 input: Value::Null,
160 ctx: None,
161 });
162 continue;
163 }
164
165 if let Some(value) = query_value {
166 let array_value = if value.is_array() {
167 value.clone()
168 } else {
169 Value::Array(vec![value.clone()])
170 };
171 let (item_type, item_format) = self.array_item_type_and_format(¶m_def.name);
172
173 let coerced_items = match array_value.as_array() {
174 Some(items) => {
175 let mut out = Vec::with_capacity(items.len());
176 for item in items {
177 if let Some(text) = item.as_str() {
178 match Self::coerce_value(text, item_type, item_format) {
179 Ok(coerced) => out.push(coerced),
180 Err(e) => {
181 errors.push(ValidationErrorDetail {
182 error_type: match item_type {
183 Some("integer") => "int_parsing".to_string(),
184 Some("number") => "float_parsing".to_string(),
185 Some("boolean") => "bool_parsing".to_string(),
186 Some("string") => match item_format {
187 Some("uuid") => "uuid_parsing".to_string(),
188 Some("date") => "date_parsing".to_string(),
189 Some("date-time") => "datetime_parsing".to_string(),
190 Some("time") => "time_parsing".to_string(),
191 Some("duration") => "duration_parsing".to_string(),
192 _ => "type_error".to_string(),
193 },
194 _ => "type_error".to_string(),
195 },
196 loc: vec!["query".to_string(), param_def.name.clone()],
197 msg: match item_type {
198 Some("integer") => "Input should be a valid integer, unable to parse string as an integer".to_string(),
199 Some("number") => "Input should be a valid number, unable to parse string as a number".to_string(),
200 Some("boolean") => "Input should be a valid boolean, unable to interpret input".to_string(),
201 Some("string") => match item_format {
202 Some("uuid") => format!("Input should be a valid UUID, {}", e),
203 Some("date") => format!("Input should be a valid date, {}", e),
204 Some("date-time") => format!("Input should be a valid datetime, {}", e),
205 Some("time") => format!("Input should be a valid time, {}", e),
206 Some("duration") => format!("Input should be a valid duration, {}", e),
207 _ => e.clone(),
208 },
209 _ => e.clone(),
210 },
211 input: Value::String(text.to_string()),
212 ctx: None,
213 });
214 }
215 }
216 } else {
217 out.push(item.clone());
218 }
219 }
220 out
221 }
222 None => Vec::new(),
223 };
224
225 params_map.insert(param_def.name.clone(), Value::Array(coerced_items));
226 }
227 continue;
228 }
229
230 let raw_value_string = match param_def.source {
231 ParameterSource::Query => raw_query_params
232 .get(¶m_def.name)
233 .and_then(|values| values.first())
234 .map(String::as_str),
235 ParameterSource::Path => path_params.get(¶m_def.name).map(String::as_str),
236 ParameterSource::Header => {
237 let header_name = param_def.name.replace('_', "-").to_lowercase();
238 headers.get(&header_name).map(String::as_str)
239 }
240 ParameterSource::Cookie => cookies.get(¶m_def.name).map(String::as_str),
241 };
242
243 tracing::debug!("raw_value_string for {}: {:?}", param_def.name, raw_value_string);
244
245 if param_def.required && raw_value_string.is_none() {
246 let source_str = match param_def.source {
247 ParameterSource::Query => "query",
248 ParameterSource::Path => "path",
249 ParameterSource::Header => "headers",
250 ParameterSource::Cookie => "cookie",
251 };
252 let param_name_for_error = if param_def.source == ParameterSource::Header {
253 param_def.name.replace('_', "-").to_lowercase()
254 } else {
255 param_def.name.clone()
256 };
257 errors.push(ValidationErrorDetail {
258 error_type: "missing".to_string(),
259 loc: vec![source_str.to_string(), param_name_for_error],
260 msg: "Field required".to_string(),
261 input: Value::Null,
262 ctx: None,
263 });
264 continue;
265 }
266
267 if let Some(value_str) = raw_value_string {
268 tracing::debug!(
269 "Coercing value '{}' to type {:?} with format {:?}",
270 value_str,
271 param_def.expected_type,
272 param_def.format
273 );
274 match Self::coerce_value(
275 value_str,
276 param_def.expected_type.as_deref(),
277 param_def.format.as_deref(),
278 ) {
279 Ok(coerced) => {
280 tracing::debug!("Coerced to: {:?}", coerced);
281 params_map.insert(param_def.name.clone(), coerced);
282 raw_values_map.insert(param_def.name.clone(), value_str.to_string());
283 }
284 Err(e) => {
285 tracing::debug!("Coercion failed: {}", e);
286 let source_str = match param_def.source {
287 ParameterSource::Query => "query",
288 ParameterSource::Path => "path",
289 ParameterSource::Header => "headers",
290 ParameterSource::Cookie => "cookie",
291 };
292 let (error_type, error_msg) =
293 match (param_def.expected_type.as_deref(), param_def.format.as_deref()) {
294 (Some("integer"), _) => (
295 "int_parsing",
296 "Input should be a valid integer, unable to parse string as an integer".to_string(),
297 ),
298 (Some("number"), _) => (
299 "float_parsing",
300 "Input should be a valid number, unable to parse string as a number".to_string(),
301 ),
302 (Some("boolean"), _) => (
303 "bool_parsing",
304 "Input should be a valid boolean, unable to interpret input".to_string(),
305 ),
306 (Some("string"), Some("uuid")) => {
307 ("uuid_parsing", format!("Input should be a valid UUID, {}", e))
308 }
309 (Some("string"), Some("date")) => {
310 ("date_parsing", format!("Input should be a valid date, {}", e))
311 }
312 (Some("string"), Some("date-time")) => {
313 ("datetime_parsing", format!("Input should be a valid datetime, {}", e))
314 }
315 (Some("string"), Some("time")) => {
316 ("time_parsing", format!("Input should be a valid time, {}", e))
317 }
318 (Some("string"), Some("duration")) => {
319 ("duration_parsing", format!("Input should be a valid duration, {}", e))
320 }
321 _ => ("type_error", e.clone()),
322 };
323 let param_name_for_error = if param_def.source == ParameterSource::Header {
324 param_def.name.replace('_', "-").to_lowercase()
325 } else {
326 param_def.name.clone()
327 };
328 errors.push(ValidationErrorDetail {
329 error_type: error_type.to_string(),
330 loc: vec![source_str.to_string(), param_name_for_error],
331 msg: error_msg,
332 input: Value::String(value_str.to_string()),
333 ctx: None,
334 });
335 }
336 }
337 }
338 }
339
340 if !errors.is_empty() {
341 tracing::debug!("Errors during extraction: {:?}", errors);
342 return Err(ValidationError { errors });
343 }
344
345 let params_json = Value::Object(params_map.clone());
346 tracing::debug!("params_json after coercion: {:?}", params_json);
347
348 let validation_schema = self.create_validation_schema();
349 tracing::debug!("validation_schema: {:?}", validation_schema);
350
351 let validator = crate::validation::SchemaValidator::new(validation_schema).map_err(|e| ValidationError {
352 errors: vec![ValidationErrorDetail {
353 error_type: "schema_error".to_string(),
354 loc: vec!["schema".to_string()],
355 msg: e,
356 input: Value::Null,
357 ctx: None,
358 }],
359 })?;
360
361 tracing::debug!("About to validate params_json against schema");
362 tracing::debug!("params_json = {:?}", params_json);
363 tracing::debug!(
364 "params_json pretty = {}",
365 serde_json::to_string_pretty(¶ms_json).unwrap_or_default()
366 );
367 tracing::debug!(
368 "schema = {}",
369 serde_json::to_string_pretty(&self.schema).unwrap_or_default()
370 );
371 match validator.validate(¶ms_json) {
372 Ok(_) => {
373 tracing::debug!("Validation succeeded");
374 Ok(params_json)
375 }
376 Err(mut validation_err) => {
377 tracing::debug!("Validation failed: {:?}", validation_err);
378
379 for error in &mut validation_err.errors {
380 if error.loc.len() >= 2 && error.loc[0] == "body" {
381 let param_name = &error.loc[1];
382 if let Some(param_def) = self.parameter_defs.iter().find(|p| &p.name == param_name) {
383 let source_str = match param_def.source {
384 ParameterSource::Query => "query",
385 ParameterSource::Path => "path",
386 ParameterSource::Header => "headers",
387 ParameterSource::Cookie => "cookie",
388 };
389 error.loc[0] = source_str.to_string();
390
391 if param_def.source == ParameterSource::Header {
392 error.loc[1] = param_def.name.replace('_', "-").to_lowercase();
393 }
394
395 if let Some(raw_value) = raw_values_map.get(¶m_def.name) {
396 error.input = Value::String(raw_value.clone());
397 }
398 }
399 }
400 }
401
402 debug_log_module!(
403 "parameters",
404 "Returning {} validation errors",
405 validation_err.errors.len()
406 );
407 for (i, error) in validation_err.errors.iter().enumerate() {
408 debug_log_module!(
409 "parameters",
410 " Error {}: type={}, loc={:?}, msg={}, input={}, ctx={:?}",
411 i,
412 error.error_type,
413 error.loc,
414 error.msg,
415 error.input,
416 error.ctx
417 );
418 }
419 #[allow(clippy::collapsible_if)]
420 if crate::debug::is_enabled() {
421 if let Ok(json_errors) = serde_json::to_value(&validation_err.errors) {
422 if let Ok(json_str) = serde_json::to_string_pretty(&json_errors) {
423 debug_log_module!("parameters", "Serialized errors:\n{}", json_str);
424 }
425 }
426 }
427
428 Err(validation_err)
429 }
430 }
431 }
432
433 fn array_item_type_and_format(&self, name: &str) -> (Option<&str>, Option<&str>) {
434 let Some(prop) = self
435 .schema
436 .get("properties")
437 .and_then(|value| value.as_object())
438 .and_then(|props| props.get(name))
439 else {
440 return (None, None);
441 };
442
443 let Some(items) = prop.get("items") else {
444 return (None, None);
445 };
446
447 let item_type = items.get("type").and_then(|value| value.as_str());
448 let item_format = items.get("format").and_then(|value| value.as_str());
449 (item_type, item_format)
450 }
451
452 fn coerce_value(value: &str, expected_type: Option<&str>, format: Option<&str>) -> Result<Value, String> {
454 if let Some(fmt) = format {
455 match fmt {
456 "uuid" => {
457 Self::validate_uuid_format(value)?;
458 return Ok(json!(value));
459 }
460 "date" => {
461 Self::validate_date_format(value)?;
462 return Ok(json!(value));
463 }
464 "date-time" => {
465 Self::validate_datetime_format(value)?;
466 return Ok(json!(value));
467 }
468 "time" => {
469 Self::validate_time_format(value)?;
470 return Ok(json!(value));
471 }
472 "duration" => {
473 Self::validate_duration_format(value)?;
474 return Ok(json!(value));
475 }
476 _ => {}
477 }
478 }
479
480 match expected_type {
481 Some("integer") => value
482 .parse::<i64>()
483 .map(|i| json!(i))
484 .map_err(|e| format!("Invalid integer: {}", e)),
485 Some("number") => value
486 .parse::<f64>()
487 .map(|f| json!(f))
488 .map_err(|e| format!("Invalid number: {}", e)),
489 Some("boolean") => {
490 if value.is_empty() {
491 return Ok(json!(false));
492 }
493 let value_lower = value.to_lowercase();
494 if value_lower == "true" || value == "1" {
495 Ok(json!(true))
496 } else if value_lower == "false" || value == "0" {
497 Ok(json!(false))
498 } else {
499 Err(format!("Invalid boolean: {}", value))
500 }
501 }
502 _ => Ok(json!(value)),
503 }
504 }
505
506 fn validate_date_format(value: &str) -> Result<(), String> {
508 jiff::civil::Date::strptime("%Y-%m-%d", value)
509 .map(|_| ())
510 .map_err(|e| format!("Invalid date format: {}", e))
511 }
512
513 fn validate_datetime_format(value: &str) -> Result<(), String> {
515 use std::str::FromStr;
516 jiff::Timestamp::from_str(value)
517 .map(|_| ())
518 .map_err(|e| format!("Invalid datetime format: {}", e))
519 }
520
521 fn validate_time_format(value: &str) -> Result<(), String> {
523 let (time_part, offset_part) = if let Some(stripped) = value.strip_suffix('Z') {
524 (stripped, "Z")
525 } else {
526 let plus = value.rfind('+');
527 let minus = value.rfind('-');
528 let split_at = match (plus, minus) {
529 (Some(p), Some(m)) => Some(std::cmp::max(p, m)),
530 (Some(p), None) => Some(p),
531 (None, Some(m)) => Some(m),
532 (None, None) => None,
533 }
534 .ok_or_else(|| "Invalid time format: missing timezone offset".to_string())?;
535
536 if split_at < 8 {
537 return Err("Invalid time format: timezone offset position is invalid".to_string());
538 }
539
540 (&value[..split_at], &value[split_at..])
541 };
542
543 let base_time = time_part.split('.').next().unwrap_or(time_part);
544 jiff::civil::Time::strptime("%H:%M:%S", base_time).map_err(|e| format!("Invalid time format: {}", e))?;
545
546 if let Some((_, frac)) = time_part.split_once('.')
547 && (frac.is_empty() || frac.len() > 9 || !frac.chars().all(|c| c.is_ascii_digit()))
548 {
549 return Err("Invalid time format: fractional seconds must be 1-9 digits".to_string());
550 }
551
552 if offset_part != "Z" {
553 let sign = offset_part
554 .chars()
555 .next()
556 .ok_or_else(|| "Invalid time format: empty timezone offset".to_string())?;
557 if sign != '+' && sign != '-' {
558 return Err("Invalid time format: timezone offset must start with + or -".to_string());
559 }
560
561 let rest = &offset_part[1..];
562 let (hours_str, minutes_str) = rest
563 .split_once(':')
564 .ok_or_else(|| "Invalid time format: timezone offset must be ±HH:MM".to_string())?;
565 let hours: u8 = hours_str
566 .parse()
567 .map_err(|_| "Invalid time format: invalid timezone hours".to_string())?;
568 let minutes: u8 = minutes_str
569 .parse()
570 .map_err(|_| "Invalid time format: invalid timezone minutes".to_string())?;
571 if hours > 23 || minutes > 59 {
572 return Err("Invalid time format: timezone offset out of range".to_string());
573 }
574 }
575
576 Ok(())
577 }
578
579 fn validate_duration_format(value: &str) -> Result<(), String> {
581 use std::str::FromStr;
582 jiff::Span::from_str(value)
583 .map(|_| ())
584 .map_err(|e| format!("Invalid duration format: {}", e))
585 }
586
587 fn validate_uuid_format(value: &str) -> Result<(), String> {
589 use std::str::FromStr;
590 uuid::Uuid::from_str(value)
591 .map(|_| ())
592 .map_err(|_e| format!("invalid character: expected an optional prefix of `urn:uuid:` followed by [0-9a-fA-F-], found `{}` at {}",
593 value.chars().next().unwrap_or('?'),
594 value.chars().position(|c| !c.is_ascii_hexdigit() && c != '-').unwrap_or(0)))
595 }
596
597 fn create_validation_schema(&self) -> Value {
600 let mut schema = self.schema.clone();
601
602 if let Some(properties) = schema.get_mut("properties").and_then(|p| p.as_object_mut()) {
603 for (_name, prop) in properties.iter_mut() {
604 if let Some(obj) = prop.as_object_mut() {
605 obj.remove("source");
606 }
607 }
608 }
609
610 schema
611 }
612}
613
614#[cfg(test)]
615mod tests {
616 use super::*;
617 use serde_json::json;
618
619 #[test]
620 fn test_array_query_parameter() {
621 let schema = json!({
622 "type": "object",
623 "properties": {
624 "device_ids": {
625 "type": "array",
626 "items": {"type": "integer"},
627 "source": "query"
628 }
629 },
630 "required": []
631 });
632
633 let validator = ParameterValidator::new(schema).unwrap();
634
635 let query_params = json!({
636 "device_ids": [1, 2]
637 });
638 let raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
639 let path_params = HashMap::new();
640
641 let result = validator.validate_and_extract(
642 &query_params,
643 &raw_query_params,
644 &path_params,
645 &HashMap::new(),
646 &HashMap::new(),
647 );
648 assert!(
649 result.is_ok(),
650 "Array query param validation failed: {:?}",
651 result.err()
652 );
653
654 let extracted = result.unwrap();
655 assert_eq!(extracted["device_ids"], json!([1, 2]));
656 }
657
658 #[test]
659 fn test_path_parameter_extraction() {
660 let schema = json!({
661 "type": "object",
662 "properties": {
663 "item_id": {
664 "type": "string",
665 "source": "path"
666 }
667 },
668 "required": ["item_id"]
669 });
670
671 let validator = ParameterValidator::new(schema).expect("Failed to create validator");
672
673 let mut path_params = HashMap::new();
674 path_params.insert("item_id".to_string(), "foobar".to_string());
675 let query_params = json!({});
676 let raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
677
678 let result = validator.validate_and_extract(
679 &query_params,
680 &raw_query_params,
681 &path_params,
682 &HashMap::new(),
683 &HashMap::new(),
684 );
685 assert!(result.is_ok(), "Validation should succeed: {:?}", result);
686
687 let params = result.unwrap();
688 assert_eq!(params, json!({"item_id": "foobar"}));
689 }
690
691 #[test]
692 fn test_boolean_path_parameter_coercion() {
693 let schema = json!({
694 "type": "object",
695 "properties": {
696 "value": {
697 "type": "boolean",
698 "source": "path"
699 }
700 },
701 "required": ["value"]
702 });
703
704 let validator = ParameterValidator::new(schema).expect("Failed to create validator");
705
706 let mut path_params = HashMap::new();
707 path_params.insert("value".to_string(), "True".to_string());
708 let query_params = json!({});
709 let raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
710
711 let result = validator.validate_and_extract(
712 &query_params,
713 &raw_query_params,
714 &path_params,
715 &HashMap::new(),
716 &HashMap::new(),
717 );
718 if result.is_err() {
719 eprintln!("Error for 'True': {:?}", result);
720 }
721 assert!(result.is_ok(), "Validation should succeed for 'True': {:?}", result);
722 let params = result.unwrap();
723 assert_eq!(params, json!({"value": true}));
724
725 path_params.insert("value".to_string(), "1".to_string());
726 let query_params_1 = json!({});
727 let result = validator.validate_and_extract(
728 &query_params_1,
729 &raw_query_params,
730 &path_params,
731 &HashMap::new(),
732 &HashMap::new(),
733 );
734 assert!(result.is_ok(), "Validation should succeed for '1': {:?}", result);
735 let params = result.unwrap();
736 assert_eq!(params, json!({"value": true}));
737
738 path_params.insert("value".to_string(), "false".to_string());
739 let query_params_false = json!({});
740 let result = validator.validate_and_extract(
741 &query_params_false,
742 &raw_query_params,
743 &path_params,
744 &HashMap::new(),
745 &HashMap::new(),
746 );
747 assert!(result.is_ok(), "Validation should succeed for 'false': {:?}", result);
748 let params = result.unwrap();
749 assert_eq!(params, json!({"value": false}));
750
751 path_params.insert("value".to_string(), "TRUE".to_string());
752 let query_params_true = json!({});
753 let result = validator.validate_and_extract(
754 &query_params_true,
755 &raw_query_params,
756 &path_params,
757 &HashMap::new(),
758 &HashMap::new(),
759 );
760 assert!(result.is_ok(), "Validation should succeed for 'TRUE': {:?}", result);
761 let params = result.unwrap();
762 assert_eq!(params, json!({"value": true}));
763 }
764
765 #[test]
766 fn test_boolean_query_parameter_coercion() {
767 let schema = json!({
768 "type": "object",
769 "properties": {
770 "flag": {
771 "type": "boolean",
772 "source": "query"
773 }
774 },
775 "required": ["flag"]
776 });
777
778 let validator = ParameterValidator::new(schema).expect("Failed to create validator");
779 let path_params = HashMap::new();
780
781 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
782 raw_query_params.insert("flag".to_string(), vec!["1".to_string()]);
783 let query_params = json!({"flag": 1});
784 let result = validator.validate_and_extract(
785 &query_params,
786 &raw_query_params,
787 &path_params,
788 &HashMap::new(),
789 &HashMap::new(),
790 );
791 assert!(result.is_ok(), "Validation should succeed for integer 1: {:?}", result);
792 let params = result.unwrap();
793 assert_eq!(params, json!({"flag": true}));
794
795 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
796 raw_query_params.insert("flag".to_string(), vec!["0".to_string()]);
797 let query_params = json!({"flag": 0});
798 let result = validator.validate_and_extract(
799 &query_params,
800 &raw_query_params,
801 &path_params,
802 &HashMap::new(),
803 &HashMap::new(),
804 );
805 assert!(result.is_ok(), "Validation should succeed for integer 0: {:?}", result);
806 let params = result.unwrap();
807 assert_eq!(params, json!({"flag": false}));
808
809 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
810 raw_query_params.insert("flag".to_string(), vec!["true".to_string()]);
811 let query_params = json!({"flag": true});
812 let result = validator.validate_and_extract(
813 &query_params,
814 &raw_query_params,
815 &path_params,
816 &HashMap::new(),
817 &HashMap::new(),
818 );
819 assert!(
820 result.is_ok(),
821 "Validation should succeed for boolean true: {:?}",
822 result
823 );
824 let params = result.unwrap();
825 assert_eq!(params, json!({"flag": true}));
826
827 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
828 raw_query_params.insert("flag".to_string(), vec!["false".to_string()]);
829 let query_params = json!({"flag": false});
830 let result = validator.validate_and_extract(
831 &query_params,
832 &raw_query_params,
833 &path_params,
834 &HashMap::new(),
835 &HashMap::new(),
836 );
837 assert!(
838 result.is_ok(),
839 "Validation should succeed for boolean false: {:?}",
840 result
841 );
842 let params = result.unwrap();
843 assert_eq!(params, json!({"flag": false}));
844 }
845
846 #[test]
847 fn test_integer_coercion_invalid_format_returns_error() {
848 let schema = json!({
849 "type": "object",
850 "properties": {
851 "count": {
852 "type": "integer",
853 "source": "query"
854 }
855 },
856 "required": ["count"]
857 });
858
859 let validator = ParameterValidator::new(schema).unwrap();
860 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
861 raw_query_params.insert("count".to_string(), vec!["not_a_number".to_string()]);
862
863 let result = validator.validate_and_extract(
864 &json!({"count": "not_a_number"}),
865 &raw_query_params,
866 &HashMap::new(),
867 &HashMap::new(),
868 &HashMap::new(),
869 );
870
871 assert!(result.is_err(), "Should fail to coerce non-integer string");
872 let err = result.unwrap_err();
873 assert_eq!(err.errors.len(), 1);
874 assert_eq!(err.errors[0].error_type, "int_parsing");
875 assert_eq!(err.errors[0].loc, vec!["query".to_string(), "count".to_string()]);
876 assert!(err.errors[0].msg.contains("valid integer"));
877 }
878
879 #[test]
880 fn test_integer_coercion_with_letters_mixed_returns_error() {
881 let schema = json!({
882 "type": "object",
883 "properties": {
884 "id": {
885 "type": "integer",
886 "source": "path"
887 }
888 },
889 "required": ["id"]
890 });
891
892 let validator = ParameterValidator::new(schema).unwrap();
893 let mut path_params = HashMap::new();
894 path_params.insert("id".to_string(), "123abc".to_string());
895
896 let result = validator.validate_and_extract(
897 &json!({}),
898 &HashMap::new(),
899 &path_params,
900 &HashMap::new(),
901 &HashMap::new(),
902 );
903
904 assert!(result.is_err());
905 let err = result.unwrap_err();
906 assert_eq!(err.errors[0].error_type, "int_parsing");
907 }
908
909 #[test]
910 fn test_integer_coercion_overflow_returns_error() {
911 let schema = json!({
912 "type": "object",
913 "properties": {
914 "big_num": {
915 "type": "integer",
916 "source": "query"
917 }
918 },
919 "required": ["big_num"]
920 });
921
922 let validator = ParameterValidator::new(schema).unwrap();
923 let too_large = "9223372036854775808";
924 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
925 raw_query_params.insert("big_num".to_string(), vec![too_large.to_string()]);
926
927 let result = validator.validate_and_extract(
928 &json!({"big_num": too_large}),
929 &raw_query_params,
930 &HashMap::new(),
931 &HashMap::new(),
932 &HashMap::new(),
933 );
934
935 assert!(result.is_err(), "Should fail on integer overflow");
936 let err = result.unwrap_err();
937 assert_eq!(err.errors[0].error_type, "int_parsing");
938 }
939
940 #[test]
941 fn test_integer_coercion_negative_overflow_returns_error() {
942 let schema = json!({
943 "type": "object",
944 "properties": {
945 "small_num": {
946 "type": "integer",
947 "source": "query"
948 }
949 },
950 "required": ["small_num"]
951 });
952
953 let validator = ParameterValidator::new(schema).unwrap();
954 let too_small = "-9223372036854775809";
955 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
956 raw_query_params.insert("small_num".to_string(), vec![too_small.to_string()]);
957
958 let result = validator.validate_and_extract(
959 &json!({"small_num": too_small}),
960 &raw_query_params,
961 &HashMap::new(),
962 &HashMap::new(),
963 &HashMap::new(),
964 );
965
966 assert!(result.is_err());
967 let err = result.unwrap_err();
968 assert_eq!(err.errors[0].error_type, "int_parsing");
969 }
970
971 #[test]
972 fn test_float_coercion_invalid_format_returns_error() {
973 let schema = json!({
974 "type": "object",
975 "properties": {
976 "price": {
977 "type": "number",
978 "source": "query"
979 }
980 },
981 "required": ["price"]
982 });
983
984 let validator = ParameterValidator::new(schema).unwrap();
985 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
986 raw_query_params.insert("price".to_string(), vec!["not.a.number".to_string()]);
987
988 let result = validator.validate_and_extract(
989 &json!({"price": "not.a.number"}),
990 &raw_query_params,
991 &HashMap::new(),
992 &HashMap::new(),
993 &HashMap::new(),
994 );
995
996 assert!(result.is_err());
997 let err = result.unwrap_err();
998 assert_eq!(err.errors[0].error_type, "float_parsing");
999 assert!(err.errors[0].msg.contains("valid number"));
1000 }
1001
1002 #[test]
1003 fn test_float_coercion_scientific_notation_success() {
1004 let schema = json!({
1005 "type": "object",
1006 "properties": {
1007 "value": {
1008 "type": "number",
1009 "source": "query"
1010 }
1011 },
1012 "required": ["value"]
1013 });
1014
1015 let validator = ParameterValidator::new(schema).unwrap();
1016 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1017 raw_query_params.insert("value".to_string(), vec!["1.5e10".to_string()]);
1018
1019 let result = validator.validate_and_extract(
1020 &json!({"value": 1.5e10}),
1021 &raw_query_params,
1022 &HashMap::new(),
1023 &HashMap::new(),
1024 &HashMap::new(),
1025 );
1026
1027 assert!(result.is_ok());
1028 let extracted = result.unwrap();
1029 assert_eq!(extracted["value"], json!(1.5e10));
1030 }
1031
1032 #[test]
1033 fn test_boolean_coercion_empty_string_returns_false() {
1034 let schema = json!({
1036 "type": "object",
1037 "properties": {
1038 "flag": {
1039 "type": "boolean",
1040 "source": "query"
1041 }
1042 },
1043 "required": ["flag"]
1044 });
1045
1046 let validator = ParameterValidator::new(schema).unwrap();
1047 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1048 raw_query_params.insert("flag".to_string(), vec!["".to_string()]);
1049
1050 let result = validator.validate_and_extract(
1051 &json!({"flag": ""}),
1052 &raw_query_params,
1053 &HashMap::new(),
1054 &HashMap::new(),
1055 &HashMap::new(),
1056 );
1057
1058 assert!(result.is_ok());
1059 let extracted = result.unwrap();
1060 assert_eq!(extracted["flag"], json!(false));
1061 }
1062
1063 #[test]
1064 fn test_boolean_coercion_whitespace_string_returns_error() {
1065 let schema = json!({
1066 "type": "object",
1067 "properties": {
1068 "flag": {
1069 "type": "boolean",
1070 "source": "query"
1071 }
1072 },
1073 "required": ["flag"]
1074 });
1075
1076 let validator = ParameterValidator::new(schema).unwrap();
1077 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1078 raw_query_params.insert("flag".to_string(), vec![" ".to_string()]);
1079
1080 let result = validator.validate_and_extract(
1081 &json!({"flag": " "}),
1082 &raw_query_params,
1083 &HashMap::new(),
1084 &HashMap::new(),
1085 &HashMap::new(),
1086 );
1087
1088 assert!(result.is_err(), "Whitespace-only string should fail boolean parsing");
1089 let err = result.unwrap_err();
1090 assert_eq!(err.errors[0].error_type, "bool_parsing");
1091 }
1092
1093 #[test]
1094 fn test_boolean_coercion_invalid_value_returns_error() {
1095 let schema = json!({
1096 "type": "object",
1097 "properties": {
1098 "enabled": {
1099 "type": "boolean",
1100 "source": "path"
1101 }
1102 },
1103 "required": ["enabled"]
1104 });
1105
1106 let validator = ParameterValidator::new(schema).unwrap();
1107 let mut path_params = HashMap::new();
1108 path_params.insert("enabled".to_string(), "maybe".to_string());
1109
1110 let result = validator.validate_and_extract(
1111 &json!({}),
1112 &HashMap::new(),
1113 &path_params,
1114 &HashMap::new(),
1115 &HashMap::new(),
1116 );
1117
1118 assert!(result.is_err());
1119 let err = result.unwrap_err();
1120 assert_eq!(err.errors[0].error_type, "bool_parsing");
1121 assert!(err.errors[0].msg.contains("valid boolean"));
1122 }
1123
1124 #[test]
1125 fn test_required_query_parameter_missing_returns_error() {
1126 let schema = json!({
1127 "type": "object",
1128 "properties": {
1129 "required_param": {
1130 "type": "string",
1131 "source": "query"
1132 }
1133 },
1134 "required": ["required_param"]
1135 });
1136
1137 let validator = ParameterValidator::new(schema).unwrap();
1138
1139 let result = validator.validate_and_extract(
1140 &json!({}),
1141 &HashMap::new(),
1142 &HashMap::new(),
1143 &HashMap::new(),
1144 &HashMap::new(),
1145 );
1146
1147 assert!(result.is_err());
1148 let err = result.unwrap_err();
1149 assert_eq!(err.errors[0].error_type, "missing");
1150 assert_eq!(
1151 err.errors[0].loc,
1152 vec!["query".to_string(), "required_param".to_string()]
1153 );
1154 assert!(err.errors[0].msg.contains("required"));
1155 }
1156
1157 #[test]
1158 fn test_required_path_parameter_missing_returns_error() {
1159 let schema = json!({
1160 "type": "object",
1161 "properties": {
1162 "user_id": {
1163 "type": "string",
1164 "source": "path"
1165 }
1166 },
1167 "required": ["user_id"]
1168 });
1169
1170 let validator = ParameterValidator::new(schema).unwrap();
1171
1172 let result = validator.validate_and_extract(
1173 &json!({}),
1174 &HashMap::new(),
1175 &HashMap::new(),
1176 &HashMap::new(),
1177 &HashMap::new(),
1178 );
1179
1180 assert!(result.is_err());
1181 let err = result.unwrap_err();
1182 assert_eq!(err.errors[0].error_type, "missing");
1183 assert_eq!(err.errors[0].loc, vec!["path".to_string(), "user_id".to_string()]);
1184 }
1185
1186 #[test]
1187 fn test_required_header_parameter_missing_returns_error() {
1188 let schema = json!({
1189 "type": "object",
1190 "properties": {
1191 "Authorization": {
1192 "type": "string",
1193 "source": "header"
1194 }
1195 },
1196 "required": ["Authorization"]
1197 });
1198
1199 let validator = ParameterValidator::new(schema).unwrap();
1200
1201 let result = validator.validate_and_extract(
1202 &json!({}),
1203 &HashMap::new(),
1204 &HashMap::new(),
1205 &HashMap::new(),
1206 &HashMap::new(),
1207 );
1208
1209 assert!(result.is_err());
1210 let err = result.unwrap_err();
1211 assert_eq!(err.errors[0].error_type, "missing");
1212 assert_eq!(
1213 err.errors[0].loc,
1214 vec!["headers".to_string(), "authorization".to_string()]
1215 );
1216 }
1217
1218 #[test]
1219 fn test_required_cookie_parameter_missing_returns_error() {
1220 let schema = json!({
1221 "type": "object",
1222 "properties": {
1223 "session_id": {
1224 "type": "string",
1225 "source": "cookie"
1226 }
1227 },
1228 "required": ["session_id"]
1229 });
1230
1231 let validator = ParameterValidator::new(schema).unwrap();
1232
1233 let result = validator.validate_and_extract(
1234 &json!({}),
1235 &HashMap::new(),
1236 &HashMap::new(),
1237 &HashMap::new(),
1238 &HashMap::new(),
1239 );
1240
1241 assert!(result.is_err());
1242 let err = result.unwrap_err();
1243 assert_eq!(err.errors[0].error_type, "missing");
1244 assert_eq!(err.errors[0].loc, vec!["cookie".to_string(), "session_id".to_string()]);
1245 }
1246
1247 #[test]
1248 fn test_optional_parameter_missing_succeeds() {
1249 let schema = json!({
1250 "type": "object",
1251 "properties": {
1252 "optional_param": {
1253 "type": "string",
1254 "source": "query",
1255 "optional": true
1256 }
1257 },
1258 "required": []
1259 });
1260
1261 let validator = ParameterValidator::new(schema).unwrap();
1262
1263 let result = validator.validate_and_extract(
1264 &json!({}),
1265 &HashMap::new(),
1266 &HashMap::new(),
1267 &HashMap::new(),
1268 &HashMap::new(),
1269 );
1270
1271 assert!(result.is_ok(), "Optional parameter should not cause error when missing");
1272 let extracted = result.unwrap();
1273 assert!(!extracted.as_object().unwrap().contains_key("optional_param"));
1274 }
1275
1276 #[test]
1277 fn test_uuid_validation_invalid_format_returns_error() {
1278 let schema = json!({
1279 "type": "object",
1280 "properties": {
1281 "id": {
1282 "type": "string",
1283 "format": "uuid",
1284 "source": "path"
1285 }
1286 },
1287 "required": ["id"]
1288 });
1289
1290 let validator = ParameterValidator::new(schema).unwrap();
1291 let mut path_params = HashMap::new();
1292 path_params.insert("id".to_string(), "not-a-uuid".to_string());
1293
1294 let result = validator.validate_and_extract(
1295 &json!({}),
1296 &HashMap::new(),
1297 &path_params,
1298 &HashMap::new(),
1299 &HashMap::new(),
1300 );
1301
1302 assert!(result.is_err());
1303 let err = result.unwrap_err();
1304 assert_eq!(err.errors[0].error_type, "uuid_parsing");
1305 assert!(err.errors[0].msg.contains("UUID"));
1306 }
1307
1308 #[test]
1309 fn test_uuid_validation_uppercase_succeeds() {
1310 let schema = json!({
1311 "type": "object",
1312 "properties": {
1313 "id": {
1314 "type": "string",
1315 "format": "uuid",
1316 "source": "query"
1317 }
1318 },
1319 "required": ["id"]
1320 });
1321
1322 let validator = ParameterValidator::new(schema).unwrap();
1323 let valid_uuid = "550e8400-e29b-41d4-a716-446655440000";
1324 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1325 raw_query_params.insert("id".to_string(), vec![valid_uuid.to_string()]);
1326
1327 let result = validator.validate_and_extract(
1328 &json!({"id": valid_uuid}),
1329 &raw_query_params,
1330 &HashMap::new(),
1331 &HashMap::new(),
1332 &HashMap::new(),
1333 );
1334
1335 assert!(result.is_ok());
1336 let extracted = result.unwrap();
1337 assert_eq!(extracted["id"], json!(valid_uuid));
1338 }
1339
1340 #[test]
1341 fn test_date_validation_invalid_format_returns_error() {
1342 let schema = json!({
1343 "type": "object",
1344 "properties": {
1345 "created_at": {
1346 "type": "string",
1347 "format": "date",
1348 "source": "query"
1349 }
1350 },
1351 "required": ["created_at"]
1352 });
1353
1354 let validator = ParameterValidator::new(schema).unwrap();
1355 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1356 raw_query_params.insert("created_at".to_string(), vec!["2024/12/10".to_string()]);
1357
1358 let result = validator.validate_and_extract(
1359 &json!({"created_at": "2024/12/10"}),
1360 &raw_query_params,
1361 &HashMap::new(),
1362 &HashMap::new(),
1363 &HashMap::new(),
1364 );
1365
1366 assert!(result.is_err());
1367 let err = result.unwrap_err();
1368 assert_eq!(err.errors[0].error_type, "date_parsing");
1369 assert!(err.errors[0].msg.contains("date"));
1370 }
1371
1372 #[test]
1373 fn test_date_validation_valid_iso_succeeds() {
1374 let schema = json!({
1375 "type": "object",
1376 "properties": {
1377 "created_at": {
1378 "type": "string",
1379 "format": "date",
1380 "source": "query"
1381 }
1382 },
1383 "required": ["created_at"]
1384 });
1385
1386 let validator = ParameterValidator::new(schema).unwrap();
1387 let valid_date = "2024-12-10";
1388 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1389 raw_query_params.insert("created_at".to_string(), vec![valid_date.to_string()]);
1390
1391 let result = validator.validate_and_extract(
1392 &json!({"created_at": valid_date}),
1393 &raw_query_params,
1394 &HashMap::new(),
1395 &HashMap::new(),
1396 &HashMap::new(),
1397 );
1398
1399 assert!(result.is_ok());
1400 let extracted = result.unwrap();
1401 assert_eq!(extracted["created_at"], json!(valid_date));
1402 }
1403
1404 #[test]
1405 fn test_datetime_validation_invalid_format_returns_error() {
1406 let schema = json!({
1407 "type": "object",
1408 "properties": {
1409 "timestamp": {
1410 "type": "string",
1411 "format": "date-time",
1412 "source": "query"
1413 }
1414 },
1415 "required": ["timestamp"]
1416 });
1417
1418 let validator = ParameterValidator::new(schema).unwrap();
1419 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1420 raw_query_params.insert("timestamp".to_string(), vec!["not-a-datetime".to_string()]);
1421
1422 let result = validator.validate_and_extract(
1423 &json!({"timestamp": "not-a-datetime"}),
1424 &raw_query_params,
1425 &HashMap::new(),
1426 &HashMap::new(),
1427 &HashMap::new(),
1428 );
1429
1430 assert!(result.is_err());
1431 let err = result.unwrap_err();
1432 assert_eq!(err.errors[0].error_type, "datetime_parsing");
1433 }
1434
1435 #[test]
1436 fn test_time_validation_invalid_format_returns_error() {
1437 let schema = json!({
1438 "type": "object",
1439 "properties": {
1440 "start_time": {
1441 "type": "string",
1442 "format": "time",
1443 "source": "query"
1444 }
1445 },
1446 "required": ["start_time"]
1447 });
1448
1449 let validator = ParameterValidator::new(schema).unwrap();
1450 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1451 raw_query_params.insert("start_time".to_string(), vec!["25:00:00".to_string()]);
1452
1453 let result = validator.validate_and_extract(
1454 &json!({"start_time": "25:00:00"}),
1455 &raw_query_params,
1456 &HashMap::new(),
1457 &HashMap::new(),
1458 &HashMap::new(),
1459 );
1460
1461 assert!(result.is_err());
1462 let err = result.unwrap_err();
1463 assert_eq!(err.errors[0].error_type, "time_parsing");
1464 }
1465
1466 #[test]
1467 fn test_time_validation_string_passthrough() {
1468 let schema = json!({
1469 "type": "object",
1470 "properties": {
1471 "start_time": {
1472 "type": "string",
1473 "source": "query"
1474 }
1475 },
1476 "required": ["start_time"]
1477 });
1478
1479 let validator = ParameterValidator::new(schema).unwrap();
1480 let time_string = "14:30:00";
1481 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1482 raw_query_params.insert("start_time".to_string(), vec![time_string.to_string()]);
1483
1484 let result = validator.validate_and_extract(
1485 &json!({"start_time": time_string}),
1486 &raw_query_params,
1487 &HashMap::new(),
1488 &HashMap::new(),
1489 &HashMap::new(),
1490 );
1491
1492 assert!(result.is_ok(), "String parameter should pass: {:?}", result);
1493 let extracted = result.unwrap();
1494 assert_eq!(extracted["start_time"], json!(time_string));
1495 }
1496
1497 #[test]
1498 fn test_duration_validation_invalid_format_returns_error() {
1499 let schema = json!({
1500 "type": "object",
1501 "properties": {
1502 "timeout": {
1503 "type": "string",
1504 "format": "duration",
1505 "source": "query"
1506 }
1507 },
1508 "required": ["timeout"]
1509 });
1510
1511 let validator = ParameterValidator::new(schema).unwrap();
1512 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1513 raw_query_params.insert("timeout".to_string(), vec!["not-a-duration".to_string()]);
1514
1515 let result = validator.validate_and_extract(
1516 &json!({"timeout": "not-a-duration"}),
1517 &raw_query_params,
1518 &HashMap::new(),
1519 &HashMap::new(),
1520 &HashMap::new(),
1521 );
1522
1523 assert!(result.is_err());
1524 let err = result.unwrap_err();
1525 assert_eq!(err.errors[0].error_type, "duration_parsing");
1526 }
1527
1528 #[test]
1529 fn test_duration_validation_iso8601_succeeds() {
1530 let schema = json!({
1531 "type": "object",
1532 "properties": {
1533 "timeout": {
1534 "type": "string",
1535 "format": "duration",
1536 "source": "query"
1537 }
1538 },
1539 "required": ["timeout"]
1540 });
1541
1542 let validator = ParameterValidator::new(schema).unwrap();
1543 let valid_duration = "PT5M";
1544 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1545 raw_query_params.insert("timeout".to_string(), vec![valid_duration.to_string()]);
1546
1547 let result = validator.validate_and_extract(
1548 &json!({"timeout": valid_duration}),
1549 &raw_query_params,
1550 &HashMap::new(),
1551 &HashMap::new(),
1552 &HashMap::new(),
1553 );
1554
1555 assert!(result.is_ok());
1556 }
1557
1558 #[test]
1559 fn test_header_name_normalization_with_underscores() {
1560 let schema = json!({
1561 "type": "object",
1562 "properties": {
1563 "X_Custom_Header": {
1564 "type": "string",
1565 "source": "header"
1566 }
1567 },
1568 "required": ["X_Custom_Header"]
1569 });
1570
1571 let validator = ParameterValidator::new(schema).unwrap();
1572 let mut headers = HashMap::new();
1573 headers.insert("x-custom-header".to_string(), "value".to_string());
1574
1575 let result =
1576 validator.validate_and_extract(&json!({}), &HashMap::new(), &HashMap::new(), &headers, &HashMap::new());
1577
1578 assert!(result.is_ok());
1579 let extracted = result.unwrap();
1580 assert_eq!(extracted["X_Custom_Header"], json!("value"));
1581 }
1582
1583 #[test]
1584 fn test_multiple_query_parameter_values_uses_first() {
1585 let schema = json!({
1586 "type": "object",
1587 "properties": {
1588 "id": {
1589 "type": "integer",
1590 "source": "query"
1591 }
1592 },
1593 "required": ["id"]
1594 });
1595
1596 let validator = ParameterValidator::new(schema).unwrap();
1597 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1598 raw_query_params.insert("id".to_string(), vec!["123".to_string(), "456".to_string()]);
1599
1600 let result = validator.validate_and_extract(
1601 &json!({"id": [123, 456]}),
1602 &raw_query_params,
1603 &HashMap::new(),
1604 &HashMap::new(),
1605 &HashMap::new(),
1606 );
1607
1608 assert!(result.is_ok(), "Should accept first value of multiple query params");
1609 let extracted = result.unwrap();
1610 assert_eq!(extracted["id"], json!(123));
1611 }
1612
1613 #[test]
1614 fn test_schema_creation_missing_source_field_returns_error() {
1615 let schema = json!({
1616 "type": "object",
1617 "properties": {
1618 "param": {
1619 "type": "string"
1620 }
1621 },
1622 "required": []
1623 });
1624
1625 let result = ParameterValidator::new(schema);
1626 assert!(result.is_err(), "Schema without 'source' field should fail");
1627 let err_msg = result.unwrap_err();
1628 assert!(err_msg.contains("source"));
1629 }
1630
1631 #[test]
1632 fn test_schema_creation_invalid_source_value_returns_error() {
1633 let schema = json!({
1634 "type": "object",
1635 "properties": {
1636 "param": {
1637 "type": "string",
1638 "source": "invalid_source"
1639 }
1640 },
1641 "required": []
1642 });
1643
1644 let result = ParameterValidator::new(schema);
1645 assert!(result.is_err());
1646 let err_msg = result.unwrap_err();
1647 assert!(err_msg.contains("Invalid source"));
1648 }
1649
1650 #[test]
1651 fn test_multiple_errors_reported_together() {
1652 let schema = json!({
1653 "type": "object",
1654 "properties": {
1655 "count": {
1656 "type": "integer",
1657 "source": "query"
1658 },
1659 "user_id": {
1660 "type": "string",
1661 "source": "path"
1662 },
1663 "token": {
1664 "type": "string",
1665 "source": "header"
1666 }
1667 },
1668 "required": ["count", "user_id", "token"]
1669 });
1670
1671 let validator = ParameterValidator::new(schema).unwrap();
1672
1673 let result = validator.validate_and_extract(
1674 &json!({}),
1675 &HashMap::new(),
1676 &HashMap::new(),
1677 &HashMap::new(),
1678 &HashMap::new(),
1679 );
1680
1681 assert!(result.is_err());
1682 let err = result.unwrap_err();
1683 assert_eq!(err.errors.len(), 3);
1684 assert!(err.errors.iter().all(|e| e.error_type == "missing"));
1685 }
1686
1687 #[test]
1688 fn test_coercion_error_includes_original_value() {
1689 let schema = json!({
1690 "type": "object",
1691 "properties": {
1692 "age": {
1693 "type": "integer",
1694 "source": "query"
1695 }
1696 },
1697 "required": ["age"]
1698 });
1699
1700 let validator = ParameterValidator::new(schema).unwrap();
1701 let invalid_value = "not_an_int";
1702 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1703 raw_query_params.insert("age".to_string(), vec![invalid_value.to_string()]);
1704
1705 let result = validator.validate_and_extract(
1706 &json!({"age": invalid_value}),
1707 &raw_query_params,
1708 &HashMap::new(),
1709 &HashMap::new(),
1710 &HashMap::new(),
1711 );
1712
1713 assert!(result.is_err());
1714 let err = result.unwrap_err();
1715 assert_eq!(err.errors[0].input, json!(invalid_value));
1716 }
1717
1718 #[test]
1719 fn test_string_parameter_passes_through() {
1720 let schema = json!({
1721 "type": "object",
1722 "properties": {
1723 "name": {
1724 "type": "string",
1725 "source": "query"
1726 }
1727 },
1728 "required": ["name"]
1729 });
1730
1731 let validator = ParameterValidator::new(schema).unwrap();
1732 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1733 raw_query_params.insert("name".to_string(), vec!["Alice".to_string()]);
1734
1735 let result = validator.validate_and_extract(
1736 &json!({"name": "Alice"}),
1737 &raw_query_params,
1738 &HashMap::new(),
1739 &HashMap::new(),
1740 &HashMap::new(),
1741 );
1742
1743 assert!(result.is_ok());
1744 let extracted = result.unwrap();
1745 assert_eq!(extracted["name"], json!("Alice"));
1746 }
1747
1748 #[test]
1749 fn test_string_with_special_characters_passes_through() {
1750 let schema = json!({
1751 "type": "object",
1752 "properties": {
1753 "message": {
1754 "type": "string",
1755 "source": "query"
1756 }
1757 },
1758 "required": ["message"]
1759 });
1760
1761 let validator = ParameterValidator::new(schema).unwrap();
1762 let special_value = "Hello! @#$%^&*() Unicode: 你好";
1763 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1764 raw_query_params.insert("message".to_string(), vec![special_value.to_string()]);
1765
1766 let result = validator.validate_and_extract(
1767 &json!({"message": special_value}),
1768 &raw_query_params,
1769 &HashMap::new(),
1770 &HashMap::new(),
1771 &HashMap::new(),
1772 );
1773
1774 assert!(result.is_ok());
1775 let extracted = result.unwrap();
1776 assert_eq!(extracted["message"], json!(special_value));
1777 }
1778
1779 #[test]
1780 fn test_array_query_parameter_missing_required_returns_error() {
1781 let schema = json!({
1782 "type": "object",
1783 "properties": {
1784 "ids": {
1785 "type": "array",
1786 "items": {"type": "integer"},
1787 "source": "query"
1788 }
1789 },
1790 "required": ["ids"]
1791 });
1792
1793 let validator = ParameterValidator::new(schema).unwrap();
1794
1795 let result = validator.validate_and_extract(
1796 &json!({}),
1797 &HashMap::new(),
1798 &HashMap::new(),
1799 &HashMap::new(),
1800 &HashMap::new(),
1801 );
1802
1803 assert!(result.is_err());
1804 let err = result.unwrap_err();
1805 assert_eq!(err.errors[0].error_type, "missing");
1806 }
1807
1808 #[test]
1809 fn test_empty_array_parameter_accepted() {
1810 let schema = json!({
1811 "type": "object",
1812 "properties": {
1813 "tags": {
1814 "type": "array",
1815 "items": {"type": "string"},
1816 "source": "query"
1817 }
1818 },
1819 "required": ["tags"]
1820 });
1821
1822 let validator = ParameterValidator::new(schema).unwrap();
1823
1824 let result = validator.validate_and_extract(
1825 &json!({"tags": []}),
1826 &HashMap::new(),
1827 &HashMap::new(),
1828 &HashMap::new(),
1829 &HashMap::new(),
1830 );
1831
1832 assert!(result.is_ok());
1833 let extracted = result.unwrap();
1834 assert_eq!(extracted["tags"], json!([]));
1835 }
1836
1837 #[test]
1838 fn test_parameter_source_from_str_query() {
1839 assert_eq!(ParameterSource::from_str("query"), Some(ParameterSource::Query));
1840 }
1841
1842 #[test]
1843 fn test_parameter_source_from_str_path() {
1844 assert_eq!(ParameterSource::from_str("path"), Some(ParameterSource::Path));
1845 }
1846
1847 #[test]
1848 fn test_parameter_source_from_str_header() {
1849 assert_eq!(ParameterSource::from_str("header"), Some(ParameterSource::Header));
1850 }
1851
1852 #[test]
1853 fn test_parameter_source_from_str_cookie() {
1854 assert_eq!(ParameterSource::from_str("cookie"), Some(ParameterSource::Cookie));
1855 }
1856
1857 #[test]
1858 fn test_parameter_source_from_str_invalid() {
1859 assert_eq!(ParameterSource::from_str("invalid"), None);
1860 }
1861
1862 #[test]
1863 fn test_integer_with_plus_sign() {
1864 let schema = json!({
1865 "type": "object",
1866 "properties": {
1867 "count": {
1868 "type": "integer",
1869 "source": "query"
1870 }
1871 },
1872 "required": ["count"]
1873 });
1874
1875 let validator = ParameterValidator::new(schema).unwrap();
1876 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1877 raw_query_params.insert("count".to_string(), vec!["+123".to_string()]);
1878
1879 let result = validator.validate_and_extract(
1880 &json!({"count": "+123"}),
1881 &raw_query_params,
1882 &HashMap::new(),
1883 &HashMap::new(),
1884 &HashMap::new(),
1885 );
1886
1887 assert!(result.is_ok());
1888 let extracted = result.unwrap();
1889 assert_eq!(extracted["count"], json!(123));
1890 }
1891
1892 #[test]
1893 fn test_float_with_leading_dot() {
1894 let schema = json!({
1895 "type": "object",
1896 "properties": {
1897 "ratio": {
1898 "type": "number",
1899 "source": "query"
1900 }
1901 },
1902 "required": ["ratio"]
1903 });
1904
1905 let validator = ParameterValidator::new(schema).unwrap();
1906 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1907 raw_query_params.insert("ratio".to_string(), vec![".5".to_string()]);
1908
1909 let result = validator.validate_and_extract(
1910 &json!({"ratio": 0.5}),
1911 &raw_query_params,
1912 &HashMap::new(),
1913 &HashMap::new(),
1914 &HashMap::new(),
1915 );
1916
1917 assert!(result.is_ok());
1918 let extracted = result.unwrap();
1919 assert_eq!(extracted["ratio"], json!(0.5));
1920 }
1921
1922 #[test]
1923 fn test_float_with_trailing_dot() {
1924 let schema = json!({
1925 "type": "object",
1926 "properties": {
1927 "value": {
1928 "type": "number",
1929 "source": "query"
1930 }
1931 },
1932 "required": ["value"]
1933 });
1934
1935 let validator = ParameterValidator::new(schema).unwrap();
1936 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1937 raw_query_params.insert("value".to_string(), vec!["5.".to_string()]);
1938
1939 let result = validator.validate_and_extract(
1940 &json!({"value": 5.0}),
1941 &raw_query_params,
1942 &HashMap::new(),
1943 &HashMap::new(),
1944 &HashMap::new(),
1945 );
1946
1947 assert!(result.is_ok());
1948 }
1949
1950 #[test]
1951 fn test_boolean_case_insensitive_true() {
1952 let schema = json!({
1953 "type": "object",
1954 "properties": {
1955 "flag": {
1956 "type": "boolean",
1957 "source": "query"
1958 }
1959 },
1960 "required": ["flag"]
1961 });
1962
1963 let validator = ParameterValidator::new(schema).unwrap();
1964 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1965 raw_query_params.insert("flag".to_string(), vec!["TrUe".to_string()]);
1966
1967 let result = validator.validate_and_extract(
1968 &json!({"flag": true}),
1969 &raw_query_params,
1970 &HashMap::new(),
1971 &HashMap::new(),
1972 &HashMap::new(),
1973 );
1974
1975 assert!(result.is_ok());
1976 let extracted = result.unwrap();
1977 assert_eq!(extracted["flag"], json!(true));
1978 }
1979
1980 #[test]
1981 fn test_boolean_case_insensitive_false() {
1982 let schema = json!({
1983 "type": "object",
1984 "properties": {
1985 "flag": {
1986 "type": "boolean",
1987 "source": "query"
1988 }
1989 },
1990 "required": ["flag"]
1991 });
1992
1993 let validator = ParameterValidator::new(schema).unwrap();
1994 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1995 raw_query_params.insert("flag".to_string(), vec!["FaLsE".to_string()]);
1996
1997 let result = validator.validate_and_extract(
1998 &json!({"flag": false}),
1999 &raw_query_params,
2000 &HashMap::new(),
2001 &HashMap::new(),
2002 &HashMap::new(),
2003 );
2004
2005 assert!(result.is_ok());
2006 let extracted = result.unwrap();
2007 assert_eq!(extracted["flag"], json!(false));
2008 }
2009}