agentics_contracts/validation/
text.rs1use agentics_error::{Result, ServiceError};
4
5pub fn require_non_empty(value: &str, field: &str) -> Result<()> {
7 if value.trim().is_empty() {
8 return Err(ServiceError::Validation(format!(
9 "{field} must not be empty"
10 )));
11 }
12
13 Ok(())
14}
15
16pub fn validate_bounded_display_text(value: &str, field: &str, max_bytes: usize) -> Result<()> {
18 if value.len() > max_bytes {
19 return Err(ServiceError::Validation(format!(
20 "{field} must be at most {max_bytes} UTF-8 bytes"
21 )));
22 }
23 if value.chars().any(is_disallowed_display_text_char) {
24 return Err(ServiceError::Validation(format!(
25 "{field} must not contain non-text control characters"
26 )));
27 }
28
29 Ok(())
30}
31
32pub fn validate_solution_note(note: &str, max_bytes: usize) -> Result<()> {
34 validate_bounded_display_text(note, "note", max_bytes)
35}
36
37fn is_disallowed_display_text_char(ch: char) -> bool {
39 ch.is_control() && !matches!(ch, '\n' | '\r' | '\t')
40}
41
42#[cfg(test)]
43mod tests {
44 use super::{require_non_empty, validate_solution_note};
45
46 #[test]
47 fn validates_display_text_bounds_and_controls() {
48 validate_solution_note("normal note\nwith tab\t", 1024).expect("text note should pass");
49
50 let oversized = "x".repeat(1025);
51 assert!(validate_solution_note(&oversized, 1024).is_err());
52 assert!(validate_solution_note("bad\u{0007}", 1024).is_err());
53 }
54
55 #[test]
56 fn rejects_empty_visible_text() {
57 assert!(require_non_empty("value", "field").is_ok());
58 assert!(require_non_empty(" \n\t", "field").is_err());
59 }
60}