1pub use uuid::Uuid;
13
14#[must_use]
16pub fn new_v7() -> Uuid {
17 Uuid::now_v7()
18}
19
20pub const PROVENANCE_NAMESPACE: Uuid = Uuid::from_bytes([
24 0x9f, 0x6c, 0x2a, 0x1e, 0x7b, 0x42, 0x4d, 0x88, 0xa3, 0x10, 0x5e, 0x0c, 0x91, 0x33, 0x77, 0xd2,
25]);
26
27#[must_use]
33pub fn new_v5(namespace: &Uuid, name: &[u8]) -> Uuid {
34 Uuid::new_v5(namespace, name)
35}
36
37#[must_use]
40pub fn portable_import_generation(operation: &Uuid) -> Uuid {
41 new_v5(operation, b"graphforge-portable-import-generation/1")
42}
43
44#[must_use]
46pub fn portable_v2_import_generation(operation: &Uuid) -> Uuid {
47 new_v5(operation, b"graphforge-portable-v2-import-generation/1")
48}
49
50#[must_use]
53pub fn portable_v2_bootstrap_generation(operation: &Uuid) -> Uuid {
54 new_v5(operation, b"graphforge-portable-v2-bootstrap-generation/1")
55}
56
57#[must_use]
59pub fn portable_v2_bootstrap_transaction(operation: &Uuid) -> Uuid {
60 new_v5(operation, b"graphforge-portable-v2-bootstrap-transaction/1")
61}
62
63#[must_use]
66pub fn composite_delta_operation(operation: &Uuid, index: usize) -> Uuid {
67 new_v5(
68 operation,
69 format!("graphforge-composite-delta-operation/1/{index}").as_bytes(),
70 )
71}
72
73#[must_use]
75pub fn composite_delta_run(transaction: &Uuid) -> Uuid {
76 new_v5(transaction, b"graphforge-composite-delta-run/1")
77}
78
79#[must_use]
86pub fn hub_clone_operation(repository: &str, immutable_version: &str) -> Uuid {
87 new_v5(
88 &Uuid::NAMESPACE_URL,
89 format!("{repository}:{immutable_version}").as_bytes(),
90 )
91}
92
93#[must_use]
101pub fn hub_research_clone_operation(
102 repository: &str,
103 immutable_version: &str,
104 version_uuid: &str,
105 identity_digest: &str,
106) -> Uuid {
107 new_v5(
108 &hub_clone_operation(repository, immutable_version),
109 format!("graphforge-hub-research-clone/1/{version_uuid}/{identity_digest}").as_bytes(),
110 )
111}
112
113#[must_use]
125pub fn hub_publish_operation(
126 repository: &str,
127 git_ref: Option<&str>,
128 version_uuid: &str,
129 project_content: &str,
130) -> Uuid {
131 new_v5(
132 &Uuid::NAMESPACE_URL,
133 format!(
134 "graphforge-hub-publish/1\0{repository}\0{}\0{version_uuid}\0{project_content}",
135 git_ref.unwrap_or("")
136 )
137 .as_bytes(),
138 )
139}
140
141#[must_use]
144pub fn to_bytes(uuid: &Uuid) -> [u8; 16] {
145 *uuid.as_bytes()
146}
147
148#[must_use]
150pub fn from_bytes(bytes: &[u8; 16]) -> Uuid {
151 Uuid::from_bytes(*bytes)
152}
153
154#[must_use]
156pub fn to_string(uuid: &Uuid) -> String {
157 uuid.hyphenated().to_string()
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[test]
165 fn uuids_are_unique() {
166 let mut seen = std::collections::HashSet::new();
167 for _ in 0..1000 {
168 assert!(seen.insert(new_v7()), "generated a duplicate UUID");
169 }
170 assert_eq!(seen.len(), 1000);
171 }
172
173 #[test]
174 fn uuids_are_time_monotone() {
175 let mut prev = new_v7();
181 for _ in 0..1000 {
182 let next = new_v7();
183 assert!(next > prev, "UUIDv7 ordering violated: {next} !> {prev}");
184 prev = next;
185 }
186 }
187
188 #[test]
189 fn byte_round_trip_preserves_value() {
190 let original = new_v7();
191 let bytes = to_bytes(&original);
192 let restored = from_bytes(&bytes);
193 assert_eq!(original, restored);
194 }
195
196 #[test]
197 fn v5_is_namespace_and_name_deterministic() {
198 let namespace = Uuid::from_bytes([7; 16]);
199 let first = new_v5(&namespace, b"graphforge");
200 assert_eq!(first, new_v5(&namespace, b"graphforge"));
201 assert_ne!(first, new_v5(&namespace, b"GraphForge"));
202 assert_eq!(first.get_version_num(), 5);
203 }
204
205 #[test]
206 fn persisted_derivation_golden_vectors() {
207 let operation = Uuid::parse_str("00112233-4455-6677-8899-aabbccddeeff").unwrap();
210 let version_a = format!("sha256:{}", "a".repeat(64));
211 let version_b = format!("sha256:{}", "b".repeat(64));
212 let vectors = [
213 (
214 portable_import_generation(&operation),
215 "02abd337-e93c-56a3-bf8f-dadbb5b1e8d5",
216 ),
217 (
218 portable_v2_import_generation(&operation),
219 "167a58d3-52f2-533e-b3c4-01d743d7537d",
220 ),
221 (
222 portable_v2_bootstrap_generation(&operation),
223 "d633da3b-0a2e-543e-bcdd-d383b3c52794",
224 ),
225 (
226 portable_v2_bootstrap_transaction(&operation),
227 "7f6e6309-9103-5c77-837a-0efffec19b70",
228 ),
229 (
230 composite_delta_operation(&operation, 0),
231 "bf613aba-38a4-5ae7-8982-1675caa13e7c",
232 ),
233 (
234 composite_delta_operation(&operation, 1),
235 "dcffd77e-a011-5480-8fb3-8493d22ff8be",
236 ),
237 (
238 composite_delta_operation(&operation, 10),
239 "de381822-c256-5bf9-904f-25692a6b6c7e",
240 ),
241 (
242 composite_delta_run(&operation),
243 "d230eba6-b8a4-549e-b032-888112a9fc62",
244 ),
245 (
246 hub_clone_operation("curatelabs/demo", &version_a),
247 "e6caa879-f226-55fc-9d67-41c15b8f9658",
248 ),
249 (
250 hub_clone_operation("curatelabs/demo", &version_b),
251 "db3c36b4-97ee-57bb-ac63-995c79fb577b",
252 ),
253 (
254 hub_clone_operation("curatelabs/other", &version_a),
255 "29ea96be-d350-5026-a85b-32cbf5b835cc",
256 ),
257 (
258 hub_research_clone_operation(
259 "curatelabs/demo",
260 &version_a,
261 "01900000-0000-7000-8000-000000000021",
262 &format!("sha256:{}", "c".repeat(64)),
263 ),
264 "a53b2fd9-7de0-5826-9fc3-cbd8f7b07c4c",
265 ),
266 (
267 hub_research_clone_operation(
268 "curatelabs/demo",
269 &version_a,
270 "01900000-0000-7000-8000-000000000022",
271 &format!("sha256:{}", "c".repeat(64)),
272 ),
273 "c4bb0970-3473-5822-a82a-516833d8fde8",
274 ),
275 (
276 hub_publish_operation(
277 "curatelabs/demo",
278 Some("main"),
279 "01900000-0000-7000-8000-000000000021",
280 &format!("sha256:{}", "e".repeat(64)),
281 ),
282 "d93f3756-aea3-568b-b2b6-984835743c81",
283 ),
284 (
285 hub_publish_operation(
286 "curatelabs/demo",
287 None,
288 "01900000-0000-7000-8000-000000000021",
289 &format!("sha256:{}", "e".repeat(64)),
290 ),
291 "b8c6df3d-28dc-589b-bf90-2378b9a388fc",
292 ),
293 (
294 hub_publish_operation(
295 "curatelabs/demo",
296 Some("main"),
297 "01900000-0000-7000-8000-000000000022",
298 &format!("sha256:{}", "e".repeat(64)),
299 ),
300 "7f331a69-7ea4-5ceb-9280-9f44b8db5a2d",
301 ),
302 (
303 hub_publish_operation(
304 "curatelabs/demo",
305 Some("main"),
306 "01900000-0000-7000-8000-000000000021",
307 &format!("sha256:{}", "f".repeat(64)),
308 ),
309 "e0a6d8fd-b812-5603-ba77-b694e25648c9",
310 ),
311 (
312 new_v5(&PROVENANCE_NAMESPACE, b"graphforge"),
313 "59a64d29-3fe8-5c2c-ad21-1f285e53d758",
314 ),
315 ];
316 let mut unique = std::collections::HashSet::new();
317 for (actual, expected) in vectors {
318 assert_eq!(
319 actual.as_bytes(),
320 Uuid::parse_str(expected).unwrap().as_bytes()
321 );
322 assert!(unique.insert(actual), "derivation domains collided");
323 }
324 let other_operation = Uuid::from_bytes([7; 16]);
325 assert_ne!(
326 portable_import_generation(&operation),
327 portable_import_generation(&other_operation)
328 );
329 assert_ne!(
330 portable_v2_import_generation(&operation),
331 portable_v2_import_generation(&other_operation)
332 );
333 assert_ne!(
334 portable_v2_bootstrap_generation(&operation),
335 portable_v2_bootstrap_generation(&other_operation)
336 );
337 assert_ne!(
338 portable_v2_bootstrap_transaction(&operation),
339 portable_v2_bootstrap_transaction(&other_operation)
340 );
341 assert_ne!(
342 composite_delta_operation(&operation, 0),
343 composite_delta_operation(&other_operation, 0)
344 );
345 assert_ne!(
346 composite_delta_run(&operation),
347 composite_delta_run(&other_operation)
348 );
349 }
350
351 #[test]
352 fn string_matches_rfc_format() {
353 let s = to_string(&new_v7());
354 assert_eq!(s.len(), 36);
356 let groups: Vec<&str> = s.split('-').collect();
357 assert_eq!(groups.len(), 5);
358 assert_eq!(
359 groups.iter().map(|g| g.len()).collect::<Vec<_>>(),
360 vec![8, 4, 4, 4, 12]
361 );
362 assert!(s.chars().all(|c| c.is_ascii_hexdigit() || c == '-'));
363 assert_eq!(groups[2].chars().next(), Some('7'));
365 }
366}