1use std::sync::atomic::{AtomicU64, Ordering};
2
3pub struct IdGenerator {
11 prefix: String,
12 counter: AtomicU64,
13}
14
15impl std::fmt::Debug for IdGenerator {
16 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17 f.debug_struct("IdGenerator")
18 .field("prefix", &self.prefix)
19 .field("counter", &self.counter.load(Ordering::Relaxed))
20 .finish()
21 }
22}
23
24impl IdGenerator {
25 pub fn new(prefix: impl Into<String>) -> Self {
35 Self {
36 prefix: prefix.into(),
37 counter: AtomicU64::new(0),
38 }
39 }
40
41 pub fn next(&self) -> String {
43 let n = self.counter.fetch_add(1, Ordering::Relaxed);
44 format!("stratum-{}-{:03}", self.prefix, n)
45 }
46
47 pub fn paired(&self) -> (String, String) {
52 let n = self.counter.fetch_add(1, Ordering::Relaxed);
53 (
54 format!("stratum-{}-{:03}-label", self.prefix, n),
55 format!("stratum-{}-{:03}-target", self.prefix, n),
56 )
57 }
58
59 pub fn group(&self, suffixes: &[&str]) -> Vec<String> {
63 let n = self.counter.fetch_add(1, Ordering::Relaxed);
64 suffixes
65 .iter()
66 .map(|suffix| format!("stratum-{}-{:03}-{}", self.prefix, n, suffix))
67 .collect()
68 }
69
70 pub fn current_count(&self) -> u64 {
72 self.counter.load(Ordering::Relaxed)
73 }
74}
75
76pub mod generators {
81 use super::IdGenerator;
82 use std::sync::LazyLock;
83
84 pub static BUTTON: LazyLock<IdGenerator> = LazyLock::new(|| IdGenerator::new("btn"));
85 pub static CHECKBOX: LazyLock<IdGenerator> = LazyLock::new(|| IdGenerator::new("chk"));
86 pub static RADIO: LazyLock<IdGenerator> = LazyLock::new(|| IdGenerator::new("radio"));
87 pub static SWITCH: LazyLock<IdGenerator> = LazyLock::new(|| IdGenerator::new("switch"));
88 pub static SLIDER: LazyLock<IdGenerator> = LazyLock::new(|| IdGenerator::new("slider"));
89 pub static DISCLOSURE: LazyLock<IdGenerator> =
90 LazyLock::new(|| IdGenerator::new("disclosure"));
91 pub static DIALOG: LazyLock<IdGenerator> = LazyLock::new(|| IdGenerator::new("dialog"));
92 pub static POPOVER: LazyLock<IdGenerator> = LazyLock::new(|| IdGenerator::new("popover"));
93 pub static TOOLTIP: LazyLock<IdGenerator> = LazyLock::new(|| IdGenerator::new("tooltip"));
94 pub static MENU: LazyLock<IdGenerator> = LazyLock::new(|| IdGenerator::new("menu"));
95 pub static SELECT: LazyLock<IdGenerator> = LazyLock::new(|| IdGenerator::new("select"));
96 pub static TABS: LazyLock<IdGenerator> = LazyLock::new(|| IdGenerator::new("tabs"));
97 pub static ACCORDION: LazyLock<IdGenerator> =
98 LazyLock::new(|| IdGenerator::new("accordion"));
99 pub static FORM: LazyLock<IdGenerator> = LazyLock::new(|| IdGenerator::new("form"));
100 pub static INPUT: LazyLock<IdGenerator> = LazyLock::new(|| IdGenerator::new("input"));
101 pub static TOAST: LazyLock<IdGenerator> = LazyLock::new(|| IdGenerator::new("toast"));
102 pub static TABLE: LazyLock<IdGenerator> = LazyLock::new(|| IdGenerator::new("table"));
103 pub static TREE: LazyLock<IdGenerator> = LazyLock::new(|| IdGenerator::new("tree"));
104 pub static TOGGLE: LazyLock<IdGenerator> = LazyLock::new(|| IdGenerator::new("toggle"));
105 pub static PRESSABLE: LazyLock<IdGenerator> =
106 LazyLock::new(|| IdGenerator::new("pressable"));
107 pub static FOCUS_SCOPE: LazyLock<IdGenerator> =
108 LazyLock::new(|| IdGenerator::new("focus-scope"));
109 pub static SEPARATOR: LazyLock<IdGenerator> =
110 LazyLock::new(|| IdGenerator::new("separator"));
111 pub static PORTAL: LazyLock<IdGenerator> = LazyLock::new(|| IdGenerator::new("portal"));
112 pub static GENERIC: LazyLock<IdGenerator> = LazyLock::new(|| IdGenerator::new("id"));
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 #[test]
120 fn id_generator_sequential() {
121 let id_gen = IdGenerator::new("test");
122 assert_eq!(id_gen.next(), "stratum-test-000");
123 assert_eq!(id_gen.next(), "stratum-test-001");
124 assert_eq!(id_gen.next(), "stratum-test-002");
125 }
126
127 #[test]
128 fn id_generator_paired() {
129 let id_gen = IdGenerator::new("field");
130 let (label, target) = id_gen.paired();
131 assert_eq!(label, "stratum-field-000-label");
132 assert_eq!(target, "stratum-field-000-target");
133 }
134
135 #[test]
136 fn id_generator_group() {
137 let id_gen = IdGenerator::new("disclosure");
138 let ids = id_gen.group(&["trigger", "content"]);
139 assert_eq!(ids.len(), 2);
140 assert_eq!(ids[0], "stratum-disclosure-000-trigger");
141 assert_eq!(ids[1], "stratum-disclosure-000-content");
142 }
143
144 #[test]
145 fn id_generator_thread_safe() {
146 use std::thread;
147
148 let id_gen = IdGenerator::new("thread");
149 let id_ref = &id_gen;
150
151 thread::scope(|s| {
152 let mut handles = vec![];
153 for _ in 0..10 {
154 handles.push(s.spawn(|| id_ref.next()));
155 }
156 let ids: Vec<String> = handles.into_iter().map(|h| h.join().unwrap()).collect();
157 let mut sorted = ids.clone();
159 sorted.sort();
160 sorted.dedup();
161 assert_eq!(sorted.len(), ids.len());
162 });
163 }
164
165 #[test]
166 fn global_generators_unique() {
167 let btn_id = generators::BUTTON.next();
168 let chk_id = generators::CHECKBOX.next();
169 assert!(btn_id.starts_with("stratum-btn-"));
170 assert!(chk_id.starts_with("stratum-chk-"));
171 assert_ne!(btn_id, chk_id);
172 }
173
174 #[test]
175 fn current_count() {
176 let id_gen = IdGenerator::new("count");
177 assert_eq!(id_gen.current_count(), 0);
178 id_gen.next();
179 assert_eq!(id_gen.current_count(), 1);
180 id_gen.next();
181 assert_eq!(id_gen.current_count(), 2);
182 }
183}