1use crate::validation::{SchemaValidator, ValidationError, ValidationErrorDetail};
7use serde_json::{Value, json};
8use std::collections::HashMap;
9use std::fmt;
10use std::sync::Arc;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum ParameterSource {
15 Query,
16 Path,
17 Header,
18 Cookie,
19}
20
21impl ParameterSource {
22 fn from_str(s: &str) -> Option<Self> {
23 match s {
24 "query" => Some(Self::Query),
25 "path" => Some(Self::Path),
26 "header" => Some(Self::Header),
27 "cookie" => Some(Self::Cookie),
28 _ => None,
29 }
30 }
31}
32
33#[derive(Debug, Clone)]
35struct ParameterDef {
36 name: String,
37 lookup_key: String,
38 error_key: String,
39 source: ParameterSource,
40 expected_type: Option<String>,
41 format: Option<String>,
42 required: bool,
43}
44
45#[derive(Clone)]
46struct ParameterValidatorInner {
47 schema: Value,
48 schema_validator: Option<SchemaValidator>,
49 parameter_defs: Vec<ParameterDef>,
50}
51
52#[derive(Clone)]
54pub struct ParameterValidator {
55 inner: Arc<ParameterValidatorInner>,
56}
57
58impl fmt::Debug for ParameterValidator {
59 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60 f.debug_struct("ParameterValidator")
61 .field("schema", &self.inner.schema)
62 .field("parameter_defs_len", &self.inner.parameter_defs.len())
63 .finish()
64 }
65}
66
67impl ParameterValidator {
68 pub fn new(schema: Value) -> Result<Self, String> {
76 let parameter_defs = Self::extract_parameter_defs(&schema)?;
77 let validation_schema = Self::create_validation_schema(&schema);
78 let schema_validator = if Self::requires_full_schema_validation(&validation_schema) {
79 Some(SchemaValidator::new(validation_schema)?)
80 } else {
81 None
82 };
83
84 Ok(Self {
85 inner: Arc::new(ParameterValidatorInner {
86 schema,
87 schema_validator,
88 parameter_defs,
89 }),
90 })
91 }
92
93 #[must_use]
95 pub fn requires_headers(&self) -> bool {
96 self.inner
97 .parameter_defs
98 .iter()
99 .any(|def| def.source == ParameterSource::Header)
100 }
101
102 #[must_use]
104 pub fn requires_cookies(&self) -> bool {
105 self.inner
106 .parameter_defs
107 .iter()
108 .any(|def| def.source == ParameterSource::Cookie)
109 }
110
111 #[must_use]
113 pub fn has_params(&self) -> bool {
114 !self.inner.parameter_defs.is_empty()
115 }
116
117 fn requires_full_schema_validation(schema: &Value) -> bool {
127 fn recurse(value: &Value) -> bool {
128 let Some(obj) = value.as_object() else {
129 return false;
130 };
131
132 for (key, child) in obj {
133 match key.as_str() {
134 "type"
136 | "format"
137 | "properties"
138 | "required"
139 | "items"
140 | "additionalProperties"
141 | "title"
142 | "description"
143 | "default"
144 | "examples"
145 | "deprecated"
146 | "readOnly"
147 | "writeOnly"
148 | "$schema"
149 | "$id" => {}
150
151 _ => return true,
153 }
154
155 if recurse(child) {
156 return true;
157 }
158 }
159
160 false
161 }
162
163 recurse(schema)
164 }
165
166 fn extract_parameter_defs(schema: &Value) -> Result<Vec<ParameterDef>, String> {
168 let mut defs = Vec::new();
169
170 let properties = schema
171 .get("properties")
172 .and_then(|p| p.as_object())
173 .cloned()
174 .unwrap_or_default();
175
176 let required_list = schema
177 .get("required")
178 .and_then(|r| r.as_array())
179 .map(|arr| arr.iter().filter_map(|v| v.as_str()).collect::<Vec<_>>())
180 .unwrap_or_default();
181
182 for (name, prop) in properties {
183 let source_str = prop.get("source").and_then(|s| s.as_str()).ok_or_else(|| {
184 anyhow::anyhow!("Invalid parameter schema")
185 .context(format!("Parameter '{name}' missing required 'source' field"))
186 .to_string()
187 })?;
188
189 let source = ParameterSource::from_str(source_str).ok_or_else(|| {
190 anyhow::anyhow!("Invalid parameter schema")
191 .context(format!(
192 "Invalid source '{source_str}' for parameter '{name}' (expected: query, path, header, or cookie)"
193 ))
194 .to_string()
195 })?;
196
197 let expected_type = prop.get("type").and_then(|t| t.as_str()).map(String::from);
198 let format = prop.get("format").and_then(|f| f.as_str()).map(String::from);
199
200 let is_optional = prop
201 .get("optional")
202 .and_then(serde_json::Value::as_bool)
203 .unwrap_or(false);
204 let required = required_list.contains(&name.as_str()) && !is_optional;
205
206 let (lookup_key, error_key) = if source == ParameterSource::Header {
207 let header_key = name.replace('_', "-").to_lowercase();
208 (header_key.clone(), header_key)
209 } else {
210 (name.clone(), name.clone())
211 };
212
213 defs.push(ParameterDef {
214 name: name.clone(),
215 lookup_key,
216 error_key,
217 source,
218 expected_type,
219 format,
220 required,
221 });
222 }
223
224 Ok(defs)
225 }
226
227 #[must_use]
229 pub fn schema(&self) -> &Value {
230 &self.inner.schema
231 }
232
233 #[allow(clippy::too_many_lines)]
243 pub fn validate_and_extract(
244 &self,
245 query_params: &Value,
246 raw_query_params: &HashMap<String, Vec<String>>,
247 path_params: &HashMap<String, String>,
248 headers: &HashMap<String, String>,
249 cookies: &HashMap<String, String>,
250 ) -> Result<Value, ValidationError> {
251 let mut params_map = serde_json::Map::new();
252 let mut errors = Vec::new();
253 for param_def in &self.inner.parameter_defs {
254 if param_def.source == ParameterSource::Query && param_def.expected_type.as_deref() == Some("array") {
255 let raw_values = raw_query_params.get(¶m_def.lookup_key);
256 let query_value = query_params.get(¶m_def.name);
257
258 if param_def.required && raw_values.is_none() && query_value.is_none() {
259 errors.push(ValidationErrorDetail {
260 error_type: "missing".to_string(),
261 loc: vec!["query".to_string(), param_def.error_key.clone()],
262 msg: "Field required".to_string(),
263 input: Value::Null,
264 ctx: None,
265 });
266 continue;
267 }
268
269 if let Some(values) = raw_values {
270 let (item_type, item_format) = self.array_item_type_and_format(¶m_def.name);
271 let mut out = Vec::with_capacity(values.len());
272 for value in values {
273 match Self::coerce_value(value, item_type, item_format) {
274 Ok(coerced) => out.push(coerced),
275 Err(e) => {
276 errors.push(ValidationErrorDetail {
277 error_type: match item_type {
278 Some("integer") => "int_parsing".to_string(),
279 Some("number") => "float_parsing".to_string(),
280 Some("boolean") => "bool_parsing".to_string(),
281 Some("string") => match item_format {
282 Some("uuid") => "uuid_parsing".to_string(),
283 Some("date") => "date_parsing".to_string(),
284 Some("date-time") => "datetime_parsing".to_string(),
285 Some("time") => "time_parsing".to_string(),
286 Some("duration") => "duration_parsing".to_string(),
287 _ => "type_error".to_string(),
288 },
289 _ => "type_error".to_string(),
290 },
291 loc: vec!["query".to_string(), param_def.error_key.clone()],
292 msg: match item_type {
293 Some("integer") => {
294 "Input should be a valid integer, unable to parse string as an integer"
295 .to_string()
296 }
297 Some("number") => {
298 "Input should be a valid number, unable to parse string as a number"
299 .to_string()
300 }
301 Some("boolean") => {
302 "Input should be a valid boolean, unable to interpret input".to_string()
303 }
304 Some("string") => match item_format {
305 Some("uuid") => format!("Input should be a valid UUID, {e}"),
306 Some("date") => format!("Input should be a valid date, {e}"),
307 Some("date-time") => format!("Input should be a valid datetime, {e}"),
308 Some("time") => format!("Input should be a valid time, {e}"),
309 Some("duration") => format!("Input should be a valid duration, {e}"),
310 _ => e,
311 },
312 _ => e,
313 },
314 input: Value::String(value.clone()),
315 ctx: None,
316 });
317 }
318 }
319 }
320 params_map.insert(param_def.name.clone(), Value::Array(out));
321 } else if let Some(value) = query_value {
322 let array_value = if value.is_array() {
323 value.clone()
324 } else {
325 Value::Array(vec![value.clone()])
326 };
327 let (item_type, item_format) = self.array_item_type_and_format(¶m_def.name);
328
329 #[allow(clippy::option_if_let_else)]
330 let coerced_items = match array_value.as_array() {
331 Some(items) => {
332 let mut out = Vec::with_capacity(items.len());
333 for item in items {
334 if let Some(text) = item.as_str() {
335 match Self::coerce_value(text, item_type, item_format) {
336 Ok(coerced) => out.push(coerced),
337 Err(e) => {
338 errors.push(ValidationErrorDetail {
339 error_type: match item_type {
340 Some("integer") => "int_parsing".to_string(),
341 Some("number") => "float_parsing".to_string(),
342 Some("boolean") => "bool_parsing".to_string(),
343 Some("string") => match item_format {
344 Some("uuid") => "uuid_parsing".to_string(),
345 Some("date") => "date_parsing".to_string(),
346 Some("date-time") => "datetime_parsing".to_string(),
347 Some("time") => "time_parsing".to_string(),
348 Some("duration") => "duration_parsing".to_string(),
349 _ => "type_error".to_string(),
350 },
351 _ => "type_error".to_string(),
352 },
353 loc: vec!["query".to_string(), param_def.error_key.clone()],
354 msg: match item_type {
355 Some("integer") => "Input should be a valid integer, unable to parse string as an integer".to_string(),
356 Some("number") => "Input should be a valid number, unable to parse string as a number".to_string(),
357 Some("boolean") => "Input should be a valid boolean, unable to interpret input".to_string(),
358 Some("string") => match item_format {
359 Some("uuid") => format!("Input should be a valid UUID, {e}"),
360 Some("date") => format!("Input should be a valid date, {e}"),
361 Some("date-time") => format!("Input should be a valid datetime, {e}"),
362 Some("time") => format!("Input should be a valid time, {e}"),
363 Some("duration") => format!("Input should be a valid duration, {e}"),
364 _ => e.clone(),
365 },
366 _ => e.clone(),
367 },
368 input: Value::String(text.to_string()),
369 ctx: None,
370 });
371 }
372 }
373 } else {
374 out.push(item.clone());
375 }
376 }
377 out
378 }
379 None => Vec::new(),
380 };
381
382 params_map.insert(param_def.name.clone(), Value::Array(coerced_items));
383 }
384 continue;
385 }
386
387 let raw_value_string = self.raw_value_for_error(param_def, raw_query_params, path_params, headers, cookies);
388
389 if param_def.required && raw_value_string.is_none() {
390 let source_str = match param_def.source {
391 ParameterSource::Query => "query",
392 ParameterSource::Path => "path",
393 ParameterSource::Header => "headers",
394 ParameterSource::Cookie => "cookie",
395 };
396 errors.push(ValidationErrorDetail {
397 error_type: "missing".to_string(),
398 loc: vec![source_str.to_string(), param_def.error_key.clone()],
399 msg: "Field required".to_string(),
400 input: Value::Null,
401 ctx: None,
402 });
403 continue;
404 }
405
406 if let Some(value_str) = raw_value_string {
407 match Self::coerce_value(
408 value_str,
409 param_def.expected_type.as_deref(),
410 param_def.format.as_deref(),
411 ) {
412 Ok(coerced) => {
413 params_map.insert(param_def.name.clone(), coerced);
414 }
415 Err(e) => {
416 let source_str = match param_def.source {
417 ParameterSource::Query => "query",
418 ParameterSource::Path => "path",
419 ParameterSource::Header => "headers",
420 ParameterSource::Cookie => "cookie",
421 };
422 let (error_type, error_msg) =
423 match (param_def.expected_type.as_deref(), param_def.format.as_deref()) {
424 (Some("integer"), _) => (
425 "int_parsing",
426 "Input should be a valid integer, unable to parse string as an integer".to_string(),
427 ),
428 (Some("number"), _) => (
429 "float_parsing",
430 "Input should be a valid number, unable to parse string as a number".to_string(),
431 ),
432 (Some("boolean"), _) => (
433 "bool_parsing",
434 "Input should be a valid boolean, unable to interpret input".to_string(),
435 ),
436 (Some("string"), Some("uuid")) => {
437 ("uuid_parsing", format!("Input should be a valid UUID, {e}"))
438 }
439 (Some("string"), Some("date")) => {
440 ("date_parsing", format!("Input should be a valid date, {e}"))
441 }
442 (Some("string"), Some("date-time")) => {
443 ("datetime_parsing", format!("Input should be a valid datetime, {e}"))
444 }
445 (Some("string"), Some("time")) => {
446 ("time_parsing", format!("Input should be a valid time, {e}"))
447 }
448 (Some("string"), Some("duration")) => {
449 ("duration_parsing", format!("Input should be a valid duration, {e}"))
450 }
451 _ => ("type_error", e),
452 };
453 errors.push(ValidationErrorDetail {
454 error_type: error_type.to_string(),
455 loc: vec![source_str.to_string(), param_def.error_key.clone()],
456 msg: error_msg,
457 input: Value::String(value_str.to_string()),
458 ctx: None,
459 });
460 }
461 }
462 }
463 }
464
465 if !errors.is_empty() {
466 return Err(ValidationError { errors });
467 }
468
469 let params_json = Value::Object(params_map);
470 if let Some(schema_validator) = &self.inner.schema_validator {
471 match schema_validator.validate(¶ms_json) {
472 Ok(()) => Ok(params_json),
473 Err(mut validation_err) => {
474 for error in &mut validation_err.errors {
475 if error.loc.len() >= 2 && error.loc[0] == "body" {
476 let param_name = &error.loc[1];
477 if let Some(param_def) = self.inner.parameter_defs.iter().find(|p| &p.name == param_name) {
478 let source_str = match param_def.source {
479 ParameterSource::Query => "query",
480 ParameterSource::Path => "path",
481 ParameterSource::Header => "headers",
482 ParameterSource::Cookie => "cookie",
483 };
484 error.loc[0] = source_str.to_string();
485 if param_def.source == ParameterSource::Header {
486 error.loc[1].clone_from(¶m_def.error_key);
487 }
488 if let Some(raw_value) =
489 self.raw_value_for_error(param_def, raw_query_params, path_params, headers, cookies)
490 {
491 error.input = Value::String(raw_value.to_string());
492 }
493 }
494 }
495 }
496 Err(validation_err)
497 }
498 }
499 } else {
500 Ok(params_json)
501 }
502 }
503
504 #[allow(clippy::unused_self)]
505 fn raw_value_for_error<'a>(
506 &self,
507 param_def: &ParameterDef,
508 raw_query_params: &'a HashMap<String, Vec<String>>,
509 path_params: &'a HashMap<String, String>,
510 headers: &'a HashMap<String, String>,
511 cookies: &'a HashMap<String, String>,
512 ) -> Option<&'a str> {
513 #[allow(clippy::too_many_arguments)]
514 match param_def.source {
515 ParameterSource::Query => raw_query_params
516 .get(¶m_def.lookup_key)
517 .and_then(|values| values.first())
518 .map(String::as_str),
519 ParameterSource::Path => path_params.get(¶m_def.lookup_key).map(String::as_str),
520 ParameterSource::Header => headers.get(¶m_def.lookup_key).map(String::as_str),
521 ParameterSource::Cookie => cookies.get(¶m_def.lookup_key).map(String::as_str),
522 }
523 }
524
525 fn array_item_type_and_format(&self, name: &str) -> (Option<&str>, Option<&str>) {
526 let Some(prop) = self
527 .inner
528 .schema
529 .get("properties")
530 .and_then(|value| value.as_object())
531 .and_then(|props| props.get(name))
532 else {
533 return (None, None);
534 };
535
536 let Some(items) = prop.get("items") else {
537 return (None, None);
538 };
539
540 let item_type = items.get("type").and_then(|value| value.as_str());
541 let item_format = items.get("format").and_then(|value| value.as_str());
542 (item_type, item_format)
543 }
544
545 fn coerce_value(value: &str, expected_type: Option<&str>, format: Option<&str>) -> Result<Value, String> {
547 if let Some(fmt) = format {
548 match fmt {
549 "uuid" => {
550 Self::validate_uuid_format(value)?;
551 return Ok(json!(value));
552 }
553 "date" => {
554 Self::validate_date_format(value)?;
555 return Ok(json!(value));
556 }
557 "date-time" => {
558 Self::validate_datetime_format(value)?;
559 return Ok(json!(value));
560 }
561 "time" => {
562 Self::validate_time_format(value)?;
563 return Ok(json!(value));
564 }
565 "duration" => {
566 Self::validate_duration_format(value)?;
567 return Ok(json!(value));
568 }
569 _ => {}
570 }
571 }
572
573 match expected_type {
574 Some("integer") => value
575 .parse::<i64>()
576 .map(|i| json!(i))
577 .map_err(|e| format!("Invalid integer: {e}")),
578 Some("number") => value
579 .parse::<f64>()
580 .map(|f| json!(f))
581 .map_err(|e| format!("Invalid number: {e}")),
582 Some("boolean") => {
583 if value.is_empty() {
584 return Ok(json!(false));
585 }
586 let value_lower = value.to_lowercase();
587 if value_lower == "true" || value == "1" {
588 Ok(json!(true))
589 } else if value_lower == "false" || value == "0" {
590 Ok(json!(false))
591 } else {
592 Err(format!("Invalid boolean: {value}"))
593 }
594 }
595 _ => Ok(json!(value)),
596 }
597 }
598
599 fn validate_date_format(value: &str) -> Result<(), String> {
601 jiff::civil::Date::strptime("%Y-%m-%d", value)
602 .map(|_| ())
603 .map_err(|e| format!("Invalid date format: {e}"))
604 }
605
606 fn validate_datetime_format(value: &str) -> Result<(), String> {
608 use std::str::FromStr;
609 jiff::Timestamp::from_str(value)
610 .map(|_| ())
611 .map_err(|e| format!("Invalid datetime format: {e}"))
612 }
613
614 fn validate_time_format(value: &str) -> Result<(), String> {
616 let (time_part, offset_part) = if let Some(stripped) = value.strip_suffix('Z') {
617 (stripped, "Z")
618 } else {
619 let plus = value.rfind('+');
620 let minus = value.rfind('-');
621 let split_at = match (plus, minus) {
622 (Some(p), Some(m)) => Some(std::cmp::max(p, m)),
623 (Some(p), None) => Some(p),
624 (None, Some(m)) => Some(m),
625 (None, None) => None,
626 }
627 .ok_or_else(|| "Invalid time format: missing timezone offset".to_string())?;
628
629 if split_at < 8 {
630 return Err("Invalid time format: timezone offset position is invalid".to_string());
631 }
632
633 (&value[..split_at], &value[split_at..])
634 };
635
636 let base_time = time_part.split('.').next().unwrap_or(time_part);
637 jiff::civil::Time::strptime("%H:%M:%S", base_time).map_err(|e| format!("Invalid time format: {e}"))?;
638
639 if let Some((_, frac)) = time_part.split_once('.')
640 && (frac.is_empty() || frac.len() > 9 || !frac.chars().all(|c| c.is_ascii_digit()))
641 {
642 return Err("Invalid time format: fractional seconds must be 1-9 digits".to_string());
643 }
644
645 if offset_part != "Z" {
646 let sign = offset_part
647 .chars()
648 .next()
649 .ok_or_else(|| "Invalid time format: empty timezone offset".to_string())?;
650 if sign != '+' && sign != '-' {
651 return Err("Invalid time format: timezone offset must start with + or -".to_string());
652 }
653
654 let rest = &offset_part[1..];
655 let (hours_str, minutes_str) = rest
656 .split_once(':')
657 .ok_or_else(|| "Invalid time format: timezone offset must be ±HH:MM".to_string())?;
658 let hours: u8 = hours_str
659 .parse()
660 .map_err(|_| "Invalid time format: invalid timezone hours".to_string())?;
661 let minutes: u8 = minutes_str
662 .parse()
663 .map_err(|_| "Invalid time format: invalid timezone minutes".to_string())?;
664 if hours > 23 || minutes > 59 {
665 return Err("Invalid time format: timezone offset out of range".to_string());
666 }
667 }
668
669 Ok(())
670 }
671
672 fn validate_duration_format(value: &str) -> Result<(), String> {
674 use std::str::FromStr;
675 jiff::Span::from_str(value)
676 .map(|_| ())
677 .map_err(|e| format!("Invalid duration format: {e}"))
678 }
679
680 fn validate_uuid_format(value: &str) -> Result<(), String> {
682 use std::str::FromStr;
683 uuid::Uuid::from_str(value)
684 .map(|_| ())
685 .map_err(|_e| format!("invalid character: expected an optional prefix of `urn:uuid:` followed by [0-9a-fA-F-], found `{}` at {}",
686 value.chars().next().unwrap_or('?'),
687 value.chars().position(|c| !c.is_ascii_hexdigit() && c != '-').unwrap_or(0)))
688 }
689
690 fn create_validation_schema(schema: &Value) -> Value {
693 let mut schema = schema.clone();
694 let mut optional_fields: Vec<String> = Vec::new();
695
696 if let Some(properties) = schema.get_mut("properties").and_then(|p| p.as_object_mut()) {
697 for (name, prop) in properties.iter_mut() {
698 if let Some(obj) = prop.as_object_mut() {
699 obj.remove("source");
700 if obj.get("optional").and_then(serde_json::Value::as_bool) == Some(true) {
701 optional_fields.push(name.clone());
702 }
703 obj.remove("optional");
704 }
705 }
706 }
707
708 if !optional_fields.is_empty()
709 && let Some(required) = schema.get_mut("required").and_then(|r| r.as_array_mut())
710 {
711 required.retain(|value| {
712 value
713 .as_str()
714 .is_some_and(|field| !optional_fields.iter().any(|opt| opt == field))
715 });
716 }
717
718 schema
719 }
720}
721
722#[cfg(test)]
723mod tests {
724 use super::*;
725 use serde_json::json;
726
727 #[test]
728 fn test_parameter_schema_missing_source_returns_error() {
729 let schema = json!({
730 "type": "object",
731 "properties": {
732 "foo": {
733 "type": "string"
734 }
735 }
736 });
737
738 let err = ParameterValidator::new(schema).expect_err("schema missing source should error");
739 assert!(
740 err.contains("missing required 'source' field"),
741 "unexpected error: {err}"
742 );
743 }
744
745 #[test]
746 fn test_parameter_schema_invalid_source_returns_error() {
747 let schema = json!({
748 "type": "object",
749 "properties": {
750 "foo": {
751 "type": "string",
752 "source": "invalid"
753 }
754 }
755 });
756
757 let err = ParameterValidator::new(schema).expect_err("invalid source should error");
758 assert!(err.contains("Invalid source"), "unexpected error: {err}");
759 }
760
761 #[test]
762 fn test_array_query_parameter() {
763 let schema = json!({
764 "type": "object",
765 "properties": {
766 "device_ids": {
767 "type": "array",
768 "items": {"type": "integer"},
769 "source": "query"
770 }
771 },
772 "required": []
773 });
774
775 let validator = ParameterValidator::new(schema).unwrap();
776
777 let query_params = json!({
778 "device_ids": [1, 2]
779 });
780 let raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
781 let path_params = HashMap::new();
782
783 let result = validator.validate_and_extract(
784 &query_params,
785 &raw_query_params,
786 &path_params,
787 &HashMap::new(),
788 &HashMap::new(),
789 );
790 assert!(
791 result.is_ok(),
792 "Array query param validation failed: {:?}",
793 result.err()
794 );
795
796 let extracted = result.unwrap();
797 assert_eq!(extracted["device_ids"], json!([1, 2]));
798 }
799
800 #[test]
801 fn test_path_parameter_extraction() {
802 let schema = json!({
803 "type": "object",
804 "properties": {
805 "item_id": {
806 "type": "string",
807 "source": "path"
808 }
809 },
810 "required": ["item_id"]
811 });
812
813 let validator = ParameterValidator::new(schema).expect("Failed to create validator");
814
815 let mut path_params = HashMap::new();
816 path_params.insert("item_id".to_string(), "foobar".to_string());
817 let query_params = json!({});
818 let raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
819
820 let result = validator.validate_and_extract(
821 &query_params,
822 &raw_query_params,
823 &path_params,
824 &HashMap::new(),
825 &HashMap::new(),
826 );
827 assert!(result.is_ok(), "Validation should succeed: {result:?}");
828
829 let params = result.unwrap();
830 assert_eq!(params, json!({"item_id": "foobar"}));
831 }
832
833 #[test]
834 fn test_boolean_path_parameter_coercion() {
835 let schema = json!({
836 "type": "object",
837 "properties": {
838 "value": {
839 "type": "boolean",
840 "source": "path"
841 }
842 },
843 "required": ["value"]
844 });
845
846 let validator = ParameterValidator::new(schema).expect("Failed to create validator");
847
848 let mut path_params = HashMap::new();
849 path_params.insert("value".to_string(), "True".to_string());
850 let query_params = json!({});
851 let raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
852
853 let result = validator.validate_and_extract(
854 &query_params,
855 &raw_query_params,
856 &path_params,
857 &HashMap::new(),
858 &HashMap::new(),
859 );
860 if result.is_err() {
861 eprintln!("Error for 'True': {result:?}");
862 }
863 assert!(result.is_ok(), "Validation should succeed for 'True': {result:?}");
864 let params = result.unwrap();
865 assert_eq!(params, json!({"value": true}));
866
867 path_params.insert("value".to_string(), "1".to_string());
868 let query_params_1 = json!({});
869 let result = validator.validate_and_extract(
870 &query_params_1,
871 &raw_query_params,
872 &path_params,
873 &HashMap::new(),
874 &HashMap::new(),
875 );
876 assert!(result.is_ok(), "Validation should succeed for '1': {result:?}");
877 let params = result.unwrap();
878 assert_eq!(params, json!({"value": true}));
879
880 path_params.insert("value".to_string(), "false".to_string());
881 let query_params_false = json!({});
882 let result = validator.validate_and_extract(
883 &query_params_false,
884 &raw_query_params,
885 &path_params,
886 &HashMap::new(),
887 &HashMap::new(),
888 );
889 assert!(result.is_ok(), "Validation should succeed for 'false': {result:?}");
890 let params = result.unwrap();
891 assert_eq!(params, json!({"value": false}));
892
893 path_params.insert("value".to_string(), "TRUE".to_string());
894 let query_params_true = json!({});
895 let result = validator.validate_and_extract(
896 &query_params_true,
897 &raw_query_params,
898 &path_params,
899 &HashMap::new(),
900 &HashMap::new(),
901 );
902 assert!(result.is_ok(), "Validation should succeed for 'TRUE': {result:?}");
903 let params = result.unwrap();
904 assert_eq!(params, json!({"value": true}));
905 }
906
907 #[test]
908 fn test_boolean_query_parameter_coercion() {
909 let schema = json!({
910 "type": "object",
911 "properties": {
912 "flag": {
913 "type": "boolean",
914 "source": "query"
915 }
916 },
917 "required": ["flag"]
918 });
919
920 let validator = ParameterValidator::new(schema).expect("Failed to create validator");
921 let path_params = HashMap::new();
922
923 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
924 raw_query_params.insert("flag".to_string(), vec!["1".to_string()]);
925 let query_params = json!({"flag": 1});
926 let result = validator.validate_and_extract(
927 &query_params,
928 &raw_query_params,
929 &path_params,
930 &HashMap::new(),
931 &HashMap::new(),
932 );
933 assert!(result.is_ok(), "Validation should succeed for integer 1: {result:?}");
934 let params = result.unwrap();
935 assert_eq!(params, json!({"flag": true}));
936
937 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
938 raw_query_params.insert("flag".to_string(), vec!["0".to_string()]);
939 let query_params = json!({"flag": 0});
940 let result = validator.validate_and_extract(
941 &query_params,
942 &raw_query_params,
943 &path_params,
944 &HashMap::new(),
945 &HashMap::new(),
946 );
947 assert!(result.is_ok(), "Validation should succeed for integer 0: {result:?}");
948 let params = result.unwrap();
949 assert_eq!(params, json!({"flag": false}));
950
951 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
952 raw_query_params.insert("flag".to_string(), vec!["true".to_string()]);
953 let query_params = json!({"flag": true});
954 let result = validator.validate_and_extract(
955 &query_params,
956 &raw_query_params,
957 &path_params,
958 &HashMap::new(),
959 &HashMap::new(),
960 );
961 assert!(result.is_ok(), "Validation should succeed for boolean true: {result:?}");
962 let params = result.unwrap();
963 assert_eq!(params, json!({"flag": true}));
964
965 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
966 raw_query_params.insert("flag".to_string(), vec!["false".to_string()]);
967 let query_params = json!({"flag": false});
968 let result = validator.validate_and_extract(
969 &query_params,
970 &raw_query_params,
971 &path_params,
972 &HashMap::new(),
973 &HashMap::new(),
974 );
975 assert!(
976 result.is_ok(),
977 "Validation should succeed for boolean false: {result:?}"
978 );
979 let params = result.unwrap();
980 assert_eq!(params, json!({"flag": false}));
981 }
982
983 #[test]
984 fn test_integer_coercion_invalid_format_returns_error() {
985 let schema = json!({
986 "type": "object",
987 "properties": {
988 "count": {
989 "type": "integer",
990 "source": "query"
991 }
992 },
993 "required": ["count"]
994 });
995
996 let validator = ParameterValidator::new(schema).unwrap();
997 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
998 raw_query_params.insert("count".to_string(), vec!["not_a_number".to_string()]);
999
1000 let result = validator.validate_and_extract(
1001 &json!({"count": "not_a_number"}),
1002 &raw_query_params,
1003 &HashMap::new(),
1004 &HashMap::new(),
1005 &HashMap::new(),
1006 );
1007
1008 assert!(result.is_err(), "Should fail to coerce non-integer string");
1009 let err = result.unwrap_err();
1010 assert_eq!(err.errors.len(), 1);
1011 assert_eq!(err.errors[0].error_type, "int_parsing");
1012 assert_eq!(err.errors[0].loc, vec!["query".to_string(), "count".to_string()]);
1013 assert!(err.errors[0].msg.contains("valid integer"));
1014 }
1015
1016 #[test]
1017 fn test_integer_coercion_with_letters_mixed_returns_error() {
1018 let schema = json!({
1019 "type": "object",
1020 "properties": {
1021 "id": {
1022 "type": "integer",
1023 "source": "path"
1024 }
1025 },
1026 "required": ["id"]
1027 });
1028
1029 let validator = ParameterValidator::new(schema).unwrap();
1030 let mut path_params = HashMap::new();
1031 path_params.insert("id".to_string(), "123abc".to_string());
1032
1033 let result = validator.validate_and_extract(
1034 &json!({}),
1035 &HashMap::new(),
1036 &path_params,
1037 &HashMap::new(),
1038 &HashMap::new(),
1039 );
1040
1041 assert!(result.is_err());
1042 let err = result.unwrap_err();
1043 assert_eq!(err.errors[0].error_type, "int_parsing");
1044 }
1045
1046 #[test]
1047 fn test_integer_coercion_overflow_returns_error() {
1048 let schema = json!({
1049 "type": "object",
1050 "properties": {
1051 "big_num": {
1052 "type": "integer",
1053 "source": "query"
1054 }
1055 },
1056 "required": ["big_num"]
1057 });
1058
1059 let validator = ParameterValidator::new(schema).unwrap();
1060 let too_large = "9223372036854775808";
1061 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1062 raw_query_params.insert("big_num".to_string(), vec![too_large.to_string()]);
1063
1064 let result = validator.validate_and_extract(
1065 &json!({"big_num": too_large}),
1066 &raw_query_params,
1067 &HashMap::new(),
1068 &HashMap::new(),
1069 &HashMap::new(),
1070 );
1071
1072 assert!(result.is_err(), "Should fail on integer overflow");
1073 let err = result.unwrap_err();
1074 assert_eq!(err.errors[0].error_type, "int_parsing");
1075 }
1076
1077 #[test]
1078 fn test_integer_coercion_negative_overflow_returns_error() {
1079 let schema = json!({
1080 "type": "object",
1081 "properties": {
1082 "small_num": {
1083 "type": "integer",
1084 "source": "query"
1085 }
1086 },
1087 "required": ["small_num"]
1088 });
1089
1090 let validator = ParameterValidator::new(schema).unwrap();
1091 let too_small = "-9223372036854775809";
1092 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1093 raw_query_params.insert("small_num".to_string(), vec![too_small.to_string()]);
1094
1095 let result = validator.validate_and_extract(
1096 &json!({"small_num": too_small}),
1097 &raw_query_params,
1098 &HashMap::new(),
1099 &HashMap::new(),
1100 &HashMap::new(),
1101 );
1102
1103 assert!(result.is_err());
1104 let err = result.unwrap_err();
1105 assert_eq!(err.errors[0].error_type, "int_parsing");
1106 }
1107
1108 #[test]
1109 fn test_optional_field_overrides_required_list() {
1110 let schema = json!({
1111 "type": "object",
1112 "properties": {
1113 "maybe": {
1114 "type": "string",
1115 "source": "query",
1116 "optional": true
1117 }
1118 },
1119 "required": ["maybe"]
1120 });
1121
1122 let validator = ParameterValidator::new(schema).unwrap();
1123
1124 let result = validator.validate_and_extract(
1125 &json!({}),
1126 &HashMap::new(),
1127 &HashMap::new(),
1128 &HashMap::new(),
1129 &HashMap::new(),
1130 );
1131
1132 assert!(result.is_ok(), "optional required field should not error: {result:?}");
1133 assert_eq!(result.unwrap(), json!({}));
1134 }
1135
1136 #[test]
1137 fn test_header_name_is_normalized_for_lookup_and_errors() {
1138 let schema = json!({
1139 "type": "object",
1140 "properties": {
1141 "x_request_id": {
1142 "type": "string",
1143 "source": "header"
1144 }
1145 },
1146 "required": ["x_request_id"]
1147 });
1148
1149 let validator = ParameterValidator::new(schema).unwrap();
1150
1151 let mut headers = HashMap::new();
1152 headers.insert("x-request-id".to_string(), "abc123".to_string());
1153
1154 let ok = validator
1155 .validate_and_extract(&json!({}), &HashMap::new(), &HashMap::new(), &headers, &HashMap::new())
1156 .unwrap();
1157 assert_eq!(ok, json!({"x_request_id": "abc123"}));
1158
1159 let err = validator
1160 .validate_and_extract(
1161 &json!({}),
1162 &HashMap::new(),
1163 &HashMap::new(),
1164 &HashMap::new(),
1165 &HashMap::new(),
1166 )
1167 .unwrap_err();
1168 assert_eq!(
1169 err.errors[0].loc,
1170 vec!["headers".to_string(), "x-request-id".to_string()]
1171 );
1172 }
1173
1174 #[test]
1175 fn test_boolean_empty_string_coerces_to_false() {
1176 let schema = json!({
1177 "type": "object",
1178 "properties": {
1179 "flag": {
1180 "type": "boolean",
1181 "source": "query"
1182 }
1183 },
1184 "required": ["flag"]
1185 });
1186
1187 let validator = ParameterValidator::new(schema).unwrap();
1188 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1189 raw_query_params.insert("flag".to_string(), vec![String::new()]);
1190
1191 let result = validator
1192 .validate_and_extract(
1193 &json!({"flag": ""}),
1194 &raw_query_params,
1195 &HashMap::new(),
1196 &HashMap::new(),
1197 &HashMap::new(),
1198 )
1199 .unwrap();
1200 assert_eq!(result, json!({"flag": false}));
1201 }
1202
1203 #[test]
1204 fn test_uuid_format_validation_returns_uuid_parsing_error() {
1205 let schema = json!({
1206 "type": "object",
1207 "properties": {
1208 "id": {
1209 "type": "string",
1210 "format": "uuid",
1211 "source": "query"
1212 }
1213 },
1214 "required": ["id"]
1215 });
1216
1217 let validator = ParameterValidator::new(schema).unwrap();
1218 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1219 raw_query_params.insert("id".to_string(), vec!["not-a-uuid".to_string()]);
1220
1221 let err = validator
1222 .validate_and_extract(
1223 &json!({"id": "not-a-uuid"}),
1224 &raw_query_params,
1225 &HashMap::new(),
1226 &HashMap::new(),
1227 &HashMap::new(),
1228 )
1229 .unwrap_err();
1230
1231 assert_eq!(err.errors[0].error_type, "uuid_parsing");
1232 assert!(
1233 err.errors[0].msg.contains("valid UUID"),
1234 "msg was {}",
1235 err.errors[0].msg
1236 );
1237 }
1238
1239 #[test]
1240 fn test_array_query_parameter_coercion_error_reports_item_parse_failure() {
1241 let schema = json!({
1242 "type": "object",
1243 "properties": {
1244 "ids": {
1245 "type": "array",
1246 "items": {"type": "integer"},
1247 "source": "query"
1248 }
1249 },
1250 "required": ["ids"]
1251 });
1252
1253 let validator = ParameterValidator::new(schema).unwrap();
1254 let query_params = json!({ "ids": ["nope"] });
1255
1256 let err = validator
1257 .validate_and_extract(
1258 &query_params,
1259 &HashMap::new(),
1260 &HashMap::new(),
1261 &HashMap::new(),
1262 &HashMap::new(),
1263 )
1264 .unwrap_err();
1265
1266 assert_eq!(err.errors[0].error_type, "int_parsing");
1267 assert_eq!(err.errors[0].loc, vec!["query".to_string(), "ids".to_string()]);
1268 }
1269
1270 #[test]
1271 fn test_float_coercion_invalid_format_returns_error() {
1272 let schema = json!({
1273 "type": "object",
1274 "properties": {
1275 "price": {
1276 "type": "number",
1277 "source": "query"
1278 }
1279 },
1280 "required": ["price"]
1281 });
1282
1283 let validator = ParameterValidator::new(schema).unwrap();
1284 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1285 raw_query_params.insert("price".to_string(), vec!["not.a.number".to_string()]);
1286
1287 let result = validator.validate_and_extract(
1288 &json!({"price": "not.a.number"}),
1289 &raw_query_params,
1290 &HashMap::new(),
1291 &HashMap::new(),
1292 &HashMap::new(),
1293 );
1294
1295 assert!(result.is_err());
1296 let err = result.unwrap_err();
1297 assert_eq!(err.errors[0].error_type, "float_parsing");
1298 assert!(err.errors[0].msg.contains("valid number"));
1299 }
1300
1301 #[test]
1302 fn test_float_coercion_scientific_notation_success() {
1303 let schema = json!({
1304 "type": "object",
1305 "properties": {
1306 "value": {
1307 "type": "number",
1308 "source": "query"
1309 }
1310 },
1311 "required": ["value"]
1312 });
1313
1314 let validator = ParameterValidator::new(schema).unwrap();
1315 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1316 raw_query_params.insert("value".to_string(), vec!["1.5e10".to_string()]);
1317
1318 let result = validator.validate_and_extract(
1319 &json!({"value": 1.5e10}),
1320 &raw_query_params,
1321 &HashMap::new(),
1322 &HashMap::new(),
1323 &HashMap::new(),
1324 );
1325
1326 assert!(result.is_ok());
1327 let extracted = result.unwrap();
1328 assert_eq!(extracted["value"], json!(1.5e10));
1329 }
1330
1331 #[test]
1332 fn test_boolean_coercion_empty_string_returns_false() {
1333 let schema = json!({
1335 "type": "object",
1336 "properties": {
1337 "flag": {
1338 "type": "boolean",
1339 "source": "query"
1340 }
1341 },
1342 "required": ["flag"]
1343 });
1344
1345 let validator = ParameterValidator::new(schema).unwrap();
1346 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1347 raw_query_params.insert("flag".to_string(), vec![String::new()]);
1348
1349 let result = validator.validate_and_extract(
1350 &json!({"flag": ""}),
1351 &raw_query_params,
1352 &HashMap::new(),
1353 &HashMap::new(),
1354 &HashMap::new(),
1355 );
1356
1357 assert!(result.is_ok());
1358 let extracted = result.unwrap();
1359 assert_eq!(extracted["flag"], json!(false));
1360 }
1361
1362 #[test]
1363 fn test_boolean_coercion_whitespace_string_returns_error() {
1364 let schema = json!({
1365 "type": "object",
1366 "properties": {
1367 "flag": {
1368 "type": "boolean",
1369 "source": "query"
1370 }
1371 },
1372 "required": ["flag"]
1373 });
1374
1375 let validator = ParameterValidator::new(schema).unwrap();
1376 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1377 raw_query_params.insert("flag".to_string(), vec![" ".to_string()]);
1378
1379 let result = validator.validate_and_extract(
1380 &json!({"flag": " "}),
1381 &raw_query_params,
1382 &HashMap::new(),
1383 &HashMap::new(),
1384 &HashMap::new(),
1385 );
1386
1387 assert!(result.is_err(), "Whitespace-only string should fail boolean parsing");
1388 let err = result.unwrap_err();
1389 assert_eq!(err.errors[0].error_type, "bool_parsing");
1390 }
1391
1392 #[test]
1393 fn test_boolean_coercion_invalid_value_returns_error() {
1394 let schema = json!({
1395 "type": "object",
1396 "properties": {
1397 "enabled": {
1398 "type": "boolean",
1399 "source": "path"
1400 }
1401 },
1402 "required": ["enabled"]
1403 });
1404
1405 let validator = ParameterValidator::new(schema).unwrap();
1406 let mut path_params = HashMap::new();
1407 path_params.insert("enabled".to_string(), "maybe".to_string());
1408
1409 let result = validator.validate_and_extract(
1410 &json!({}),
1411 &HashMap::new(),
1412 &path_params,
1413 &HashMap::new(),
1414 &HashMap::new(),
1415 );
1416
1417 assert!(result.is_err());
1418 let err = result.unwrap_err();
1419 assert_eq!(err.errors[0].error_type, "bool_parsing");
1420 assert!(err.errors[0].msg.contains("valid boolean"));
1421 }
1422
1423 #[test]
1424 fn test_required_query_parameter_missing_returns_error() {
1425 let schema = json!({
1426 "type": "object",
1427 "properties": {
1428 "required_param": {
1429 "type": "string",
1430 "source": "query"
1431 }
1432 },
1433 "required": ["required_param"]
1434 });
1435
1436 let validator = ParameterValidator::new(schema).unwrap();
1437
1438 let result = validator.validate_and_extract(
1439 &json!({}),
1440 &HashMap::new(),
1441 &HashMap::new(),
1442 &HashMap::new(),
1443 &HashMap::new(),
1444 );
1445
1446 assert!(result.is_err());
1447 let err = result.unwrap_err();
1448 assert_eq!(err.errors[0].error_type, "missing");
1449 assert_eq!(
1450 err.errors[0].loc,
1451 vec!["query".to_string(), "required_param".to_string()]
1452 );
1453 assert!(err.errors[0].msg.contains("required"));
1454 }
1455
1456 #[test]
1457 fn test_required_path_parameter_missing_returns_error() {
1458 let schema = json!({
1459 "type": "object",
1460 "properties": {
1461 "user_id": {
1462 "type": "string",
1463 "source": "path"
1464 }
1465 },
1466 "required": ["user_id"]
1467 });
1468
1469 let validator = ParameterValidator::new(schema).unwrap();
1470
1471 let result = validator.validate_and_extract(
1472 &json!({}),
1473 &HashMap::new(),
1474 &HashMap::new(),
1475 &HashMap::new(),
1476 &HashMap::new(),
1477 );
1478
1479 assert!(result.is_err());
1480 let err = result.unwrap_err();
1481 assert_eq!(err.errors[0].error_type, "missing");
1482 assert_eq!(err.errors[0].loc, vec!["path".to_string(), "user_id".to_string()]);
1483 }
1484
1485 #[test]
1486 fn test_required_header_parameter_missing_returns_error() {
1487 let schema = json!({
1488 "type": "object",
1489 "properties": {
1490 "Authorization": {
1491 "type": "string",
1492 "source": "header"
1493 }
1494 },
1495 "required": ["Authorization"]
1496 });
1497
1498 let validator = ParameterValidator::new(schema).unwrap();
1499
1500 let result = validator.validate_and_extract(
1501 &json!({}),
1502 &HashMap::new(),
1503 &HashMap::new(),
1504 &HashMap::new(),
1505 &HashMap::new(),
1506 );
1507
1508 assert!(result.is_err());
1509 let err = result.unwrap_err();
1510 assert_eq!(err.errors[0].error_type, "missing");
1511 assert_eq!(
1512 err.errors[0].loc,
1513 vec!["headers".to_string(), "authorization".to_string()]
1514 );
1515 }
1516
1517 #[test]
1518 fn test_required_cookie_parameter_missing_returns_error() {
1519 let schema = json!({
1520 "type": "object",
1521 "properties": {
1522 "session_id": {
1523 "type": "string",
1524 "source": "cookie"
1525 }
1526 },
1527 "required": ["session_id"]
1528 });
1529
1530 let validator = ParameterValidator::new(schema).unwrap();
1531
1532 let result = validator.validate_and_extract(
1533 &json!({}),
1534 &HashMap::new(),
1535 &HashMap::new(),
1536 &HashMap::new(),
1537 &HashMap::new(),
1538 );
1539
1540 assert!(result.is_err());
1541 let err = result.unwrap_err();
1542 assert_eq!(err.errors[0].error_type, "missing");
1543 assert_eq!(err.errors[0].loc, vec!["cookie".to_string(), "session_id".to_string()]);
1544 }
1545
1546 #[test]
1547 fn test_optional_parameter_missing_succeeds() {
1548 let schema = json!({
1549 "type": "object",
1550 "properties": {
1551 "optional_param": {
1552 "type": "string",
1553 "source": "query",
1554 "optional": true
1555 }
1556 },
1557 "required": []
1558 });
1559
1560 let validator = ParameterValidator::new(schema).unwrap();
1561
1562 let result = validator.validate_and_extract(
1563 &json!({}),
1564 &HashMap::new(),
1565 &HashMap::new(),
1566 &HashMap::new(),
1567 &HashMap::new(),
1568 );
1569
1570 assert!(result.is_ok(), "Optional parameter should not cause error when missing");
1571 let extracted = result.unwrap();
1572 assert!(!extracted.as_object().unwrap().contains_key("optional_param"));
1573 }
1574
1575 #[test]
1576 fn test_uuid_validation_invalid_format_returns_error() {
1577 let schema = json!({
1578 "type": "object",
1579 "properties": {
1580 "id": {
1581 "type": "string",
1582 "format": "uuid",
1583 "source": "path"
1584 }
1585 },
1586 "required": ["id"]
1587 });
1588
1589 let validator = ParameterValidator::new(schema).unwrap();
1590 let mut path_params = HashMap::new();
1591 path_params.insert("id".to_string(), "not-a-uuid".to_string());
1592
1593 let result = validator.validate_and_extract(
1594 &json!({}),
1595 &HashMap::new(),
1596 &path_params,
1597 &HashMap::new(),
1598 &HashMap::new(),
1599 );
1600
1601 assert!(result.is_err());
1602 let err = result.unwrap_err();
1603 assert_eq!(err.errors[0].error_type, "uuid_parsing");
1604 assert!(err.errors[0].msg.contains("UUID"));
1605 }
1606
1607 #[test]
1608 fn test_uuid_validation_uppercase_succeeds() {
1609 let schema = json!({
1610 "type": "object",
1611 "properties": {
1612 "id": {
1613 "type": "string",
1614 "format": "uuid",
1615 "source": "query"
1616 }
1617 },
1618 "required": ["id"]
1619 });
1620
1621 let validator = ParameterValidator::new(schema).unwrap();
1622 let valid_uuid = "550e8400-e29b-41d4-a716-446655440000";
1623 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1624 raw_query_params.insert("id".to_string(), vec![valid_uuid.to_string()]);
1625
1626 let result = validator.validate_and_extract(
1627 &json!({"id": valid_uuid}),
1628 &raw_query_params,
1629 &HashMap::new(),
1630 &HashMap::new(),
1631 &HashMap::new(),
1632 );
1633
1634 assert!(result.is_ok());
1635 let extracted = result.unwrap();
1636 assert_eq!(extracted["id"], json!(valid_uuid));
1637 }
1638
1639 #[test]
1640 fn test_date_validation_invalid_format_returns_error() {
1641 let schema = json!({
1642 "type": "object",
1643 "properties": {
1644 "created_at": {
1645 "type": "string",
1646 "format": "date",
1647 "source": "query"
1648 }
1649 },
1650 "required": ["created_at"]
1651 });
1652
1653 let validator = ParameterValidator::new(schema).unwrap();
1654 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1655 raw_query_params.insert("created_at".to_string(), vec!["2024/12/10".to_string()]);
1656
1657 let result = validator.validate_and_extract(
1658 &json!({"created_at": "2024/12/10"}),
1659 &raw_query_params,
1660 &HashMap::new(),
1661 &HashMap::new(),
1662 &HashMap::new(),
1663 );
1664
1665 assert!(result.is_err());
1666 let err = result.unwrap_err();
1667 assert_eq!(err.errors[0].error_type, "date_parsing");
1668 assert!(err.errors[0].msg.contains("date"));
1669 }
1670
1671 #[test]
1672 fn test_date_validation_valid_iso_succeeds() {
1673 let schema = json!({
1674 "type": "object",
1675 "properties": {
1676 "created_at": {
1677 "type": "string",
1678 "format": "date",
1679 "source": "query"
1680 }
1681 },
1682 "required": ["created_at"]
1683 });
1684
1685 let validator = ParameterValidator::new(schema).unwrap();
1686 let valid_date = "2024-12-10";
1687 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1688 raw_query_params.insert("created_at".to_string(), vec![valid_date.to_string()]);
1689
1690 let result = validator.validate_and_extract(
1691 &json!({"created_at": valid_date}),
1692 &raw_query_params,
1693 &HashMap::new(),
1694 &HashMap::new(),
1695 &HashMap::new(),
1696 );
1697
1698 assert!(result.is_ok());
1699 let extracted = result.unwrap();
1700 assert_eq!(extracted["created_at"], json!(valid_date));
1701 }
1702
1703 #[test]
1704 fn test_datetime_validation_invalid_format_returns_error() {
1705 let schema = json!({
1706 "type": "object",
1707 "properties": {
1708 "timestamp": {
1709 "type": "string",
1710 "format": "date-time",
1711 "source": "query"
1712 }
1713 },
1714 "required": ["timestamp"]
1715 });
1716
1717 let validator = ParameterValidator::new(schema).unwrap();
1718 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1719 raw_query_params.insert("timestamp".to_string(), vec!["not-a-datetime".to_string()]);
1720
1721 let result = validator.validate_and_extract(
1722 &json!({"timestamp": "not-a-datetime"}),
1723 &raw_query_params,
1724 &HashMap::new(),
1725 &HashMap::new(),
1726 &HashMap::new(),
1727 );
1728
1729 assert!(result.is_err());
1730 let err = result.unwrap_err();
1731 assert_eq!(err.errors[0].error_type, "datetime_parsing");
1732 }
1733
1734 #[test]
1735 fn test_time_validation_invalid_format_returns_error() {
1736 let schema = json!({
1737 "type": "object",
1738 "properties": {
1739 "start_time": {
1740 "type": "string",
1741 "format": "time",
1742 "source": "query"
1743 }
1744 },
1745 "required": ["start_time"]
1746 });
1747
1748 let validator = ParameterValidator::new(schema).unwrap();
1749 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1750 raw_query_params.insert("start_time".to_string(), vec!["25:00:00".to_string()]);
1751
1752 let result = validator.validate_and_extract(
1753 &json!({"start_time": "25:00:00"}),
1754 &raw_query_params,
1755 &HashMap::new(),
1756 &HashMap::new(),
1757 &HashMap::new(),
1758 );
1759
1760 assert!(result.is_err());
1761 let err = result.unwrap_err();
1762 assert_eq!(err.errors[0].error_type, "time_parsing");
1763 }
1764
1765 #[test]
1766 fn test_time_validation_string_passthrough() {
1767 let schema = json!({
1768 "type": "object",
1769 "properties": {
1770 "start_time": {
1771 "type": "string",
1772 "source": "query"
1773 }
1774 },
1775 "required": ["start_time"]
1776 });
1777
1778 let validator = ParameterValidator::new(schema).unwrap();
1779 let time_string = "14:30:00";
1780 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1781 raw_query_params.insert("start_time".to_string(), vec![time_string.to_string()]);
1782
1783 let result = validator.validate_and_extract(
1784 &json!({"start_time": time_string}),
1785 &raw_query_params,
1786 &HashMap::new(),
1787 &HashMap::new(),
1788 &HashMap::new(),
1789 );
1790
1791 assert!(result.is_ok(), "String parameter should pass: {result:?}");
1792 let extracted = result.unwrap();
1793 assert_eq!(extracted["start_time"], json!(time_string));
1794 }
1795
1796 #[test]
1797 fn test_duration_validation_invalid_format_returns_error() {
1798 let schema = json!({
1799 "type": "object",
1800 "properties": {
1801 "timeout": {
1802 "type": "string",
1803 "format": "duration",
1804 "source": "query"
1805 }
1806 },
1807 "required": ["timeout"]
1808 });
1809
1810 let validator = ParameterValidator::new(schema).unwrap();
1811 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1812 raw_query_params.insert("timeout".to_string(), vec!["not-a-duration".to_string()]);
1813
1814 let result = validator.validate_and_extract(
1815 &json!({"timeout": "not-a-duration"}),
1816 &raw_query_params,
1817 &HashMap::new(),
1818 &HashMap::new(),
1819 &HashMap::new(),
1820 );
1821
1822 assert!(result.is_err());
1823 let err = result.unwrap_err();
1824 assert_eq!(err.errors[0].error_type, "duration_parsing");
1825 }
1826
1827 #[test]
1828 fn test_duration_validation_iso8601_succeeds() {
1829 let schema = json!({
1830 "type": "object",
1831 "properties": {
1832 "timeout": {
1833 "type": "string",
1834 "format": "duration",
1835 "source": "query"
1836 }
1837 },
1838 "required": ["timeout"]
1839 });
1840
1841 let validator = ParameterValidator::new(schema).unwrap();
1842 let valid_duration = "PT5M";
1843 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1844 raw_query_params.insert("timeout".to_string(), vec![valid_duration.to_string()]);
1845
1846 let result = validator.validate_and_extract(
1847 &json!({"timeout": valid_duration}),
1848 &raw_query_params,
1849 &HashMap::new(),
1850 &HashMap::new(),
1851 &HashMap::new(),
1852 );
1853
1854 assert!(result.is_ok());
1855 }
1856
1857 #[test]
1858 fn test_header_name_normalization_with_underscores() {
1859 let schema = json!({
1860 "type": "object",
1861 "properties": {
1862 "X_Custom_Header": {
1863 "type": "string",
1864 "source": "header"
1865 }
1866 },
1867 "required": ["X_Custom_Header"]
1868 });
1869
1870 let validator = ParameterValidator::new(schema).unwrap();
1871 let mut headers = HashMap::new();
1872 headers.insert("x-custom-header".to_string(), "value".to_string());
1873
1874 let result =
1875 validator.validate_and_extract(&json!({}), &HashMap::new(), &HashMap::new(), &headers, &HashMap::new());
1876
1877 assert!(result.is_ok());
1878 let extracted = result.unwrap();
1879 assert_eq!(extracted["X_Custom_Header"], json!("value"));
1880 }
1881
1882 #[test]
1883 fn test_multiple_query_parameter_values_uses_first() {
1884 let schema = json!({
1885 "type": "object",
1886 "properties": {
1887 "id": {
1888 "type": "integer",
1889 "source": "query"
1890 }
1891 },
1892 "required": ["id"]
1893 });
1894
1895 let validator = ParameterValidator::new(schema).unwrap();
1896 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1897 raw_query_params.insert("id".to_string(), vec!["123".to_string(), "456".to_string()]);
1898
1899 let result = validator.validate_and_extract(
1900 &json!({"id": [123, 456]}),
1901 &raw_query_params,
1902 &HashMap::new(),
1903 &HashMap::new(),
1904 &HashMap::new(),
1905 );
1906
1907 assert!(result.is_ok(), "Should accept first value of multiple query params");
1908 let extracted = result.unwrap();
1909 assert_eq!(extracted["id"], json!(123));
1910 }
1911
1912 #[test]
1913 fn test_schema_creation_missing_source_field_returns_error() {
1914 let schema = json!({
1915 "type": "object",
1916 "properties": {
1917 "param": {
1918 "type": "string"
1919 }
1920 },
1921 "required": []
1922 });
1923
1924 let result = ParameterValidator::new(schema);
1925 assert!(result.is_err(), "Schema without 'source' field should fail");
1926 let err_msg = result.unwrap_err();
1927 assert!(err_msg.contains("source"));
1928 }
1929
1930 #[test]
1931 fn test_schema_creation_invalid_source_value_returns_error() {
1932 let schema = json!({
1933 "type": "object",
1934 "properties": {
1935 "param": {
1936 "type": "string",
1937 "source": "invalid_source"
1938 }
1939 },
1940 "required": []
1941 });
1942
1943 let result = ParameterValidator::new(schema);
1944 assert!(result.is_err());
1945 let err_msg = result.unwrap_err();
1946 assert!(err_msg.contains("Invalid source"));
1947 }
1948
1949 #[test]
1950 fn test_multiple_errors_reported_together() {
1951 let schema = json!({
1952 "type": "object",
1953 "properties": {
1954 "count": {
1955 "type": "integer",
1956 "source": "query"
1957 },
1958 "user_id": {
1959 "type": "string",
1960 "source": "path"
1961 },
1962 "token": {
1963 "type": "string",
1964 "source": "header"
1965 }
1966 },
1967 "required": ["count", "user_id", "token"]
1968 });
1969
1970 let validator = ParameterValidator::new(schema).unwrap();
1971
1972 let result = validator.validate_and_extract(
1973 &json!({}),
1974 &HashMap::new(),
1975 &HashMap::new(),
1976 &HashMap::new(),
1977 &HashMap::new(),
1978 );
1979
1980 assert!(result.is_err());
1981 let err = result.unwrap_err();
1982 assert_eq!(err.errors.len(), 3);
1983 assert!(err.errors.iter().all(|e| e.error_type == "missing"));
1984 }
1985
1986 #[test]
1987 fn test_coercion_error_includes_original_value() {
1988 let schema = json!({
1989 "type": "object",
1990 "properties": {
1991 "age": {
1992 "type": "integer",
1993 "source": "query"
1994 }
1995 },
1996 "required": ["age"]
1997 });
1998
1999 let validator = ParameterValidator::new(schema).unwrap();
2000 let invalid_value = "not_an_int";
2001 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
2002 raw_query_params.insert("age".to_string(), vec![invalid_value.to_string()]);
2003
2004 let result = validator.validate_and_extract(
2005 &json!({"age": invalid_value}),
2006 &raw_query_params,
2007 &HashMap::new(),
2008 &HashMap::new(),
2009 &HashMap::new(),
2010 );
2011
2012 assert!(result.is_err());
2013 let err = result.unwrap_err();
2014 assert_eq!(err.errors[0].input, json!(invalid_value));
2015 }
2016
2017 #[test]
2018 fn test_string_parameter_passes_through() {
2019 let schema = json!({
2020 "type": "object",
2021 "properties": {
2022 "name": {
2023 "type": "string",
2024 "source": "query"
2025 }
2026 },
2027 "required": ["name"]
2028 });
2029
2030 let validator = ParameterValidator::new(schema).unwrap();
2031 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
2032 raw_query_params.insert("name".to_string(), vec!["Alice".to_string()]);
2033
2034 let result = validator.validate_and_extract(
2035 &json!({"name": "Alice"}),
2036 &raw_query_params,
2037 &HashMap::new(),
2038 &HashMap::new(),
2039 &HashMap::new(),
2040 );
2041
2042 assert!(result.is_ok());
2043 let extracted = result.unwrap();
2044 assert_eq!(extracted["name"], json!("Alice"));
2045 }
2046
2047 #[test]
2048 fn test_string_with_special_characters_passes_through() {
2049 let schema = json!({
2050 "type": "object",
2051 "properties": {
2052 "message": {
2053 "type": "string",
2054 "source": "query"
2055 }
2056 },
2057 "required": ["message"]
2058 });
2059
2060 let validator = ParameterValidator::new(schema).unwrap();
2061 let special_value = "Hello! @#$%^&*() Unicode: 你好";
2062 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
2063 raw_query_params.insert("message".to_string(), vec![special_value.to_string()]);
2064
2065 let result = validator.validate_and_extract(
2066 &json!({"message": special_value}),
2067 &raw_query_params,
2068 &HashMap::new(),
2069 &HashMap::new(),
2070 &HashMap::new(),
2071 );
2072
2073 assert!(result.is_ok());
2074 let extracted = result.unwrap();
2075 assert_eq!(extracted["message"], json!(special_value));
2076 }
2077
2078 #[test]
2079 fn test_array_query_parameter_missing_required_returns_error() {
2080 let schema = json!({
2081 "type": "object",
2082 "properties": {
2083 "ids": {
2084 "type": "array",
2085 "items": {"type": "integer"},
2086 "source": "query"
2087 }
2088 },
2089 "required": ["ids"]
2090 });
2091
2092 let validator = ParameterValidator::new(schema).unwrap();
2093
2094 let result = validator.validate_and_extract(
2095 &json!({}),
2096 &HashMap::new(),
2097 &HashMap::new(),
2098 &HashMap::new(),
2099 &HashMap::new(),
2100 );
2101
2102 assert!(result.is_err());
2103 let err = result.unwrap_err();
2104 assert_eq!(err.errors[0].error_type, "missing");
2105 }
2106
2107 #[test]
2108 fn test_empty_array_parameter_accepted() {
2109 let schema = json!({
2110 "type": "object",
2111 "properties": {
2112 "tags": {
2113 "type": "array",
2114 "items": {"type": "string"},
2115 "source": "query"
2116 }
2117 },
2118 "required": ["tags"]
2119 });
2120
2121 let validator = ParameterValidator::new(schema).unwrap();
2122
2123 let result = validator.validate_and_extract(
2124 &json!({"tags": []}),
2125 &HashMap::new(),
2126 &HashMap::new(),
2127 &HashMap::new(),
2128 &HashMap::new(),
2129 );
2130
2131 assert!(result.is_ok());
2132 let extracted = result.unwrap();
2133 assert_eq!(extracted["tags"], json!([]));
2134 }
2135
2136 #[test]
2137 fn test_parameter_source_from_str_query() {
2138 assert_eq!(ParameterSource::from_str("query"), Some(ParameterSource::Query));
2139 }
2140
2141 #[test]
2142 fn test_parameter_source_from_str_path() {
2143 assert_eq!(ParameterSource::from_str("path"), Some(ParameterSource::Path));
2144 }
2145
2146 #[test]
2147 fn test_parameter_source_from_str_header() {
2148 assert_eq!(ParameterSource::from_str("header"), Some(ParameterSource::Header));
2149 }
2150
2151 #[test]
2152 fn test_parameter_source_from_str_cookie() {
2153 assert_eq!(ParameterSource::from_str("cookie"), Some(ParameterSource::Cookie));
2154 }
2155
2156 #[test]
2157 fn test_parameter_source_from_str_invalid() {
2158 assert_eq!(ParameterSource::from_str("invalid"), None);
2159 }
2160
2161 #[test]
2162 fn test_integer_with_plus_sign() {
2163 let schema = json!({
2164 "type": "object",
2165 "properties": {
2166 "count": {
2167 "type": "integer",
2168 "source": "query"
2169 }
2170 },
2171 "required": ["count"]
2172 });
2173
2174 let validator = ParameterValidator::new(schema).unwrap();
2175 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
2176 raw_query_params.insert("count".to_string(), vec!["+123".to_string()]);
2177
2178 let result = validator.validate_and_extract(
2179 &json!({"count": "+123"}),
2180 &raw_query_params,
2181 &HashMap::new(),
2182 &HashMap::new(),
2183 &HashMap::new(),
2184 );
2185
2186 assert!(result.is_ok());
2187 let extracted = result.unwrap();
2188 assert_eq!(extracted["count"], json!(123));
2189 }
2190
2191 #[test]
2192 fn test_float_with_leading_dot() {
2193 let schema = json!({
2194 "type": "object",
2195 "properties": {
2196 "ratio": {
2197 "type": "number",
2198 "source": "query"
2199 }
2200 },
2201 "required": ["ratio"]
2202 });
2203
2204 let validator = ParameterValidator::new(schema).unwrap();
2205 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
2206 raw_query_params.insert("ratio".to_string(), vec![".5".to_string()]);
2207
2208 let result = validator.validate_and_extract(
2209 &json!({"ratio": 0.5}),
2210 &raw_query_params,
2211 &HashMap::new(),
2212 &HashMap::new(),
2213 &HashMap::new(),
2214 );
2215
2216 assert!(result.is_ok());
2217 let extracted = result.unwrap();
2218 assert_eq!(extracted["ratio"], json!(0.5));
2219 }
2220
2221 #[test]
2222 fn test_float_with_trailing_dot() {
2223 let schema = json!({
2224 "type": "object",
2225 "properties": {
2226 "value": {
2227 "type": "number",
2228 "source": "query"
2229 }
2230 },
2231 "required": ["value"]
2232 });
2233
2234 let validator = ParameterValidator::new(schema).unwrap();
2235 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
2236 raw_query_params.insert("value".to_string(), vec!["5.".to_string()]);
2237
2238 let result = validator.validate_and_extract(
2239 &json!({"value": 5.0}),
2240 &raw_query_params,
2241 &HashMap::new(),
2242 &HashMap::new(),
2243 &HashMap::new(),
2244 );
2245
2246 assert!(result.is_ok());
2247 }
2248
2249 #[test]
2250 fn test_boolean_case_insensitive_true() {
2251 let schema = json!({
2252 "type": "object",
2253 "properties": {
2254 "flag": {
2255 "type": "boolean",
2256 "source": "query"
2257 }
2258 },
2259 "required": ["flag"]
2260 });
2261
2262 let validator = ParameterValidator::new(schema).unwrap();
2263 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
2264 raw_query_params.insert("flag".to_string(), vec!["TrUe".to_string()]);
2265
2266 let result = validator.validate_and_extract(
2267 &json!({"flag": true}),
2268 &raw_query_params,
2269 &HashMap::new(),
2270 &HashMap::new(),
2271 &HashMap::new(),
2272 );
2273
2274 assert!(result.is_ok());
2275 let extracted = result.unwrap();
2276 assert_eq!(extracted["flag"], json!(true));
2277 }
2278
2279 #[test]
2280 fn test_boolean_case_insensitive_false() {
2281 let schema = json!({
2282 "type": "object",
2283 "properties": {
2284 "flag": {
2285 "type": "boolean",
2286 "source": "query"
2287 }
2288 },
2289 "required": ["flag"]
2290 });
2291
2292 let validator = ParameterValidator::new(schema).unwrap();
2293 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
2294 raw_query_params.insert("flag".to_string(), vec!["FaLsE".to_string()]);
2295
2296 let result = validator.validate_and_extract(
2297 &json!({"flag": false}),
2298 &raw_query_params,
2299 &HashMap::new(),
2300 &HashMap::new(),
2301 &HashMap::new(),
2302 );
2303
2304 assert!(result.is_ok());
2305 let extracted = result.unwrap();
2306 assert_eq!(extracted["flag"], json!(false));
2307 }
2308
2309 #[test]
2310 fn test_missing_required_header_uses_kebab_case_in_error_loc() {
2311 let schema = json!({
2312 "type": "object",
2313 "properties": {
2314 "x_api_key": {
2315 "type": "string",
2316 "source": "header"
2317 }
2318 },
2319 "required": ["x_api_key"]
2320 });
2321
2322 let validator = ParameterValidator::new(schema).unwrap();
2323
2324 let result = validator.validate_and_extract(
2325 &json!({}),
2326 &HashMap::new(),
2327 &HashMap::new(),
2328 &HashMap::new(),
2329 &HashMap::new(),
2330 );
2331
2332 assert!(result.is_err(), "expected missing header to fail");
2333 let err = result.unwrap_err();
2334 assert_eq!(err.errors.len(), 1);
2335 assert_eq!(err.errors[0].error_type, "missing");
2336 assert_eq!(err.errors[0].loc, vec!["headers".to_string(), "x-api-key".to_string()]);
2337 }
2338
2339 #[test]
2340 fn test_missing_required_cookie_reports_cookie_loc() {
2341 let schema = json!({
2342 "type": "object",
2343 "properties": {
2344 "session": {
2345 "type": "string",
2346 "source": "cookie"
2347 }
2348 },
2349 "required": ["session"]
2350 });
2351
2352 let validator = ParameterValidator::new(schema).unwrap();
2353
2354 let result = validator.validate_and_extract(
2355 &json!({}),
2356 &HashMap::new(),
2357 &HashMap::new(),
2358 &HashMap::new(),
2359 &HashMap::new(),
2360 );
2361
2362 assert!(result.is_err(), "expected missing cookie to fail");
2363 let err = result.unwrap_err();
2364 assert_eq!(err.errors.len(), 1);
2365 assert_eq!(err.errors[0].error_type, "missing");
2366 assert_eq!(err.errors[0].loc, vec!["cookie".to_string(), "session".to_string()]);
2367 }
2368
2369 #[test]
2370 fn test_query_boolean_empty_string_coerces_to_false() {
2371 let schema = json!({
2372 "type": "object",
2373 "properties": {
2374 "flag": {
2375 "type": "boolean",
2376 "source": "query"
2377 }
2378 },
2379 "required": ["flag"]
2380 });
2381
2382 let validator = ParameterValidator::new(schema).unwrap();
2383 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
2384 raw_query_params.insert("flag".to_string(), vec![String::new()]);
2385
2386 let result = validator.validate_and_extract(
2387 &json!({"flag": ""}),
2388 &raw_query_params,
2389 &HashMap::new(),
2390 &HashMap::new(),
2391 &HashMap::new(),
2392 );
2393
2394 assert!(result.is_ok(), "expected empty string to coerce");
2395 let extracted = result.unwrap();
2396 assert_eq!(extracted["flag"], json!(false));
2397 }
2398
2399 #[test]
2400 fn test_query_array_wraps_scalar_value_and_coerces_items() {
2401 let schema = json!({
2402 "type": "object",
2403 "properties": {
2404 "ids": {
2405 "type": "array",
2406 "items": {"type": "integer"},
2407 "source": "query"
2408 }
2409 },
2410 "required": ["ids"]
2411 });
2412
2413 let validator = ParameterValidator::new(schema).unwrap();
2414
2415 let result = validator.validate_and_extract(
2416 &json!({"ids": "1"}),
2417 &HashMap::new(),
2418 &HashMap::new(),
2419 &HashMap::new(),
2420 &HashMap::new(),
2421 );
2422
2423 assert!(result.is_ok(), "expected scalar query value to coerce into array");
2424 let extracted = result.unwrap();
2425 assert_eq!(extracted["ids"], json!([1]));
2426 }
2427
2428 #[test]
2429 fn test_query_array_invalid_item_returns_parsing_error() {
2430 let schema = json!({
2431 "type": "object",
2432 "properties": {
2433 "ids": {
2434 "type": "array",
2435 "items": {"type": "integer"},
2436 "source": "query"
2437 }
2438 },
2439 "required": ["ids"]
2440 });
2441
2442 let validator = ParameterValidator::new(schema).unwrap();
2443
2444 let result = validator.validate_and_extract(
2445 &json!({"ids": ["x"]}),
2446 &HashMap::new(),
2447 &HashMap::new(),
2448 &HashMap::new(),
2449 &HashMap::new(),
2450 );
2451
2452 assert!(result.is_err(), "expected invalid array item to fail");
2453 let err = result.unwrap_err();
2454 assert_eq!(err.errors.len(), 1);
2455 assert_eq!(err.errors[0].error_type, "int_parsing");
2456 assert_eq!(err.errors[0].loc, vec!["query".to_string(), "ids".to_string()]);
2457 }
2458
2459 #[test]
2460 fn test_uuid_date_datetime_time_and_duration_formats() {
2461 let schema = json!({
2462 "type": "object",
2463 "properties": {
2464 "id": {
2465 "type": "string",
2466 "format": "uuid",
2467 "source": "path"
2468 },
2469 "date": {
2470 "type": "string",
2471 "format": "date",
2472 "source": "query"
2473 },
2474 "dt": {
2475 "type": "string",
2476 "format": "date-time",
2477 "source": "query"
2478 },
2479 "time": {
2480 "type": "string",
2481 "format": "time",
2482 "source": "query"
2483 },
2484 "duration": {
2485 "type": "string",
2486 "format": "duration",
2487 "source": "query"
2488 }
2489 },
2490 "required": ["id", "date", "dt", "time", "duration"]
2491 });
2492
2493 let validator = ParameterValidator::new(schema).unwrap();
2494
2495 let mut path_params = HashMap::new();
2496 path_params.insert("id".to_string(), "550e8400-e29b-41d4-a716-446655440000".to_string());
2497
2498 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
2499 raw_query_params.insert("date".to_string(), vec!["2025-01-02".to_string()]);
2500 raw_query_params.insert("dt".to_string(), vec!["2025-01-02T03:04:05Z".to_string()]);
2501 raw_query_params.insert("time".to_string(), vec!["03:04:05Z".to_string()]);
2502 raw_query_params.insert("duration".to_string(), vec!["PT1S".to_string()]);
2503
2504 let result = validator.validate_and_extract(
2505 &json!({
2506 "date": "2025-01-02",
2507 "dt": "2025-01-02T03:04:05Z",
2508 "time": "03:04:05Z",
2509 "duration": "PT1S"
2510 }),
2511 &raw_query_params,
2512 &path_params,
2513 &HashMap::new(),
2514 &HashMap::new(),
2515 );
2516 assert!(result.is_ok(), "expected all format values to validate: {result:?}");
2517 }
2518
2519 #[test]
2520 fn test_optional_fields_are_not_required_in_validation_schema() {
2521 let schema = json!({
2522 "type": "object",
2523 "properties": {
2524 "maybe": {
2525 "type": "string",
2526 "source": "query",
2527 "optional": true
2528 }
2529 },
2530 "required": ["maybe"]
2531 });
2532
2533 let validator = ParameterValidator::new(schema).unwrap();
2534 let result = validator.validate_and_extract(
2535 &json!({}),
2536 &HashMap::new(),
2537 &HashMap::new(),
2538 &HashMap::new(),
2539 &HashMap::new(),
2540 );
2541
2542 assert!(result.is_ok(), "optional field in required list should not fail");
2543 let extracted = result.unwrap();
2544 assert_eq!(extracted, json!({}));
2545 }
2546}