Skip to main content

spikard_core/
parameters.rs

1//! Parameter validation using JSON Schema
2//!
3//! This module provides validation for request parameters (query, path, header, cookie)
4//! using JSON Schema as the validation contract.
5
6use crate::validation::{SchemaValidator, ValidationError, ValidationErrorDetail};
7use serde_json::{Value, json};
8use std::collections::HashMap;
9use std::fmt;
10use std::sync::Arc;
11
12/// Parameter source - where the parameter comes from
13#[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/// Parameter definition extracted from schema
34#[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/// Parameter validator that uses JSON Schema
53#[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    /// Create a new parameter validator from a JSON Schema
69    ///
70    /// The schema should describe all parameters with their types and constraints.
71    /// Each property MUST have a "source" field indicating where the parameter comes from.
72    ///
73    /// # Errors
74    /// Returns an error if the schema is invalid or malformed.
75    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    /// Whether this validator needs access to request headers.
94    #[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    /// Whether this validator needs access to request cookies.
103    #[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    /// Whether the validator has any parameter definitions.
112    #[must_use]
113    pub fn has_params(&self) -> bool {
114        !self.inner.parameter_defs.is_empty()
115    }
116
117    /// Determine whether a parameter schema needs full JSON Schema validation.
118    ///
119    /// The hot path in `validate_and_extract()` already enforces:
120    /// - required vs optional presence
121    /// - type coercion
122    /// - format parsing (uuid/date/date-time/time/duration)
123    ///
124    /// When the schema contains only structural keywords and metadata, we can skip the
125    /// (relatively expensive) jsonschema validator without changing semantics.
126    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                    // Structural keywords we support in the coercion pass, and metadata keywords.
135                    "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                    // Anything else may impose constraints we don't enforce manually.
152                    _ => return true,
153                }
154
155                if recurse(child) {
156                    return true;
157                }
158            }
159
160            false
161        }
162
163        recurse(schema)
164    }
165
166    /// Extract parameter definitions from the schema
167    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    /// Get the underlying JSON Schema
228    #[must_use]
229    pub fn schema(&self) -> &Value {
230        &self.inner.schema
231    }
232
233    /// Validate and extract parameters from the request
234    ///
235    /// This builds a JSON object from query/path/header/cookie params and validates it.
236    /// It performs type coercion (e.g., "123" → 123) based on the schema.
237    ///
238    /// Returns the validated JSON object that can be directly converted to Python kwargs.
239    ///
240    /// # Errors
241    /// Returns a validation error if parameter validation fails.
242    // reason: too_many_lines — multi-source parameter validation (query/path/header/cookie)
243    // with per-type coercion cannot be meaningfully split without losing context.
244    #[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(&param_def.lookup_key);
258                let query_value = query_params.get(&param_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(&param_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(&param_def.name);
330
331                    // reason: option_if_let_else — the Some arm has a loop with early-return
332                    // logic that does not fit neatly into a closure.
333                    #[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(&params_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(&param_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(&param_def.lookup_key)
523                .and_then(|values| values.first())
524                .map(String::as_str),
525            ParameterSource::Path => path_params.get(&param_def.lookup_key).map(String::as_str),
526            ParameterSource::Header => headers.get(&param_def.lookup_key).map(String::as_str),
527            ParameterSource::Cookie => cookies.get(&param_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    /// Coerce a string value to the expected JSON type
552    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    /// Validate ISO 8601 date format: YYYY-MM-DD
606    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    /// Validate ISO 8601 datetime format
613    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    /// Validate ISO 8601 time format: HH:MM:SS or HH:MM:SS.ffffff
621    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    /// Validate duration format (simplified - accept ISO 8601 duration or simple formats)
679    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    /// Validate UUID format
687    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    /// Create a validation schema without the "source" fields
697    /// (JSON Schema doesn't recognize "source" as a standard field)
698    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        // BUG: Empty string returns false instead of error - this is behavior to verify
1340        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}