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