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::debug_log_module;
7use crate::validation::{ValidationError, ValidationErrorDetail};
8use serde_json::{Value, json};
9use std::collections::HashMap;
10
11/// Parameter source - where the parameter comes from
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum ParameterSource {
14    Query,
15    Path,
16    Header,
17    Cookie,
18}
19
20impl ParameterSource {
21    fn from_str(s: &str) -> Option<Self> {
22        match s {
23            "query" => Some(Self::Query),
24            "path" => Some(Self::Path),
25            "header" => Some(Self::Header),
26            "cookie" => Some(Self::Cookie),
27            _ => None,
28        }
29    }
30}
31
32/// Parameter definition extracted from schema
33#[derive(Debug, Clone)]
34struct ParameterDef {
35    name: String,
36    source: ParameterSource,
37    expected_type: Option<String>,
38    format: Option<String>,
39    required: bool,
40}
41
42/// Parameter validator that uses JSON Schema
43#[derive(Clone, Debug)]
44pub struct ParameterValidator {
45    schema: Value,
46    parameter_defs: Vec<ParameterDef>,
47}
48
49impl ParameterValidator {
50    /// Create a new parameter validator from a JSON Schema
51    ///
52    /// The schema should describe all parameters with their types and constraints.
53    /// Each property MUST have a "source" field indicating where the parameter comes from.
54    pub fn new(schema: Value) -> Result<Self, String> {
55        let parameter_defs = Self::extract_parameter_defs(&schema)?;
56
57        Ok(Self { schema, parameter_defs })
58    }
59
60    /// Extract parameter definitions from the schema
61    fn extract_parameter_defs(schema: &Value) -> Result<Vec<ParameterDef>, String> {
62        let mut defs = Vec::new();
63
64        let properties = schema
65            .get("properties")
66            .and_then(|p| p.as_object())
67            .cloned()
68            .unwrap_or_default();
69
70        let required_list = schema
71            .get("required")
72            .and_then(|r| r.as_array())
73            .map(|arr| arr.iter().filter_map(|v| v.as_str()).collect::<Vec<_>>())
74            .unwrap_or_default();
75
76        for (name, prop) in properties {
77            let source_str = prop.get("source").and_then(|s| s.as_str()).ok_or_else(|| {
78                anyhow::anyhow!("Invalid parameter schema")
79                    .context(format!("Parameter '{}' missing required 'source' field", name))
80                    .to_string()
81            })?;
82
83            let source = ParameterSource::from_str(source_str).ok_or_else(|| {
84                anyhow::anyhow!("Invalid parameter schema")
85                    .context(format!(
86                        "Invalid source '{}' for parameter '{}' (expected: query, path, header, or cookie)",
87                        source_str, name
88                    ))
89                    .to_string()
90            })?;
91
92            let expected_type = prop.get("type").and_then(|t| t.as_str()).map(String::from);
93            let format = prop.get("format").and_then(|f| f.as_str()).map(String::from);
94
95            let is_optional = prop.get("optional").and_then(|v| v.as_bool()).unwrap_or(false);
96            let required = required_list.contains(&name.as_str()) && !is_optional;
97
98            defs.push(ParameterDef {
99                name: name.clone(),
100                source,
101                expected_type,
102                format,
103                required,
104            });
105        }
106
107        Ok(defs)
108    }
109
110    /// Get the underlying JSON Schema
111    pub fn schema(&self) -> &Value {
112        &self.schema
113    }
114
115    /// Validate and extract parameters from the request
116    ///
117    /// This builds a JSON object from query/path/header/cookie params and validates it.
118    /// It performs type coercion (e.g., "123" → 123) based on the schema.
119    ///
120    /// Returns the validated JSON object that can be directly converted to Python kwargs.
121    pub fn validate_and_extract(
122        &self,
123        query_params: &Value,
124        raw_query_params: &HashMap<String, Vec<String>>,
125        path_params: &HashMap<String, String>,
126        headers: &HashMap<String, String>,
127        cookies: &HashMap<String, String>,
128    ) -> Result<Value, ValidationError> {
129        tracing::debug!(
130            "validate_and_extract called with query_params: {:?}, path_params: {:?}, headers: {} items, cookies: {} items",
131            query_params,
132            path_params,
133            headers.len(),
134            cookies.len()
135        );
136        tracing::debug!("parameter_defs count: {}", self.parameter_defs.len());
137
138        let mut params_map = serde_json::Map::new();
139        let mut errors = Vec::new();
140        let mut raw_values_map: HashMap<String, String> = HashMap::new();
141
142        for param_def in &self.parameter_defs {
143            tracing::debug!(
144                "Processing param: {:?}, source: {:?}, required: {}, expected_type: {:?}",
145                param_def.name,
146                param_def.source,
147                param_def.required,
148                param_def.expected_type
149            );
150
151            if param_def.source == ParameterSource::Query && param_def.expected_type.as_deref() == Some("array") {
152                let query_value = query_params.get(&param_def.name);
153
154                if param_def.required && query_value.is_none() {
155                    errors.push(ValidationErrorDetail {
156                        error_type: "missing".to_string(),
157                        loc: vec!["query".to_string(), param_def.name.clone()],
158                        msg: "Field required".to_string(),
159                        input: Value::Null,
160                        ctx: None,
161                    });
162                    continue;
163                }
164
165                if let Some(value) = query_value {
166                    let array_value = if value.is_array() {
167                        value.clone()
168                    } else {
169                        Value::Array(vec![value.clone()])
170                    };
171                    let (item_type, item_format) = self.array_item_type_and_format(&param_def.name);
172
173                    let coerced_items = match array_value.as_array() {
174                        Some(items) => {
175                            let mut out = Vec::with_capacity(items.len());
176                            for item in items {
177                                if let Some(text) = item.as_str() {
178                                    match Self::coerce_value(text, item_type, item_format) {
179                                        Ok(coerced) => out.push(coerced),
180                                        Err(e) => {
181                                            errors.push(ValidationErrorDetail {
182                                                error_type: match item_type {
183                                                    Some("integer") => "int_parsing".to_string(),
184                                                    Some("number") => "float_parsing".to_string(),
185                                                    Some("boolean") => "bool_parsing".to_string(),
186                                                    Some("string") => match item_format {
187                                                        Some("uuid") => "uuid_parsing".to_string(),
188                                                        Some("date") => "date_parsing".to_string(),
189                                                        Some("date-time") => "datetime_parsing".to_string(),
190                                                        Some("time") => "time_parsing".to_string(),
191                                                        Some("duration") => "duration_parsing".to_string(),
192                                                        _ => "type_error".to_string(),
193                                                    },
194                                                    _ => "type_error".to_string(),
195                                                },
196                                                loc: vec!["query".to_string(), param_def.name.clone()],
197                                                msg: match item_type {
198                                                    Some("integer") => "Input should be a valid integer, unable to parse string as an integer".to_string(),
199                                                    Some("number") => "Input should be a valid number, unable to parse string as a number".to_string(),
200                                                    Some("boolean") => "Input should be a valid boolean, unable to interpret input".to_string(),
201                                                    Some("string") => match item_format {
202                                                        Some("uuid") => format!("Input should be a valid UUID, {}", e),
203                                                        Some("date") => format!("Input should be a valid date, {}", e),
204                                                        Some("date-time") => format!("Input should be a valid datetime, {}", e),
205                                                        Some("time") => format!("Input should be a valid time, {}", e),
206                                                        Some("duration") => format!("Input should be a valid duration, {}", e),
207                                                        _ => e.clone(),
208                                                    },
209                                                    _ => e.clone(),
210                                                },
211                                                input: Value::String(text.to_string()),
212                                                ctx: None,
213                                            });
214                                        }
215                                    }
216                                } else {
217                                    out.push(item.clone());
218                                }
219                            }
220                            out
221                        }
222                        None => Vec::new(),
223                    };
224
225                    params_map.insert(param_def.name.clone(), Value::Array(coerced_items));
226                }
227                continue;
228            }
229
230            let raw_value_string = match param_def.source {
231                ParameterSource::Query => raw_query_params
232                    .get(&param_def.name)
233                    .and_then(|values| values.first())
234                    .map(String::as_str),
235                ParameterSource::Path => path_params.get(&param_def.name).map(String::as_str),
236                ParameterSource::Header => {
237                    let header_name = param_def.name.replace('_', "-").to_lowercase();
238                    headers.get(&header_name).map(String::as_str)
239                }
240                ParameterSource::Cookie => cookies.get(&param_def.name).map(String::as_str),
241            };
242
243            tracing::debug!("raw_value_string for {}: {:?}", param_def.name, raw_value_string);
244
245            if param_def.required && raw_value_string.is_none() {
246                let source_str = match param_def.source {
247                    ParameterSource::Query => "query",
248                    ParameterSource::Path => "path",
249                    ParameterSource::Header => "headers",
250                    ParameterSource::Cookie => "cookie",
251                };
252                let param_name_for_error = if param_def.source == ParameterSource::Header {
253                    param_def.name.replace('_', "-").to_lowercase()
254                } else {
255                    param_def.name.clone()
256                };
257                errors.push(ValidationErrorDetail {
258                    error_type: "missing".to_string(),
259                    loc: vec![source_str.to_string(), param_name_for_error],
260                    msg: "Field required".to_string(),
261                    input: Value::Null,
262                    ctx: None,
263                });
264                continue;
265            }
266
267            if let Some(value_str) = raw_value_string {
268                tracing::debug!(
269                    "Coercing value '{}' to type {:?} with format {:?}",
270                    value_str,
271                    param_def.expected_type,
272                    param_def.format
273                );
274                match Self::coerce_value(
275                    value_str,
276                    param_def.expected_type.as_deref(),
277                    param_def.format.as_deref(),
278                ) {
279                    Ok(coerced) => {
280                        tracing::debug!("Coerced to: {:?}", coerced);
281                        params_map.insert(param_def.name.clone(), coerced);
282                        raw_values_map.insert(param_def.name.clone(), value_str.to_string());
283                    }
284                    Err(e) => {
285                        tracing::debug!("Coercion failed: {}", e);
286                        let source_str = match param_def.source {
287                            ParameterSource::Query => "query",
288                            ParameterSource::Path => "path",
289                            ParameterSource::Header => "headers",
290                            ParameterSource::Cookie => "cookie",
291                        };
292                        let (error_type, error_msg) =
293                            match (param_def.expected_type.as_deref(), param_def.format.as_deref()) {
294                                (Some("integer"), _) => (
295                                    "int_parsing",
296                                    "Input should be a valid integer, unable to parse string as an integer".to_string(),
297                                ),
298                                (Some("number"), _) => (
299                                    "float_parsing",
300                                    "Input should be a valid number, unable to parse string as a number".to_string(),
301                                ),
302                                (Some("boolean"), _) => (
303                                    "bool_parsing",
304                                    "Input should be a valid boolean, unable to interpret input".to_string(),
305                                ),
306                                (Some("string"), Some("uuid")) => {
307                                    ("uuid_parsing", format!("Input should be a valid UUID, {}", e))
308                                }
309                                (Some("string"), Some("date")) => {
310                                    ("date_parsing", format!("Input should be a valid date, {}", e))
311                                }
312                                (Some("string"), Some("date-time")) => {
313                                    ("datetime_parsing", format!("Input should be a valid datetime, {}", e))
314                                }
315                                (Some("string"), Some("time")) => {
316                                    ("time_parsing", format!("Input should be a valid time, {}", e))
317                                }
318                                (Some("string"), Some("duration")) => {
319                                    ("duration_parsing", format!("Input should be a valid duration, {}", e))
320                                }
321                                _ => ("type_error", e.clone()),
322                            };
323                        let param_name_for_error = if param_def.source == ParameterSource::Header {
324                            param_def.name.replace('_', "-").to_lowercase()
325                        } else {
326                            param_def.name.clone()
327                        };
328                        errors.push(ValidationErrorDetail {
329                            error_type: error_type.to_string(),
330                            loc: vec![source_str.to_string(), param_name_for_error],
331                            msg: error_msg,
332                            input: Value::String(value_str.to_string()),
333                            ctx: None,
334                        });
335                    }
336                }
337            }
338        }
339
340        if !errors.is_empty() {
341            tracing::debug!("Errors during extraction: {:?}", errors);
342            return Err(ValidationError { errors });
343        }
344
345        let params_json = Value::Object(params_map.clone());
346        tracing::debug!("params_json after coercion: {:?}", params_json);
347
348        let validation_schema = self.create_validation_schema();
349        tracing::debug!("validation_schema: {:?}", validation_schema);
350
351        let validator = crate::validation::SchemaValidator::new(validation_schema).map_err(|e| ValidationError {
352            errors: vec![ValidationErrorDetail {
353                error_type: "schema_error".to_string(),
354                loc: vec!["schema".to_string()],
355                msg: e,
356                input: Value::Null,
357                ctx: None,
358            }],
359        })?;
360
361        tracing::debug!("About to validate params_json against schema");
362        tracing::debug!("params_json = {:?}", params_json);
363        tracing::debug!(
364            "params_json pretty = {}",
365            serde_json::to_string_pretty(&params_json).unwrap_or_default()
366        );
367        tracing::debug!(
368            "schema = {}",
369            serde_json::to_string_pretty(&self.schema).unwrap_or_default()
370        );
371        match validator.validate(&params_json) {
372            Ok(_) => {
373                tracing::debug!("Validation succeeded");
374                Ok(params_json)
375            }
376            Err(mut validation_err) => {
377                tracing::debug!("Validation failed: {:?}", validation_err);
378
379                for error in &mut validation_err.errors {
380                    if error.loc.len() >= 2 && error.loc[0] == "body" {
381                        let param_name = &error.loc[1];
382                        if let Some(param_def) = self.parameter_defs.iter().find(|p| &p.name == param_name) {
383                            let source_str = match param_def.source {
384                                ParameterSource::Query => "query",
385                                ParameterSource::Path => "path",
386                                ParameterSource::Header => "headers",
387                                ParameterSource::Cookie => "cookie",
388                            };
389                            error.loc[0] = source_str.to_string();
390
391                            if param_def.source == ParameterSource::Header {
392                                error.loc[1] = param_def.name.replace('_', "-").to_lowercase();
393                            }
394
395                            if let Some(raw_value) = raw_values_map.get(&param_def.name) {
396                                error.input = Value::String(raw_value.clone());
397                            }
398                        }
399                    }
400                }
401
402                debug_log_module!(
403                    "parameters",
404                    "Returning {} validation errors",
405                    validation_err.errors.len()
406                );
407                for (i, error) in validation_err.errors.iter().enumerate() {
408                    debug_log_module!(
409                        "parameters",
410                        "  Error {}: type={}, loc={:?}, msg={}, input={}, ctx={:?}",
411                        i,
412                        error.error_type,
413                        error.loc,
414                        error.msg,
415                        error.input,
416                        error.ctx
417                    );
418                }
419                #[allow(clippy::collapsible_if)]
420                if crate::debug::is_enabled() {
421                    if let Ok(json_errors) = serde_json::to_value(&validation_err.errors) {
422                        if let Ok(json_str) = serde_json::to_string_pretty(&json_errors) {
423                            debug_log_module!("parameters", "Serialized errors:\n{}", json_str);
424                        }
425                    }
426                }
427
428                Err(validation_err)
429            }
430        }
431    }
432
433    fn array_item_type_and_format(&self, name: &str) -> (Option<&str>, Option<&str>) {
434        let Some(prop) = self
435            .schema
436            .get("properties")
437            .and_then(|value| value.as_object())
438            .and_then(|props| props.get(name))
439        else {
440            return (None, None);
441        };
442
443        let Some(items) = prop.get("items") else {
444            return (None, None);
445        };
446
447        let item_type = items.get("type").and_then(|value| value.as_str());
448        let item_format = items.get("format").and_then(|value| value.as_str());
449        (item_type, item_format)
450    }
451
452    /// Coerce a string value to the expected JSON type
453    fn coerce_value(value: &str, expected_type: Option<&str>, format: Option<&str>) -> Result<Value, String> {
454        if let Some(fmt) = format {
455            match fmt {
456                "uuid" => {
457                    Self::validate_uuid_format(value)?;
458                    return Ok(json!(value));
459                }
460                "date" => {
461                    Self::validate_date_format(value)?;
462                    return Ok(json!(value));
463                }
464                "date-time" => {
465                    Self::validate_datetime_format(value)?;
466                    return Ok(json!(value));
467                }
468                "time" => {
469                    Self::validate_time_format(value)?;
470                    return Ok(json!(value));
471                }
472                "duration" => {
473                    Self::validate_duration_format(value)?;
474                    return Ok(json!(value));
475                }
476                _ => {}
477            }
478        }
479
480        match expected_type {
481            Some("integer") => value
482                .parse::<i64>()
483                .map(|i| json!(i))
484                .map_err(|e| format!("Invalid integer: {}", e)),
485            Some("number") => value
486                .parse::<f64>()
487                .map(|f| json!(f))
488                .map_err(|e| format!("Invalid number: {}", e)),
489            Some("boolean") => {
490                if value.is_empty() {
491                    return Ok(json!(false));
492                }
493                let value_lower = value.to_lowercase();
494                if value_lower == "true" || value == "1" {
495                    Ok(json!(true))
496                } else if value_lower == "false" || value == "0" {
497                    Ok(json!(false))
498                } else {
499                    Err(format!("Invalid boolean: {}", value))
500                }
501            }
502            _ => Ok(json!(value)),
503        }
504    }
505
506    /// Validate ISO 8601 date format: YYYY-MM-DD
507    fn validate_date_format(value: &str) -> Result<(), String> {
508        jiff::civil::Date::strptime("%Y-%m-%d", value)
509            .map(|_| ())
510            .map_err(|e| format!("Invalid date format: {}", e))
511    }
512
513    /// Validate ISO 8601 datetime format
514    fn validate_datetime_format(value: &str) -> Result<(), String> {
515        use std::str::FromStr;
516        jiff::Timestamp::from_str(value)
517            .map(|_| ())
518            .map_err(|e| format!("Invalid datetime format: {}", e))
519    }
520
521    /// Validate ISO 8601 time format: HH:MM:SS or HH:MM:SS.ffffff
522    fn validate_time_format(value: &str) -> Result<(), String> {
523        let (time_part, offset_part) = if let Some(stripped) = value.strip_suffix('Z') {
524            (stripped, "Z")
525        } else {
526            let plus = value.rfind('+');
527            let minus = value.rfind('-');
528            let split_at = match (plus, minus) {
529                (Some(p), Some(m)) => Some(std::cmp::max(p, m)),
530                (Some(p), None) => Some(p),
531                (None, Some(m)) => Some(m),
532                (None, None) => None,
533            }
534            .ok_or_else(|| "Invalid time format: missing timezone offset".to_string())?;
535
536            if split_at < 8 {
537                return Err("Invalid time format: timezone offset position is invalid".to_string());
538            }
539
540            (&value[..split_at], &value[split_at..])
541        };
542
543        let base_time = time_part.split('.').next().unwrap_or(time_part);
544        jiff::civil::Time::strptime("%H:%M:%S", base_time).map_err(|e| format!("Invalid time format: {}", e))?;
545
546        if let Some((_, frac)) = time_part.split_once('.')
547            && (frac.is_empty() || frac.len() > 9 || !frac.chars().all(|c| c.is_ascii_digit()))
548        {
549            return Err("Invalid time format: fractional seconds must be 1-9 digits".to_string());
550        }
551
552        if offset_part != "Z" {
553            let sign = offset_part
554                .chars()
555                .next()
556                .ok_or_else(|| "Invalid time format: empty timezone offset".to_string())?;
557            if sign != '+' && sign != '-' {
558                return Err("Invalid time format: timezone offset must start with + or -".to_string());
559            }
560
561            let rest = &offset_part[1..];
562            let (hours_str, minutes_str) = rest
563                .split_once(':')
564                .ok_or_else(|| "Invalid time format: timezone offset must be ±HH:MM".to_string())?;
565            let hours: u8 = hours_str
566                .parse()
567                .map_err(|_| "Invalid time format: invalid timezone hours".to_string())?;
568            let minutes: u8 = minutes_str
569                .parse()
570                .map_err(|_| "Invalid time format: invalid timezone minutes".to_string())?;
571            if hours > 23 || minutes > 59 {
572                return Err("Invalid time format: timezone offset out of range".to_string());
573            }
574        }
575
576        Ok(())
577    }
578
579    /// Validate duration format (simplified - accept ISO 8601 duration or simple formats)
580    fn validate_duration_format(value: &str) -> Result<(), String> {
581        use std::str::FromStr;
582        jiff::Span::from_str(value)
583            .map(|_| ())
584            .map_err(|e| format!("Invalid duration format: {}", e))
585    }
586
587    /// Validate UUID format
588    fn validate_uuid_format(value: &str) -> Result<(), String> {
589        use std::str::FromStr;
590        uuid::Uuid::from_str(value)
591            .map(|_| ())
592            .map_err(|_e| format!("invalid character: expected an optional prefix of `urn:uuid:` followed by [0-9a-fA-F-], found `{}` at {}",
593                value.chars().next().unwrap_or('?'),
594                value.chars().position(|c| !c.is_ascii_hexdigit() && c != '-').unwrap_or(0)))
595    }
596
597    /// Create a validation schema without the "source" fields
598    /// (JSON Schema doesn't recognize "source" as a standard field)
599    fn create_validation_schema(&self) -> Value {
600        let mut schema = self.schema.clone();
601
602        if let Some(properties) = schema.get_mut("properties").and_then(|p| p.as_object_mut()) {
603            for (_name, prop) in properties.iter_mut() {
604                if let Some(obj) = prop.as_object_mut() {
605                    obj.remove("source");
606                }
607            }
608        }
609
610        schema
611    }
612}
613
614#[cfg(test)]
615mod tests {
616    use super::*;
617    use serde_json::json;
618
619    #[test]
620    fn test_array_query_parameter() {
621        let schema = json!({
622            "type": "object",
623            "properties": {
624                "device_ids": {
625                    "type": "array",
626                    "items": {"type": "integer"},
627                    "source": "query"
628                }
629            },
630            "required": []
631        });
632
633        let validator = ParameterValidator::new(schema).unwrap();
634
635        let query_params = json!({
636            "device_ids": [1, 2]
637        });
638        let raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
639        let path_params = HashMap::new();
640
641        let result = validator.validate_and_extract(
642            &query_params,
643            &raw_query_params,
644            &path_params,
645            &HashMap::new(),
646            &HashMap::new(),
647        );
648        assert!(
649            result.is_ok(),
650            "Array query param validation failed: {:?}",
651            result.err()
652        );
653
654        let extracted = result.unwrap();
655        assert_eq!(extracted["device_ids"], json!([1, 2]));
656    }
657
658    #[test]
659    fn test_path_parameter_extraction() {
660        let schema = json!({
661            "type": "object",
662            "properties": {
663                "item_id": {
664                    "type": "string",
665                    "source": "path"
666                }
667            },
668            "required": ["item_id"]
669        });
670
671        let validator = ParameterValidator::new(schema).expect("Failed to create validator");
672
673        let mut path_params = HashMap::new();
674        path_params.insert("item_id".to_string(), "foobar".to_string());
675        let query_params = json!({});
676        let raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
677
678        let result = validator.validate_and_extract(
679            &query_params,
680            &raw_query_params,
681            &path_params,
682            &HashMap::new(),
683            &HashMap::new(),
684        );
685        assert!(result.is_ok(), "Validation should succeed: {:?}", result);
686
687        let params = result.unwrap();
688        assert_eq!(params, json!({"item_id": "foobar"}));
689    }
690
691    #[test]
692    fn test_boolean_path_parameter_coercion() {
693        let schema = json!({
694            "type": "object",
695            "properties": {
696                "value": {
697                    "type": "boolean",
698                    "source": "path"
699                }
700            },
701            "required": ["value"]
702        });
703
704        let validator = ParameterValidator::new(schema).expect("Failed to create validator");
705
706        let mut path_params = HashMap::new();
707        path_params.insert("value".to_string(), "True".to_string());
708        let query_params = json!({});
709        let raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
710
711        let result = validator.validate_and_extract(
712            &query_params,
713            &raw_query_params,
714            &path_params,
715            &HashMap::new(),
716            &HashMap::new(),
717        );
718        if result.is_err() {
719            eprintln!("Error for 'True': {:?}", result);
720        }
721        assert!(result.is_ok(), "Validation should succeed for 'True': {:?}", result);
722        let params = result.unwrap();
723        assert_eq!(params, json!({"value": true}));
724
725        path_params.insert("value".to_string(), "1".to_string());
726        let query_params_1 = json!({});
727        let result = validator.validate_and_extract(
728            &query_params_1,
729            &raw_query_params,
730            &path_params,
731            &HashMap::new(),
732            &HashMap::new(),
733        );
734        assert!(result.is_ok(), "Validation should succeed for '1': {:?}", result);
735        let params = result.unwrap();
736        assert_eq!(params, json!({"value": true}));
737
738        path_params.insert("value".to_string(), "false".to_string());
739        let query_params_false = json!({});
740        let result = validator.validate_and_extract(
741            &query_params_false,
742            &raw_query_params,
743            &path_params,
744            &HashMap::new(),
745            &HashMap::new(),
746        );
747        assert!(result.is_ok(), "Validation should succeed for 'false': {:?}", result);
748        let params = result.unwrap();
749        assert_eq!(params, json!({"value": false}));
750
751        path_params.insert("value".to_string(), "TRUE".to_string());
752        let query_params_true = json!({});
753        let result = validator.validate_and_extract(
754            &query_params_true,
755            &raw_query_params,
756            &path_params,
757            &HashMap::new(),
758            &HashMap::new(),
759        );
760        assert!(result.is_ok(), "Validation should succeed for 'TRUE': {:?}", result);
761        let params = result.unwrap();
762        assert_eq!(params, json!({"value": true}));
763    }
764
765    #[test]
766    fn test_boolean_query_parameter_coercion() {
767        let schema = json!({
768            "type": "object",
769            "properties": {
770                "flag": {
771                    "type": "boolean",
772                    "source": "query"
773                }
774            },
775            "required": ["flag"]
776        });
777
778        let validator = ParameterValidator::new(schema).expect("Failed to create validator");
779        let path_params = HashMap::new();
780
781        let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
782        raw_query_params.insert("flag".to_string(), vec!["1".to_string()]);
783        let query_params = json!({"flag": 1});
784        let result = validator.validate_and_extract(
785            &query_params,
786            &raw_query_params,
787            &path_params,
788            &HashMap::new(),
789            &HashMap::new(),
790        );
791        assert!(result.is_ok(), "Validation should succeed for integer 1: {:?}", result);
792        let params = result.unwrap();
793        assert_eq!(params, json!({"flag": true}));
794
795        let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
796        raw_query_params.insert("flag".to_string(), vec!["0".to_string()]);
797        let query_params = json!({"flag": 0});
798        let result = validator.validate_and_extract(
799            &query_params,
800            &raw_query_params,
801            &path_params,
802            &HashMap::new(),
803            &HashMap::new(),
804        );
805        assert!(result.is_ok(), "Validation should succeed for integer 0: {:?}", result);
806        let params = result.unwrap();
807        assert_eq!(params, json!({"flag": false}));
808
809        let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
810        raw_query_params.insert("flag".to_string(), vec!["true".to_string()]);
811        let query_params = json!({"flag": true});
812        let result = validator.validate_and_extract(
813            &query_params,
814            &raw_query_params,
815            &path_params,
816            &HashMap::new(),
817            &HashMap::new(),
818        );
819        assert!(
820            result.is_ok(),
821            "Validation should succeed for boolean true: {:?}",
822            result
823        );
824        let params = result.unwrap();
825        assert_eq!(params, json!({"flag": true}));
826
827        let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
828        raw_query_params.insert("flag".to_string(), vec!["false".to_string()]);
829        let query_params = json!({"flag": false});
830        let result = validator.validate_and_extract(
831            &query_params,
832            &raw_query_params,
833            &path_params,
834            &HashMap::new(),
835            &HashMap::new(),
836        );
837        assert!(
838            result.is_ok(),
839            "Validation should succeed for boolean false: {:?}",
840            result
841        );
842        let params = result.unwrap();
843        assert_eq!(params, json!({"flag": false}));
844    }
845
846    #[test]
847    fn test_integer_coercion_invalid_format_returns_error() {
848        let schema = json!({
849            "type": "object",
850            "properties": {
851                "count": {
852                    "type": "integer",
853                    "source": "query"
854                }
855            },
856            "required": ["count"]
857        });
858
859        let validator = ParameterValidator::new(schema).unwrap();
860        let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
861        raw_query_params.insert("count".to_string(), vec!["not_a_number".to_string()]);
862
863        let result = validator.validate_and_extract(
864            &json!({"count": "not_a_number"}),
865            &raw_query_params,
866            &HashMap::new(),
867            &HashMap::new(),
868            &HashMap::new(),
869        );
870
871        assert!(result.is_err(), "Should fail to coerce non-integer string");
872        let err = result.unwrap_err();
873        assert_eq!(err.errors.len(), 1);
874        assert_eq!(err.errors[0].error_type, "int_parsing");
875        assert_eq!(err.errors[0].loc, vec!["query".to_string(), "count".to_string()]);
876        assert!(err.errors[0].msg.contains("valid integer"));
877    }
878
879    #[test]
880    fn test_integer_coercion_with_letters_mixed_returns_error() {
881        let schema = json!({
882            "type": "object",
883            "properties": {
884                "id": {
885                    "type": "integer",
886                    "source": "path"
887                }
888            },
889            "required": ["id"]
890        });
891
892        let validator = ParameterValidator::new(schema).unwrap();
893        let mut path_params = HashMap::new();
894        path_params.insert("id".to_string(), "123abc".to_string());
895
896        let result = validator.validate_and_extract(
897            &json!({}),
898            &HashMap::new(),
899            &path_params,
900            &HashMap::new(),
901            &HashMap::new(),
902        );
903
904        assert!(result.is_err());
905        let err = result.unwrap_err();
906        assert_eq!(err.errors[0].error_type, "int_parsing");
907    }
908
909    #[test]
910    fn test_integer_coercion_overflow_returns_error() {
911        let schema = json!({
912            "type": "object",
913            "properties": {
914                "big_num": {
915                    "type": "integer",
916                    "source": "query"
917                }
918            },
919            "required": ["big_num"]
920        });
921
922        let validator = ParameterValidator::new(schema).unwrap();
923        let too_large = "9223372036854775808";
924        let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
925        raw_query_params.insert("big_num".to_string(), vec![too_large.to_string()]);
926
927        let result = validator.validate_and_extract(
928            &json!({"big_num": too_large}),
929            &raw_query_params,
930            &HashMap::new(),
931            &HashMap::new(),
932            &HashMap::new(),
933        );
934
935        assert!(result.is_err(), "Should fail on integer overflow");
936        let err = result.unwrap_err();
937        assert_eq!(err.errors[0].error_type, "int_parsing");
938    }
939
940    #[test]
941    fn test_integer_coercion_negative_overflow_returns_error() {
942        let schema = json!({
943            "type": "object",
944            "properties": {
945                "small_num": {
946                    "type": "integer",
947                    "source": "query"
948                }
949            },
950            "required": ["small_num"]
951        });
952
953        let validator = ParameterValidator::new(schema).unwrap();
954        let too_small = "-9223372036854775809";
955        let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
956        raw_query_params.insert("small_num".to_string(), vec![too_small.to_string()]);
957
958        let result = validator.validate_and_extract(
959            &json!({"small_num": too_small}),
960            &raw_query_params,
961            &HashMap::new(),
962            &HashMap::new(),
963            &HashMap::new(),
964        );
965
966        assert!(result.is_err());
967        let err = result.unwrap_err();
968        assert_eq!(err.errors[0].error_type, "int_parsing");
969    }
970
971    #[test]
972    fn test_float_coercion_invalid_format_returns_error() {
973        let schema = json!({
974            "type": "object",
975            "properties": {
976                "price": {
977                    "type": "number",
978                    "source": "query"
979                }
980            },
981            "required": ["price"]
982        });
983
984        let validator = ParameterValidator::new(schema).unwrap();
985        let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
986        raw_query_params.insert("price".to_string(), vec!["not.a.number".to_string()]);
987
988        let result = validator.validate_and_extract(
989            &json!({"price": "not.a.number"}),
990            &raw_query_params,
991            &HashMap::new(),
992            &HashMap::new(),
993            &HashMap::new(),
994        );
995
996        assert!(result.is_err());
997        let err = result.unwrap_err();
998        assert_eq!(err.errors[0].error_type, "float_parsing");
999        assert!(err.errors[0].msg.contains("valid number"));
1000    }
1001
1002    #[test]
1003    fn test_float_coercion_scientific_notation_success() {
1004        let schema = json!({
1005            "type": "object",
1006            "properties": {
1007                "value": {
1008                    "type": "number",
1009                    "source": "query"
1010                }
1011            },
1012            "required": ["value"]
1013        });
1014
1015        let validator = ParameterValidator::new(schema).unwrap();
1016        let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1017        raw_query_params.insert("value".to_string(), vec!["1.5e10".to_string()]);
1018
1019        let result = validator.validate_and_extract(
1020            &json!({"value": 1.5e10}),
1021            &raw_query_params,
1022            &HashMap::new(),
1023            &HashMap::new(),
1024            &HashMap::new(),
1025        );
1026
1027        assert!(result.is_ok());
1028        let extracted = result.unwrap();
1029        assert_eq!(extracted["value"], json!(1.5e10));
1030    }
1031
1032    #[test]
1033    fn test_boolean_coercion_empty_string_returns_false() {
1034        // BUG: Empty string returns false instead of error - this is behavior to verify
1035        let schema = json!({
1036            "type": "object",
1037            "properties": {
1038                "flag": {
1039                    "type": "boolean",
1040                    "source": "query"
1041                }
1042            },
1043            "required": ["flag"]
1044        });
1045
1046        let validator = ParameterValidator::new(schema).unwrap();
1047        let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1048        raw_query_params.insert("flag".to_string(), vec!["".to_string()]);
1049
1050        let result = validator.validate_and_extract(
1051            &json!({"flag": ""}),
1052            &raw_query_params,
1053            &HashMap::new(),
1054            &HashMap::new(),
1055            &HashMap::new(),
1056        );
1057
1058        assert!(result.is_ok());
1059        let extracted = result.unwrap();
1060        assert_eq!(extracted["flag"], json!(false));
1061    }
1062
1063    #[test]
1064    fn test_boolean_coercion_whitespace_string_returns_error() {
1065        let schema = json!({
1066            "type": "object",
1067            "properties": {
1068                "flag": {
1069                    "type": "boolean",
1070                    "source": "query"
1071                }
1072            },
1073            "required": ["flag"]
1074        });
1075
1076        let validator = ParameterValidator::new(schema).unwrap();
1077        let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1078        raw_query_params.insert("flag".to_string(), vec!["   ".to_string()]);
1079
1080        let result = validator.validate_and_extract(
1081            &json!({"flag": "   "}),
1082            &raw_query_params,
1083            &HashMap::new(),
1084            &HashMap::new(),
1085            &HashMap::new(),
1086        );
1087
1088        assert!(result.is_err(), "Whitespace-only string should fail boolean parsing");
1089        let err = result.unwrap_err();
1090        assert_eq!(err.errors[0].error_type, "bool_parsing");
1091    }
1092
1093    #[test]
1094    fn test_boolean_coercion_invalid_value_returns_error() {
1095        let schema = json!({
1096            "type": "object",
1097            "properties": {
1098                "enabled": {
1099                    "type": "boolean",
1100                    "source": "path"
1101                }
1102            },
1103            "required": ["enabled"]
1104        });
1105
1106        let validator = ParameterValidator::new(schema).unwrap();
1107        let mut path_params = HashMap::new();
1108        path_params.insert("enabled".to_string(), "maybe".to_string());
1109
1110        let result = validator.validate_and_extract(
1111            &json!({}),
1112            &HashMap::new(),
1113            &path_params,
1114            &HashMap::new(),
1115            &HashMap::new(),
1116        );
1117
1118        assert!(result.is_err());
1119        let err = result.unwrap_err();
1120        assert_eq!(err.errors[0].error_type, "bool_parsing");
1121        assert!(err.errors[0].msg.contains("valid boolean"));
1122    }
1123
1124    #[test]
1125    fn test_required_query_parameter_missing_returns_error() {
1126        let schema = json!({
1127            "type": "object",
1128            "properties": {
1129                "required_param": {
1130                    "type": "string",
1131                    "source": "query"
1132                }
1133            },
1134            "required": ["required_param"]
1135        });
1136
1137        let validator = ParameterValidator::new(schema).unwrap();
1138
1139        let result = validator.validate_and_extract(
1140            &json!({}),
1141            &HashMap::new(),
1142            &HashMap::new(),
1143            &HashMap::new(),
1144            &HashMap::new(),
1145        );
1146
1147        assert!(result.is_err());
1148        let err = result.unwrap_err();
1149        assert_eq!(err.errors[0].error_type, "missing");
1150        assert_eq!(
1151            err.errors[0].loc,
1152            vec!["query".to_string(), "required_param".to_string()]
1153        );
1154        assert!(err.errors[0].msg.contains("required"));
1155    }
1156
1157    #[test]
1158    fn test_required_path_parameter_missing_returns_error() {
1159        let schema = json!({
1160            "type": "object",
1161            "properties": {
1162                "user_id": {
1163                    "type": "string",
1164                    "source": "path"
1165                }
1166            },
1167            "required": ["user_id"]
1168        });
1169
1170        let validator = ParameterValidator::new(schema).unwrap();
1171
1172        let result = validator.validate_and_extract(
1173            &json!({}),
1174            &HashMap::new(),
1175            &HashMap::new(),
1176            &HashMap::new(),
1177            &HashMap::new(),
1178        );
1179
1180        assert!(result.is_err());
1181        let err = result.unwrap_err();
1182        assert_eq!(err.errors[0].error_type, "missing");
1183        assert_eq!(err.errors[0].loc, vec!["path".to_string(), "user_id".to_string()]);
1184    }
1185
1186    #[test]
1187    fn test_required_header_parameter_missing_returns_error() {
1188        let schema = json!({
1189            "type": "object",
1190            "properties": {
1191                "Authorization": {
1192                    "type": "string",
1193                    "source": "header"
1194                }
1195            },
1196            "required": ["Authorization"]
1197        });
1198
1199        let validator = ParameterValidator::new(schema).unwrap();
1200
1201        let result = validator.validate_and_extract(
1202            &json!({}),
1203            &HashMap::new(),
1204            &HashMap::new(),
1205            &HashMap::new(),
1206            &HashMap::new(),
1207        );
1208
1209        assert!(result.is_err());
1210        let err = result.unwrap_err();
1211        assert_eq!(err.errors[0].error_type, "missing");
1212        assert_eq!(
1213            err.errors[0].loc,
1214            vec!["headers".to_string(), "authorization".to_string()]
1215        );
1216    }
1217
1218    #[test]
1219    fn test_required_cookie_parameter_missing_returns_error() {
1220        let schema = json!({
1221            "type": "object",
1222            "properties": {
1223                "session_id": {
1224                    "type": "string",
1225                    "source": "cookie"
1226                }
1227            },
1228            "required": ["session_id"]
1229        });
1230
1231        let validator = ParameterValidator::new(schema).unwrap();
1232
1233        let result = validator.validate_and_extract(
1234            &json!({}),
1235            &HashMap::new(),
1236            &HashMap::new(),
1237            &HashMap::new(),
1238            &HashMap::new(),
1239        );
1240
1241        assert!(result.is_err());
1242        let err = result.unwrap_err();
1243        assert_eq!(err.errors[0].error_type, "missing");
1244        assert_eq!(err.errors[0].loc, vec!["cookie".to_string(), "session_id".to_string()]);
1245    }
1246
1247    #[test]
1248    fn test_optional_parameter_missing_succeeds() {
1249        let schema = json!({
1250            "type": "object",
1251            "properties": {
1252                "optional_param": {
1253                    "type": "string",
1254                    "source": "query",
1255                    "optional": true
1256                }
1257            },
1258            "required": []
1259        });
1260
1261        let validator = ParameterValidator::new(schema).unwrap();
1262
1263        let result = validator.validate_and_extract(
1264            &json!({}),
1265            &HashMap::new(),
1266            &HashMap::new(),
1267            &HashMap::new(),
1268            &HashMap::new(),
1269        );
1270
1271        assert!(result.is_ok(), "Optional parameter should not cause error when missing");
1272        let extracted = result.unwrap();
1273        assert!(!extracted.as_object().unwrap().contains_key("optional_param"));
1274    }
1275
1276    #[test]
1277    fn test_uuid_validation_invalid_format_returns_error() {
1278        let schema = json!({
1279            "type": "object",
1280            "properties": {
1281                "id": {
1282                    "type": "string",
1283                    "format": "uuid",
1284                    "source": "path"
1285                }
1286            },
1287            "required": ["id"]
1288        });
1289
1290        let validator = ParameterValidator::new(schema).unwrap();
1291        let mut path_params = HashMap::new();
1292        path_params.insert("id".to_string(), "not-a-uuid".to_string());
1293
1294        let result = validator.validate_and_extract(
1295            &json!({}),
1296            &HashMap::new(),
1297            &path_params,
1298            &HashMap::new(),
1299            &HashMap::new(),
1300        );
1301
1302        assert!(result.is_err());
1303        let err = result.unwrap_err();
1304        assert_eq!(err.errors[0].error_type, "uuid_parsing");
1305        assert!(err.errors[0].msg.contains("UUID"));
1306    }
1307
1308    #[test]
1309    fn test_uuid_validation_uppercase_succeeds() {
1310        let schema = json!({
1311            "type": "object",
1312            "properties": {
1313                "id": {
1314                    "type": "string",
1315                    "format": "uuid",
1316                    "source": "query"
1317                }
1318            },
1319            "required": ["id"]
1320        });
1321
1322        let validator = ParameterValidator::new(schema).unwrap();
1323        let valid_uuid = "550e8400-e29b-41d4-a716-446655440000";
1324        let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1325        raw_query_params.insert("id".to_string(), vec![valid_uuid.to_string()]);
1326
1327        let result = validator.validate_and_extract(
1328            &json!({"id": valid_uuid}),
1329            &raw_query_params,
1330            &HashMap::new(),
1331            &HashMap::new(),
1332            &HashMap::new(),
1333        );
1334
1335        assert!(result.is_ok());
1336        let extracted = result.unwrap();
1337        assert_eq!(extracted["id"], json!(valid_uuid));
1338    }
1339
1340    #[test]
1341    fn test_date_validation_invalid_format_returns_error() {
1342        let schema = json!({
1343            "type": "object",
1344            "properties": {
1345                "created_at": {
1346                    "type": "string",
1347                    "format": "date",
1348                    "source": "query"
1349                }
1350            },
1351            "required": ["created_at"]
1352        });
1353
1354        let validator = ParameterValidator::new(schema).unwrap();
1355        let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1356        raw_query_params.insert("created_at".to_string(), vec!["2024/12/10".to_string()]);
1357
1358        let result = validator.validate_and_extract(
1359            &json!({"created_at": "2024/12/10"}),
1360            &raw_query_params,
1361            &HashMap::new(),
1362            &HashMap::new(),
1363            &HashMap::new(),
1364        );
1365
1366        assert!(result.is_err());
1367        let err = result.unwrap_err();
1368        assert_eq!(err.errors[0].error_type, "date_parsing");
1369        assert!(err.errors[0].msg.contains("date"));
1370    }
1371
1372    #[test]
1373    fn test_date_validation_valid_iso_succeeds() {
1374        let schema = json!({
1375            "type": "object",
1376            "properties": {
1377                "created_at": {
1378                    "type": "string",
1379                    "format": "date",
1380                    "source": "query"
1381                }
1382            },
1383            "required": ["created_at"]
1384        });
1385
1386        let validator = ParameterValidator::new(schema).unwrap();
1387        let valid_date = "2024-12-10";
1388        let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1389        raw_query_params.insert("created_at".to_string(), vec![valid_date.to_string()]);
1390
1391        let result = validator.validate_and_extract(
1392            &json!({"created_at": valid_date}),
1393            &raw_query_params,
1394            &HashMap::new(),
1395            &HashMap::new(),
1396            &HashMap::new(),
1397        );
1398
1399        assert!(result.is_ok());
1400        let extracted = result.unwrap();
1401        assert_eq!(extracted["created_at"], json!(valid_date));
1402    }
1403
1404    #[test]
1405    fn test_datetime_validation_invalid_format_returns_error() {
1406        let schema = json!({
1407            "type": "object",
1408            "properties": {
1409                "timestamp": {
1410                    "type": "string",
1411                    "format": "date-time",
1412                    "source": "query"
1413                }
1414            },
1415            "required": ["timestamp"]
1416        });
1417
1418        let validator = ParameterValidator::new(schema).unwrap();
1419        let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1420        raw_query_params.insert("timestamp".to_string(), vec!["not-a-datetime".to_string()]);
1421
1422        let result = validator.validate_and_extract(
1423            &json!({"timestamp": "not-a-datetime"}),
1424            &raw_query_params,
1425            &HashMap::new(),
1426            &HashMap::new(),
1427            &HashMap::new(),
1428        );
1429
1430        assert!(result.is_err());
1431        let err = result.unwrap_err();
1432        assert_eq!(err.errors[0].error_type, "datetime_parsing");
1433    }
1434
1435    #[test]
1436    fn test_time_validation_invalid_format_returns_error() {
1437        let schema = json!({
1438            "type": "object",
1439            "properties": {
1440                "start_time": {
1441                    "type": "string",
1442                    "format": "time",
1443                    "source": "query"
1444                }
1445            },
1446            "required": ["start_time"]
1447        });
1448
1449        let validator = ParameterValidator::new(schema).unwrap();
1450        let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1451        raw_query_params.insert("start_time".to_string(), vec!["25:00:00".to_string()]);
1452
1453        let result = validator.validate_and_extract(
1454            &json!({"start_time": "25:00:00"}),
1455            &raw_query_params,
1456            &HashMap::new(),
1457            &HashMap::new(),
1458            &HashMap::new(),
1459        );
1460
1461        assert!(result.is_err());
1462        let err = result.unwrap_err();
1463        assert_eq!(err.errors[0].error_type, "time_parsing");
1464    }
1465
1466    #[test]
1467    fn test_time_validation_string_passthrough() {
1468        let schema = json!({
1469            "type": "object",
1470            "properties": {
1471                "start_time": {
1472                    "type": "string",
1473                    "source": "query"
1474                }
1475            },
1476            "required": ["start_time"]
1477        });
1478
1479        let validator = ParameterValidator::new(schema).unwrap();
1480        let time_string = "14:30:00";
1481        let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1482        raw_query_params.insert("start_time".to_string(), vec![time_string.to_string()]);
1483
1484        let result = validator.validate_and_extract(
1485            &json!({"start_time": time_string}),
1486            &raw_query_params,
1487            &HashMap::new(),
1488            &HashMap::new(),
1489            &HashMap::new(),
1490        );
1491
1492        assert!(result.is_ok(), "String parameter should pass: {:?}", result);
1493        let extracted = result.unwrap();
1494        assert_eq!(extracted["start_time"], json!(time_string));
1495    }
1496
1497    #[test]
1498    fn test_duration_validation_invalid_format_returns_error() {
1499        let schema = json!({
1500            "type": "object",
1501            "properties": {
1502                "timeout": {
1503                    "type": "string",
1504                    "format": "duration",
1505                    "source": "query"
1506                }
1507            },
1508            "required": ["timeout"]
1509        });
1510
1511        let validator = ParameterValidator::new(schema).unwrap();
1512        let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1513        raw_query_params.insert("timeout".to_string(), vec!["not-a-duration".to_string()]);
1514
1515        let result = validator.validate_and_extract(
1516            &json!({"timeout": "not-a-duration"}),
1517            &raw_query_params,
1518            &HashMap::new(),
1519            &HashMap::new(),
1520            &HashMap::new(),
1521        );
1522
1523        assert!(result.is_err());
1524        let err = result.unwrap_err();
1525        assert_eq!(err.errors[0].error_type, "duration_parsing");
1526    }
1527
1528    #[test]
1529    fn test_duration_validation_iso8601_succeeds() {
1530        let schema = json!({
1531            "type": "object",
1532            "properties": {
1533                "timeout": {
1534                    "type": "string",
1535                    "format": "duration",
1536                    "source": "query"
1537                }
1538            },
1539            "required": ["timeout"]
1540        });
1541
1542        let validator = ParameterValidator::new(schema).unwrap();
1543        let valid_duration = "PT5M";
1544        let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1545        raw_query_params.insert("timeout".to_string(), vec![valid_duration.to_string()]);
1546
1547        let result = validator.validate_and_extract(
1548            &json!({"timeout": valid_duration}),
1549            &raw_query_params,
1550            &HashMap::new(),
1551            &HashMap::new(),
1552            &HashMap::new(),
1553        );
1554
1555        assert!(result.is_ok());
1556    }
1557
1558    #[test]
1559    fn test_header_name_normalization_with_underscores() {
1560        let schema = json!({
1561            "type": "object",
1562            "properties": {
1563                "X_Custom_Header": {
1564                    "type": "string",
1565                    "source": "header"
1566                }
1567            },
1568            "required": ["X_Custom_Header"]
1569        });
1570
1571        let validator = ParameterValidator::new(schema).unwrap();
1572        let mut headers = HashMap::new();
1573        headers.insert("x-custom-header".to_string(), "value".to_string());
1574
1575        let result =
1576            validator.validate_and_extract(&json!({}), &HashMap::new(), &HashMap::new(), &headers, &HashMap::new());
1577
1578        assert!(result.is_ok());
1579        let extracted = result.unwrap();
1580        assert_eq!(extracted["X_Custom_Header"], json!("value"));
1581    }
1582
1583    #[test]
1584    fn test_multiple_query_parameter_values_uses_first() {
1585        let schema = json!({
1586            "type": "object",
1587            "properties": {
1588                "id": {
1589                    "type": "integer",
1590                    "source": "query"
1591                }
1592            },
1593            "required": ["id"]
1594        });
1595
1596        let validator = ParameterValidator::new(schema).unwrap();
1597        let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1598        raw_query_params.insert("id".to_string(), vec!["123".to_string(), "456".to_string()]);
1599
1600        let result = validator.validate_and_extract(
1601            &json!({"id": [123, 456]}),
1602            &raw_query_params,
1603            &HashMap::new(),
1604            &HashMap::new(),
1605            &HashMap::new(),
1606        );
1607
1608        assert!(result.is_ok(), "Should accept first value of multiple query params");
1609        let extracted = result.unwrap();
1610        assert_eq!(extracted["id"], json!(123));
1611    }
1612
1613    #[test]
1614    fn test_schema_creation_missing_source_field_returns_error() {
1615        let schema = json!({
1616            "type": "object",
1617            "properties": {
1618                "param": {
1619                    "type": "string"
1620                }
1621            },
1622            "required": []
1623        });
1624
1625        let result = ParameterValidator::new(schema);
1626        assert!(result.is_err(), "Schema without 'source' field should fail");
1627        let err_msg = result.unwrap_err();
1628        assert!(err_msg.contains("source"));
1629    }
1630
1631    #[test]
1632    fn test_schema_creation_invalid_source_value_returns_error() {
1633        let schema = json!({
1634            "type": "object",
1635            "properties": {
1636                "param": {
1637                    "type": "string",
1638                    "source": "invalid_source"
1639                }
1640            },
1641            "required": []
1642        });
1643
1644        let result = ParameterValidator::new(schema);
1645        assert!(result.is_err());
1646        let err_msg = result.unwrap_err();
1647        assert!(err_msg.contains("Invalid source"));
1648    }
1649
1650    #[test]
1651    fn test_multiple_errors_reported_together() {
1652        let schema = json!({
1653            "type": "object",
1654            "properties": {
1655                "count": {
1656                    "type": "integer",
1657                    "source": "query"
1658                },
1659                "user_id": {
1660                    "type": "string",
1661                    "source": "path"
1662                },
1663                "token": {
1664                    "type": "string",
1665                    "source": "header"
1666                }
1667            },
1668            "required": ["count", "user_id", "token"]
1669        });
1670
1671        let validator = ParameterValidator::new(schema).unwrap();
1672
1673        let result = validator.validate_and_extract(
1674            &json!({}),
1675            &HashMap::new(),
1676            &HashMap::new(),
1677            &HashMap::new(),
1678            &HashMap::new(),
1679        );
1680
1681        assert!(result.is_err());
1682        let err = result.unwrap_err();
1683        assert_eq!(err.errors.len(), 3);
1684        assert!(err.errors.iter().all(|e| e.error_type == "missing"));
1685    }
1686
1687    #[test]
1688    fn test_coercion_error_includes_original_value() {
1689        let schema = json!({
1690            "type": "object",
1691            "properties": {
1692                "age": {
1693                    "type": "integer",
1694                    "source": "query"
1695                }
1696            },
1697            "required": ["age"]
1698        });
1699
1700        let validator = ParameterValidator::new(schema).unwrap();
1701        let invalid_value = "not_an_int";
1702        let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1703        raw_query_params.insert("age".to_string(), vec![invalid_value.to_string()]);
1704
1705        let result = validator.validate_and_extract(
1706            &json!({"age": invalid_value}),
1707            &raw_query_params,
1708            &HashMap::new(),
1709            &HashMap::new(),
1710            &HashMap::new(),
1711        );
1712
1713        assert!(result.is_err());
1714        let err = result.unwrap_err();
1715        assert_eq!(err.errors[0].input, json!(invalid_value));
1716    }
1717
1718    #[test]
1719    fn test_string_parameter_passes_through() {
1720        let schema = json!({
1721            "type": "object",
1722            "properties": {
1723                "name": {
1724                    "type": "string",
1725                    "source": "query"
1726                }
1727            },
1728            "required": ["name"]
1729        });
1730
1731        let validator = ParameterValidator::new(schema).unwrap();
1732        let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1733        raw_query_params.insert("name".to_string(), vec!["Alice".to_string()]);
1734
1735        let result = validator.validate_and_extract(
1736            &json!({"name": "Alice"}),
1737            &raw_query_params,
1738            &HashMap::new(),
1739            &HashMap::new(),
1740            &HashMap::new(),
1741        );
1742
1743        assert!(result.is_ok());
1744        let extracted = result.unwrap();
1745        assert_eq!(extracted["name"], json!("Alice"));
1746    }
1747
1748    #[test]
1749    fn test_string_with_special_characters_passes_through() {
1750        let schema = json!({
1751            "type": "object",
1752            "properties": {
1753                "message": {
1754                    "type": "string",
1755                    "source": "query"
1756                }
1757            },
1758            "required": ["message"]
1759        });
1760
1761        let validator = ParameterValidator::new(schema).unwrap();
1762        let special_value = "Hello! @#$%^&*() Unicode: 你好";
1763        let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1764        raw_query_params.insert("message".to_string(), vec![special_value.to_string()]);
1765
1766        let result = validator.validate_and_extract(
1767            &json!({"message": special_value}),
1768            &raw_query_params,
1769            &HashMap::new(),
1770            &HashMap::new(),
1771            &HashMap::new(),
1772        );
1773
1774        assert!(result.is_ok());
1775        let extracted = result.unwrap();
1776        assert_eq!(extracted["message"], json!(special_value));
1777    }
1778
1779    #[test]
1780    fn test_array_query_parameter_missing_required_returns_error() {
1781        let schema = json!({
1782            "type": "object",
1783            "properties": {
1784                "ids": {
1785                    "type": "array",
1786                    "items": {"type": "integer"},
1787                    "source": "query"
1788                }
1789            },
1790            "required": ["ids"]
1791        });
1792
1793        let validator = ParameterValidator::new(schema).unwrap();
1794
1795        let result = validator.validate_and_extract(
1796            &json!({}),
1797            &HashMap::new(),
1798            &HashMap::new(),
1799            &HashMap::new(),
1800            &HashMap::new(),
1801        );
1802
1803        assert!(result.is_err());
1804        let err = result.unwrap_err();
1805        assert_eq!(err.errors[0].error_type, "missing");
1806    }
1807
1808    #[test]
1809    fn test_empty_array_parameter_accepted() {
1810        let schema = json!({
1811            "type": "object",
1812            "properties": {
1813                "tags": {
1814                    "type": "array",
1815                    "items": {"type": "string"},
1816                    "source": "query"
1817                }
1818            },
1819            "required": ["tags"]
1820        });
1821
1822        let validator = ParameterValidator::new(schema).unwrap();
1823
1824        let result = validator.validate_and_extract(
1825            &json!({"tags": []}),
1826            &HashMap::new(),
1827            &HashMap::new(),
1828            &HashMap::new(),
1829            &HashMap::new(),
1830        );
1831
1832        assert!(result.is_ok());
1833        let extracted = result.unwrap();
1834        assert_eq!(extracted["tags"], json!([]));
1835    }
1836
1837    #[test]
1838    fn test_parameter_source_from_str_query() {
1839        assert_eq!(ParameterSource::from_str("query"), Some(ParameterSource::Query));
1840    }
1841
1842    #[test]
1843    fn test_parameter_source_from_str_path() {
1844        assert_eq!(ParameterSource::from_str("path"), Some(ParameterSource::Path));
1845    }
1846
1847    #[test]
1848    fn test_parameter_source_from_str_header() {
1849        assert_eq!(ParameterSource::from_str("header"), Some(ParameterSource::Header));
1850    }
1851
1852    #[test]
1853    fn test_parameter_source_from_str_cookie() {
1854        assert_eq!(ParameterSource::from_str("cookie"), Some(ParameterSource::Cookie));
1855    }
1856
1857    #[test]
1858    fn test_parameter_source_from_str_invalid() {
1859        assert_eq!(ParameterSource::from_str("invalid"), None);
1860    }
1861
1862    #[test]
1863    fn test_integer_with_plus_sign() {
1864        let schema = json!({
1865            "type": "object",
1866            "properties": {
1867                "count": {
1868                    "type": "integer",
1869                    "source": "query"
1870                }
1871            },
1872            "required": ["count"]
1873        });
1874
1875        let validator = ParameterValidator::new(schema).unwrap();
1876        let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1877        raw_query_params.insert("count".to_string(), vec!["+123".to_string()]);
1878
1879        let result = validator.validate_and_extract(
1880            &json!({"count": "+123"}),
1881            &raw_query_params,
1882            &HashMap::new(),
1883            &HashMap::new(),
1884            &HashMap::new(),
1885        );
1886
1887        assert!(result.is_ok());
1888        let extracted = result.unwrap();
1889        assert_eq!(extracted["count"], json!(123));
1890    }
1891
1892    #[test]
1893    fn test_float_with_leading_dot() {
1894        let schema = json!({
1895            "type": "object",
1896            "properties": {
1897                "ratio": {
1898                    "type": "number",
1899                    "source": "query"
1900                }
1901            },
1902            "required": ["ratio"]
1903        });
1904
1905        let validator = ParameterValidator::new(schema).unwrap();
1906        let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1907        raw_query_params.insert("ratio".to_string(), vec![".5".to_string()]);
1908
1909        let result = validator.validate_and_extract(
1910            &json!({"ratio": 0.5}),
1911            &raw_query_params,
1912            &HashMap::new(),
1913            &HashMap::new(),
1914            &HashMap::new(),
1915        );
1916
1917        assert!(result.is_ok());
1918        let extracted = result.unwrap();
1919        assert_eq!(extracted["ratio"], json!(0.5));
1920    }
1921
1922    #[test]
1923    fn test_float_with_trailing_dot() {
1924        let schema = json!({
1925            "type": "object",
1926            "properties": {
1927                "value": {
1928                    "type": "number",
1929                    "source": "query"
1930                }
1931            },
1932            "required": ["value"]
1933        });
1934
1935        let validator = ParameterValidator::new(schema).unwrap();
1936        let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1937        raw_query_params.insert("value".to_string(), vec!["5.".to_string()]);
1938
1939        let result = validator.validate_and_extract(
1940            &json!({"value": 5.0}),
1941            &raw_query_params,
1942            &HashMap::new(),
1943            &HashMap::new(),
1944            &HashMap::new(),
1945        );
1946
1947        assert!(result.is_ok());
1948    }
1949
1950    #[test]
1951    fn test_boolean_case_insensitive_true() {
1952        let schema = json!({
1953            "type": "object",
1954            "properties": {
1955                "flag": {
1956                    "type": "boolean",
1957                    "source": "query"
1958                }
1959            },
1960            "required": ["flag"]
1961        });
1962
1963        let validator = ParameterValidator::new(schema).unwrap();
1964        let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1965        raw_query_params.insert("flag".to_string(), vec!["TrUe".to_string()]);
1966
1967        let result = validator.validate_and_extract(
1968            &json!({"flag": true}),
1969            &raw_query_params,
1970            &HashMap::new(),
1971            &HashMap::new(),
1972            &HashMap::new(),
1973        );
1974
1975        assert!(result.is_ok());
1976        let extracted = result.unwrap();
1977        assert_eq!(extracted["flag"], json!(true));
1978    }
1979
1980    #[test]
1981    fn test_boolean_case_insensitive_false() {
1982        let schema = json!({
1983            "type": "object",
1984            "properties": {
1985                "flag": {
1986                    "type": "boolean",
1987                    "source": "query"
1988                }
1989            },
1990            "required": ["flag"]
1991        });
1992
1993        let validator = ParameterValidator::new(schema).unwrap();
1994        let mut raw_query_params: HashMap<String, Vec<String>> = HashMap::new();
1995        raw_query_params.insert("flag".to_string(), vec!["FaLsE".to_string()]);
1996
1997        let result = validator.validate_and_extract(
1998            &json!({"flag": false}),
1999            &raw_query_params,
2000            &HashMap::new(),
2001            &HashMap::new(),
2002            &HashMap::new(),
2003        );
2004
2005        assert!(result.is_ok());
2006        let extracted = result.unwrap();
2007        assert_eq!(extracted["flag"], json!(false));
2008    }
2009}