Skip to main content

zettel_core/
id.rs

1// crates/zettel-core/src/id.rs - Core ID manipulation for Luhmann-style Zettelkasten
2
3use regex::Regex;
4use serde::{Deserialize, Serialize};
5use std::fmt;
6use std::str::FromStr;
7use thiserror::Error;
8
9/// Errors that can occur during ID operations
10#[derive(Error, Debug, Clone, PartialEq)]
11pub enum IdError {
12    #[error("Invalid ID format: {0}")]
13    InvalidFormat(String),
14
15    #[error("Empty ID not allowed")]
16    EmptyId,
17
18    #[error("Invalid component: {0}")]
19    InvalidComponent(String),
20
21    #[error("ID overflow: cannot increment {0}")]
22    Overflow(String),
23
24    #[error("No parent for root ID: {0}")]
25    NoParent(String),
26
27    #[error("Parsing error: {0}")]
28    ParseError(String),
29}
30
31/// Result type for ID operations
32pub type IdResult<T> = Result<T, IdError>;
33
34/// Represents a single component of an ID (either numeric or alphabetic)
35#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
36pub enum IdComponent {
37    /// Numeric component (e.g., "1", "42", "123")
38    Numeric(u32),
39    /// Alphabetic component (e.g., "a", "z", "aa", "abc")
40    Alpha(String),
41}
42
43impl IdComponent {
44    /// Create a new numeric component
45    pub fn numeric(value: u32) -> Self {
46        Self::Numeric(value)
47    }
48
49    /// Create a new alphabetic component
50    pub fn alpha<S: Into<String>>(value: S) -> IdResult<Self> {
51        let value = value.into();
52        if value.is_empty() {
53            return Err(IdError::InvalidComponent(
54                "Empty alphabetic component".to_string(),
55            ));
56        }
57
58        if !value.chars().all(|c| c.is_ascii_lowercase()) {
59            return Err(IdError::InvalidComponent(format!(
60                "Alphabetic component must contain only lowercase letters: {}",
61                value
62            )));
63        }
64
65        Ok(Self::Alpha(value))
66    }
67
68    /// Get the next component in sequence
69    pub fn increment(&self) -> IdResult<Self> {
70        match self {
71            Self::Numeric(n) => {
72                if *n == u32::MAX {
73                    Err(IdError::Overflow(format!("Numeric component: {}", n)))
74                } else {
75                    Ok(Self::Numeric(n + 1))
76                }
77            }
78            Self::Alpha(s) => {
79                let incremented = increment_alpha_string(s)?;
80                Ok(Self::Alpha(incremented))
81            }
82        }
83    }
84
85    /// Check if this is a numeric component
86    pub fn is_numeric(&self) -> bool {
87        matches!(self, Self::Numeric(_))
88    }
89
90    /// Check if this is an alphabetic component
91    pub fn is_alpha(&self) -> bool {
92        matches!(self, Self::Alpha(_))
93    }
94
95    /// Get the value as a string
96    pub fn as_str(&self) -> String {
97        match self {
98            Self::Numeric(n) => n.to_string(),
99            Self::Alpha(s) => s.clone(),
100        }
101    }
102}
103
104impl fmt::Display for IdComponent {
105    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106        match self {
107            Self::Numeric(n) => write!(f, "{}", n),
108            Self::Alpha(s) => write!(f, "{}", s),
109        }
110    }
111}
112
113impl FromStr for IdComponent {
114    type Err = IdError;
115
116    fn from_str(s: &str) -> IdResult<Self> {
117        if s.is_empty() {
118            return Err(IdError::InvalidComponent("Empty component".to_string()));
119        }
120
121        // Try parsing as number first
122        if let Ok(num) = s.parse::<u32>() {
123            return Ok(Self::Numeric(num));
124        }
125
126        // Otherwise treat as alphabetic
127        Self::alpha(s)
128    }
129}
130
131/// Represents a complete Luhmann-style ID (e.g., "1", "1a", "1a2b", "42c17z")
132#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
133pub struct Id {
134    components: Vec<IdComponent>,
135}
136
137impl Id {
138    /// Create a new ID from components
139    pub fn new(components: Vec<IdComponent>) -> IdResult<Self> {
140        if components.is_empty() {
141            return Err(IdError::EmptyId);
142        }
143
144        // Validate Luhmann alternating pattern: number -> letter -> number -> letter...
145        for (i, component) in components.iter().enumerate() {
146            let should_be_numeric = i % 2 == 0; // Even indices should be numeric
147
148            if should_be_numeric && !component.is_numeric() {
149                return Err(IdError::InvalidFormat(format!(
150                    "Component {} should be numeric but got: {}",
151                    i, component
152                )));
153            }
154
155            if !should_be_numeric && !component.is_alpha() {
156                return Err(IdError::InvalidFormat(format!(
157                    "Component {} should be alphabetic but got: {}",
158                    i, component
159                )));
160            }
161        }
162
163        Ok(Self { components })
164    }
165
166    /// Create ID from a single numeric component (e.g., "1", "42")
167    pub fn from_number(n: u32) -> Self {
168        Self {
169            components: vec![IdComponent::Numeric(n)],
170        }
171    }
172
173    /// Parse ID from string (e.g., "1a2b3")
174    pub fn parse<S: AsRef<str>>(s: S) -> IdResult<Self> {
175        let s = s.as_ref();
176        if s.is_empty() {
177            return Err(IdError::EmptyId);
178        }
179
180        let components = parse_id_string(s)?;
181        Self::new(components)
182    }
183
184    /// Get all components
185    pub fn components(&self) -> &[IdComponent] {
186        &self.components
187    }
188
189    /// Get the depth/level of this ID (number of components)
190    pub fn depth(&self) -> usize {
191        self.components.len()
192    }
193
194    /// Check if this is a root ID (single numeric component)
195    pub fn is_root(&self) -> bool {
196        self.components.len() == 1 && self.components[0].is_numeric()
197    }
198
199    /// Get the parent ID by removing the last component
200    pub fn parent(&self) -> IdResult<Option<Self>> {
201        if self.components.len() <= 1 {
202            return Ok(None); // Root has no parent
203        }
204
205        let mut parent_components = self.components.clone();
206        parent_components.pop();
207
208        Ok(Some(Self {
209            components: parent_components,
210        }))
211    }
212
213    /// Get the next sibling ID (increment last component)
214    pub fn next_sibling(&self) -> IdResult<Self> {
215        if self.components.is_empty() {
216            return Err(IdError::EmptyId);
217        }
218
219        let mut sibling_components = self.components.clone();
220        let last_idx = sibling_components.len() - 1;
221        sibling_components[last_idx] = sibling_components[last_idx].increment()?;
222
223        Ok(Self {
224            components: sibling_components,
225        })
226    }
227
228    /// Get the first child ID (append appropriate component type)
229    pub fn first_child(&self) -> Self {
230        let mut child_components = self.components.clone();
231
232        // Luhmann pattern: numeric -> alpha -> numeric -> alpha...
233        let next_component = if self.components.len() % 2 == 0 {
234            // Even length means last is alpha, next should be numeric
235            IdComponent::Numeric(1)
236        } else {
237            // Odd length means last is numeric, next should be alpha
238            IdComponent::Alpha("a".to_string())
239        };
240
241        child_components.push(next_component);
242        Self {
243            components: child_components,
244        }
245    }
246
247    /// Check if this ID is an ancestor of another ID
248    pub fn is_ancestor_of(&self, other: &Id) -> bool {
249        if self.components.len() >= other.components.len() {
250            return false; // Can't be ancestor if same length or longer
251        }
252
253        // Check if our components are a prefix of the other's components
254        self.components
255            .iter()
256            .zip(other.components.iter())
257            .all(|(a, b)| a == b)
258    }
259
260    /// Check if this ID is a descendant of another ID
261    pub fn is_descendant_of(&self, other: &Id) -> bool {
262        other.is_ancestor_of(self)
263    }
264
265    /// Check if this ID is a sibling of another ID (same parent)
266    pub fn is_sibling_of(&self, other: &Id) -> bool {
267        if self.components.len() != other.components.len() {
268            return false; // Different depths can't be siblings
269        }
270
271        if self.components.len() <= 1 {
272            return true; // All roots are siblings
273        }
274
275        // Check if all components except the last are the same
276        self.components[..self.components.len() - 1]
277            .iter()
278            .zip(other.components[..other.components.len() - 1].iter())
279            .all(|(a, b)| a == b)
280    }
281
282    /// Get all ancestor IDs from root to direct parent
283    pub fn ancestors(&self) -> Vec<Id> {
284        let mut ancestors = Vec::new();
285
286        for i in 1..self.components.len() {
287            let ancestor_components = self.components[..i].to_vec();
288            ancestors.push(Id {
289                components: ancestor_components,
290            });
291        }
292
293        ancestors
294    }
295
296    /// Convert to string representation
297    pub fn to_string(&self) -> String {
298        self.components
299            .iter()
300            .map(|c| c.as_str())
301            .collect::<Vec<_>>()
302            .join("")
303    }
304}
305
306impl fmt::Display for Id {
307    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
308        write!(f, "{}", self.to_string())
309    }
310}
311
312impl FromStr for Id {
313    type Err = IdError;
314
315    fn from_str(s: &str) -> IdResult<Self> {
316        Self::parse(s)
317    }
318}
319
320/// ID Manager handles ID generation, validation, and operations within a vault context
321pub struct IdManager<F> {
322    config: IdConfig,
323    existence_checker: F,
324}
325
326/// Configuration for ID parsing and generation
327///
328/// This is now a re-export of the config system's IdConfig to maintain
329/// API compatibility while centralizing configuration management.
330pub use crate::config::IdConfig;
331
332impl<F> IdManager<F>
333where
334    F: Fn(&str) -> bool, // Function to check if an ID exists
335{
336    /// Create new ID manager with configuration and existence checker
337    pub fn new(config: IdConfig, existence_checker: F) -> Self {
338        Self {
339            config,
340            existence_checker,
341        }
342    }
343
344    /// Extract ID from filename based on configured rules
345    pub fn extract_from_filename(&self, filename: &str) -> Option<Id> {
346        let patterns = self.get_filename_patterns();
347
348        for pattern in patterns {
349            if let Some(captures) = pattern.captures(filename) {
350                if let Some(id_match) = captures.get(1) {
351                    if let Ok(id) = Id::parse(id_match.as_str()) {
352                        return Some(id);
353                    }
354                }
355            }
356        }
357
358        None
359    }
360
361    /// Generate next available sibling ID
362    pub fn next_available_sibling(&self, current_id: &Id) -> IdResult<Id> {
363        let mut candidate = current_id.next_sibling()?;
364
365        // Keep incrementing until we find an available ID
366        while (self.existence_checker)(&candidate.to_string()) {
367            candidate = candidate.next_sibling()?;
368        }
369
370        Ok(candidate)
371    }
372
373    /// Generate next available child ID
374    pub fn next_available_child(&self, parent_id: &Id) -> Id {
375        let mut candidate = parent_id.first_child();
376
377        // Keep incrementing until we find an available ID
378        while (self.existence_checker)(&candidate.to_string()) {
379            // For children, we increment the last component
380            if let Ok(next) = candidate.next_sibling() {
381                candidate = next;
382            } else {
383                // If we can't increment, something's wrong - return the first child
384                break;
385            }
386        }
387
388        candidate
389    }
390
391    /// Validate an ID string
392    pub fn validate_id(&self, id_str: &str) -> IdResult<Id> {
393        Id::parse(id_str)
394    }
395
396    /// Check if an ID exists in the vault
397    pub fn id_exists(&self, id: &Id) -> bool {
398        (self.existence_checker)(&id.to_string())
399    }
400
401    /// Get filename patterns based on configuration
402    fn get_filename_patterns(&self) -> Vec<Regex> {
403        let id_pattern = if self.config.allow_unicode {
404            r"([0-9\p{L}]+(?:[0-9\p{L}]*)*)"
405        } else {
406            r"([0-9a-z]+(?:[0-9a-z]*)*)"
407        };
408
409        let mut patterns = Vec::new();
410
411        match self.config.match_rule.as_str() {
412            "strict" => {
413                // Filename is exactly the ID: "1a2.md"
414                patterns.push(Regex::new(&format!(r"^{}$", id_pattern)).unwrap());
415            }
416            "separator" => {
417                // ID followed by separator: "1a2 - Title.md"
418                let escaped_sep = regex::escape(&self.config.separator);
419                patterns.push(Regex::new(&format!(r"^{}{}.*", id_pattern, escaped_sep)).unwrap());
420            }
421            "fuzzy" => {
422                // ID at start, anything after first non-alphanumeric: "1a2_title.md", "1a2-title.md"
423                patterns.push(Regex::new(&format!(r"^{}[^0-9a-z].*", id_pattern)).unwrap());
424                // Also match strict format as fallback
425                patterns.push(Regex::new(&format!(r"^{}$", id_pattern)).unwrap());
426            }
427            _ => {
428                // Default to strict
429                patterns.push(Regex::new(&format!(r"^{}$", id_pattern)).unwrap());
430            }
431        }
432
433        patterns
434    }
435}
436
437/// Parse ID string into components
438fn parse_id_string(s: &str) -> IdResult<Vec<IdComponent>> {
439    if s.is_empty() {
440        return Err(IdError::EmptyId);
441    }
442
443    let mut components = Vec::new();
444    let mut current = String::new();
445    let mut expecting_numeric = true; // Luhmann IDs start with numbers
446
447    for ch in s.chars() {
448        if ch.is_ascii_digit() {
449            if !expecting_numeric && !current.is_empty() {
450                // We were building an alpha component, finish it
451                components.push(IdComponent::alpha(current.clone())?);
452                current.clear();
453                expecting_numeric = true;
454            }
455            current.push(ch);
456        } else if ch.is_ascii_lowercase() {
457            if expecting_numeric && !current.is_empty() {
458                // We were building a numeric component, finish it
459                let num: u32 = current
460                    .parse()
461                    .map_err(|_| IdError::ParseError(format!("Invalid number: {}", current)))?;
462                components.push(IdComponent::Numeric(num));
463                current.clear();
464                expecting_numeric = false;
465            }
466            current.push(ch);
467        } else {
468            return Err(IdError::InvalidFormat(format!(
469                "Invalid character '{}' in ID: {}",
470                ch, s
471            )));
472        }
473    }
474
475    // Handle the last component
476    if !current.is_empty() {
477        if expecting_numeric {
478            let num: u32 = current
479                .parse()
480                .map_err(|_| IdError::ParseError(format!("Invalid number: {}", current)))?;
481            components.push(IdComponent::Numeric(num));
482        } else {
483            components.push(IdComponent::alpha(current)?);
484        }
485    }
486
487    if components.is_empty() {
488        return Err(IdError::EmptyId);
489    }
490
491    Ok(components)
492}
493
494/// Increment an alphabetic string (a -> b, z -> aa, az -> ba)
495fn increment_alpha_string(s: &str) -> IdResult<String> {
496    if s.is_empty() {
497        return Err(IdError::InvalidComponent(
498            "Empty alphabetic string".to_string(),
499        ));
500    }
501
502    let mut chars: Vec<char> = s.chars().collect();
503    let mut carry = true;
504
505    // Process from right to left (like adding 1 to a number)
506    for i in (0..chars.len()).rev() {
507        if !carry {
508            break;
509        }
510
511        if chars[i] == 'z' {
512            chars[i] = 'a';
513            // carry remains true
514        } else {
515            chars[i] = (chars[i] as u8 + 1) as char;
516            carry = false;
517        }
518    }
519
520    // If we still have carry, we need to add a new 'a' at the beginning
521    if carry {
522        chars.insert(0, 'a');
523    }
524
525    Ok(chars.into_iter().collect())
526}
527
528#[cfg(test)]
529mod tests {
530    use super::*;
531
532    #[test]
533    fn test_id_component_creation() {
534        let num = IdComponent::numeric(42);
535        assert_eq!(num.as_str(), "42");
536        assert!(num.is_numeric());
537        assert!(!num.is_alpha());
538
539        let alpha = IdComponent::alpha("abc").unwrap();
540        assert_eq!(alpha.as_str(), "abc");
541        assert!(alpha.is_alpha());
542        assert!(!alpha.is_numeric());
543
544        // Test invalid alpha component
545        assert!(IdComponent::alpha("").is_err());
546        assert!(IdComponent::alpha("ABC").is_err());
547        assert!(IdComponent::alpha("a1b").is_err());
548        assert!(IdComponent::alpha("123").is_err());
549    }
550
551    #[test]
552    fn test_component_increment() {
553        // Numeric increment
554        let num = IdComponent::numeric(5);
555        let next = num.increment().unwrap();
556        assert_eq!(next, IdComponent::numeric(6));
557
558        // Test numeric overflow
559        let max_num = IdComponent::numeric(u32::MAX);
560        assert!(max_num.increment().is_err());
561
562        // Alpha increment
563        let alpha = IdComponent::alpha("a").unwrap();
564        let next = alpha.increment().unwrap();
565        assert_eq!(next, IdComponent::alpha("b").unwrap());
566
567        let z = IdComponent::alpha("z").unwrap();
568        let next = z.increment().unwrap();
569        assert_eq!(next, IdComponent::alpha("aa").unwrap());
570
571        let az = IdComponent::alpha("az").unwrap();
572        let next = az.increment().unwrap();
573        assert_eq!(next, IdComponent::alpha("ba").unwrap());
574    }
575
576    #[test]
577    fn test_alpha_string_increment() {
578        assert_eq!(increment_alpha_string("a").unwrap(), "b");
579        assert_eq!(increment_alpha_string("z").unwrap(), "aa");
580        assert_eq!(increment_alpha_string("az").unwrap(), "ba");
581        assert_eq!(increment_alpha_string("zz").unwrap(), "aaa");
582        assert_eq!(increment_alpha_string("abc").unwrap(), "abd");
583        assert_eq!(increment_alpha_string("abz").unwrap(), "aca");
584
585        // Test empty string error
586        assert!(increment_alpha_string("").is_err());
587    }
588
589    #[test]
590    fn test_id_parsing() {
591        // Simple cases
592        let id = Id::parse("1").unwrap();
593        assert_eq!(id.components().len(), 1);
594        assert!(id.is_root());
595        assert_eq!(id.depth(), 1);
596
597        let id = Id::parse("1a").unwrap();
598        assert_eq!(id.components().len(), 2);
599        assert!(!id.is_root());
600        assert_eq!(id.depth(), 2);
601
602        let id = Id::parse("1a2b3c").unwrap();
603        assert_eq!(id.components().len(), 6);
604        assert_eq!(id.depth(), 6);
605
606        // Complex IDs
607        let id = Id::parse("42z123a5").unwrap();
608        assert_eq!(id.components().len(), 4);
609        assert_eq!(id.components()[0], IdComponent::numeric(42));
610        assert_eq!(id.components()[1], IdComponent::alpha("z").unwrap());
611        assert_eq!(id.components()[2], IdComponent::numeric(123));
612        assert_eq!(id.components()[3], IdComponent::alpha("a").unwrap());
613
614        // Invalid cases
615        assert!(Id::parse("").is_err());
616        assert!(Id::parse("a").is_err()); // Must start with number
617        assert!(Id::parse("1A").is_err()); // No uppercase letters
618        assert!(Id::parse("1-2").is_err()); // No special characters
619        assert!(Id::parse("1 a").is_err()); // No spaces
620    }
621
622    #[test]
623    fn test_id_display() {
624        let id = Id::parse("1a2b").unwrap();
625        assert_eq!(id.to_string(), "1a2b");
626        assert_eq!(format!("{}", id), "1a2b");
627    }
628
629    #[test]
630    fn test_id_from_str() {
631        let id: Id = "1a2".parse().unwrap();
632        assert_eq!(id.to_string(), "1a2");
633
634        let result: Result<Id, _> = "invalid".parse();
635        assert!(result.is_err());
636    }
637
638    #[test]
639    fn test_id_relationships() {
640        let root = Id::parse("1").unwrap();
641        let child = Id::parse("1a").unwrap();
642        let grandchild = Id::parse("1a2").unwrap();
643        let sibling = Id::parse("2").unwrap();
644        let child_sibling = Id::parse("1b").unwrap();
645
646        // Parent relationships
647        assert_eq!(child.parent().unwrap(), Some(root.clone()));
648        assert_eq!(grandchild.parent().unwrap(), Some(child.clone()));
649        assert_eq!(root.parent().unwrap(), None);
650
651        // Ancestor/descendant relationships
652        assert!(root.is_ancestor_of(&child));
653        assert!(root.is_ancestor_of(&grandchild));
654        assert!(child.is_ancestor_of(&grandchild));
655        assert!(!child.is_ancestor_of(&root));
656        assert!(!root.is_ancestor_of(&sibling));
657
658        assert!(child.is_descendant_of(&root));
659        assert!(grandchild.is_descendant_of(&root));
660        assert!(grandchild.is_descendant_of(&child));
661        assert!(!root.is_descendant_of(&child));
662
663        // Sibling relationships
664        assert!(root.is_sibling_of(&sibling));
665        assert!(child.is_sibling_of(&child_sibling));
666        assert!(!root.is_sibling_of(&child));
667        assert!(!child.is_sibling_of(&grandchild));
668    }
669
670    #[test]
671    fn test_ancestors() {
672        let id = Id::parse("1a2b").unwrap();
673        let ancestors = id.ancestors();
674
675        assert_eq!(ancestors.len(), 3);
676        assert_eq!(ancestors[0].to_string(), "1");
677        assert_eq!(ancestors[1].to_string(), "1a");
678        assert_eq!(ancestors[2].to_string(), "1a2");
679
680        let root = Id::parse("1").unwrap();
681        assert_eq!(root.ancestors().len(), 0);
682    }
683
684    #[test]
685    fn test_id_generation() {
686        let id = Id::parse("1a2").unwrap();
687
688        // Next sibling
689        let sibling = id.next_sibling().unwrap();
690        assert_eq!(sibling.to_string(), "1a3");
691
692        // First child follows alternating pattern
693        let child = id.first_child();
694        assert_eq!(child.to_string(), "1a2a"); // After number comes alpha
695
696        // Child of child should be numeric
697        let grandchild = child.first_child();
698        assert_eq!(grandchild.to_string(), "1a2a1"); // After alpha comes number
699
700        // Test root ID child
701        let root = Id::parse("1").unwrap();
702        let root_child = root.first_child();
703        assert_eq!(root_child.to_string(), "1a"); // After number comes alpha
704    }
705
706    #[test]
707    fn test_id_from_number() {
708        let id = Id::from_number(42);
709        assert_eq!(id.to_string(), "42");
710        assert!(id.is_root());
711        assert_eq!(id.depth(), 1);
712    }
713
714    #[test]
715    fn test_id_manager_with_default_config() {
716        let config = IdConfig::default();
717        let manager = IdManager::new(config, |_| false);
718
719        // Test fuzzy matching (default)
720        assert_eq!(
721            manager
722                .extract_from_filename("1a2_note.md")
723                .map(|id| id.to_string()),
724            Some("1a2".to_string())
725        );
726        assert_eq!(
727            manager
728                .extract_from_filename("1a2-note.md")
729                .map(|id| id.to_string()),
730            Some("1a2".to_string())
731        );
732        assert_eq!(
733            manager
734                .extract_from_filename("1a2.md")
735                .map(|id| id.to_string()),
736            Some("1a2".to_string())
737        );
738    }
739
740    #[test]
741    fn test_id_manager_strict_matching() {
742        let config = IdConfig {
743            match_rule: "strict".to_string(),
744            separator: " - ".to_string(),
745            allow_unicode: false,
746            max_depth: 10,
747        };
748        let manager = IdManager::new(config, |_| false);
749
750        // Strict mode: exact match only
751        assert_eq!(
752            manager
753                .extract_from_filename("1a2")
754                .map(|id| id.to_string()),
755            Some("1a2".to_string())
756        );
757        assert_eq!(
758            manager.extract_from_filename("1a2.md"),
759            None // strict mode doesn't match with extension
760        );
761        assert_eq!(manager.extract_from_filename("1a2-note.md"), None);
762    }
763
764    #[test]
765    fn test_id_manager_separator_matching() {
766        let config = IdConfig {
767            match_rule: "separator".to_string(),
768            separator: " - ".to_string(),
769            allow_unicode: false,
770            max_depth: 10,
771        };
772        let manager = IdManager::new(config, |_| false);
773
774        assert_eq!(
775            manager
776                .extract_from_filename("1a2 - My Note.md")
777                .map(|id| id.to_string()),
778            Some("1a2".to_string())
779        );
780        assert_eq!(
781            manager
782                .extract_from_filename("1a2 - Another Note Title.md")
783                .map(|id| id.to_string()),
784            Some("1a2".to_string())
785        );
786        // Should not match without separator
787        assert_eq!(manager.extract_from_filename("1a2.md"), None);
788    }
789
790    #[test]
791    fn test_id_manager_generation_with_existing_ids() {
792        use std::collections::HashSet;
793
794        let mut existing_ids = HashSet::new();
795        existing_ids.insert("1".to_string());
796        existing_ids.insert("1a".to_string());
797        existing_ids.insert("2".to_string());
798        existing_ids.insert("3".to_string());
799
800        let config = IdConfig::default();
801        let manager = IdManager::new(config, |id: &str| existing_ids.contains(id));
802
803        // Next available sibling of "1" should be "4" (since "2" and "3" exist)
804        let current = Id::parse("1").unwrap();
805        let next_sibling = manager.next_available_sibling(&current).unwrap();
806        assert_eq!(next_sibling.to_string(), "4");
807
808        // Next available child of "1" should be "1b" (since "1a" exists)
809        let next_child = manager.next_available_child(&current);
810        assert_eq!(next_child.to_string(), "1b");
811
812        // Test with alpha components
813        existing_ids.insert("1b".to_string());
814        existing_ids.insert("1c".to_string());
815        let next_child = manager.next_available_child(&current);
816        assert_eq!(next_child.to_string(), "1d");
817    }
818
819    #[test]
820    fn test_id_validation() {
821        let config = IdConfig::default();
822        let manager = IdManager::new(config, |_| false);
823
824        // Valid IDs
825        assert!(manager.validate_id("1").is_ok());
826        assert!(manager.validate_id("1a").is_ok());
827        assert!(manager.validate_id("1a2b3c").is_ok());
828        assert!(manager.validate_id("42z").is_ok());
829
830        // Invalid IDs
831        assert!(manager.validate_id("").is_err());
832        assert!(manager.validate_id("a").is_err());
833        assert!(manager.validate_id("1A").is_err());
834        assert!(manager.validate_id("1-2").is_err());
835    }
836
837    #[test]
838    fn test_id_exists() {
839        use std::collections::HashSet;
840
841        let mut existing_ids = HashSet::new();
842        existing_ids.insert("1".to_string());
843        existing_ids.insert("1a2".to_string());
844
845        let config = IdConfig::default();
846        let manager = IdManager::new(config, |id: &str| existing_ids.contains(id));
847
848        let id1 = Id::parse("1").unwrap();
849        let id2 = Id::parse("1a2").unwrap();
850        let id3 = Id::parse("2").unwrap();
851
852        assert!(manager.id_exists(&id1));
853        assert!(manager.id_exists(&id2));
854        assert!(!manager.id_exists(&id3));
855    }
856
857    #[test]
858    fn test_edge_cases() {
859        // Test alphabetic sequence overflow
860        let zz = IdComponent::alpha("zz").unwrap();
861        let incremented = zz.increment().unwrap();
862        assert_eq!(incremented, IdComponent::alpha("aaa").unwrap());
863
864        let zzz = IdComponent::alpha("zzz").unwrap();
865        let incremented = zzz.increment().unwrap();
866        assert_eq!(incremented, IdComponent::alpha("aaaa").unwrap());
867
868        // Test complex ID structure
869        let complex = Id::parse("999z999z999").unwrap();
870        assert_eq!(complex.depth(), 5);
871        assert!(complex.is_descendant_of(&Id::parse("999").unwrap()));
872        assert!(complex.is_descendant_of(&Id::parse("999z").unwrap()));
873        assert!(complex.is_descendant_of(&Id::parse("999z999").unwrap()));
874        assert!(complex.is_descendant_of(&Id::parse("999z999z").unwrap()));
875
876        // Test sibling generation at different levels
877        let id = Id::parse("999z999z999").unwrap();
878        let sibling = id.next_sibling().unwrap();
879        assert_eq!(sibling.to_string(), "999z999z1000");
880    }
881
882    #[test]
883    fn test_component_parsing() {
884        // Test numeric components
885        let comp: IdComponent = "42".parse().unwrap();
886        assert_eq!(comp, IdComponent::numeric(42));
887
888        let comp: IdComponent = "0".parse().unwrap();
889        assert_eq!(comp, IdComponent::numeric(0));
890
891        // Test alpha components
892        let comp: IdComponent = "abc".parse().unwrap();
893        assert_eq!(comp, IdComponent::alpha("abc").unwrap());
894
895        let comp: IdComponent = "z".parse().unwrap();
896        assert_eq!(comp, IdComponent::alpha("z").unwrap());
897
898        // Test invalid components
899        let result: Result<IdComponent, _> = "".parse();
900        assert!(result.is_err());
901
902        let result: Result<IdComponent, _> = "ABC".parse();
903        assert!(result.is_err());
904
905        let result: Result<IdComponent, _> = "a1b".parse();
906        assert!(result.is_err());
907    }
908
909    #[test]
910    fn test_parse_id_string_function() {
911        // Test the internal parsing function directly
912        let components = parse_id_string("1a2b").unwrap();
913        assert_eq!(components.len(), 4);
914        assert_eq!(components[0], IdComponent::numeric(1));
915        assert_eq!(components[1], IdComponent::alpha("a").unwrap());
916        assert_eq!(components[2], IdComponent::numeric(2));
917        assert_eq!(components[3], IdComponent::alpha("b").unwrap());
918
919        // Test single component
920        let components = parse_id_string("42").unwrap();
921        assert_eq!(components.len(), 1);
922        assert_eq!(components[0], IdComponent::numeric(42));
923
924        let components = parse_id_string("z").unwrap();
925        assert_eq!(components.len(), 1);
926        assert_eq!(components[0], IdComponent::alpha("z").unwrap());
927
928        // Test error cases
929        assert!(parse_id_string("").is_err());
930        assert!(parse_id_string("1A").is_err());
931        assert!(parse_id_string("a1").is_err()); // Must start with number
932    }
933
934    #[test]
935    fn test_id_new_validation() {
936        // Valid alternating pattern
937        let components = vec![
938            IdComponent::numeric(1),
939            IdComponent::alpha("a").unwrap(),
940            IdComponent::numeric(2),
941        ];
942        assert!(Id::new(components).is_ok());
943
944        // Invalid: starts with alpha
945        let components = vec![IdComponent::alpha("a").unwrap(), IdComponent::numeric(1)];
946        assert!(Id::new(components).is_err());
947
948        // Invalid: two numerics in a row
949        let components = vec![IdComponent::numeric(1), IdComponent::numeric(2)];
950        assert!(Id::new(components).is_err());
951
952        // Invalid: two alphas in a row
953        let components = vec![
954            IdComponent::numeric(1),
955            IdComponent::alpha("a").unwrap(),
956            IdComponent::alpha("b").unwrap(),
957        ];
958        assert!(Id::new(components).is_err());
959
960        // Invalid: empty components
961        assert!(Id::new(vec![]).is_err());
962    }
963
964    #[test]
965    fn test_overflow_handling() {
966        // Test numeric overflow
967        let large_comp = IdComponent::numeric(u32::MAX);
968        assert!(large_comp.increment().is_err());
969
970        // Test that we can still create IDs with large numbers
971        let large_id = Id::from_number(u32::MAX);
972        assert_eq!(large_id.to_string(), u32::MAX.to_string());
973
974        // Test that sibling generation fails with overflow
975        assert!(large_id.next_sibling().is_err());
976    }
977}