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