1use std::collections::HashMap;
7use std::fmt;
8use std::sync::atomic::{AtomicU64, Ordering};
9use std::sync::Arc;
10
11#[derive(Debug, Clone, PartialEq)]
13pub enum BlankNodeError {
14 EmptyId,
16 InvalidPrefix(String),
18 UnknownScope(String),
20 InvalidBaseUri(String),
22 MappingConflict(String),
24 LimitExceeded(usize),
26}
27
28impl fmt::Display for BlankNodeError {
29 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30 match self {
31 BlankNodeError::EmptyId => write!(f, "Blank node ID cannot be empty"),
32 BlankNodeError::InvalidPrefix(p) => write!(f, "Invalid prefix: {p}"),
33 BlankNodeError::UnknownScope(s) => write!(f, "Unknown scope: {s}"),
34 BlankNodeError::InvalidBaseUri(u) => write!(f, "Invalid base URI: {u}"),
35 BlankNodeError::MappingConflict(msg) => write!(f, "Mapping conflict: {msg}"),
36 BlankNodeError::LimitExceeded(n) => write!(f, "Allocator limit exceeded: {n}"),
37 }
38 }
39}
40
41impl std::error::Error for BlankNodeError {}
42
43#[derive(Debug, Clone, PartialEq, Eq, Hash)]
45pub struct BlankNodeId {
46 pub id: String,
48 pub scope: Option<String>,
50}
51
52impl BlankNodeId {
53 pub fn new(id: impl Into<String>) -> Self {
55 Self {
56 id: id.into(),
57 scope: None,
58 }
59 }
60
61 pub fn with_scope(id: impl Into<String>, scope: impl Into<String>) -> Self {
63 Self {
64 id: id.into(),
65 scope: Some(scope.into()),
66 }
67 }
68
69 pub fn to_ntriples(&self) -> String {
71 format!("_:{}", self.id)
72 }
73}
74
75impl fmt::Display for BlankNodeId {
76 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77 write!(f, "_:{}", self.id)
78 }
79}
80
81#[derive(Debug, Clone)]
83pub struct AllocatorConfig {
84 pub default_prefix: String,
86 pub max_allocations: usize,
88 pub skolem_base: String,
90}
91
92impl Default for AllocatorConfig {
93 fn default() -> Self {
94 Self {
95 default_prefix: "b".to_string(),
96 max_allocations: 0,
97 skolem_base: "https://example.org/.well-known/genid/".to_string(),
98 }
99 }
100}
101
102#[derive(Debug, Clone, Default)]
104pub struct AllocationStats {
105 pub total_allocated: u64,
107 pub per_scope: HashMap<String, u64>,
109 pub active_scopes: usize,
111 pub skolemized: u64,
113 pub deskolemized: u64,
115}
116
117pub struct BlankNodeAllocator {
121 counter: Arc<AtomicU64>,
123 scope_counters: HashMap<String, u64>,
125 scope_ids: HashMap<String, Vec<String>>,
127 config: AllocatorConfig,
129 stats: AllocationStats,
131}
132
133impl BlankNodeAllocator {
134 pub fn new() -> Self {
136 Self {
137 counter: Arc::new(AtomicU64::new(0)),
138 scope_counters: HashMap::new(),
139 scope_ids: HashMap::new(),
140 config: AllocatorConfig::default(),
141 stats: AllocationStats::default(),
142 }
143 }
144
145 pub fn with_config(config: AllocatorConfig) -> Result<Self, BlankNodeError> {
147 validate_prefix(&config.default_prefix)?;
148 if !config.skolem_base.is_empty() && !config.skolem_base.starts_with("http") {
149 return Err(BlankNodeError::InvalidBaseUri(config.skolem_base.clone()));
150 }
151 Ok(Self {
152 counter: Arc::new(AtomicU64::new(0)),
153 scope_counters: HashMap::new(),
154 scope_ids: HashMap::new(),
155 config,
156 stats: AllocationStats::default(),
157 })
158 }
159
160 pub fn next(&self) -> Result<BlankNodeId, BlankNodeError> {
164 let max = self.config.max_allocations;
165 let n = self.counter.fetch_add(1, Ordering::Relaxed);
166 if max > 0 && (n as usize) >= max {
167 self.counter.fetch_sub(1, Ordering::Relaxed);
169 return Err(BlankNodeError::LimitExceeded(max));
170 }
171 Ok(BlankNodeId::new(format!(
172 "{}{n}",
173 self.config.default_prefix
174 )))
175 }
176
177 pub fn next_with_prefix(&self, prefix: &str) -> Result<BlankNodeId, BlankNodeError> {
179 validate_prefix(prefix)?;
180 let n = self.counter.fetch_add(1, Ordering::Relaxed);
181 let max = self.config.max_allocations;
182 if max > 0 && (n as usize) >= max {
183 self.counter.fetch_sub(1, Ordering::Relaxed);
184 return Err(BlankNodeError::LimitExceeded(max));
185 }
186 Ok(BlankNodeId::new(format!("{prefix}{n}")))
187 }
188
189 pub fn current_counter(&self) -> u64 {
191 self.counter.load(Ordering::Relaxed)
192 }
193
194 pub fn reset_counter(&self) {
196 self.counter.store(0, Ordering::Relaxed);
197 }
198
199 pub fn create_scope(&mut self, name: impl Into<String>) -> Result<(), BlankNodeError> {
203 let name = name.into();
204 if name.is_empty() {
205 return Err(BlankNodeError::EmptyId);
206 }
207 self.scope_counters.entry(name.clone()).or_insert(0);
208 self.scope_ids.entry(name.clone()).or_default();
209 self.stats.active_scopes = self.scope_counters.len();
210 Ok(())
211 }
212
213 pub fn next_in_scope(&mut self, scope: &str) -> Result<BlankNodeId, BlankNodeError> {
215 let scope_counter = self
216 .scope_counters
217 .get_mut(scope)
218 .ok_or_else(|| BlankNodeError::UnknownScope(scope.to_string()))?;
219 let n = *scope_counter;
220 *scope_counter += 1;
221 let id_str = format!("{}_{}{n}", scope, self.config.default_prefix);
222 let blank = BlankNodeId::with_scope(id_str.clone(), scope);
223 if let Some(ids) = self.scope_ids.get_mut(scope) {
224 ids.push(id_str);
225 }
226 self.stats.total_allocated += 1;
227 *self.stats.per_scope.entry(scope.to_string()).or_insert(0) += 1;
228 Ok(blank)
229 }
230
231 pub fn ids_in_scope(&self, scope: &str) -> Result<&[String], BlankNodeError> {
233 self.scope_ids
234 .get(scope)
235 .map(|v| v.as_slice())
236 .ok_or_else(|| BlankNodeError::UnknownScope(scope.to_string()))
237 }
238
239 pub fn remove_scope(&mut self, scope: &str) -> Result<(), BlankNodeError> {
241 if !self.scope_counters.contains_key(scope) {
242 return Err(BlankNodeError::UnknownScope(scope.to_string()));
243 }
244 self.scope_counters.remove(scope);
245 self.scope_ids.remove(scope);
246 self.stats.active_scopes = self.scope_counters.len();
247 Ok(())
248 }
249
250 pub fn active_scopes(&self) -> Vec<String> {
252 self.scope_counters.keys().cloned().collect()
253 }
254
255 pub fn skolemize(&mut self, blank_id: &str) -> Result<String, BlankNodeError> {
261 if blank_id.is_empty() {
262 return Err(BlankNodeError::EmptyId);
263 }
264 if self.config.skolem_base.is_empty() {
265 return Err(BlankNodeError::InvalidBaseUri(
266 "Skolem base URI is not configured".to_string(),
267 ));
268 }
269 self.stats.skolemized += 1;
270 Ok(format!("{}{blank_id}", self.config.skolem_base))
271 }
272
273 pub fn skolemize_with_base(
275 &mut self,
276 blank_id: &str,
277 base: &str,
278 ) -> Result<String, BlankNodeError> {
279 if blank_id.is_empty() {
280 return Err(BlankNodeError::EmptyId);
281 }
282 if base.is_empty() || !base.starts_with("http") {
283 return Err(BlankNodeError::InvalidBaseUri(base.to_string()));
284 }
285 self.stats.skolemized += 1;
286 let sep = if base.ends_with('/') { "" } else { "/" };
287 Ok(format!("{base}{sep}{blank_id}"))
288 }
289
290 pub fn deskolemize(&mut self, iri: &str) -> Result<BlankNodeId, BlankNodeError> {
294 let stripped = iri
295 .strip_prefix('<')
296 .unwrap_or(iri)
297 .strip_suffix('>')
298 .unwrap_or(iri);
299 if let Some(local) = stripped.strip_prefix(self.config.skolem_base.as_str()) {
300 if local.is_empty() {
301 return Err(BlankNodeError::EmptyId);
302 }
303 self.stats.deskolemized += 1;
304 return Ok(BlankNodeId::new(local));
305 }
306 Err(BlankNodeError::InvalidBaseUri(format!(
307 "IRI does not match skolem base: {iri}"
308 )))
309 }
310
311 pub fn deskolemize_with_base(
313 &mut self,
314 iri: &str,
315 base: &str,
316 ) -> Result<BlankNodeId, BlankNodeError> {
317 let stripped = iri
318 .strip_prefix('<')
319 .unwrap_or(iri)
320 .strip_suffix('>')
321 .unwrap_or(iri);
322 if let Some(local) = stripped.strip_prefix(base) {
323 let local = local.strip_prefix('/').unwrap_or(local);
324 if local.is_empty() {
325 return Err(BlankNodeError::EmptyId);
326 }
327 self.stats.deskolemized += 1;
328 return Ok(BlankNodeId::new(local));
329 }
330 Err(BlankNodeError::InvalidBaseUri(format!(
331 "IRI does not match base: {iri}"
332 )))
333 }
334
335 pub fn discover_mapping(
343 source: &[&str],
344 target: &[&str],
345 ) -> Result<HashMap<String, String>, BlankNodeError> {
346 if source.len() != target.len() {
347 return Err(BlankNodeError::MappingConflict(format!(
348 "Source has {} blank nodes but target has {}",
349 source.len(),
350 target.len()
351 )));
352 }
353 let mut mapping = HashMap::new();
354 let mut used_targets: std::collections::HashSet<String> = std::collections::HashSet::new();
355 for (i, s) in source.iter().enumerate() {
356 let t = target
357 .get(i)
358 .ok_or_else(|| BlankNodeError::MappingConflict("Index out of bounds".into()))?;
359 if used_targets.contains(*t) {
360 return Err(BlankNodeError::MappingConflict(format!(
361 "Target blank node '{t}' is already mapped"
362 )));
363 }
364 mapping.insert(s.to_string(), t.to_string());
365 used_targets.insert(t.to_string());
366 }
367 Ok(mapping)
368 }
369
370 pub fn apply_mapping(ids: &[&str], mapping: &HashMap<String, String>) -> Vec<String> {
372 ids.iter()
373 .map(|id| mapping.get(*id).cloned().unwrap_or_else(|| id.to_string()))
374 .collect()
375 }
376
377 pub fn verify_mapping(mapping: &HashMap<String, String>) -> Result<(), BlankNodeError> {
379 let mut seen = std::collections::HashSet::new();
380 for (k, v) in mapping {
381 if !seen.insert(v.clone()) {
382 return Err(BlankNodeError::MappingConflict(format!(
383 "Duplicate target '{v}' for source '{k}'"
384 )));
385 }
386 }
387 Ok(())
388 }
389
390 pub fn rename_with_prefix(
396 ids: &[&str],
397 new_prefix: &str,
398 ) -> Result<Vec<BlankNodeId>, BlankNodeError> {
399 validate_prefix(new_prefix)?;
400 let mut result = Vec::with_capacity(ids.len());
401 for (i, _) in ids.iter().enumerate() {
402 result.push(BlankNodeId::new(format!("{new_prefix}{i}")));
403 }
404 Ok(result)
405 }
406
407 pub fn rename_prefix(
410 ids: &[&str],
411 old_prefix: &str,
412 new_prefix: &str,
413 ) -> Result<Vec<BlankNodeId>, BlankNodeError> {
414 validate_prefix(new_prefix)?;
415 let mut result = Vec::with_capacity(ids.len());
416 for id in ids {
417 if let Some(suffix) = id.strip_prefix(old_prefix) {
418 result.push(BlankNodeId::new(format!("{new_prefix}{suffix}")));
419 } else {
420 result.push(BlankNodeId::new(id.to_string()));
421 }
422 }
423 Ok(result)
424 }
425
426 pub fn scope_rename(ids: &[&str], scope: &str) -> Result<Vec<BlankNodeId>, BlankNodeError> {
428 if scope.is_empty() {
429 return Err(BlankNodeError::EmptyId);
430 }
431 let mut result = Vec::with_capacity(ids.len());
432 for id in ids {
433 result.push(BlankNodeId::with_scope(format!("{scope}_{id}"), scope));
434 }
435 Ok(result)
436 }
437
438 pub fn stats(&self) -> &AllocationStats {
442 &self.stats
443 }
444
445 pub fn config(&self) -> &AllocatorConfig {
447 &self.config
448 }
449
450 pub fn shared_counter(&self) -> Arc<AtomicU64> {
452 Arc::clone(&self.counter)
453 }
454}
455
456impl Default for BlankNodeAllocator {
457 fn default() -> Self {
458 Self::new()
459 }
460}
461
462fn validate_prefix(prefix: &str) -> Result<(), BlankNodeError> {
464 if prefix.is_empty() {
465 return Err(BlankNodeError::InvalidPrefix(
466 "prefix must not be empty".to_string(),
467 ));
468 }
469 if !prefix
470 .chars()
471 .all(|c| c.is_ascii_alphanumeric() || c == '_')
472 {
473 return Err(BlankNodeError::InvalidPrefix(prefix.to_string()));
474 }
475 Ok(())
476}
477
478#[cfg(test)]
483mod tests {
484 use super::*;
485
486 #[test]
489 fn test_next_generates_sequential_ids() {
490 let alloc = BlankNodeAllocator::new();
491 let a = alloc.next().expect("first");
492 let b = alloc.next().expect("second");
493 assert_eq!(a.id, "b0");
494 assert_eq!(b.id, "b1");
495 }
496
497 #[test]
498 fn test_next_with_prefix() {
499 let alloc = BlankNodeAllocator::new();
500 let id = alloc.next_with_prefix("node").expect("prefix");
501 assert!(id.id.starts_with("node"));
502 }
503
504 #[test]
505 fn test_counter_reset() {
506 let alloc = BlankNodeAllocator::new();
507 let _ = alloc.next();
508 let _ = alloc.next();
509 assert_eq!(alloc.current_counter(), 2);
510 alloc.reset_counter();
511 assert_eq!(alloc.current_counter(), 0);
512 let id = alloc.next().expect("after reset");
513 assert_eq!(id.id, "b0");
514 }
515
516 #[test]
517 fn test_to_ntriples() {
518 let id = BlankNodeId::new("x42");
519 assert_eq!(id.to_ntriples(), "_:x42");
520 }
521
522 #[test]
523 fn test_display_trait() {
524 let id = BlankNodeId::new("abc");
525 assert_eq!(format!("{id}"), "_:abc");
526 }
527
528 #[test]
531 fn test_with_config_custom_prefix() {
532 let cfg = AllocatorConfig {
533 default_prefix: "node".to_string(),
534 ..Default::default()
535 };
536 let alloc = BlankNodeAllocator::with_config(cfg).expect("cfg");
537 let id = alloc.next().expect("next");
538 assert_eq!(id.id, "node0");
539 }
540
541 #[test]
542 fn test_max_allocations_limit() {
543 let cfg = AllocatorConfig {
544 max_allocations: 2,
545 ..Default::default()
546 };
547 let alloc = BlankNodeAllocator::with_config(cfg).expect("cfg");
548 assert!(alloc.next().is_ok());
549 assert!(alloc.next().is_ok());
550 let err = alloc.next().expect_err("should exceed limit");
551 assert!(matches!(err, BlankNodeError::LimitExceeded(2)));
552 }
553
554 #[test]
555 fn test_invalid_prefix_rejected() {
556 let cfg = AllocatorConfig {
557 default_prefix: "no-dash".to_string(),
558 ..Default::default()
559 };
560 let result = BlankNodeAllocator::with_config(cfg);
561 assert!(result.is_err());
562 }
563
564 #[test]
565 fn test_invalid_skolem_base_rejected() {
566 let cfg = AllocatorConfig {
567 skolem_base: "ftp://bad".to_string(),
568 ..Default::default()
569 };
570 let result = BlankNodeAllocator::with_config(cfg);
571 assert!(result.is_err());
572 }
573
574 #[test]
577 fn test_create_and_use_scope() {
578 let mut alloc = BlankNodeAllocator::new();
579 alloc.create_scope("doc1").expect("create");
580 let id = alloc.next_in_scope("doc1").expect("scoped");
581 assert!(id.id.starts_with("doc1_"));
582 assert_eq!(id.scope.as_deref(), Some("doc1"));
583 }
584
585 #[test]
586 fn test_scope_counter_independent() {
587 let mut alloc = BlankNodeAllocator::new();
588 alloc.create_scope("a").expect("a");
589 alloc.create_scope("b").expect("b");
590 let a0 = alloc.next_in_scope("a").expect("a0");
591 let b0 = alloc.next_in_scope("b").expect("b0");
592 let a1 = alloc.next_in_scope("a").expect("a1");
593 assert_eq!(a0.id, "a_b0");
594 assert_eq!(b0.id, "b_b0");
595 assert_eq!(a1.id, "a_b1");
596 }
597
598 #[test]
599 fn test_unknown_scope_error() {
600 let mut alloc = BlankNodeAllocator::new();
601 let err = alloc.next_in_scope("missing").expect_err("unknown scope");
602 assert!(matches!(err, BlankNodeError::UnknownScope(_)));
603 }
604
605 #[test]
606 fn test_ids_in_scope() {
607 let mut alloc = BlankNodeAllocator::new();
608 alloc.create_scope("s").expect("create");
609 alloc.next_in_scope("s").expect("s0");
610 alloc.next_in_scope("s").expect("s1");
611 let ids = alloc.ids_in_scope("s").expect("list");
612 assert_eq!(ids.len(), 2);
613 }
614
615 #[test]
616 fn test_remove_scope() {
617 let mut alloc = BlankNodeAllocator::new();
618 alloc.create_scope("tmp").expect("create");
619 alloc.next_in_scope("tmp").expect("n");
620 alloc.remove_scope("tmp").expect("remove");
621 assert!(!alloc.active_scopes().contains(&"tmp".to_string()));
622 }
623
624 #[test]
625 fn test_remove_unknown_scope_errors() {
626 let mut alloc = BlankNodeAllocator::new();
627 let err = alloc.remove_scope("nope").expect_err("remove unknown");
628 assert!(matches!(err, BlankNodeError::UnknownScope(_)));
629 }
630
631 #[test]
632 fn test_create_scope_empty_name_fails() {
633 let mut alloc = BlankNodeAllocator::new();
634 let err = alloc.create_scope("").expect_err("empty scope");
635 assert!(matches!(err, BlankNodeError::EmptyId));
636 }
637
638 #[test]
639 fn test_active_scopes_list() {
640 let mut alloc = BlankNodeAllocator::new();
641 alloc.create_scope("x").expect("x");
642 alloc.create_scope("y").expect("y");
643 let scopes = alloc.active_scopes();
644 assert!(scopes.contains(&"x".to_string()));
645 assert!(scopes.contains(&"y".to_string()));
646 }
647
648 #[test]
651 fn test_skolemize_default_base() {
652 let mut alloc = BlankNodeAllocator::new();
653 let iri = alloc.skolemize("b0").expect("skolemize");
654 assert_eq!(iri, "https://example.org/.well-known/genid/b0");
655 }
656
657 #[test]
658 fn test_skolemize_custom_base() {
659 let mut alloc = BlankNodeAllocator::new();
660 let iri = alloc
661 .skolemize_with_base("n1", "https://data.org/genid/")
662 .expect("custom");
663 assert_eq!(iri, "https://data.org/genid/n1");
664 }
665
666 #[test]
667 fn test_skolemize_custom_base_no_trailing_slash() {
668 let mut alloc = BlankNodeAllocator::new();
669 let iri = alloc
670 .skolemize_with_base("n1", "https://data.org/genid")
671 .expect("no slash");
672 assert_eq!(iri, "https://data.org/genid/n1");
673 }
674
675 #[test]
676 fn test_skolemize_empty_id_fails() {
677 let mut alloc = BlankNodeAllocator::new();
678 let err = alloc.skolemize("").expect_err("empty");
679 assert!(matches!(err, BlankNodeError::EmptyId));
680 }
681
682 #[test]
683 fn test_skolemize_no_base_configured() {
684 let cfg = AllocatorConfig {
685 skolem_base: String::new(),
686 ..Default::default()
687 };
688 let mut alloc = BlankNodeAllocator::with_config(cfg).expect("cfg");
689 let err = alloc.skolemize("b0").expect_err("no base");
690 assert!(matches!(err, BlankNodeError::InvalidBaseUri(_)));
691 }
692
693 #[test]
696 fn test_deskolemize_default_base() {
697 let mut alloc = BlankNodeAllocator::new();
698 let bn = alloc
699 .deskolemize("https://example.org/.well-known/genid/b42")
700 .expect("de");
701 assert_eq!(bn.id, "b42");
702 }
703
704 #[test]
705 fn test_deskolemize_with_angle_brackets() {
706 let mut alloc = BlankNodeAllocator::new();
707 let bn = alloc
708 .deskolemize("<https://example.org/.well-known/genid/n7>")
709 .expect("de");
710 assert_eq!(bn.id, "n7");
711 }
712
713 #[test]
714 fn test_deskolemize_wrong_base_fails() {
715 let mut alloc = BlankNodeAllocator::new();
716 let err = alloc
717 .deskolemize("https://other.org/genid/b0")
718 .expect_err("wrong base");
719 assert!(matches!(err, BlankNodeError::InvalidBaseUri(_)));
720 }
721
722 #[test]
723 fn test_deskolemize_with_custom_base() {
724 let mut alloc = BlankNodeAllocator::new();
725 let bn = alloc
726 .deskolemize_with_base("https://data.org/genid/x1", "https://data.org/genid/")
727 .expect("custom de");
728 assert_eq!(bn.id, "x1");
729 }
730
731 #[test]
732 fn test_roundtrip_skolemize_deskolemize() {
733 let mut alloc = BlankNodeAllocator::new();
734 let iri = alloc.skolemize("abc123").expect("sk");
735 let bn = alloc.deskolemize(&iri).expect("de");
736 assert_eq!(bn.id, "abc123");
737 }
738
739 #[test]
742 fn test_discover_mapping_equal_sizes() {
743 let source = vec!["b0", "b1", "b2"];
744 let target = vec!["x0", "x1", "x2"];
745 let map = BlankNodeAllocator::discover_mapping(&source, &target).expect("map");
746 assert_eq!(map.get("b0"), Some(&"x0".to_string()));
747 assert_eq!(map.get("b2"), Some(&"x2".to_string()));
748 }
749
750 #[test]
751 fn test_discover_mapping_size_mismatch() {
752 let source = vec!["b0", "b1"];
753 let target = vec!["x0"];
754 let err = BlankNodeAllocator::discover_mapping(&source, &target).expect_err("mismatch");
755 assert!(matches!(err, BlankNodeError::MappingConflict(_)));
756 }
757
758 #[test]
759 fn test_discover_mapping_duplicate_target() {
760 let source = vec!["b0", "b1"];
761 let target = vec!["x0", "x0"];
762 let err = BlankNodeAllocator::discover_mapping(&source, &target).expect_err("dup");
763 assert!(matches!(err, BlankNodeError::MappingConflict(_)));
764 }
765
766 #[test]
767 fn test_apply_mapping() {
768 let mut mapping = HashMap::new();
769 mapping.insert("b0".to_string(), "x0".to_string());
770 mapping.insert("b1".to_string(), "x1".to_string());
771 let result = BlankNodeAllocator::apply_mapping(&["b0", "b1", "b2"], &mapping);
772 assert_eq!(result, vec!["x0", "x1", "b2"]);
773 }
774
775 #[test]
776 fn test_verify_mapping_valid() {
777 let mut mapping = HashMap::new();
778 mapping.insert("a".to_string(), "x".to_string());
779 mapping.insert("b".to_string(), "y".to_string());
780 assert!(BlankNodeAllocator::verify_mapping(&mapping).is_ok());
781 }
782
783 #[test]
784 fn test_verify_mapping_duplicate_values() {
785 let mut mapping = HashMap::new();
786 mapping.insert("a".to_string(), "x".to_string());
787 mapping.insert("b".to_string(), "x".to_string());
788 let err = BlankNodeAllocator::verify_mapping(&mapping).expect_err("dup");
789 assert!(matches!(err, BlankNodeError::MappingConflict(_)));
790 }
791
792 #[test]
795 fn test_rename_with_prefix() {
796 let ids = vec!["b0", "b1", "b2"];
797 let result = BlankNodeAllocator::rename_with_prefix(&ids, "merged_").expect("rename");
798 assert_eq!(result[0].id, "merged_0");
799 assert_eq!(result[1].id, "merged_1");
800 assert_eq!(result[2].id, "merged_2");
801 }
802
803 #[test]
804 fn test_rename_prefix_swap() {
805 let ids = vec!["old_0", "old_1"];
806 let result = BlankNodeAllocator::rename_prefix(&ids, "old_", "new_").expect("swap");
807 assert_eq!(result[0].id, "new_0");
808 assert_eq!(result[1].id, "new_1");
809 }
810
811 #[test]
812 fn test_rename_prefix_no_match_keeps_original() {
813 let ids = vec!["other_0"];
814 let result = BlankNodeAllocator::rename_prefix(&ids, "old_", "new_").expect("no match");
815 assert_eq!(result[0].id, "other_0");
816 }
817
818 #[test]
819 fn test_scope_rename() {
820 let ids = vec!["b0", "b1"];
821 let result = BlankNodeAllocator::scope_rename(&ids, "graph1").expect("scope rename");
822 assert_eq!(result[0].id, "graph1_b0");
823 assert_eq!(result[0].scope, Some("graph1".to_string()));
824 assert_eq!(result[1].id, "graph1_b1");
825 }
826
827 #[test]
828 fn test_scope_rename_empty_scope_fails() {
829 let ids = vec!["b0"];
830 let err = BlankNodeAllocator::scope_rename(&ids, "").expect_err("empty");
831 assert!(matches!(err, BlankNodeError::EmptyId));
832 }
833
834 #[test]
835 fn test_rename_invalid_prefix_fails() {
836 let ids = vec!["b0"];
837 let err = BlankNodeAllocator::rename_with_prefix(&ids, "bad-prefix").expect_err("invalid");
838 assert!(matches!(err, BlankNodeError::InvalidPrefix(_)));
839 }
840
841 #[test]
844 fn test_stats_after_scope_allocation() {
845 let mut alloc = BlankNodeAllocator::new();
846 alloc.create_scope("doc").expect("create");
847 alloc.next_in_scope("doc").expect("n1");
848 alloc.next_in_scope("doc").expect("n2");
849 let stats = alloc.stats();
850 assert_eq!(stats.total_allocated, 2);
851 assert_eq!(stats.per_scope.get("doc"), Some(&2));
852 }
853
854 #[test]
855 fn test_stats_skolemized_count() {
856 let mut alloc = BlankNodeAllocator::new();
857 alloc.skolemize("b0").expect("sk");
858 alloc.skolemize("b1").expect("sk");
859 assert_eq!(alloc.stats().skolemized, 2);
860 }
861
862 #[test]
863 fn test_stats_deskolemized_count() {
864 let mut alloc = BlankNodeAllocator::new();
865 alloc
866 .deskolemize("https://example.org/.well-known/genid/b0")
867 .expect("de");
868 assert_eq!(alloc.stats().deskolemized, 1);
869 }
870
871 #[test]
874 fn test_shared_counter_across_threads() {
875 let alloc = BlankNodeAllocator::new();
876 let counter = alloc.shared_counter();
877 let handles: Vec<_> = (0..4)
878 .map(|_| {
879 let c = Arc::clone(&counter);
880 std::thread::spawn(move || {
881 for _ in 0..100 {
882 c.fetch_add(1, Ordering::Relaxed);
883 }
884 })
885 })
886 .collect();
887 for h in handles {
888 h.join().expect("thread join");
889 }
890 assert_eq!(counter.load(Ordering::Relaxed), 400);
891 }
892
893 #[test]
894 fn test_blank_node_id_equality() {
895 let a = BlankNodeId::new("x");
896 let b = BlankNodeId::new("x");
897 assert_eq!(a, b);
898 }
899
900 #[test]
901 fn test_blank_node_id_with_scope_equality() {
902 let a = BlankNodeId::with_scope("x0", "doc1");
903 let b = BlankNodeId::with_scope("x0", "doc1");
904 assert_eq!(a, b);
905 }
906
907 #[test]
908 fn test_blank_node_id_hash() {
909 use std::collections::HashSet;
910 let mut set = HashSet::new();
911 set.insert(BlankNodeId::new("a"));
912 set.insert(BlankNodeId::new("a"));
913 set.insert(BlankNodeId::new("b"));
914 assert_eq!(set.len(), 2);
915 }
916
917 #[test]
918 fn test_default_allocator() {
919 let alloc = BlankNodeAllocator::default();
920 let id = alloc.next().expect("default");
921 assert_eq!(id.id, "b0");
922 }
923
924 #[test]
925 fn test_config_accessor() {
926 let alloc = BlankNodeAllocator::new();
927 assert_eq!(alloc.config().default_prefix, "b");
928 }
929
930 #[test]
931 fn test_empty_mapping_is_valid() {
932 let map: HashMap<String, String> = HashMap::new();
933 assert!(BlankNodeAllocator::verify_mapping(&map).is_ok());
934 }
935
936 #[test]
937 fn test_discover_mapping_empty() {
938 let source: Vec<&str> = vec![];
939 let target: Vec<&str> = vec![];
940 let map = BlankNodeAllocator::discover_mapping(&source, &target).expect("empty");
941 assert!(map.is_empty());
942 }
943}