silicon_browser_shared/
validation.rs1use thiserror::Error;
2
3pub(crate) const MAX_IDENTIFIER_CHARS: usize = 512;
4
5#[derive(Clone, Debug, PartialEq, Eq, Error)]
7pub enum ValidationError {
8 #[error("{field} is required")]
9 Required { field: &'static str },
10 #[error("{field} must be at most {max} characters")]
11 TooLong { field: &'static str, max: usize },
12 #[error("{field} must contain at most {max} items")]
13 TooMany { field: &'static str, max: usize },
14 #[error("{field} must be between {min} and {max}")]
15 OutOfRange { field: &'static str, min: u64, max: u64 },
16 #[error("invalid {field}: {reason}")]
17 Invalid { field: &'static str, reason: String },
18 #[error("{left} conflicts with {right}")]
19 Conflict { left: &'static str, right: &'static str },
20}
21
22pub trait Validate {
23 fn validate(&self) -> Result<(), ValidationError>;
24}
25
26pub(crate) fn required(value: &str, field: &'static str) -> Result<(), ValidationError> {
27 if value.trim().is_empty() { Err(ValidationError::Required { field }) } else { Ok(()) }
28}
29
30pub(crate) fn bounded(value: &str, field: &'static str, max: usize) -> Result<(), ValidationError> {
31 required(value, field)?;
32 if value.chars().count() > max {
33 return Err(ValidationError::TooLong { field, max });
34 }
35 Ok(())
36}
37
38pub(crate) fn collection_len(len: usize, field: &'static str, max: usize) -> Result<(), ValidationError> {
39 if len > max {
40 return Err(ValidationError::TooMany { field, max });
41 }
42 Ok(())
43}
44
45pub(crate) fn identifier(value: &str, field: &'static str) -> Result<(), ValidationError> {
46 bounded(value, field, MAX_IDENTIFIER_CHARS)?;
47 if value.chars().any(|ch| ch.is_control() || ch.is_whitespace() || matches!(ch, '/' | '\\')) {
48 return Err(ValidationError::Invalid {
49 field,
50 reason: "must not contain whitespace, control characters, or path separators".into(),
51 });
52 }
53 Ok(())
54}
55
56pub(crate) fn purpose(value: &str) -> Result<(), ValidationError> {
57 bounded(value, "purpose", 2_000)
58}