use backbone_core::{EntityValidator, ValidationErrors, ValidationError};
use backbone_core::{NonNegative, OptionalNotBlank, RequiredString};
use crate::domain::entity::Event;
pub type EventValidator = EntityValidator<Event>;
pub fn event_validator() -> EventValidator {
EntityValidator::new()
.rule(RequiredString::new("name", |e: &Event| &e.name))
.rule(RequiredString::new("date_tz", |e: &Event| &e.date_tz))
.rule(NonNegative::new("seats_max", |e: &Event| e.seats_max as i64))
.rule(OptionalNotBlank::new("event_url", |e: &Event| e.event_url.as_deref()))
.rule(EventUrlIsAbsolute)
}
use backbone_core::FieldRule;
struct EventUrlIsAbsolute;
impl FieldRule<Event> for EventUrlIsAbsolute {
fn validate(&self, entity: &Event) -> Vec<ValidationError> {
match entity.event_url.as_deref() {
Some(url) if !is_absolute_url(url) => vec![ValidationError::new(
"event_url",
"must be an absolute address carrying a scheme and a host, \
for example https://example.org/summit",
)
.with_code("pattern")],
_ => vec![],
}
}
}
fn is_absolute_url(url: &str) -> bool {
let Some((scheme, rest)) = url.split_once("://") else {
return false;
};
let mut scheme_chars = scheme.chars();
let scheme_ok = scheme_chars.next().is_some_and(|c| c.is_ascii_alphabetic())
&& scheme_chars.all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '.' | '-'));
let host = rest
.split(['/', '?', '#'])
.next()
.unwrap_or_default();
scheme_ok && !host.is_empty() && !host.chars().any(char::is_whitespace)
}
#[cfg(test)]
mod event_url_tests {
use super::is_absolute_url;
#[test]
fn absolute_addresses_pass_and_bare_hosts_do_not() {
for ok in [
"https://example.org/summit",
"http://example.org",
"webcal://calendar.example.org/feed.ics",
] {
assert!(is_absolute_url(ok), "{ok} should be accepted");
}
for bad in [
"example.org/summit",
"://example.org",
"https://",
"https:// example.org",
"9https://example.org",
] {
assert!(!is_absolute_url(bad), "{bad} should be refused");
}
}
}