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!(
962 result.is_ok(),
963 "Validation should succeed for boolean true: {:?}",
964 result
965 );
966 let params = result.unwrap();
967 assert_eq!(params, json!({"flag": true}));
968
969 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
970 raw_query_params.insert("flag".to_string(), vec!["false".to_string()]);
971 let query_params = json!({"flag": false});
972 let result = validator.validate_and_extract(
973 &query_params,
974 &raw_query_params,
975 &path_params,
976 &HashMap::new(),
977 &HashMap::new(),
978 );
979 assert!(
980 result.is_ok(),
981 "Validation should succeed for boolean false: {:?}",
982 result
983 );
984 let params = result.unwrap();
985 assert_eq!(params, json!({"flag": false}));
986 }
987
988 #[test]
989 fn test_integer_coercion_invalid_format_returns_error() {
990 let schema = json!({
991 "type": "object",
992 "properties": {
993 "count": {
994 "type": "integer",
995 "source": "query"
996 }
997 },
998 "required": ["count"]
999 });
1000
1001 let validator = ParameterValidator::new(schema).unwrap();
1002 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1003 raw_query_params.insert("count".to_string(), vec!["not_a_number".to_string()]);
1004
1005 let result = validator.validate_and_extract(
1006 &json!({"count": "not_a_number"}),
1007 &raw_query_params,
1008 &HashMap::new(),
1009 &HashMap::new(),
1010 &HashMap::new(),
1011 );
1012
1013 assert!(result.is_err(), "Should fail to coerce non-integer string");
1014 let err = result.unwrap_err();
1015 assert_eq!(err.errors.len(), 1);
1016 assert_eq!(err.errors[0].error_type, "int_parsing");
1017 assert_eq!(err.errors[0].loc, vec!["query".to_string(), "count".to_string()]);
1018 assert!(err.errors[0].msg.contains("valid integer"));
1019 }
1020
1021 #[test]
1022 fn test_integer_coercion_with_letters_mixed_returns_error() {
1023 let schema = json!({
1024 "type": "object",
1025 "properties": {
1026 "id": {
1027 "type": "integer",
1028 "source": "path"
1029 }
1030 },
1031 "required": ["id"]
1032 });
1033
1034 let validator = ParameterValidator::new(schema).unwrap();
1035 let mut path_params = HashMap::new();
1036 path_params.insert("id".to_string(), "123abc".to_string());
1037
1038 let result = validator.validate_and_extract(
1039 &json!({}),
1040 &HashMap::new(),
1041 &path_params,
1042 &HashMap::new(),
1043 &HashMap::new(),
1044 );
1045
1046 assert!(result.is_err());
1047 let err = result.unwrap_err();
1048 assert_eq!(err.errors[0].error_type, "int_parsing");
1049 }
1050
1051 #[test]
1052 fn test_integer_coercion_overflow_returns_error() {
1053 let schema = json!({
1054 "type": "object",
1055 "properties": {
1056 "big_num": {
1057 "type": "integer",
1058 "source": "query"
1059 }
1060 },
1061 "required": ["big_num"]
1062 });
1063
1064 let validator = ParameterValidator::new(schema).unwrap();
1065 let too_large = "9223372036854775808";
1066 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1067 raw_query_params.insert("big_num".to_string(), vec![too_large.to_string()]);
1068
1069 let result = validator.validate_and_extract(
1070 &json!({"big_num": too_large}),
1071 &raw_query_params,
1072 &HashMap::new(),
1073 &HashMap::new(),
1074 &HashMap::new(),
1075 );
1076
1077 assert!(result.is_err(), "Should fail on integer overflow");
1078 let err = result.unwrap_err();
1079 assert_eq!(err.errors[0].error_type, "int_parsing");
1080 }
1081
1082 #[test]
1083 fn test_integer_coercion_negative_overflow_returns_error() {
1084 let schema = json!({
1085 "type": "object",
1086 "properties": {
1087 "small_num": {
1088 "type": "integer",
1089 "source": "query"
1090 }
1091 },
1092 "required": ["small_num"]
1093 });
1094
1095 let validator = ParameterValidator::new(schema).unwrap();
1096 let too_small = "-9223372036854775809";
1097 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1098 raw_query_params.insert("small_num".to_string(), vec![too_small.to_string()]);
1099
1100 let result = validator.validate_and_extract(
1101 &json!({"small_num": too_small}),
1102 &raw_query_params,
1103 &HashMap::new(),
1104 &HashMap::new(),
1105 &HashMap::new(),
1106 );
1107
1108 assert!(result.is_err());
1109 let err = result.unwrap_err();
1110 assert_eq!(err.errors[0].error_type, "int_parsing");
1111 }
1112
1113 #[test]
1114 fn test_optional_field_overrides_required_list() {
1115 let schema = json!({
1116 "type": "object",
1117 "properties": {
1118 "maybe": {
1119 "type": "string",
1120 "source": "query",
1121 "optional": true
1122 }
1123 },
1124 "required": ["maybe"]
1125 });
1126
1127 let validator = ParameterValidator::new(schema).unwrap();
1128
1129 let result = validator.validate_and_extract(
1130 &json!({}),
1131 &HashMap::new(),
1132 &HashMap::new(),
1133 &HashMap::new(),
1134 &HashMap::new(),
1135 );
1136
1137 assert!(result.is_ok(), "optional required field should not error: {result:?}");
1138 assert_eq!(result.unwrap(), json!({}));
1139 }
1140
1141 #[test]
1142 fn test_header_name_is_normalized_for_lookup_and_errors() {
1143 let schema = json!({
1144 "type": "object",
1145 "properties": {
1146 "x_request_id": {
1147 "type": "string",
1148 "source": "header"
1149 }
1150 },
1151 "required": ["x_request_id"]
1152 });
1153
1154 let validator = ParameterValidator::new(schema).unwrap();
1155
1156 let mut headers = HashMap::new();
1157 headers.insert("x-request-id".to_string(), "abc123".to_string());
1158
1159 let ok = validator
1160 .validate_and_extract(&json!({}), &HashMap::new(), &HashMap::new(), &headers, &HashMap::new())
1161 .unwrap();
1162 assert_eq!(ok, json!({"x_request_id": "abc123"}));
1163
1164 let err = validator
1165 .validate_and_extract(
1166 &json!({}),
1167 &HashMap::new(),
1168 &HashMap::new(),
1169 &HashMap::new(),
1170 &HashMap::new(),
1171 )
1172 .unwrap_err();
1173 assert_eq!(
1174 err.errors[0].loc,
1175 vec!["headers".to_string(), "x-request-id".to_string()]
1176 );
1177 }
1178
1179 #[test]
1180 fn test_boolean_empty_string_coerces_to_false() {
1181 let schema = json!({
1182 "type": "object",
1183 "properties": {
1184 "flag": {
1185 "type": "boolean",
1186 "source": "query"
1187 }
1188 },
1189 "required": ["flag"]
1190 });
1191
1192 let validator = ParameterValidator::new(schema).unwrap();
1193 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1194 raw_query_params.insert("flag".to_string(), vec!["".to_string()]);
1195
1196 let result = validator
1197 .validate_and_extract(
1198 &json!({"flag": ""}),
1199 &raw_query_params,
1200 &HashMap::new(),
1201 &HashMap::new(),
1202 &HashMap::new(),
1203 )
1204 .unwrap();
1205 assert_eq!(result, json!({"flag": false}));
1206 }
1207
1208 #[test]
1209 fn test_uuid_format_validation_returns_uuid_parsing_error() {
1210 let schema = json!({
1211 "type": "object",
1212 "properties": {
1213 "id": {
1214 "type": "string",
1215 "format": "uuid",
1216 "source": "query"
1217 }
1218 },
1219 "required": ["id"]
1220 });
1221
1222 let validator = ParameterValidator::new(schema).unwrap();
1223 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1224 raw_query_params.insert("id".to_string(), vec!["not-a-uuid".to_string()]);
1225
1226 let err = validator
1227 .validate_and_extract(
1228 &json!({"id": "not-a-uuid"}),
1229 &raw_query_params,
1230 &HashMap::new(),
1231 &HashMap::new(),
1232 &HashMap::new(),
1233 )
1234 .unwrap_err();
1235
1236 assert_eq!(err.errors[0].error_type, "uuid_parsing");
1237 assert!(
1238 err.errors[0].msg.contains("valid UUID"),
1239 "msg was {}",
1240 err.errors[0].msg
1241 );
1242 }
1243
1244 #[test]
1245 fn test_array_query_parameter_coercion_error_reports_item_parse_failure() {
1246 let schema = json!({
1247 "type": "object",
1248 "properties": {
1249 "ids": {
1250 "type": "array",
1251 "items": {"type": "integer"},
1252 "source": "query"
1253 }
1254 },
1255 "required": ["ids"]
1256 });
1257
1258 let validator = ParameterValidator::new(schema).unwrap();
1259 let query_params = json!({ "ids": ["nope"] });
1260
1261 let err = validator
1262 .validate_and_extract(
1263 &query_params,
1264 &HashMap::new(),
1265 &HashMap::new(),
1266 &HashMap::new(),
1267 &HashMap::new(),
1268 )
1269 .unwrap_err();
1270
1271 assert_eq!(err.errors[0].error_type, "int_parsing");
1272 assert_eq!(err.errors[0].loc, vec!["query".to_string(), "ids".to_string()]);
1273 }
1274
1275 #[test]
1276 fn test_float_coercion_invalid_format_returns_error() {
1277 let schema = json!({
1278 "type": "object",
1279 "properties": {
1280 "price": {
1281 "type": "number",
1282 "source": "query"
1283 }
1284 },
1285 "required": ["price"]
1286 });
1287
1288 let validator = ParameterValidator::new(schema).unwrap();
1289 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1290 raw_query_params.insert("price".to_string(), vec!["not.a.number".to_string()]);
1291
1292 let result = validator.validate_and_extract(
1293 &json!({"price": "not.a.number"}),
1294 &raw_query_params,
1295 &HashMap::new(),
1296 &HashMap::new(),
1297 &HashMap::new(),
1298 );
1299
1300 assert!(result.is_err());
1301 let err = result.unwrap_err();
1302 assert_eq!(err.errors[0].error_type, "float_parsing");
1303 assert!(err.errors[0].msg.contains("valid number"));
1304 }
1305
1306 #[test]
1307 fn test_float_coercion_scientific_notation_success() {
1308 let schema = json!({
1309 "type": "object",
1310 "properties": {
1311 "value": {
1312 "type": "number",
1313 "source": "query"
1314 }
1315 },
1316 "required": ["value"]
1317 });
1318
1319 let validator = ParameterValidator::new(schema).unwrap();
1320 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1321 raw_query_params.insert("value".to_string(), vec!["1.5e10".to_string()]);
1322
1323 let result = validator.validate_and_extract(
1324 &json!({"value": 1.5e10}),
1325 &raw_query_params,
1326 &HashMap::new(),
1327 &HashMap::new(),
1328 &HashMap::new(),
1329 );
1330
1331 assert!(result.is_ok());
1332 let extracted = result.unwrap();
1333 assert_eq!(extracted["value"], json!(1.5e10));
1334 }
1335
1336 #[test]
1337 fn test_boolean_coercion_empty_string_returns_false() {
1338 let schema = json!({
1340 "type": "object",
1341 "properties": {
1342 "flag": {
1343 "type": "boolean",
1344 "source": "query"
1345 }
1346 },
1347 "required": ["flag"]
1348 });
1349
1350 let validator = ParameterValidator::new(schema).unwrap();
1351 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1352 raw_query_params.insert("flag".to_string(), vec!["".to_string()]);
1353
1354 let result = validator.validate_and_extract(
1355 &json!({"flag": ""}),
1356 &raw_query_params,
1357 &HashMap::new(),
1358 &HashMap::new(),
1359 &HashMap::new(),
1360 );
1361
1362 assert!(result.is_ok());
1363 let extracted = result.unwrap();
1364 assert_eq!(extracted["flag"], json!(false));
1365 }
1366
1367 #[test]
1368 fn test_boolean_coercion_whitespace_string_returns_error() {
1369 let schema = json!({
1370 "type": "object",
1371 "properties": {
1372 "flag": {
1373 "type": "boolean",
1374 "source": "query"
1375 }
1376 },
1377 "required": ["flag"]
1378 });
1379
1380 let validator = ParameterValidator::new(schema).unwrap();
1381 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1382 raw_query_params.insert("flag".to_string(), vec![" ".to_string()]);
1383
1384 let result = validator.validate_and_extract(
1385 &json!({"flag": " "}),
1386 &raw_query_params,
1387 &HashMap::new(),
1388 &HashMap::new(),
1389 &HashMap::new(),
1390 );
1391
1392 assert!(result.is_err(), "Whitespace-only string should fail boolean parsing");
1393 let err = result.unwrap_err();
1394 assert_eq!(err.errors[0].error_type, "bool_parsing");
1395 }
1396
1397 #[test]
1398 fn test_boolean_coercion_invalid_value_returns_error() {
1399 let schema = json!({
1400 "type": "object",
1401 "properties": {
1402 "enabled": {
1403 "type": "boolean",
1404 "source": "path"
1405 }
1406 },
1407 "required": ["enabled"]
1408 });
1409
1410 let validator = ParameterValidator::new(schema).unwrap();
1411 let mut path_params = HashMap::new();
1412 path_params.insert("enabled".to_string(), "maybe".to_string());
1413
1414 let result = validator.validate_and_extract(
1415 &json!({}),
1416 &HashMap::new(),
1417 &path_params,
1418 &HashMap::new(),
1419 &HashMap::new(),
1420 );
1421
1422 assert!(result.is_err());
1423 let err = result.unwrap_err();
1424 assert_eq!(err.errors[0].error_type, "bool_parsing");
1425 assert!(err.errors[0].msg.contains("valid boolean"));
1426 }
1427
1428 #[test]
1429 fn test_required_query_parameter_missing_returns_error() {
1430 let schema = json!({
1431 "type": "object",
1432 "properties": {
1433 "required_param": {
1434 "type": "string",
1435 "source": "query"
1436 }
1437 },
1438 "required": ["required_param"]
1439 });
1440
1441 let validator = ParameterValidator::new(schema).unwrap();
1442
1443 let result = validator.validate_and_extract(
1444 &json!({}),
1445 &HashMap::new(),
1446 &HashMap::new(),
1447 &HashMap::new(),
1448 &HashMap::new(),
1449 );
1450
1451 assert!(result.is_err());
1452 let err = result.unwrap_err();
1453 assert_eq!(err.errors[0].error_type, "missing");
1454 assert_eq!(
1455 err.errors[0].loc,
1456 vec!["query".to_string(), "required_param".to_string()]
1457 );
1458 assert!(err.errors[0].msg.contains("required"));
1459 }
1460
1461 #[test]
1462 fn test_required_path_parameter_missing_returns_error() {
1463 let schema = json!({
1464 "type": "object",
1465 "properties": {
1466 "user_id": {
1467 "type": "string",
1468 "source": "path"
1469 }
1470 },
1471 "required": ["user_id"]
1472 });
1473
1474 let validator = ParameterValidator::new(schema).unwrap();
1475
1476 let result = validator.validate_and_extract(
1477 &json!({}),
1478 &HashMap::new(),
1479 &HashMap::new(),
1480 &HashMap::new(),
1481 &HashMap::new(),
1482 );
1483
1484 assert!(result.is_err());
1485 let err = result.unwrap_err();
1486 assert_eq!(err.errors[0].error_type, "missing");
1487 assert_eq!(err.errors[0].loc, vec!["path".to_string(), "user_id".to_string()]);
1488 }
1489
1490 #[test]
1491 fn test_required_header_parameter_missing_returns_error() {
1492 let schema = json!({
1493 "type": "object",
1494 "properties": {
1495 "Authorization": {
1496 "type": "string",
1497 "source": "header"
1498 }
1499 },
1500 "required": ["Authorization"]
1501 });
1502
1503 let validator = ParameterValidator::new(schema).unwrap();
1504
1505 let result = validator.validate_and_extract(
1506 &json!({}),
1507 &HashMap::new(),
1508 &HashMap::new(),
1509 &HashMap::new(),
1510 &HashMap::new(),
1511 );
1512
1513 assert!(result.is_err());
1514 let err = result.unwrap_err();
1515 assert_eq!(err.errors[0].error_type, "missing");
1516 assert_eq!(
1517 err.errors[0].loc,
1518 vec!["headers".to_string(), "authorization".to_string()]
1519 );
1520 }
1521
1522 #[test]
1523 fn test_required_cookie_parameter_missing_returns_error() {
1524 let schema = json!({
1525 "type": "object",
1526 "properties": {
1527 "session_id": {
1528 "type": "string",
1529 "source": "cookie"
1530 }
1531 },
1532 "required": ["session_id"]
1533 });
1534
1535 let validator = ParameterValidator::new(schema).unwrap();
1536
1537 let result = validator.validate_and_extract(
1538 &json!({}),
1539 &HashMap::new(),
1540 &HashMap::new(),
1541 &HashMap::new(),
1542 &HashMap::new(),
1543 );
1544
1545 assert!(result.is_err());
1546 let err = result.unwrap_err();
1547 assert_eq!(err.errors[0].error_type, "missing");
1548 assert_eq!(err.errors[0].loc, vec!["cookie".to_string(), "session_id".to_string()]);
1549 }
1550
1551 #[test]
1552 fn test_optional_parameter_missing_succeeds() {
1553 let schema = json!({
1554 "type": "object",
1555 "properties": {
1556 "optional_param": {
1557 "type": "string",
1558 "source": "query",
1559 "optional": true
1560 }
1561 },
1562 "required": []
1563 });
1564
1565 let validator = ParameterValidator::new(schema).unwrap();
1566
1567 let result = validator.validate_and_extract(
1568 &json!({}),
1569 &HashMap::new(),
1570 &HashMap::new(),
1571 &HashMap::new(),
1572 &HashMap::new(),
1573 );
1574
1575 assert!(result.is_ok(), "Optional parameter should not cause error when missing");
1576 let extracted = result.unwrap();
1577 assert!(!extracted.as_object().unwrap().contains_key("optional_param"));
1578 }
1579
1580 #[test]
1581 fn test_uuid_validation_invalid_format_returns_error() {
1582 let schema = json!({
1583 "type": "object",
1584 "properties": {
1585 "id": {
1586 "type": "string",
1587 "format": "uuid",
1588 "source": "path"
1589 }
1590 },
1591 "required": ["id"]
1592 });
1593
1594 let validator = ParameterValidator::new(schema).unwrap();
1595 let mut path_params = HashMap::new();
1596 path_params.insert("id".to_string(), "not-a-uuid".to_string());
1597
1598 let result = validator.validate_and_extract(
1599 &json!({}),
1600 &HashMap::new(),
1601 &path_params,
1602 &HashMap::new(),
1603 &HashMap::new(),
1604 );
1605
1606 assert!(result.is_err());
1607 let err = result.unwrap_err();
1608 assert_eq!(err.errors[0].error_type, "uuid_parsing");
1609 assert!(err.errors[0].msg.contains("UUID"));
1610 }
1611
1612 #[test]
1613 fn test_uuid_validation_uppercase_succeeds() {
1614 let schema = json!({
1615 "type": "object",
1616 "properties": {
1617 "id": {
1618 "type": "string",
1619 "format": "uuid",
1620 "source": "query"
1621 }
1622 },
1623 "required": ["id"]
1624 });
1625
1626 let validator = ParameterValidator::new(schema).unwrap();
1627 let valid_uuid = "550e8400-e29b-41d4-a716-446655440000";
1628 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1629 raw_query_params.insert("id".to_string(), vec![valid_uuid.to_string()]);
1630
1631 let result = validator.validate_and_extract(
1632 &json!({"id": valid_uuid}),
1633 &raw_query_params,
1634 &HashMap::new(),
1635 &HashMap::new(),
1636 &HashMap::new(),
1637 );
1638
1639 assert!(result.is_ok());
1640 let extracted = result.unwrap();
1641 assert_eq!(extracted["id"], json!(valid_uuid));
1642 }
1643
1644 #[test]
1645 fn test_date_validation_invalid_format_returns_error() {
1646 let schema = json!({
1647 "type": "object",
1648 "properties": {
1649 "created_at": {
1650 "type": "string",
1651 "format": "date",
1652 "source": "query"
1653 }
1654 },
1655 "required": ["created_at"]
1656 });
1657
1658 let validator = ParameterValidator::new(schema).unwrap();
1659 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1660 raw_query_params.insert("created_at".to_string(), vec!["2024/12/10".to_string()]);
1661
1662 let result = validator.validate_and_extract(
1663 &json!({"created_at": "2024/12/10"}),
1664 &raw_query_params,
1665 &HashMap::new(),
1666 &HashMap::new(),
1667 &HashMap::new(),
1668 );
1669
1670 assert!(result.is_err());
1671 let err = result.unwrap_err();
1672 assert_eq!(err.errors[0].error_type, "date_parsing");
1673 assert!(err.errors[0].msg.contains("date"));
1674 }
1675
1676 #[test]
1677 fn test_date_validation_valid_iso_succeeds() {
1678 let schema = json!({
1679 "type": "object",
1680 "properties": {
1681 "created_at": {
1682 "type": "string",
1683 "format": "date",
1684 "source": "query"
1685 }
1686 },
1687 "required": ["created_at"]
1688 });
1689
1690 let validator = ParameterValidator::new(schema).unwrap();
1691 let valid_date = "2024-12-10";
1692 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1693 raw_query_params.insert("created_at".to_string(), vec![valid_date.to_string()]);
1694
1695 let result = validator.validate_and_extract(
1696 &json!({"created_at": valid_date}),
1697 &raw_query_params,
1698 &HashMap::new(),
1699 &HashMap::new(),
1700 &HashMap::new(),
1701 );
1702
1703 assert!(result.is_ok());
1704 let extracted = result.unwrap();
1705 assert_eq!(extracted["created_at"], json!(valid_date));
1706 }
1707
1708 #[test]
1709 fn test_datetime_validation_invalid_format_returns_error() {
1710 let schema = json!({
1711 "type": "object",
1712 "properties": {
1713 "timestamp": {
1714 "type": "string",
1715 "format": "date-time",
1716 "source": "query"
1717 }
1718 },
1719 "required": ["timestamp"]
1720 });
1721
1722 let validator = ParameterValidator::new(schema).unwrap();
1723 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1724 raw_query_params.insert("timestamp".to_string(), vec!["not-a-datetime".to_string()]);
1725
1726 let result = validator.validate_and_extract(
1727 &json!({"timestamp": "not-a-datetime"}),
1728 &raw_query_params,
1729 &HashMap::new(),
1730 &HashMap::new(),
1731 &HashMap::new(),
1732 );
1733
1734 assert!(result.is_err());
1735 let err = result.unwrap_err();
1736 assert_eq!(err.errors[0].error_type, "datetime_parsing");
1737 }
1738
1739 #[test]
1740 fn test_time_validation_invalid_format_returns_error() {
1741 let schema = json!({
1742 "type": "object",
1743 "properties": {
1744 "start_time": {
1745 "type": "string",
1746 "format": "time",
1747 "source": "query"
1748 }
1749 },
1750 "required": ["start_time"]
1751 });
1752
1753 let validator = ParameterValidator::new(schema).unwrap();
1754 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1755 raw_query_params.insert("start_time".to_string(), vec!["25:00:00".to_string()]);
1756
1757 let result = validator.validate_and_extract(
1758 &json!({"start_time": "25:00:00"}),
1759 &raw_query_params,
1760 &HashMap::new(),
1761 &HashMap::new(),
1762 &HashMap::new(),
1763 );
1764
1765 assert!(result.is_err());
1766 let err = result.unwrap_err();
1767 assert_eq!(err.errors[0].error_type, "time_parsing");
1768 }
1769
1770 #[test]
1771 fn test_time_validation_string_passthrough() {
1772 let schema = json!({
1773 "type": "object",
1774 "properties": {
1775 "start_time": {
1776 "type": "string",
1777 "source": "query"
1778 }
1779 },
1780 "required": ["start_time"]
1781 });
1782
1783 let validator = ParameterValidator::new(schema).unwrap();
1784 let time_string = "14:30:00";
1785 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1786 raw_query_params.insert("start_time".to_string(), vec![time_string.to_string()]);
1787
1788 let result = validator.validate_and_extract(
1789 &json!({"start_time": time_string}),
1790 &raw_query_params,
1791 &HashMap::new(),
1792 &HashMap::new(),
1793 &HashMap::new(),
1794 );
1795
1796 assert!(result.is_ok(), "String parameter should pass: {:?}", result);
1797 let extracted = result.unwrap();
1798 assert_eq!(extracted["start_time"], json!(time_string));
1799 }
1800
1801 #[test]
1802 fn test_duration_validation_invalid_format_returns_error() {
1803 let schema = json!({
1804 "type": "object",
1805 "properties": {
1806 "timeout": {
1807 "type": "string",
1808 "format": "duration",
1809 "source": "query"
1810 }
1811 },
1812 "required": ["timeout"]
1813 });
1814
1815 let validator = ParameterValidator::new(schema).unwrap();
1816 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1817 raw_query_params.insert("timeout".to_string(), vec!["not-a-duration".to_string()]);
1818
1819 let result = validator.validate_and_extract(
1820 &json!({"timeout": "not-a-duration"}),
1821 &raw_query_params,
1822 &HashMap::new(),
1823 &HashMap::new(),
1824 &HashMap::new(),
1825 );
1826
1827 assert!(result.is_err());
1828 let err = result.unwrap_err();
1829 assert_eq!(err.errors[0].error_type, "duration_parsing");
1830 }
1831
1832 #[test]
1833 fn test_duration_validation_iso8601_succeeds() {
1834 let schema = json!({
1835 "type": "object",
1836 "properties": {
1837 "timeout": {
1838 "type": "string",
1839 "format": "duration",
1840 "source": "query"
1841 }
1842 },
1843 "required": ["timeout"]
1844 });
1845
1846 let validator = ParameterValidator::new(schema).unwrap();
1847 let valid_duration = "PT5M";
1848 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1849 raw_query_params.insert("timeout".to_string(), vec![valid_duration.to_string()]);
1850
1851 let result = validator.validate_and_extract(
1852 &json!({"timeout": valid_duration}),
1853 &raw_query_params,
1854 &HashMap::new(),
1855 &HashMap::new(),
1856 &HashMap::new(),
1857 );
1858
1859 assert!(result.is_ok());
1860 }
1861
1862 #[test]
1863 fn test_header_name_normalization_with_underscores() {
1864 let schema = json!({
1865 "type": "object",
1866 "properties": {
1867 "X_Custom_Header": {
1868 "type": "string",
1869 "source": "header"
1870 }
1871 },
1872 "required": ["X_Custom_Header"]
1873 });
1874
1875 let validator = ParameterValidator::new(schema).unwrap();
1876 let mut headers = HashMap::new();
1877 headers.insert("x-custom-header".to_string(), "value".to_string());
1878
1879 let result =
1880 validator.validate_and_extract(&json!({}), &HashMap::new(), &HashMap::new(), &headers, &HashMap::new());
1881
1882 assert!(result.is_ok());
1883 let extracted = result.unwrap();
1884 assert_eq!(extracted["X_Custom_Header"], json!("value"));
1885 }
1886
1887 #[test]
1888 fn test_multiple_query_parameter_values_uses_first() {
1889 let schema = json!({
1890 "type": "object",
1891 "properties": {
1892 "id": {
1893 "type": "integer",
1894 "source": "query"
1895 }
1896 },
1897 "required": ["id"]
1898 });
1899
1900 let validator = ParameterValidator::new(schema).unwrap();
1901 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1902 raw_query_params.insert("id".to_string(), vec!["123".to_string(), "456".to_string()]);
1903
1904 let result = validator.validate_and_extract(
1905 &json!({"id": [123, 456]}),
1906 &raw_query_params,
1907 &HashMap::new(),
1908 &HashMap::new(),
1909 &HashMap::new(),
1910 );
1911
1912 assert!(result.is_ok(), "Should accept first value of multiple query params");
1913 let extracted = result.unwrap();
1914 assert_eq!(extracted["id"], json!(123));
1915 }
1916
1917 #[test]
1918 fn test_schema_creation_missing_source_field_returns_error() {
1919 let schema = json!({
1920 "type": "object",
1921 "properties": {
1922 "param": {
1923 "type": "string"
1924 }
1925 },
1926 "required": []
1927 });
1928
1929 let result = ParameterValidator::new(schema);
1930 assert!(result.is_err(), "Schema without 'source' field should fail");
1931 let err_msg = result.unwrap_err();
1932 assert!(err_msg.contains("source"));
1933 }
1934
1935 #[test]
1936 fn test_schema_creation_invalid_source_value_returns_error() {
1937 let schema = json!({
1938 "type": "object",
1939 "properties": {
1940 "param": {
1941 "type": "string",
1942 "source": "invalid_source"
1943 }
1944 },
1945 "required": []
1946 });
1947
1948 let result = ParameterValidator::new(schema);
1949 assert!(result.is_err());
1950 let err_msg = result.unwrap_err();
1951 assert!(err_msg.contains("Invalid source"));
1952 }
1953
1954 #[test]
1955 fn test_multiple_errors_reported_together() {
1956 let schema = json!({
1957 "type": "object",
1958 "properties": {
1959 "count": {
1960 "type": "integer",
1961 "source": "query"
1962 },
1963 "user_id": {
1964 "type": "string",
1965 "source": "path"
1966 },
1967 "token": {
1968 "type": "string",
1969 "source": "header"
1970 }
1971 },
1972 "required": ["count", "user_id", "token"]
1973 });
1974
1975 let validator = ParameterValidator::new(schema).unwrap();
1976
1977 let result = validator.validate_and_extract(
1978 &json!({}),
1979 &HashMap::new(),
1980 &HashMap::new(),
1981 &HashMap::new(),
1982 &HashMap::new(),
1983 );
1984
1985 assert!(result.is_err());
1986 let err = result.unwrap_err();
1987 assert_eq!(err.errors.len(), 3);
1988 assert!(err.errors.iter().all(|e| e.error_type == "missing"));
1989 }
1990
1991 #[test]
1992 fn test_coercion_error_includes_original_value() {
1993 let schema = json!({
1994 "type": "object",
1995 "properties": {
1996 "age": {
1997 "type": "integer",
1998 "source": "query"
1999 }
2000 },
2001 "required": ["age"]
2002 });
2003
2004 let validator = ParameterValidator::new(schema).unwrap();
2005 let invalid_value = "not_an_int";
2006 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
2007 raw_query_params.insert("age".to_string(), vec![invalid_value.to_string()]);
2008
2009 let result = validator.validate_and_extract(
2010 &json!({"age": invalid_value}),
2011 &raw_query_params,
2012 &HashMap::new(),
2013 &HashMap::new(),
2014 &HashMap::new(),
2015 );
2016
2017 assert!(result.is_err());
2018 let err = result.unwrap_err();
2019 assert_eq!(err.errors[0].input, json!(invalid_value));
2020 }
2021
2022 #[test]
2023 fn test_string_parameter_passes_through() {
2024 let schema = json!({
2025 "type": "object",
2026 "properties": {
2027 "name": {
2028 "type": "string",
2029 "source": "query"
2030 }
2031 },
2032 "required": ["name"]
2033 });
2034
2035 let validator = ParameterValidator::new(schema).unwrap();
2036 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
2037 raw_query_params.insert("name".to_string(), vec!["Alice".to_string()]);
2038
2039 let result = validator.validate_and_extract(
2040 &json!({"name": "Alice"}),
2041 &raw_query_params,
2042 &HashMap::new(),
2043 &HashMap::new(),
2044 &HashMap::new(),
2045 );
2046
2047 assert!(result.is_ok());
2048 let extracted = result.unwrap();
2049 assert_eq!(extracted["name"], json!("Alice"));
2050 }
2051
2052 #[test]
2053 fn test_string_with_special_characters_passes_through() {
2054 let schema = json!({
2055 "type": "object",
2056 "properties": {
2057 "message": {
2058 "type": "string",
2059 "source": "query"
2060 }
2061 },
2062 "required": ["message"]
2063 });
2064
2065 let validator = ParameterValidator::new(schema).unwrap();
2066 let special_value = "Hello! @#$%^&*() Unicode: 你好";
2067 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
2068 raw_query_params.insert("message".to_string(), vec![special_value.to_string()]);
2069
2070 let result = validator.validate_and_extract(
2071 &json!({"message": special_value}),
2072 &raw_query_params,
2073 &HashMap::new(),
2074 &HashMap::new(),
2075 &HashMap::new(),
2076 );
2077
2078 assert!(result.is_ok());
2079 let extracted = result.unwrap();
2080 assert_eq!(extracted["message"], json!(special_value));
2081 }
2082
2083 #[test]
2084 fn test_array_query_parameter_missing_required_returns_error() {
2085 let schema = json!({
2086 "type": "object",
2087 "properties": {
2088 "ids": {
2089 "type": "array",
2090 "items": {"type": "integer"},
2091 "source": "query"
2092 }
2093 },
2094 "required": ["ids"]
2095 });
2096
2097 let validator = ParameterValidator::new(schema).unwrap();
2098
2099 let result = validator.validate_and_extract(
2100 &json!({}),
2101 &HashMap::new(),
2102 &HashMap::new(),
2103 &HashMap::new(),
2104 &HashMap::new(),
2105 );
2106
2107 assert!(result.is_err());
2108 let err = result.unwrap_err();
2109 assert_eq!(err.errors[0].error_type, "missing");
2110 }
2111
2112 #[test]
2113 fn test_empty_array_parameter_accepted() {
2114 let schema = json!({
2115 "type": "object",
2116 "properties": {
2117 "tags": {
2118 "type": "array",
2119 "items": {"type": "string"},
2120 "source": "query"
2121 }
2122 },
2123 "required": ["tags"]
2124 });
2125
2126 let validator = ParameterValidator::new(schema).unwrap();
2127
2128 let result = validator.validate_and_extract(
2129 &json!({"tags": []}),
2130 &HashMap::new(),
2131 &HashMap::new(),
2132 &HashMap::new(),
2133 &HashMap::new(),
2134 );
2135
2136 assert!(result.is_ok());
2137 let extracted = result.unwrap();
2138 assert_eq!(extracted["tags"], json!([]));
2139 }
2140
2141 #[test]
2142 fn test_parameter_source_from_str_query() {
2143 assert_eq!(ParameterSource::from_str("query"), Some(ParameterSource::Query));
2144 }
2145
2146 #[test]
2147 fn test_parameter_source_from_str_path() {
2148 assert_eq!(ParameterSource::from_str("path"), Some(ParameterSource::Path));
2149 }
2150
2151 #[test]
2152 fn test_parameter_source_from_str_header() {
2153 assert_eq!(ParameterSource::from_str("header"), Some(ParameterSource::Header));
2154 }
2155
2156 #[test]
2157 fn test_parameter_source_from_str_cookie() {
2158 assert_eq!(ParameterSource::from_str("cookie"), Some(ParameterSource::Cookie));
2159 }
2160
2161 #[test]
2162 fn test_parameter_source_from_str_invalid() {
2163 assert_eq!(ParameterSource::from_str("invalid"), None);
2164 }
2165
2166 #[test]
2167 fn test_integer_with_plus_sign() {
2168 let schema = json!({
2169 "type": "object",
2170 "properties": {
2171 "count": {
2172 "type": "integer",
2173 "source": "query"
2174 }
2175 },
2176 "required": ["count"]
2177 });
2178
2179 let validator = ParameterValidator::new(schema).unwrap();
2180 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
2181 raw_query_params.insert("count".to_string(), vec!["+123".to_string()]);
2182
2183 let result = validator.validate_and_extract(
2184 &json!({"count": "+123"}),
2185 &raw_query_params,
2186 &HashMap::new(),
2187 &HashMap::new(),
2188 &HashMap::new(),
2189 );
2190
2191 assert!(result.is_ok());
2192 let extracted = result.unwrap();
2193 assert_eq!(extracted["count"], json!(123));
2194 }
2195
2196 #[test]
2197 fn test_float_with_leading_dot() {
2198 let schema = json!({
2199 "type": "object",
2200 "properties": {
2201 "ratio": {
2202 "type": "number",
2203 "source": "query"
2204 }
2205 },
2206 "required": ["ratio"]
2207 });
2208
2209 let validator = ParameterValidator::new(schema).unwrap();
2210 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
2211 raw_query_params.insert("ratio".to_string(), vec![".5".to_string()]);
2212
2213 let result = validator.validate_and_extract(
2214 &json!({"ratio": 0.5}),
2215 &raw_query_params,
2216 &HashMap::new(),
2217 &HashMap::new(),
2218 &HashMap::new(),
2219 );
2220
2221 assert!(result.is_ok());
2222 let extracted = result.unwrap();
2223 assert_eq!(extracted["ratio"], json!(0.5));
2224 }
2225
2226 #[test]
2227 fn test_float_with_trailing_dot() {
2228 let schema = json!({
2229 "type": "object",
2230 "properties": {
2231 "value": {
2232 "type": "number",
2233 "source": "query"
2234 }
2235 },
2236 "required": ["value"]
2237 });
2238
2239 let validator = ParameterValidator::new(schema).unwrap();
2240 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
2241 raw_query_params.insert("value".to_string(), vec!["5.".to_string()]);
2242
2243 let result = validator.validate_and_extract(
2244 &json!({"value": 5.0}),
2245 &raw_query_params,
2246 &HashMap::new(),
2247 &HashMap::new(),
2248 &HashMap::new(),
2249 );
2250
2251 assert!(result.is_ok());
2252 }
2253
2254 #[test]
2255 fn test_boolean_case_insensitive_true() {
2256 let schema = json!({
2257 "type": "object",
2258 "properties": {
2259 "flag": {
2260 "type": "boolean",
2261 "source": "query"
2262 }
2263 },
2264 "required": ["flag"]
2265 });
2266
2267 let validator = ParameterValidator::new(schema).unwrap();
2268 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
2269 raw_query_params.insert("flag".to_string(), vec!["TrUe".to_string()]);
2270
2271 let result = validator.validate_and_extract(
2272 &json!({"flag": true}),
2273 &raw_query_params,
2274 &HashMap::new(),
2275 &HashMap::new(),
2276 &HashMap::new(),
2277 );
2278
2279 assert!(result.is_ok());
2280 let extracted = result.unwrap();
2281 assert_eq!(extracted["flag"], json!(true));
2282 }
2283
2284 #[test]
2285 fn test_boolean_case_insensitive_false() {
2286 let schema = json!({
2287 "type": "object",
2288 "properties": {
2289 "flag": {
2290 "type": "boolean",
2291 "source": "query"
2292 }
2293 },
2294 "required": ["flag"]
2295 });
2296
2297 let validator = ParameterValidator::new(schema).unwrap();
2298 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
2299 raw_query_params.insert("flag".to_string(), vec!["FaLsE".to_string()]);
2300
2301 let result = validator.validate_and_extract(
2302 &json!({"flag": false}),
2303 &raw_query_params,
2304 &HashMap::new(),
2305 &HashMap::new(),
2306 &HashMap::new(),
2307 );
2308
2309 assert!(result.is_ok());
2310 let extracted = result.unwrap();
2311 assert_eq!(extracted["flag"], json!(false));
2312 }
2313
2314 #[test]
2315 fn test_missing_required_header_uses_kebab_case_in_error_loc() {
2316 let schema = json!({
2317 "type": "object",
2318 "properties": {
2319 "x_api_key": {
2320 "type": "string",
2321 "source": "header"
2322 }
2323 },
2324 "required": ["x_api_key"]
2325 });
2326
2327 let validator = ParameterValidator::new(schema).unwrap();
2328
2329 let result = validator.validate_and_extract(
2330 &json!({}),
2331 &HashMap::new(),
2332 &HashMap::new(),
2333 &HashMap::new(),
2334 &HashMap::new(),
2335 );
2336
2337 assert!(result.is_err(), "expected missing header to fail");
2338 let err = result.unwrap_err();
2339 assert_eq!(err.errors.len(), 1);
2340 assert_eq!(err.errors[0].error_type, "missing");
2341 assert_eq!(err.errors[0].loc, vec!["headers".to_string(), "x-api-key".to_string()]);
2342 }
2343
2344 #[test]
2345 fn test_missing_required_cookie_reports_cookie_loc() {
2346 let schema = json!({
2347 "type": "object",
2348 "properties": {
2349 "session": {
2350 "type": "string",
2351 "source": "cookie"
2352 }
2353 },
2354 "required": ["session"]
2355 });
2356
2357 let validator = ParameterValidator::new(schema).unwrap();
2358
2359 let result = validator.validate_and_extract(
2360 &json!({}),
2361 &HashMap::new(),
2362 &HashMap::new(),
2363 &HashMap::new(),
2364 &HashMap::new(),
2365 );
2366
2367 assert!(result.is_err(), "expected missing cookie to fail");
2368 let err = result.unwrap_err();
2369 assert_eq!(err.errors.len(), 1);
2370 assert_eq!(err.errors[0].error_type, "missing");
2371 assert_eq!(err.errors[0].loc, vec!["cookie".to_string(), "session".to_string()]);
2372 }
2373
2374 #[test]
2375 fn test_query_boolean_empty_string_coerces_to_false() {
2376 let schema = json!({
2377 "type": "object",
2378 "properties": {
2379 "flag": {
2380 "type": "boolean",
2381 "source": "query"
2382 }
2383 },
2384 "required": ["flag"]
2385 });
2386
2387 let validator = ParameterValidator::new(schema).unwrap();
2388 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
2389 raw_query_params.insert("flag".to_string(), vec!["".to_string()]);
2390
2391 let result = validator.validate_and_extract(
2392 &json!({"flag": ""}),
2393 &raw_query_params,
2394 &HashMap::new(),
2395 &HashMap::new(),
2396 &HashMap::new(),
2397 );
2398
2399 assert!(result.is_ok(), "expected empty string to coerce");
2400 let extracted = result.unwrap();
2401 assert_eq!(extracted["flag"], json!(false));
2402 }
2403
2404 #[test]
2405 fn test_query_array_wraps_scalar_value_and_coerces_items() {
2406 let schema = json!({
2407 "type": "object",
2408 "properties": {
2409 "ids": {
2410 "type": "array",
2411 "items": {"type": "integer"},
2412 "source": "query"
2413 }
2414 },
2415 "required": ["ids"]
2416 });
2417
2418 let validator = ParameterValidator::new(schema).unwrap();
2419
2420 let result = validator.validate_and_extract(
2421 &json!({"ids": "1"}),
2422 &HashMap::new(),
2423 &HashMap::new(),
2424 &HashMap::new(),
2425 &HashMap::new(),
2426 );
2427
2428 assert!(result.is_ok(), "expected scalar query value to coerce into array");
2429 let extracted = result.unwrap();
2430 assert_eq!(extracted["ids"], json!([1]));
2431 }
2432
2433 #[test]
2434 fn test_query_array_invalid_item_returns_parsing_error() {
2435 let schema = json!({
2436 "type": "object",
2437 "properties": {
2438 "ids": {
2439 "type": "array",
2440 "items": {"type": "integer"},
2441 "source": "query"
2442 }
2443 },
2444 "required": ["ids"]
2445 });
2446
2447 let validator = ParameterValidator::new(schema).unwrap();
2448
2449 let result = validator.validate_and_extract(
2450 &json!({"ids": ["x"]}),
2451 &HashMap::new(),
2452 &HashMap::new(),
2453 &HashMap::new(),
2454 &HashMap::new(),
2455 );
2456
2457 assert!(result.is_err(), "expected invalid array item to fail");
2458 let err = result.unwrap_err();
2459 assert_eq!(err.errors.len(), 1);
2460 assert_eq!(err.errors[0].error_type, "int_parsing");
2461 assert_eq!(err.errors[0].loc, vec!["query".to_string(), "ids".to_string()]);
2462 }
2463
2464 #[test]
2465 fn test_uuid_date_datetime_time_and_duration_formats() {
2466 let schema = json!({
2467 "type": "object",
2468 "properties": {
2469 "id": {
2470 "type": "string",
2471 "format": "uuid",
2472 "source": "path"
2473 },
2474 "date": {
2475 "type": "string",
2476 "format": "date",
2477 "source": "query"
2478 },
2479 "dt": {
2480 "type": "string",
2481 "format": "date-time",
2482 "source": "query"
2483 },
2484 "time": {
2485 "type": "string",
2486 "format": "time",
2487 "source": "query"
2488 },
2489 "duration": {
2490 "type": "string",
2491 "format": "duration",
2492 "source": "query"
2493 }
2494 },
2495 "required": ["id", "date", "dt", "time", "duration"]
2496 });
2497
2498 let validator = ParameterValidator::new(schema).unwrap();
2499
2500 let mut path_params = HashMap::new();
2501 path_params.insert("id".to_string(), "550e8400-e29b-41d4-a716-446655440000".to_string());
2502
2503 let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
2504 raw_query_params.insert("date".to_string(), vec!["2025-01-02".to_string()]);
2505 raw_query_params.insert("dt".to_string(), vec!["2025-01-02T03:04:05Z".to_string()]);
2506 raw_query_params.insert("time".to_string(), vec!["03:04:05Z".to_string()]);
2507 raw_query_params.insert("duration".to_string(), vec!["PT1S".to_string()]);
2508
2509 let result = validator.validate_and_extract(
2510 &json!({
2511 "date": "2025-01-02",
2512 "dt": "2025-01-02T03:04:05Z",
2513 "time": "03:04:05Z",
2514 "duration": "PT1S"
2515 }),
2516 &raw_query_params,
2517 &path_params,
2518 &HashMap::new(),
2519 &HashMap::new(),
2520 );
2521 assert!(result.is_ok(), "expected all format values to validate: {result:?}");
2522 }
2523
2524 #[test]
2525 fn test_optional_fields_are_not_required_in_validation_schema() {
2526 let schema = json!({
2527 "type": "object",
2528 "properties": {
2529 "maybe": {
2530 "type": "string",
2531 "source": "query",
2532 "optional": true
2533 }
2534 },
2535 "required": ["maybe"]
2536 });
2537
2538 let validator = ParameterValidator::new(schema).unwrap();
2539 let result = validator.validate_and_extract(
2540 &json!({}),
2541 &HashMap::new(),
2542 &HashMap::new(),
2543 &HashMap::new(),
2544 &HashMap::new(),
2545 );
2546
2547 assert!(result.is_ok(), "optional field in required list should not fail");
2548 let extracted = result.unwrap();
2549 assert_eq!(extracted, json!({}));
2550 }
2551}