1use crate::daemon::coerce::CoercionError;
16use uuid::Uuid;
17
18pub fn validate_session_id(session_id: &str) -> Result<Uuid, CoercionError> {
42 Uuid::parse_str(session_id).map_err(|_| CoercionError::new(
43 &format!("Invalid UUID format: '{}'", session_id),
44 std::io::Error::new(std::io::ErrorKind::InvalidInput, "Invalid UUID"),
45 Some(session_id.into()),
46 )
47 .with_parameter_path("session_id".to_string())
48 .with_expected_type("UUID format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (36 chars with hyphens)")
49 .with_hint("Create a session first using the 'session' tool with action='create', or search for existing sessions using 'semantic_search'"))
50}
51
52pub fn validate_workspace_id(workspace_id: &str) -> Result<Uuid, CoercionError> {
63 Uuid::parse_str(workspace_id).map_err(|_| CoercionError::new(
64 &format!("Invalid workspace ID format: '{}'", workspace_id),
65 std::io::Error::new(std::io::ErrorKind::InvalidInput, "Invalid UUID"),
66 Some(workspace_id.into()),
67 )
68 .with_parameter_path("workspace_id".to_string())
69 .with_expected_type("UUID format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (36 chars with hyphens)")
70 .with_hint("Use the 'manage_workspace' tool with action='list' to see available workspaces, or create one with action='create'"))
71}
72
73pub const VALID_INTERACTION_TYPES: &[&str] = &[
75 "qa",
76 "decision_made",
77 "problem_solved",
78 "code_change",
79 "requirement_added",
80 "concept_defined",
81];
82
83pub fn validate_interaction_type(interaction_type: &str) -> Result<(), CoercionError> {
103 if VALID_INTERACTION_TYPES.contains(&interaction_type.to_lowercase().as_str()) {
104 Ok(())
105 } else {
106 Err(CoercionError::new(
107 &format!("Invalid interaction_type: '{}'", interaction_type),
108 std::io::Error::new(std::io::ErrorKind::InvalidInput, "Invalid type"),
109 Some(interaction_type.into()),
110 )
111 .with_parameter_path("interaction_type".to_string())
112 .with_expected_type(&format!("one of: {}", VALID_INTERACTION_TYPES.join(", ")))
113 .with_hint("Use exact lowercase term with underscores. Valid types: qa, decision_made, problem_solved, code_change, requirement_added, concept_defined"))
114 }
115}
116
117pub fn validate_scope(scope: &str) -> Result<(), CoercionError> {
134 const VALID_SCOPES: &[&str] = &["session", "workspace", "global"];
135
136 if VALID_SCOPES.contains(&scope.to_lowercase().as_str()) {
137 Ok(())
138 } else {
139 Err(CoercionError::new(
140 &format!("Invalid scope: '{}'", scope),
141 std::io::Error::new(std::io::ErrorKind::InvalidInput, "Invalid scope"),
142 Some(scope.into()),
143 )
144 .with_parameter_path("scope".to_string())
145 .with_expected_type(&format!("one of: {}", VALID_SCOPES.join(", ")))
146 .with_hint("Valid scopes: 'session' (requires scope_id), 'workspace' (requires scope_id), 'global' (default, no scope_id needed)"))
147 }
148}
149
150pub fn validate_session_action(action: &str) -> Result<(), CoercionError> {
166 const VALID_ACTIONS: &[&str] = &[
167 "create",
168 "list",
169 "load",
170 "search",
171 "update_metadata",
172 "delete",
173 ];
174
175 if VALID_ACTIONS.contains(&action.to_lowercase().as_str()) {
176 Ok(())
177 } else {
178 Err(CoercionError::new(
179 &format!("Invalid action: '{}'", action),
180 std::io::Error::new(std::io::ErrorKind::InvalidInput, "Invalid action"),
181 Some(action.into()),
182 )
183 .with_parameter_path("action".to_string())
184 .with_expected_type(&format!("one of: {}", VALID_ACTIONS.join(", ")))
185 .with_hint("Valid actions: create (name, description), list, load (session_id), search (query), update_metadata (session_id + name/description), delete (session_id)"))
186 }
187}
188
189pub fn validate_workspace_action(action: &str) -> Result<(), CoercionError> {
209 const VALID_ACTIONS: &[&str] = &[
210 "create",
211 "list",
212 "get",
213 "delete",
214 "add_session",
215 "remove_session",
216 ];
217
218 if VALID_ACTIONS.contains(&action.to_lowercase().as_str()) {
219 Ok(())
220 } else {
221 Err(CoercionError::new(
222 &format!("Invalid action: '{}'", action),
223 std::io::Error::new(std::io::ErrorKind::InvalidInput, "Invalid action"),
224 Some(action.into()),
225 )
226 .with_parameter_path("action".to_string())
227 .with_expected_type(&format!("one of: {}", VALID_ACTIONS.join(", ")))
228 .with_hint("Valid actions: create (with name/description), list, get (workspace_id), delete (workspace_id), add_session (workspace_id, session_id, role), remove_session (workspace_id, session_id)"))
229 }
230}
231
232pub fn validate_session_role(role: &str) -> Result<(), CoercionError> {
250 const VALID_ROLES: &[&str] = &["primary", "related", "dependency", "shared"];
251
252 if VALID_ROLES.contains(&role.to_lowercase().as_str()) {
253 Ok(())
254 } else {
255 Err(CoercionError::new(
256 &format!("Invalid role: '{}'", role),
257 std::io::Error::new(std::io::ErrorKind::InvalidInput, "Invalid role"),
258 Some(role.into()),
259 )
260 .with_parameter_path("role".to_string())
261 .with_expected_type(&format!("one of: {}", VALID_ROLES.join(", ")))
262 .with_hint("Valid session roles: primary (main session), related (related context), dependency (required context), shared (shared context)"))
263 }
264}
265
266pub fn validate_recency_bias(recency_bias: Option<f32>) -> Result<Option<f32>, CoercionError> {
304 const MAX_RECENCY_BIAS: f32 = 10.0;
305 const MIN_RECENCY_BIAS: f32 = 0.0;
306
307 if let Some(value) = recency_bias {
308 if value.is_nan() || value.is_infinite() {
309 return Err(CoercionError::new(
310 "Invalid recency_bias value",
311 std::io::Error::new(std::io::ErrorKind::InvalidInput, "NaN or Infinity not allowed"),
312 Some(serde_json::Value::String(value.to_string())),
313 )
314 .with_parameter_path("recency_bias".to_string())
315 .with_expected_type("finite f32 between 0.0 and 10.0")
316 .with_hint("Use a finite value between 0.0 (disabled) and 10.0 (aggressive decay). Recommended: 0.0-1.0 for most use cases."));
317 }
318 if !(MIN_RECENCY_BIAS..=MAX_RECENCY_BIAS).contains(&value) {
319 return Err(CoercionError::new(
320 "recency_bias out of range",
321 std::io::Error::new(
322 std::io::ErrorKind::InvalidInput,
323 "Value must be between 0.0 and 10.0",
324 ),
325 Some(serde_json::Value::String(value.to_string())),
326 )
327 .with_parameter_path("recency_bias".to_string())
328 .with_expected_type("f32 in range [0.0, 10.0]")
329 .with_hint(&format!(
330 "Use recency_bias between {} and {}, or omit for default (0.0 = disabled)",
331 MIN_RECENCY_BIAS, MAX_RECENCY_BIAS
332 )));
333 }
334 Ok(Some(value))
335 } else {
336 Ok(None)
337 }
338}
339
340pub fn validate_limits(
371 limit: Option<usize>,
372 default: usize,
373 max: usize,
374) -> Result<usize, CoercionError> {
375 let value = limit.unwrap_or(default);
376 if value > max {
377 Err(CoercionError::new(
378 &format!("Limit {} exceeds maximum of {}", value, max),
379 std::io::Error::new(std::io::ErrorKind::InvalidInput, "Limit too large"),
380 Some(value.into()),
381 )
382 .with_parameter_path("limit".to_string())
383 .with_expected_type(&format!("number between 1 and {}", max))
384 .with_hint(&format!(
385 "Use limit between 1 and {}, or omit for default ({})",
386 max, default
387 )))
388 } else if value == 0 {
389 Err(CoercionError::new(
390 "Limit must be at least 1",
391 std::io::Error::new(std::io::ErrorKind::InvalidInput, "Limit too small"),
392 Some(value.into()),
393 )
394 .with_parameter_path("limit".to_string())
395 .with_expected_type(&format!("number between 1 and {}", max))
396 .with_hint(&format!(
397 "Use limit between 1 and {}, or omit for default ({})",
398 max, default
399 )))
400 } else {
401 Ok(value)
402 }
403}
404
405#[cfg(test)]
406mod tests {
407 use super::*;
408
409 #[test]
410 fn test_validate_session_id_valid() {
411 assert!(validate_session_id("60c598e2-d602-4e07-a328-c458006d48c7").is_ok());
412 }
413
414 #[test]
415 fn test_validate_session_id_invalid() {
416 let result = validate_session_id("invalid");
417 assert!(result.is_err());
418 let error = result.unwrap_err();
419 assert_eq!(error.parameter_path, Some("session_id".to_string()));
420 assert!(error.hint.is_some());
421 }
422
423 #[test]
424 fn test_validate_workspace_id_valid() {
425 assert!(validate_workspace_id("60c598e2-d602-4e07-a328-c458006d48c7").is_ok());
426 }
427
428 #[test]
429 fn test_validate_workspace_id_invalid() {
430 let result = validate_workspace_id("not-a-uuid");
431 assert!(result.is_err());
432 let error = result.unwrap_err();
433 assert_eq!(error.parameter_path, Some("workspace_id".to_string()));
434 }
435
436 #[test]
437 fn test_validate_interaction_type_valid() {
438 assert!(validate_interaction_type("qa").is_ok());
439 assert!(validate_interaction_type("decision_made").is_ok());
440 assert!(validate_interaction_type("problem_solved").is_ok());
441 assert!(validate_interaction_type("code_change").is_ok());
442 assert!(validate_interaction_type("requirement_added").is_ok());
443 assert!(validate_interaction_type("concept_defined").is_ok());
444 }
445
446 #[test]
447 fn test_validate_interaction_type_invalid() {
448 let result = validate_interaction_type("made_decision");
449 assert!(result.is_err());
450 let error = result.unwrap_err();
451 assert_eq!(error.parameter_path, Some("interaction_type".to_string()));
452 assert!(error.hint.unwrap().contains("decision_made"));
453 }
454
455 #[test]
456 fn test_validate_scope_valid() {
457 assert!(validate_scope("session").is_ok());
458 assert!(validate_scope("workspace").is_ok());
459 assert!(validate_scope("global").is_ok());
460 }
461
462 #[test]
463 fn test_validate_scope_invalid() {
464 let result = validate_scope("invalid_scope");
465 assert!(result.is_err());
466 let error = result.unwrap_err();
467 assert_eq!(error.parameter_path, Some("scope".to_string()));
468 }
469
470 #[test]
471 fn test_validate_session_action_valid() {
472 assert!(validate_session_action("create").is_ok());
473 assert!(validate_session_action("list").is_ok());
474 }
475
476 #[test]
477 fn test_validate_session_action_invalid() {
478 let result = validate_session_action("nuke_everything");
483 assert!(result.is_err());
484 let error = result.unwrap_err();
485 let hint = error.hint.as_ref().unwrap();
486 assert!(hint.contains("create") && hint.contains("list"));
487 }
488
489 #[test]
490 fn test_validate_workspace_action_valid() {
491 assert!(validate_workspace_action("create").is_ok());
492 assert!(validate_workspace_action("list").is_ok());
493 assert!(validate_workspace_action("get").is_ok());
494 assert!(validate_workspace_action("delete").is_ok());
495 assert!(validate_workspace_action("add_session").is_ok());
496 assert!(validate_workspace_action("remove_session").is_ok());
497 }
498
499 #[test]
500 fn test_validate_workspace_action_invalid() {
501 let result = validate_workspace_action("invalid_action");
502 assert!(result.is_err());
503 }
504
505 #[test]
506 fn test_validate_session_role_valid() {
507 assert!(validate_session_role("primary").is_ok());
508 assert!(validate_session_role("related").is_ok());
509 assert!(validate_session_role("dependency").is_ok());
510 assert!(validate_session_role("shared").is_ok());
511 }
512
513 #[test]
514 fn test_validate_session_role_invalid() {
515 let result = validate_session_role("admin");
516 assert!(result.is_err());
517 let error = result.unwrap_err();
518 assert_eq!(error.parameter_path, Some("role".to_string()));
519 assert!(error.hint.unwrap().contains("primary"));
520 }
521
522 #[test]
523 fn test_validate_limits_within_bounds() {
524 assert_eq!(validate_limits(Some(5), 10, 100).unwrap(), 5);
525 assert_eq!(validate_limits(Some(10), 10, 100).unwrap(), 10);
526 assert_eq!(validate_limits(Some(100), 10, 100).unwrap(), 100);
527 }
528
529 #[test]
530 fn test_validate_limits_use_default() {
531 assert_eq!(validate_limits(None, 10, 100).unwrap(), 10);
532 assert_eq!(validate_limits(None, 50, 100).unwrap(), 50);
533 }
534
535 #[test]
536 fn test_validate_limits_exceeds_maximum() {
537 let result = validate_limits(Some(200), 10, 100);
538 assert!(result.is_err());
539 let error = result.unwrap_err();
540 assert_eq!(error.parameter_path, Some("limit".to_string()));
541 assert!(error.message.contains("exceeds maximum"));
542 }
543
544 #[test]
545 fn test_validate_limits_zero() {
546 let result = validate_limits(Some(0), 10, 100);
547 assert!(result.is_err());
548 let error = result.unwrap_err();
549 assert!(error.message.contains("at least 1"));
550 }
551
552 #[test]
553 fn test_validate_recency_bias_valid() {
554 assert_eq!(validate_recency_bias(Some(0.0)).unwrap(), Some(0.0));
556 assert_eq!(validate_recency_bias(Some(0.5)).unwrap(), Some(0.5));
557 assert_eq!(validate_recency_bias(Some(1.0)).unwrap(), Some(1.0));
558 assert_eq!(validate_recency_bias(Some(10.0)).unwrap(), Some(10.0));
559 }
560
561 #[test]
562 fn test_validate_recency_bias_none() {
563 assert_eq!(validate_recency_bias(None).unwrap(), None);
564 }
565
566 #[test]
567 fn test_validate_recency_bias_negative() {
568 let result = validate_recency_bias(Some(-1.0));
569 assert!(result.is_err());
570 let error = result.unwrap_err();
571 assert_eq!(error.parameter_path, Some("recency_bias".to_string()));
572 assert!(error.message.contains("out of range"));
573 }
574
575 #[test]
576 fn test_validate_recency_bias_nan() {
577 let result = validate_recency_bias(Some(f32::NAN));
578 assert!(result.is_err());
579 let error = result.unwrap_err();
580 assert_eq!(error.parameter_path, Some("recency_bias".to_string()));
581 assert!(error.message.contains("Invalid recency_bias value"));
582 }
583
584 #[test]
585 fn test_validate_recency_bias_infinity() {
586 let result = validate_recency_bias(Some(f32::INFINITY));
587 assert!(result.is_err());
588 let error = result.unwrap_err();
589 assert!(error.message.contains("NaN or Infinity not allowed"));
590 }
591
592 #[test]
593 fn test_validate_recency_bias_exceeds_maximum() {
594 let result = validate_recency_bias(Some(100.0));
595 assert!(result.is_err());
596 let error = result.unwrap_err();
597 assert!(error.message.contains("out of range"));
598 assert_eq!(error.parameter_path, Some("recency_bias".to_string()));
599 }
600}