1use crate::debug_log_module;
7use crate::validation::{ValidationError, ValidationErrorDetail};
8use serde_json::{Value, json};
9use std::collections::HashMap;
10
11#[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#[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#[derive(Clone)]
44pub struct ParameterValidator {
45 schema: Value,
46 parameter_defs: Vec<ParameterDef>,
47}
48
49impl ParameterValidator {
50 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 fn extract_parameter_defs(schema: &Value) -> Result<Vec<ParameterDef>, String> {
62 let mut defs = Vec::new();
63
64 let properties = schema.get("properties").and_then(|p| p.as_object()).ok_or_else(|| {
65 anyhow::anyhow!("Parameter schema validation failed")
66 .context("Schema must have 'properties' object")
67 .to_string()
68 })?;
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 pub fn schema(&self) -> &Value {
112 &self.schema
113 }
114
115 pub fn validate_and_extract(
122 &self,
123 query_params: &Value,
124 raw_query_params: &HashMap<String, 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(¶m_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 params_map.insert(param_def.name.clone(), array_value);
172 }
173 continue;
174 }
175
176 let raw_value_string = match param_def.source {
177 ParameterSource::Query => raw_query_params.get(¶m_def.name),
178 ParameterSource::Path => path_params.get(¶m_def.name),
179 ParameterSource::Header => {
180 let header_name = param_def.name.replace('_', "-").to_lowercase();
181 headers.get(&header_name)
182 }
183 ParameterSource::Cookie => cookies.get(¶m_def.name),
184 };
185
186 tracing::debug!("raw_value_string for {}: {:?}", param_def.name, raw_value_string);
187
188 if param_def.required && raw_value_string.is_none() {
189 let source_str = match param_def.source {
190 ParameterSource::Query => "query",
191 ParameterSource::Path => "path",
192 ParameterSource::Header => "headers",
193 ParameterSource::Cookie => "cookie",
194 };
195 let param_name_for_error = if param_def.source == ParameterSource::Header {
196 param_def.name.replace('_', "-").to_lowercase()
197 } else {
198 param_def.name.clone()
199 };
200 errors.push(ValidationErrorDetail {
201 error_type: "missing".to_string(),
202 loc: vec![source_str.to_string(), param_name_for_error],
203 msg: "Field required".to_string(),
204 input: Value::Null,
205 ctx: None,
206 });
207 continue;
208 }
209
210 if let Some(value_str) = raw_value_string {
211 tracing::debug!(
212 "Coercing value '{}' to type {:?} with format {:?}",
213 value_str,
214 param_def.expected_type,
215 param_def.format
216 );
217 match Self::coerce_value(
218 value_str,
219 param_def.expected_type.as_deref(),
220 param_def.format.as_deref(),
221 ) {
222 Ok(coerced) => {
223 tracing::debug!("Coerced to: {:?}", coerced);
224 params_map.insert(param_def.name.clone(), coerced);
225 raw_values_map.insert(param_def.name.clone(), value_str.clone());
226 }
227 Err(e) => {
228 tracing::debug!("Coercion failed: {}", e);
229 let source_str = match param_def.source {
230 ParameterSource::Query => "query",
231 ParameterSource::Path => "path",
232 ParameterSource::Header => "headers",
233 ParameterSource::Cookie => "cookie",
234 };
235 let (error_type, error_msg) =
236 match (param_def.expected_type.as_deref(), param_def.format.as_deref()) {
237 (Some("integer"), _) => (
238 "int_parsing",
239 "Input should be a valid integer, unable to parse string as an integer".to_string(),
240 ),
241 (Some("number"), _) => (
242 "float_parsing",
243 "Input should be a valid number, unable to parse string as a number".to_string(),
244 ),
245 (Some("boolean"), _) => (
246 "bool_parsing",
247 "Input should be a valid boolean, unable to interpret input".to_string(),
248 ),
249 (Some("string"), Some("uuid")) => {
250 ("uuid_parsing", format!("Input should be a valid UUID, {}", e))
251 }
252 (Some("string"), Some("date")) => {
253 ("date_parsing", format!("Input should be a valid date, {}", e))
254 }
255 (Some("string"), Some("date-time")) => {
256 ("datetime_parsing", format!("Input should be a valid datetime, {}", e))
257 }
258 (Some("string"), Some("time")) => {
259 ("time_parsing", format!("Input should be a valid time, {}", e))
260 }
261 (Some("string"), Some("duration")) => {
262 ("duration_parsing", format!("Input should be a valid duration, {}", e))
263 }
264 _ => ("type_error", e.clone()),
265 };
266 let param_name_for_error = if param_def.source == ParameterSource::Header {
267 param_def.name.replace('_', "-").to_lowercase()
268 } else {
269 param_def.name.clone()
270 };
271 errors.push(ValidationErrorDetail {
272 error_type: error_type.to_string(),
273 loc: vec![source_str.to_string(), param_name_for_error],
274 msg: error_msg,
275 input: Value::String(value_str.clone()),
276 ctx: None,
277 });
278 }
279 }
280 }
281 }
282
283 if !errors.is_empty() {
284 tracing::debug!("Errors during extraction: {:?}", errors);
285 return Err(ValidationError { errors });
286 }
287
288 let params_json = Value::Object(params_map.clone());
289 tracing::debug!("params_json after coercion: {:?}", params_json);
290
291 let validation_schema = self.create_validation_schema();
292 tracing::debug!("validation_schema: {:?}", validation_schema);
293
294 let validator = crate::validation::SchemaValidator::new(validation_schema).map_err(|e| ValidationError {
295 errors: vec![ValidationErrorDetail {
296 error_type: "schema_error".to_string(),
297 loc: vec!["schema".to_string()],
298 msg: e,
299 input: Value::Null,
300 ctx: None,
301 }],
302 })?;
303
304 tracing::debug!("About to validate params_json against schema");
305 tracing::debug!("params_json = {:?}", params_json);
306 tracing::debug!(
307 "params_json pretty = {}",
308 serde_json::to_string_pretty(¶ms_json).unwrap_or_default()
309 );
310 tracing::debug!(
311 "schema = {}",
312 serde_json::to_string_pretty(&self.schema).unwrap_or_default()
313 );
314 match validator.validate(¶ms_json) {
315 Ok(_) => {
316 tracing::debug!("Validation succeeded");
317 Ok(params_json)
318 }
319 Err(mut validation_err) => {
320 tracing::debug!("Validation failed: {:?}", validation_err);
321
322 for error in &mut validation_err.errors {
323 if error.loc.len() >= 2 && error.loc[0] == "body" {
324 let param_name = &error.loc[1];
325 if let Some(param_def) = self.parameter_defs.iter().find(|p| &p.name == param_name) {
326 let source_str = match param_def.source {
327 ParameterSource::Query => "query",
328 ParameterSource::Path => "path",
329 ParameterSource::Header => "headers",
330 ParameterSource::Cookie => "cookie",
331 };
332 error.loc[0] = source_str.to_string();
333
334 if param_def.source == ParameterSource::Header {
335 error.loc[1] = param_def.name.replace('_', "-").to_lowercase();
336 }
337
338 if let Some(raw_value) = raw_values_map.get(¶m_def.name) {
339 error.input = Value::String(raw_value.clone());
340 }
341 }
342 }
343 }
344
345 debug_log_module!(
346 "parameters",
347 "Returning {} validation errors",
348 validation_err.errors.len()
349 );
350 for (i, error) in validation_err.errors.iter().enumerate() {
351 debug_log_module!(
352 "parameters",
353 " Error {}: type={}, loc={:?}, msg={}, input={}, ctx={:?}",
354 i,
355 error.error_type,
356 error.loc,
357 error.msg,
358 error.input,
359 error.ctx
360 );
361 }
362 #[allow(clippy::collapsible_if)]
363 if crate::debug::is_enabled() {
364 if let Ok(json_errors) = serde_json::to_value(&validation_err.errors) {
365 if let Ok(json_str) = serde_json::to_string_pretty(&json_errors) {
366 debug_log_module!("parameters", "Serialized errors:\n{}", json_str);
367 }
368 }
369 }
370
371 Err(validation_err)
372 }
373 }
374 }
375
376 fn coerce_value(value: &str, expected_type: Option<&str>, format: Option<&str>) -> Result<Value, String> {
378 if let Some(fmt) = format {
379 match fmt {
380 "uuid" => {
381 Self::validate_uuid_format(value)?;
382 return Ok(json!(value));
383 }
384 "date" => {
385 Self::validate_date_format(value)?;
386 return Ok(json!(value));
387 }
388 "date-time" => {
389 Self::validate_datetime_format(value)?;
390 return Ok(json!(value));
391 }
392 "time" => {
393 Self::validate_time_format(value)?;
394 return Ok(json!(value));
395 }
396 "duration" => {
397 Self::validate_duration_format(value)?;
398 return Ok(json!(value));
399 }
400 _ => {}
401 }
402 }
403
404 match expected_type {
405 Some("integer") => value
406 .parse::<i64>()
407 .map(|i| json!(i))
408 .map_err(|e| format!("Invalid integer: {}", e)),
409 Some("number") => value
410 .parse::<f64>()
411 .map(|f| json!(f))
412 .map_err(|e| format!("Invalid number: {}", e)),
413 Some("boolean") => {
414 if value.is_empty() {
415 return Ok(json!(false));
416 }
417 let value_lower = value.to_lowercase();
418 if value_lower == "true" || value == "1" {
419 Ok(json!(true))
420 } else if value_lower == "false" || value == "0" {
421 Ok(json!(false))
422 } else {
423 Err(format!("Invalid boolean: {}", value))
424 }
425 }
426 _ => Ok(json!(value)),
427 }
428 }
429
430 fn validate_date_format(value: &str) -> Result<(), String> {
432 jiff::civil::Date::strptime("%Y-%m-%d", value)
433 .map(|_| ())
434 .map_err(|e| format!("Invalid date format: {}", e))
435 }
436
437 fn validate_datetime_format(value: &str) -> Result<(), String> {
439 use std::str::FromStr;
440 jiff::Timestamp::from_str(value)
441 .map(|_| ())
442 .map_err(|e| format!("Invalid datetime format: {}", e))
443 }
444
445 fn validate_time_format(value: &str) -> Result<(), String> {
447 jiff::civil::Time::strptime("%H:%M:%S", value)
448 .or_else(|_| jiff::civil::Time::strptime("%H:%M", value))
449 .map(|_| ())
450 .map_err(|e| format!("Invalid time format: {}", e))
451 }
452
453 fn validate_duration_format(value: &str) -> Result<(), String> {
455 use std::str::FromStr;
456 jiff::Span::from_str(value)
457 .map(|_| ())
458 .map_err(|e| format!("Invalid duration format: {}", e))
459 }
460
461 fn validate_uuid_format(value: &str) -> Result<(), String> {
463 use std::str::FromStr;
464 uuid::Uuid::from_str(value)
465 .map(|_| ())
466 .map_err(|_e| format!("invalid character: expected an optional prefix of `urn:uuid:` followed by [0-9a-fA-F-], found `{}` at {}",
467 value.chars().next().unwrap_or('?'),
468 value.chars().position(|c| !c.is_ascii_hexdigit() && c != '-').unwrap_or(0)))
469 }
470
471 fn create_validation_schema(&self) -> Value {
474 let mut schema = self.schema.clone();
475
476 if let Some(properties) = schema.get_mut("properties").and_then(|p| p.as_object_mut()) {
477 for (_name, prop) in properties.iter_mut() {
478 if let Some(obj) = prop.as_object_mut() {
479 obj.remove("source");
480 }
481 }
482 }
483
484 schema
485 }
486}
487
488#[cfg(test)]
489mod tests {
490 use super::*;
491 use serde_json::json;
492
493 #[test]
494 fn test_array_query_parameter() {
495 let schema = json!({
496 "type": "object",
497 "properties": {
498 "device_ids": {
499 "type": "array",
500 "items": {"type": "integer"},
501 "source": "query"
502 }
503 },
504 "required": []
505 });
506
507 let validator = ParameterValidator::new(schema).unwrap();
508
509 let query_params = json!({
510 "device_ids": [1, 2]
511 });
512 let raw_query_params = HashMap::new();
513 let path_params = HashMap::new();
514
515 let result = validator.validate_and_extract(
516 &query_params,
517 &raw_query_params,
518 &path_params,
519 &HashMap::new(),
520 &HashMap::new(),
521 );
522 assert!(
523 result.is_ok(),
524 "Array query param validation failed: {:?}",
525 result.err()
526 );
527
528 let extracted = result.unwrap();
529 assert_eq!(extracted["device_ids"], json!([1, 2]));
530 }
531
532 #[test]
533 fn test_path_parameter_extraction() {
534 let schema = json!({
535 "type": "object",
536 "properties": {
537 "item_id": {
538 "type": "string",
539 "source": "path"
540 }
541 },
542 "required": ["item_id"]
543 });
544
545 let validator = ParameterValidator::new(schema).expect("Failed to create validator");
546
547 let mut path_params = HashMap::new();
548 path_params.insert("item_id".to_string(), "foobar".to_string());
549 let query_params = json!({});
550 let raw_query_params = HashMap::new();
551
552 let result = validator.validate_and_extract(
553 &query_params,
554 &raw_query_params,
555 &path_params,
556 &HashMap::new(),
557 &HashMap::new(),
558 );
559 assert!(result.is_ok(), "Validation should succeed: {:?}", result);
560
561 let params = result.unwrap();
562 assert_eq!(params, json!({"item_id": "foobar"}));
563 }
564
565 #[test]
566 fn test_boolean_path_parameter_coercion() {
567 let schema = json!({
568 "type": "object",
569 "properties": {
570 "value": {
571 "type": "boolean",
572 "source": "path"
573 }
574 },
575 "required": ["value"]
576 });
577
578 let validator = ParameterValidator::new(schema).expect("Failed to create validator");
579
580 let mut path_params = HashMap::new();
581 path_params.insert("value".to_string(), "True".to_string());
582 let query_params = json!({});
583 let raw_query_params = HashMap::new();
584
585 let result = validator.validate_and_extract(
586 &query_params,
587 &raw_query_params,
588 &path_params,
589 &HashMap::new(),
590 &HashMap::new(),
591 );
592 if result.is_err() {
593 eprintln!("Error for 'True': {:?}", result);
594 }
595 assert!(result.is_ok(), "Validation should succeed for 'True': {:?}", result);
596 let params = result.unwrap();
597 assert_eq!(params, json!({"value": true}));
598
599 path_params.insert("value".to_string(), "1".to_string());
600 let query_params_1 = json!({});
601 let result = validator.validate_and_extract(
602 &query_params_1,
603 &raw_query_params,
604 &path_params,
605 &HashMap::new(),
606 &HashMap::new(),
607 );
608 assert!(result.is_ok(), "Validation should succeed for '1': {:?}", result);
609 let params = result.unwrap();
610 assert_eq!(params, json!({"value": true}));
611
612 path_params.insert("value".to_string(), "false".to_string());
613 let query_params_false = json!({});
614 let result = validator.validate_and_extract(
615 &query_params_false,
616 &raw_query_params,
617 &path_params,
618 &HashMap::new(),
619 &HashMap::new(),
620 );
621 assert!(result.is_ok(), "Validation should succeed for 'false': {:?}", result);
622 let params = result.unwrap();
623 assert_eq!(params, json!({"value": false}));
624
625 path_params.insert("value".to_string(), "TRUE".to_string());
626 let query_params_true = json!({});
627 let result = validator.validate_and_extract(
628 &query_params_true,
629 &raw_query_params,
630 &path_params,
631 &HashMap::new(),
632 &HashMap::new(),
633 );
634 assert!(result.is_ok(), "Validation should succeed for 'TRUE': {:?}", result);
635 let params = result.unwrap();
636 assert_eq!(params, json!({"value": true}));
637 }
638
639 #[test]
640 fn test_boolean_query_parameter_coercion() {
641 let schema = json!({
642 "type": "object",
643 "properties": {
644 "flag": {
645 "type": "boolean",
646 "source": "query"
647 }
648 },
649 "required": ["flag"]
650 });
651
652 let validator = ParameterValidator::new(schema).expect("Failed to create validator");
653 let path_params = HashMap::new();
654
655 let mut raw_query_params = HashMap::new();
656 raw_query_params.insert("flag".to_string(), "1".to_string());
657 let query_params = json!({"flag": 1});
658 let result = validator.validate_and_extract(
659 &query_params,
660 &raw_query_params,
661 &path_params,
662 &HashMap::new(),
663 &HashMap::new(),
664 );
665 assert!(result.is_ok(), "Validation should succeed for integer 1: {:?}", result);
666 let params = result.unwrap();
667 assert_eq!(params, json!({"flag": true}));
668
669 let mut raw_query_params = HashMap::new();
670 raw_query_params.insert("flag".to_string(), "0".to_string());
671 let query_params = json!({"flag": 0});
672 let result = validator.validate_and_extract(
673 &query_params,
674 &raw_query_params,
675 &path_params,
676 &HashMap::new(),
677 &HashMap::new(),
678 );
679 assert!(result.is_ok(), "Validation should succeed for integer 0: {:?}", result);
680 let params = result.unwrap();
681 assert_eq!(params, json!({"flag": false}));
682
683 let mut raw_query_params = HashMap::new();
684 raw_query_params.insert("flag".to_string(), "true".to_string());
685 let query_params = json!({"flag": true});
686 let result = validator.validate_and_extract(
687 &query_params,
688 &raw_query_params,
689 &path_params,
690 &HashMap::new(),
691 &HashMap::new(),
692 );
693 assert!(
694 result.is_ok(),
695 "Validation should succeed for boolean true: {:?}",
696 result
697 );
698 let params = result.unwrap();
699 assert_eq!(params, json!({"flag": true}));
700
701 let mut raw_query_params = HashMap::new();
702 raw_query_params.insert("flag".to_string(), "false".to_string());
703 let query_params = json!({"flag": false});
704 let result = validator.validate_and_extract(
705 &query_params,
706 &raw_query_params,
707 &path_params,
708 &HashMap::new(),
709 &HashMap::new(),
710 );
711 assert!(
712 result.is_ok(),
713 "Validation should succeed for boolean false: {:?}",
714 result
715 );
716 let params = result.unwrap();
717 assert_eq!(params, json!({"flag": false}));
718 }
719}