1use alloc::borrow::Cow;
4
5use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
6
7use crate::{
8 BootUptime, RawUptime, TaiTimestamp, TickRate, Tickdelta, Tickstamp, Timedelta, Timestamp,
9 Uptime,
10};
11
12const SPAN_PATTERN: &str =
15 "^(0|-?(?=[0-9])([0-9]+d)?([0-9]+h)?([0-9]+m)?([0-9]+s)?([0-9]+ms)?([0-9]+us)?([0-9]+ns)?)$";
16
17macro_rules! date_time_pattern {
19 ($zone:literal) => {
20 concat!(
21 "^[0-9]{4}-[0-9]{2}-[0-9]{2}[Tt][0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?",
22 $zone,
23 "$"
24 )
25 };
26}
27
28const TICKS_PATTERN: &str = "^-?[0-9]+ ticks$";
30
31macro_rules! spelled_schema {
33 ($type:ident, $description:literal, $pattern:expr $(, format = $format:literal)? $(,)?) => {
34 impl JsonSchema for $type {
35 fn schema_name() -> Cow<'static, str> {
36 Cow::Borrowed(stringify!($type))
37 }
38
39 fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
40 json_schema!({
41 "type": "string",
42 $("format": $format,)?
43 "description": $description,
44 "pattern": $pattern,
45 })
46 }
47
48 fn inline_schema() -> bool {
49 true
50 }
51 }
52 };
53}
54
55spelled_schema!(
56 Timestamp,
57 "An RFC 3339 instant in UTC, with a fraction of one to nine digits or none, as \"2026-09-16T07:45:35Z\".",
58 date_time_pattern!("[Zz]"),
59 format = "date-time",
60);
61spelled_schema!(
62 TaiTimestamp,
63 "An RFC 3339 date and time in TAI, with a fraction of one to nine digits or none, as \"2026-09-16T07:46:12 TAI\".",
64 date_time_pattern!(" TAI"),
65);
66spelled_schema!(Uptime, "The span since the monotonic clock's origin, as \"1m30s\".", SPAN_PATTERN);
67spelled_schema!(
68 RawUptime,
69 "The span since the raw monotonic clock's origin, as \"1m30s\".",
70 SPAN_PATTERN,
71);
72spelled_schema!(BootUptime, "The span since boot, as \"1m30s\".", SPAN_PATTERN);
73spelled_schema!(
74 Tickstamp,
75 "A counter's reading, a count of ticks, as \"24 ticks\".",
76 TICKS_PATTERN
77);
78spelled_schema!(
79 Timedelta,
80 "A signed span: \"0\", or counts with units, each at most once and coarsest first, of d, h, m, s, ms, us, ns, as \"1m30s\".",
81 SPAN_PATTERN,
82);
83spelled_schema!(Tickdelta, "A signed count of ticks, as \"24 ticks\".", TICKS_PATTERN);
84spelled_schema!(
85 TickRate,
86 "A counter's rate, a count of hertz above zero, as \"24000000 Hz\".",
87 "^[0-9]*[1-9][0-9]* Hz$",
88);
89
90#[cfg(test)]
91mod tests {
92 use core::str::FromStr;
93
94 use fancy_regex::Regex;
95 use rstest::rstest;
96 use schemars::{JsonSchema, schema_for};
97
98 use crate::{
99 BootUptime, RawUptime, TaiTimestamp, TickRate, Tickdelta, Tickstamp, Timedelta, Timestamp,
100 Uptime,
101 };
102
103 fn pattern<T: JsonSchema>() -> Regex {
105 let schema = schema_for!(T);
106 let pattern = schema.get("pattern").and_then(|pattern| pattern.as_str());
107 Regex::new(pattern.expect("a pattern")).expect("a pattern fancy-regex reads")
109 }
110
111 fn assert_agree<T: JsonSchema + FromStr>(text: &str) {
113 let takes = pattern::<T>().is_match(text).expect("a match that ends");
114 assert_eq!(
115 takes,
116 text.parse::<T>().is_ok(),
117 "the pattern takes {text:?} as the parser does"
118 );
119 }
120
121 #[rstest]
123 #[case::zero("0")]
124 #[case::zero_seconds("0s")]
125 #[case::every_unit("1d2h3m4s5ms6us7ns")]
126 #[case::a_gap_inside("1m0s123ms")]
127 #[case::backwards("-3us")]
128 #[case::a_count_past_the_next_unit("1000ms")]
129 #[case::empty("")]
130 #[case::a_sign_alone("-")]
131 #[case::a_backwards_zero("-0")]
132 #[case::no_unit("1")]
133 #[case::no_count("s")]
134 #[case::a_unit_twice("1m1m")]
135 #[case::finest_first("1s1m")]
136 #[case::spaced("5 minutes")]
137 #[case::an_unknown_unit("1msec")]
138 fn the_span_pattern_takes_what_the_parser_reads(#[case] text: &str) {
139 assert_agree::<Timedelta>(text);
140 assert_agree::<Uptime>(text);
141 assert_agree::<RawUptime>(text);
142 assert_agree::<BootUptime>(text);
143 }
144
145 #[rstest]
147 #[case::nanoseconds("2026-09-16T07:45:35.123456789Z")]
148 #[case::either_case("1970-01-01t00:00:00z")]
149 #[case::one_fraction_digit("1970-01-01T00:00:00.5Z")]
150 #[case::in_tai("2026-09-16T07:46:12.123456789 TAI")]
151 #[case::a_date_alone("2026-09-16")]
152 #[case::no_zone("2026-09-16T07:45:35")]
153 #[case::an_offset("2026-09-16T07:45:35+01:00")]
154 #[case::trailing_text("2026-09-16T07:45:35Z ")]
155 #[case::a_point_without_digits("2026-09-16T07:45:35.Z")]
156 #[case::ten_fraction_digits("2026-09-16T07:45:35.1234567890Z")]
157 #[case::a_letter_for_a_digit("2026-09-1xT07:45:35Z")]
158 #[case::tai_in_lower_case("2026-09-16T07:46:12 tai")]
159 fn the_instant_patterns_take_what_their_parsers_read(#[case] text: &str) {
160 assert_agree::<Timestamp>(text);
161 assert_agree::<TaiTimestamp>(text);
162 }
163
164 #[rstest]
166 #[case::ticks("24 ticks")]
167 #[case::backwards("-24 ticks")]
168 #[case::hertz("24000000 Hz")]
169 #[case::leading_zeros("0024 Hz")]
170 #[case::zero_hertz("0 Hz")]
171 #[case::no_unit("24")]
172 #[case::a_sign_display_never_writes("+24 ticks")]
173 #[case::one_tick("1 tick")]
174 #[case::kilohertz("24 kHz")]
175 fn the_count_patterns_take_what_their_parsers_read(#[case] text: &str) {
176 assert_agree::<Tickstamp>(text);
177 assert_agree::<Tickdelta>(text);
178 assert_agree::<TickRate>(text);
179 }
180}