1use crate::error::{ErrorCode, EthosError};
21
22macro_rules! id_kind {
23 ($fmt_fn:ident, $parse_fn:ident, $prefix:literal, $width:literal, $max:literal, $doc:literal) => {
24 #[doc = $doc]
25 pub fn $fmt_fn(n: u32) -> Result<String, EthosError> {
29 if !(1..=$max).contains(&n) {
30 return Err(EthosError::new(
31 ErrorCode::InternalError,
32 concat!("id overflow: ", $prefix, " width ", $width),
33 ));
34 }
35 Ok(format!(concat!($prefix, "{:0", $width, "}"), n))
36 }
37
38 pub fn $parse_fn(id: &str) -> Option<u32> {
40 let digits = id.strip_prefix($prefix)?;
41 if digits.len() != $width || !digits.bytes().all(|b| b.is_ascii_digit()) {
42 return None;
43 }
44 let n: u32 = digits.parse().ok()?;
45 (1..=$max).contains(&n).then_some(n)
46 }
47 };
48}
49
50id_kind!(
51 page_id,
52 parse_page_id,
53 "p",
54 4,
55 9999,
56 "Format a page id (`p%04d`, 1-based original document index)."
57);
58id_kind!(
59 element_id,
60 parse_element_id,
61 "e",
62 6,
63 999_999,
64 "Format an element id (`e%06d`, reading order)."
65);
66id_kind!(
67 span_id,
68 parse_span_id,
69 "s",
70 6,
71 999_999,
72 "Format a span id (`s%06d`, content-stream order)."
73);
74id_kind!(
75 table_id,
76 parse_table_id,
77 "t",
78 4,
79 9999,
80 "Format a table id (`t%04d`, reading-order anchor)."
81);
82id_kind!(
83 chunk_id,
84 parse_chunk_id,
85 "c",
86 6,
87 999_999,
88 "Format a chunk id (`c%06d`, chunker emission order)."
89);
90id_kind!(
91 region_id,
92 parse_region_id,
93 "r",
94 4,
95 9999,
96 "Format a region id (`r%04d`, page/y/x/stream order)."
97);
98id_kind!(
99 warning_id,
100 parse_warning_id,
101 "w",
102 4,
103 9999,
104 "Format a warning id (`w%04d`, sorted emission — contract §5)."
105);
106id_kind!(
107 finding_id,
108 parse_finding_id,
109 "f",
110 4,
111 9999,
112 "Format a security finding id (`f%04d`)."
113);
114id_kind!(
115 check_id,
116 parse_check_id,
117 "v",
118 4,
119 9999,
120 "Format a verification check id (`v%04d`, input order)."
121);
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126 use proptest::prelude::*;
127
128 #[test]
129 fn formats_match_contract() {
130 assert_eq!(page_id(1).unwrap(), "p0001");
131 assert_eq!(element_id(42).unwrap(), "e000042");
132 assert_eq!(span_id(999_999).unwrap(), "s999999");
133 assert_eq!(warning_id(7).unwrap(), "w0007");
134 assert!(page_id(0).is_err());
135 assert!(page_id(10_000).is_err());
136 assert!(element_id(1_000_000).is_err());
137 }
138
139 #[test]
140 fn parse_rejects_malformed() {
141 assert_eq!(parse_page_id("p0001"), Some(1));
142 assert_eq!(parse_page_id("p001"), None);
143 assert_eq!(parse_page_id("p00001"), None);
144 assert_eq!(parse_page_id("q0001"), None);
145 assert_eq!(parse_page_id("p0000"), None);
146 assert_eq!(parse_element_id("e000042"), Some(42));
147 assert_eq!(parse_element_id("e00004x"), None);
148 }
149
150 proptest! {
151 #[test]
152 fn round_trip(n in 1u32..=9999) {
153 prop_assert_eq!(parse_page_id(&page_id(n).unwrap()), Some(n));
154 prop_assert_eq!(parse_table_id(&table_id(n).unwrap()), Some(n));
155 prop_assert_eq!(parse_warning_id(&warning_id(n).unwrap()), Some(n));
156 }
157 }
158}