1use serde::{Deserialize, Serialize};
10use std::fmt;
11
12#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
36pub struct WidgetId(u128);
37
38impl WidgetId {
39 pub fn app_root() -> Self {
42 Self::explicit("fission.app.root")
43 }
44
45 pub const fn from_u128(val: u128) -> Self {
50 Self(val)
51 }
52
53 pub fn as_u128(&self) -> u128 {
55 self.0
56 }
57
58 pub fn explicit(key: &str) -> Self {
65 let mut hasher = blake3::Hasher::new();
66 hasher.update(b"explicit:");
67 hasher.update(key.as_bytes());
68 let hash = hasher.finalize();
69 Self(u128::from_le_bytes(
70 hash.as_bytes()[0..16].try_into().unwrap(),
71 ))
72 }
73
74 pub fn derived(parent: u128, path: &[u32]) -> Self {
80 let mut hasher = blake3::Hasher::new();
81 hasher.update(b"derived:");
82 hasher.update(&parent.to_le_bytes());
83 for index in path {
84 hasher.update(&index.to_le_bytes());
85 }
86 let hash = hasher.finalize();
87 Self(u128::from_le_bytes(
88 hash.as_bytes()[0..16].try_into().unwrap(),
89 ))
90 }
91
92 pub fn scoped(parent: u128, key: &str) -> Self {
97 let mut hasher = blake3::Hasher::new();
98 hasher.update(b"scoped:");
99 hasher.update(&parent.to_le_bytes());
100 hasher.update(key.as_bytes());
101 let hash = hasher.finalize();
102 Self(u128::from_le_bytes(
103 hash.as_bytes()[0..16].try_into().unwrap(),
104 ))
105 }
106
107 #[doc(hidden)]
110 pub fn scoped_location(parent: u128, file: &str, line: u32, column: u32) -> Self {
111 let mut hasher = blake3::Hasher::new();
112 hasher.update(b"source-location:");
113 hasher.update(&parent.to_le_bytes());
114 hasher.update(file.as_bytes());
115 hasher.update(&line.to_le_bytes());
116 hasher.update(&column.to_le_bytes());
117 let hash = hasher.finalize();
118 Self(u128::from_le_bytes(
119 hash.as_bytes()[0..16].try_into().unwrap(),
120 ))
121 }
122}
123
124impl fmt::Debug for WidgetId {
125 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126 write!(f, "WidgetId({:032x})", self.0)
127 }
128}
129
130impl fmt::Display for WidgetId {
131 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132 write!(f, "{:032x}", self.0)
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::WidgetId;
139
140 #[test]
141 fn all_identity_domains_are_deterministic_and_distinct() {
142 let parent = WidgetId::explicit("parent");
143 let explicit = WidgetId::explicit("child");
144 let derived = WidgetId::derived(parent.as_u128(), &[4, 2]);
145 let scoped = WidgetId::scoped(parent.as_u128(), "child");
146
147 assert_eq!(explicit, WidgetId::explicit("child"));
148 assert_eq!(derived, WidgetId::derived(parent.as_u128(), &[4, 2]));
149 assert_eq!(scoped, WidgetId::scoped(parent.as_u128(), "child"));
150 assert_ne!(explicit, derived);
151 assert_ne!(explicit, scoped);
152 assert_ne!(derived, scoped);
153 }
154
155 #[test]
156 fn structural_path_segments_and_order_are_significant() {
157 let parent = WidgetId::explicit("parent");
158 assert_ne!(
159 WidgetId::derived(parent.as_u128(), &[1, 2]),
160 WidgetId::derived(parent.as_u128(), &[2, 1])
161 );
162 assert_ne!(
163 WidgetId::derived(parent.as_u128(), &[1, 2]),
164 WidgetId::derived(parent.as_u128(), &[1, 2, 0])
165 );
166 }
167
168 #[test]
169 fn parent_identity_namespaces_structural_and_scoped_children() {
170 let first = WidgetId::explicit("first-parent");
171 let second = WidgetId::explicit("second-parent");
172 assert_ne!(
173 WidgetId::derived(first.as_u128(), &[0]),
174 WidgetId::derived(second.as_u128(), &[0])
175 );
176 assert_ne!(
177 WidgetId::scoped(first.as_u128(), "item"),
178 WidgetId::scoped(second.as_u128(), "item")
179 );
180 }
181
182 #[test]
183 fn source_location_identity_includes_every_input() {
184 let parent = WidgetId::explicit("parent");
185 let base = WidgetId::scoped_location(parent.as_u128(), "src/app.rs", 10, 4);
186 assert_eq!(
187 base,
188 WidgetId::scoped_location(parent.as_u128(), "src/app.rs", 10, 4)
189 );
190 assert_ne!(
191 base,
192 WidgetId::scoped_location(parent.as_u128(), "src/other.rs", 10, 4)
193 );
194 assert_ne!(
195 base,
196 WidgetId::scoped_location(parent.as_u128(), "src/app.rs", 11, 4)
197 );
198 assert_ne!(
199 base,
200 WidgetId::scoped_location(parent.as_u128(), "src/app.rs", 10, 5)
201 );
202 }
203}