1pub mod action_plan;
4pub mod action_result;
5pub mod config;
6pub mod gateway;
7pub mod state;
8
9use time::OffsetDateTime;
10use time::format_description::well_known::Rfc3339;
11
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct ValidationError {
14 pub code: &'static str,
15}
16
17impl ValidationError {
18 pub const fn new(code: &'static str) -> Self {
19 Self { code }
20 }
21}
22
23pub(crate) fn require_non_empty(value: &str, code: &'static str) -> Result<(), ValidationError> {
24 if value.trim().is_empty() {
25 return Err(ValidationError::new(code));
26 }
27 Ok(())
28}
29
30pub(crate) fn parse_rfc3339(
31 value: &str,
32 code: &'static str,
33) -> Result<OffsetDateTime, ValidationError> {
34 OffsetDateTime::parse(value, &Rfc3339).map_err(|_| ValidationError::new(code))
35}
36
37#[cfg(test)]
38mod tests {
39 use super::*;
40
41 #[test]
42 fn require_non_empty_accepts_non_blank_values() {
43 assert!(require_non_empty("value", "code").is_ok());
44 assert!(require_non_empty(" value ", "code").is_ok());
46 }
47
48 #[test]
49 fn require_non_empty_rejects_empty_and_blank_values() {
50 for value in ["", " ", "\t", "\n", " \t\n "] {
51 let err = require_non_empty(value, "missing_field").expect_err("should reject");
52 assert_eq!(err.code, "missing_field");
53 }
54 }
55
56 #[test]
57 fn parse_rfc3339_accepts_valid_timestamps() {
58 assert!(parse_rfc3339("2026-04-12T10:00:00Z", "bad").is_ok());
59 assert!(parse_rfc3339("2026-04-12T10:00:00+08:00", "bad").is_ok());
60 assert!(parse_rfc3339("2026-04-12T10:00:00.123Z", "bad").is_ok());
61 }
62
63 #[test]
64 fn parse_rfc3339_rejects_invalid_timestamps() {
65 for value in [
66 "",
67 "not-a-time",
68 "2026-04-12",
70 "2026-04-12T10:00:00",
71 ] {
72 let err = parse_rfc3339(value, "invalid_time").expect_err("should reject");
73 assert_eq!(err.code, "invalid_time");
74 }
75 }
76
77 #[test]
78 fn parse_rfc3339_compares_instants_across_offsets() {
79 let utc = parse_rfc3339("2026-04-12T10:00:00Z", "bad").expect("utc");
81 let plus8 = parse_rfc3339("2026-04-12T18:00:00+08:00", "bad").expect("plus8");
82 assert_eq!(utc, plus8);
83 }
84
85 #[test]
86 fn validation_error_carries_and_compares_by_code() {
87 let err = ValidationError::new("some_code");
88 assert_eq!(err.code, "some_code");
89 assert_eq!(err, ValidationError::new("some_code"));
90 assert_ne!(err, ValidationError::new("other_code"));
91 }
92}