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