Skip to main content

consortium/
node_set.rs

1//! Node name sets with bracket-based range expansion.
2//!
3//! Rust implementation of `ClusterShell.NodeSet`.
4//!
5//! Supports bracket notation like `node[1-10,15]`, set operations
6//! (union `,`, difference `!`, intersection `&`, symmetric difference `^`),
7//! and automatic folding for compact display.
8
9use crate::range_set::RangeSet;
10use std::collections::HashMap;
11use std::fmt;
12use std::str::FromStr;
13use thiserror::Error;
14
15// ============================================================================
16// Errors
17// ============================================================================
18
19#[derive(Debug, Error)]
20pub enum NodeSetError {
21    #[error("parse error: {msg} (in \"{part}\")")]
22    ParseError { part: String, msg: String },
23
24    #[error("range error: {0}")]
25    RangeError(#[from] crate::range_set::RangeSetError),
26
27    #[error("external error: {0}")]
28    ExternalError(String),
29}
30
31pub type Result<T> = std::result::Result<T, NodeSetError>;
32
33// ============================================================================
34// Internal: NodeSetBase
35// ============================================================================
36
37/// Internal representation: maps pattern templates (with `%s` placeholders for
38/// range indices) to their `RangeSet` (or `None` for non-indexed nodes).
39///
40/// Examples:
41///  - `"node%s"` → `Some(RangeSet{1,2,3})` represents `node1`, `node2`, `node3`
42///  - `"switch"` → `None` represents a single non-indexed node `switch`
43#[derive(Debug, Clone)]
44struct NodeSetBase {
45    patterns: HashMap<String, Option<RangeSet>>,
46    autostep: Option<u32>,
47}
48
49impl NodeSetBase {
50    fn new() -> Self {
51        Self {
52            patterns: HashMap::new(),
53            autostep: None,
54        }
55    }
56
57    fn with_autostep(autostep: Option<u32>) -> Self {
58        Self {
59            patterns: HashMap::new(),
60            autostep,
61        }
62    }
63
64    /// Add a pattern with its rangeset. If the pattern already exists, merge.
65    fn add(&mut self, pat: &str, rangeset: Option<RangeSet>) {
66        if let Some(existing) = self.patterns.get_mut(pat) {
67            if let (Some(existing_rs), Some(new_rs)) = (existing.as_mut(), &rangeset) {
68                existing_rs.update(new_rs);
69            }
70            // If existing is None (non-indexed) and we add None, nothing to do.
71            // If mixed indexed/non-indexed, keep existing behavior.
72        } else {
73            self.patterns.insert(pat.to_string(), rangeset);
74        }
75    }
76
77    fn len(&self) -> usize {
78        self.patterns
79            .values()
80            .map(|rs| match rs {
81                Some(rs) => rs.len(),
82                None => 1, // non-indexed node
83            })
84            .sum()
85    }
86
87    fn is_empty(&self) -> bool {
88        self.patterns.is_empty()
89    }
90
91    fn clear(&mut self) {
92        self.patterns.clear();
93    }
94
95    /// Update (union) with another NodeSetBase.
96    fn update(&mut self, other: &NodeSetBase) {
97        for (pat, rangeset) in &other.patterns {
98            self.add(pat, rangeset.clone());
99        }
100    }
101
102    /// Intersection update: keep only elements in both.
103    fn intersection_update(&mut self, other: &NodeSetBase) {
104        let mut result = HashMap::new();
105        for (pat, other_rs) in &other.patterns {
106            if let Some(self_rs) = self.patterns.get(pat) {
107                match (self_rs, other_rs) {
108                    (Some(s), Some(o)) => {
109                        let intersection = s.intersection(o);
110                        if intersection.len() > 0 {
111                            result.insert(pat.clone(), Some(intersection));
112                        }
113                    }
114                    (None, None) => {
115                        // Both are non-indexed, keep it
116                        result.insert(pat.clone(), None);
117                    }
118                    _ => {} // mixed indexed/non-indexed: no intersection
119                }
120            }
121        }
122        self.patterns = result;
123    }
124
125    /// Difference update: remove elements in other from self.
126    fn difference_update(&mut self, other: &NodeSetBase) {
127        let mut purge = Vec::new();
128        for (pat, other_rs) in &other.patterns {
129            if let Some(self_rs) = self.patterns.get_mut(pat) {
130                match (self_rs, other_rs) {
131                    (Some(s), Some(o)) => {
132                        s.difference_update(o);
133                        if s.len() == 0 {
134                            purge.push(pat.clone());
135                        }
136                    }
137                    (None, None) => {
138                        purge.push(pat.clone());
139                    }
140                    _ => {} // mixed: no removal
141                }
142            }
143        }
144        for pat in purge {
145            self.patterns.remove(&pat);
146        }
147    }
148
149    /// Symmetric difference update.
150    fn symmetric_difference_update(&mut self, other: &NodeSetBase) {
151        let mut purge = Vec::new();
152
153        // Process patterns in self against other
154        for (pat, self_rs) in self.patterns.iter_mut() {
155            if let Some(other_rs) = other.patterns.get(pat) {
156                match (self_rs.as_mut(), other_rs) {
157                    (Some(s), Some(o)) => {
158                        s.symmetric_difference_update(o);
159                        if s.len() == 0 {
160                            purge.push(pat.clone());
161                        }
162                    }
163                    (None, None) => {
164                        // Both non-indexed: xor removes
165                        purge.push(pat.clone());
166                    }
167                    _ => {}
168                }
169            }
170        }
171
172        // Add patterns in other but not in self
173        for (pat, other_rs) in &other.patterns {
174            if !self.patterns.contains_key(pat) {
175                self.add(pat, other_rs.clone());
176            }
177        }
178
179        for pat in purge {
180            self.patterns.remove(&pat);
181        }
182    }
183
184    /// Check if a node is contained.
185    ///
186    /// Strategy: parse the node into (pattern, digit_strings), then look
187    /// up by matching stored patterns. For multi-dimensional stored patterns
188    /// (expanded), we also try the fully-expanded form.
189    fn contains(&self, node: &str) -> bool {
190        let (pat, indices) = parse_node_indices(node);
191
192        // Try exact pattern match first
193        if let Some(rs_opt) = self.patterns.get(&pat) {
194            return match (rs_opt, indices.as_slice()) {
195                (Some(rs), &[(_, _)]) => {
196                    // Use string-based lookup to handle padding correctly
197                    let digit_str = extract_last_digit_str(node);
198                    rs.contains_str(&digit_str)
199                }
200                (None, &[]) => true,
201                _ => false,
202            };
203        }
204
205        // For multi-dimensional patterns that were expanded during parsing,
206        // the stored patterns have fewer %s placeholders. Try substituting
207        // leading indices to find a match.
208        if indices.len() > 1 {
209            // Extract digit strings from the node
210            let digit_strs = extract_all_digit_strs(node);
211            // Try substituting first N-1 indices into the pattern to match stored keys
212            for n_sub in 1..indices.len() {
213                let mut candidate_pat = pat.clone();
214                for i in 0..n_sub {
215                    candidate_pat = candidate_pat.replacen("%s", &digit_strs[i], 1);
216                }
217                if let Some(Some(rs)) = self.patterns.get(&candidate_pat) {
218                    // Check last digit against the rangeset
219                    let last_digit = &digit_strs[digit_strs.len() - 1];
220                    if rs.contains_str(last_digit) {
221                        return true;
222                    }
223                }
224            }
225        }
226
227        false
228    }
229
230    /// Iterate all individual node names in sorted order.
231    fn iter(&self) -> Vec<String> {
232        let mut nodes = Vec::new();
233        for (pat, rs_opt) in &self.patterns {
234            match rs_opt {
235                Some(rs) => {
236                    for s in rs.striter() {
237                        nodes.push(pat.replacen("%s", &s, 1));
238                    }
239                }
240                None => {
241                    nodes.push(pat.clone());
242                }
243            }
244        }
245        nodes.sort();
246        nodes
247    }
248}
249
250impl fmt::Display for NodeSetBase {
251    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
252        let mut parts: Vec<String> = Vec::new();
253        // Sort patterns for deterministic output
254        let mut sorted_pats: Vec<_> = self.patterns.iter().collect();
255        sorted_pats.sort_by(|a, b| a.0.cmp(b.0));
256
257        for (pat, rs_opt) in sorted_pats {
258            match rs_opt {
259                Some(rs) => {
260                    let rs_str = format!("{}", rs);
261                    if rs.len() == 1 {
262                        // Single element: no brackets
263                        parts.push(pat.replacen("%s", &rs_str, 1));
264                    } else {
265                        // Multiple elements: brackets
266                        let bracketed = format!("[{}]", rs_str);
267                        parts.push(pat.replacen("%s", &bracketed, 1));
268                    }
269                }
270                None => {
271                    parts.push(pat.clone());
272                }
273            }
274        }
275        write!(f, "{}", parts.join(","))
276    }
277}
278
279// ============================================================================
280// Parsing helpers
281// ============================================================================
282
283/// Scan a single node name like `node003` into (pattern, optional int value).
284/// Returns (pattern_with_%s, Option<integer_value>).
285#[allow(dead_code)]
286fn scan_node_single(node: &str) -> (String, Option<i64>) {
287    // Split into alternating non-digit/digit segments
288    // e.g., "node003" -> [("node", "003")]
289    // e.g., "rack2-node05" -> [("rack", "2"), ("-node", "05")]
290    // e.g., "switch" -> [("switch", "")]
291
292    let mut pat = String::new();
293    let mut has_index = false;
294    let mut last_value: Option<i64> = None;
295
296    let chars: Vec<char> = node.chars().collect();
297    let len = chars.len();
298    let mut i = 0;
299
300    while i < len {
301        // Consume non-digit prefix
302        let prefix_start = i;
303        while i < len && !chars[i].is_ascii_digit() {
304            i += 1;
305        }
306        let prefix = &node[prefix_start..i];
307
308        // Consume digit suffix
309        let digit_start = i;
310        while i < len && chars[i].is_ascii_digit() {
311            i += 1;
312        }
313        let digits = &node[digit_start..i];
314
315        if digits.is_empty() {
316            pat.push_str(prefix);
317        } else {
318            pat.push_str(prefix);
319            pat.push_str("%s");
320            has_index = true;
321            last_value = Some(digits.parse().unwrap_or(0));
322        }
323    }
324
325    if has_index {
326        (pat, last_value)
327    } else {
328        (pat, None)
329    }
330}
331
332/// Parse a single node into (pattern, Vec<(value, pad)>).
333/// Handles multi-dimensional indices like "rack2-node05".
334fn parse_node_indices(node: &str) -> (String, Vec<(i64, u32)>) {
335    let mut pat = String::new();
336    let mut indices = Vec::new();
337
338    let chars: Vec<char> = node.chars().collect();
339    let len = chars.len();
340    let mut i = 0;
341
342    while i < len {
343        let prefix_start = i;
344        while i < len && !chars[i].is_ascii_digit() {
345            i += 1;
346        }
347        pat.push_str(&node[prefix_start..i]);
348
349        let digit_start = i;
350        while i < len && chars[i].is_ascii_digit() {
351            i += 1;
352        }
353        let digits = &node[digit_start..i];
354
355        if !digits.is_empty() {
356            pat.push_str("%s");
357            let value: i64 = digits.parse().unwrap_or(0);
358            let pad = if value != 0 {
359                let stripped = digits.trim_start_matches('0');
360                if digits.len() > stripped.len() {
361                    digits.len() as u32
362                } else {
363                    0
364                }
365            } else if digits.len() > 1 {
366                digits.len() as u32
367            } else {
368                0
369            };
370            indices.push((value, pad));
371        }
372    }
373
374    (pat, indices)
375}
376
377/// Parse a nodeset string, handling brackets and operators.
378///
379/// Supports:
380///  - Simple nodes: `node1`, `switch`
381///  - Bracket ranges: `node[1-10]`, `node[1,3,5-8]`
382///  - Multiple patterns: `web[1-3],db[1-2]`
383///  - Operators: `!` (difference), `&` (intersection), `^` (xor)
384///  - Leading/trailing digit amendment around brackets
385fn parse_nodeset_string(input: &str, autostep: Option<u32>) -> Result<NodeSetBase> {
386    let input = input.trim();
387    if input.is_empty() {
388        return Ok(NodeSetBase::with_autostep(autostep));
389    }
390
391    let mut result = NodeSetBase::with_autostep(autostep);
392    let mut current_op = Op::Update; // Default operation is union
393
394    // Tokenize into segments separated by operators (, ! & ^)
395    // But operators inside brackets should be ignored
396    let tokens = tokenize(input)?;
397
398    for token in tokens {
399        match token {
400            Token::Operator(op) => {
401                current_op = op;
402            }
403            Token::Pattern(s) => {
404                let parsed = parse_single_pattern(&s, autostep)?;
405                match current_op {
406                    Op::Update => result.update(&parsed),
407                    Op::Difference => result.difference_update(&parsed),
408                    Op::Intersection => result.intersection_update(&parsed),
409                    Op::SymmetricDifference => result.symmetric_difference_update(&parsed),
410                }
411            }
412        }
413    }
414
415    Ok(result)
416}
417
418#[derive(Debug, Clone, Copy)]
419enum Op {
420    Update,
421    Difference,
422    Intersection,
423    SymmetricDifference,
424}
425
426#[derive(Debug)]
427enum Token {
428    Operator(Op),
429    Pattern(String),
430}
431
432/// Tokenize a nodeset string, respecting brackets.
433fn tokenize(input: &str) -> Result<Vec<Token>> {
434    let mut tokens = Vec::new();
435    let mut current = String::new();
436    let mut bracket_depth = 0;
437    let chars: Vec<char> = input.chars().collect();
438    let mut i = 0;
439
440    while i < chars.len() {
441        let ch = chars[i];
442        match ch {
443            '[' => {
444                bracket_depth += 1;
445                current.push(ch);
446            }
447            ']' => {
448                if bracket_depth == 0 {
449                    return Err(NodeSetError::ParseError {
450                        part: input.to_string(),
451                        msg: "illegal closing bracket".to_string(),
452                    });
453                }
454                bracket_depth -= 1;
455                current.push(ch);
456            }
457            ',' | '!' | '&' | '^' if bracket_depth == 0 => {
458                // This is an operator
459                if !current.is_empty() {
460                    tokens.push(Token::Pattern(current.trim().to_string()));
461                    current = String::new();
462                }
463                let op = match ch {
464                    ',' => Op::Update,
465                    '!' => Op::Difference,
466                    '&' => Op::Intersection,
467                    '^' => Op::SymmetricDifference,
468                    _ => unreachable!(),
469                };
470                tokens.push(Token::Operator(op));
471            }
472            _ => {
473                current.push(ch);
474            }
475        }
476        i += 1;
477    }
478
479    if bracket_depth != 0 {
480        return Err(NodeSetError::ParseError {
481            part: input.to_string(),
482            msg: "missing bracket".to_string(),
483        });
484    }
485
486    if !current.is_empty() {
487        tokens.push(Token::Pattern(current.trim().to_string()));
488    }
489
490    // Validate: first token should be a pattern (or empty input)
491    // Insert an implicit Update at the beginning if first is a pattern
492    let mut result = Vec::new();
493    let mut expect_pattern = true;
494    for token in tokens {
495        match &token {
496            Token::Pattern(_) => {
497                if expect_pattern {
498                    if result.is_empty() {
499                        // First pattern: implicit Update
500                        result.push(Token::Operator(Op::Update));
501                    }
502                    result.push(token);
503                    expect_pattern = false;
504                } else {
505                    // Two patterns in a row without operator — shouldn't happen
506                    // with our tokenizer since comma is an operator
507                    result.push(Token::Operator(Op::Update));
508                    result.push(token);
509                    expect_pattern = false;
510                }
511            }
512            Token::Operator(_) => {
513                result.push(token);
514                expect_pattern = true;
515            }
516        }
517    }
518
519    Ok(result)
520}
521
522/// Parse a single pattern segment (no operators at this level).
523/// Handles brackets and plain node names.
524fn parse_single_pattern(pat: &str, autostep: Option<u32>) -> Result<NodeSetBase> {
525    let mut base = NodeSetBase::with_autostep(autostep);
526
527    if pat.contains('[') {
528        // Bracket notation: e.g., "node[1-10]", "rack[1-2]-node[1-5]"
529        parse_bracketed(pat, autostep, &mut base)?;
530    } else {
531        // Plain node name like "node1" or "switch"
532        let (pattern, indices) = parse_node_indices(pat);
533        if indices.is_empty() {
534            base.add(&pattern, None);
535        } else {
536            // Build a rangeset from the single index values
537            let mut rs = RangeSet::new();
538            rs.set_autostep(autostep);
539            // For single-dimensional simple nodes
540            if indices.len() == 1 {
541                let (val, pad) = indices[0];
542                rs.add_int(val, pad);
543                base.add(&pattern, Some(rs));
544            } else {
545                // Multi-dimensional: expand leading indices into the pattern,
546                // keeping only the last dimension as the rangeset.
547                // e.g., "192.168.1.29" → pattern "%s.%s.%s.%s", indices [(192,0),(168,0),(1,0),(29,0)]
548                //   → expanded pattern "192.168.1.%s", rangeset {29}
549                let digit_strs = extract_all_digit_strs(pat);
550                let mut expanded_pat = pattern.clone();
551                for ds in &digit_strs[..digit_strs.len() - 1] {
552                    expanded_pat = expanded_pat.replacen("%s", ds, 1);
553                }
554                let (val, pad) = indices[indices.len() - 1];
555                rs.add_int(val, pad);
556                base.add(&expanded_pat, Some(rs));
557            }
558        }
559    }
560
561    Ok(base)
562}
563
564/// Parse bracketed notation: "node[1-10,15]", "rack[1-2]-node[1-5]"
565fn parse_bracketed(pat: &str, autostep: Option<u32>, base: &mut NodeSetBase) -> Result<()> {
566    // Split into prefix[range]suffix segments
567    let mut remaining = pat;
568    let mut template = String::new();
569    let mut rangesets: Vec<RangeSet> = Vec::new();
570
571    while let Some(bracket_start) = remaining.find('[') {
572        let prefix = &remaining[..bracket_start];
573
574        // Handle leading digits amendment:
575        // "node10[1-3]" → prefix="node10", amend range to "101-103"
576        let (clean_prefix, leading_digits) = split_trailing_digits(prefix);
577        template.push_str(clean_prefix);
578        template.push_str("%s");
579
580        let after_bracket = &remaining[bracket_start + 1..];
581        let bracket_end = after_bracket
582            .find(']')
583            .ok_or_else(|| NodeSetError::ParseError {
584                part: pat.to_string(),
585                msg: "missing bracket".to_string(),
586            })?;
587
588        let range_str = &after_bracket[..bracket_end];
589        remaining = &after_bracket[bracket_end + 1..];
590
591        // Handle trailing digits amendment:
592        // "node[1-3]5" → suffix starts with "5", amend range
593        let (trailing_digits, clean_remaining) = split_leading_digits(remaining);
594
595        // Amend the range string if we have leading/trailing digits
596        let amended_range = amend_range(range_str, &leading_digits, &trailing_digits)?;
597
598        let rs = RangeSet::parse(&amended_range, autostep)?;
599        rangesets.push(rs);
600
601        remaining = clean_remaining;
602    }
603
604    // Append any remaining suffix
605    if !remaining.is_empty() {
606        // Check for trailing node-like content (non-digit)
607        template.push_str(remaining);
608    }
609
610    // For single-dimensional case
611    if rangesets.len() == 1 {
612        base.add(&template, Some(rangesets.into_iter().next().unwrap()));
613    } else if rangesets.is_empty() {
614        base.add(&template, None);
615    } else {
616        // Multi-dimensional: expand all combinations
617        // e.g., rack[1-2]-node[1-3] → 6 nodes
618        expand_multi_dim(&template, &rangesets, base);
619    }
620
621    Ok(())
622}
623
624/// Expand multi-dimensional bracket notation into individual patterns.
625fn expand_multi_dim(template: &str, rangesets: &[RangeSet], base: &mut NodeSetBase) {
626    // For multi-dimensional, we expand all but the last dimension
627    // and create individual patterns with the last dimension as the rangeset.
628    if rangesets.is_empty() {
629        return;
630    }
631
632    if rangesets.len() == 1 {
633        base.add(template, Some(rangesets[0].clone()));
634        return;
635    }
636
637    // Collect all values from each dimension except the last
638    let last_rs = &rangesets[rangesets.len() - 1];
639    let prefixes = expand_dimensions(&rangesets[..rangesets.len() - 1]);
640
641    // For each combination of prefix dimensions, create a pattern
642    for prefix_vals in &prefixes {
643        let mut pat = template.to_string();
644        for val in prefix_vals {
645            pat = pat.replacen("%s", val, 1);
646        }
647        // pat now has one %s left for the last dimension
648        base.add(&pat, Some(last_rs.clone()));
649    }
650}
651
652/// Expand multiple RangeSets into all combinations of their string values.
653fn expand_dimensions(rangesets: &[RangeSet]) -> Vec<Vec<String>> {
654    if rangesets.is_empty() {
655        return vec![vec![]];
656    }
657
658    let first_vals: Vec<String> = rangesets[0].striter().collect();
659    let rest = expand_dimensions(&rangesets[1..]);
660
661    let mut result = Vec::new();
662    for val in &first_vals {
663        for suffix in &rest {
664            let mut combined = vec![val.clone()];
665            combined.extend(suffix.iter().cloned());
666            result.push(combined);
667        }
668    }
669    result
670}
671
672/// Extract the last run of digits from a node string, preserving padding.
673/// e.g., "node001" → "001", "rack2-node05" → "05"
674fn extract_last_digit_str(node: &str) -> String {
675    let bytes = node.as_bytes();
676    let mut end = bytes.len();
677    while end > 0 && !bytes[end - 1].is_ascii_digit() {
678        end -= 1;
679    }
680    let mut start = end;
681    while start > 0 && bytes[start - 1].is_ascii_digit() {
682        start -= 1;
683    }
684    node[start..end].to_string()
685}
686
687/// Extract all runs of digits from a node string, preserving padding.
688/// e.g., "rack2-node05" → ["2", "05"]
689fn extract_all_digit_strs(node: &str) -> Vec<String> {
690    let mut result = Vec::new();
691    let chars: Vec<char> = node.chars().collect();
692    let len = chars.len();
693    let mut i = 0;
694
695    while i < len {
696        // Skip non-digits
697        while i < len && !chars[i].is_ascii_digit() {
698            i += 1;
699        }
700        let start = i;
701        while i < len && chars[i].is_ascii_digit() {
702            i += 1;
703        }
704        if i > start {
705            result.push(node[start..i].to_string());
706        }
707    }
708    result
709}
710
711/// Split trailing digits from a prefix: "node10" → ("node", "10")
712fn split_trailing_digits(s: &str) -> (&str, String) {
713    let bytes = s.as_bytes();
714    let mut i = bytes.len();
715    while i > 0 && bytes[i - 1].is_ascii_digit() {
716        i -= 1;
717    }
718    (&s[..i], s[i..].to_string())
719}
720
721/// Split leading digits from remaining: "5-ilo" → ("5", "-ilo")
722fn split_leading_digits(s: &str) -> (String, &str) {
723    let bytes = s.as_bytes();
724    let mut i = 0;
725    while i < bytes.len() && bytes[i].is_ascii_digit() {
726        i += 1;
727    }
728    (s[..i].to_string(), &s[i..])
729}
730
731/// Amend a range string with leading/trailing digits.
732///
733/// Leading digits: "10" + "[1-3]" → "101-103"
734/// Trailing digits: "[1-3]" + "5" → "15-35/10"
735fn amend_range(range: &str, leading: &str, trailing: &str) -> Result<String> {
736    let mut result = range.to_string();
737
738    if !leading.is_empty() {
739        // Prepend leading digits to each bound
740        // "1-3,5-7" with leading "10" → "101-103,105-107"
741        let rs = RangeSet::parse(&result, None)?;
742        let mut parts = Vec::new();
743        // Get contiguous sub-ranges
744        for val_str in rs.striter() {
745            let combined = format!("{}{}", leading, val_str);
746            parts.push(combined);
747        }
748        // Re-parse the expanded list
749        result = parts.join(",");
750    }
751
752    if !trailing.is_empty() {
753        // Append trailing digits to each bound
754        let rs = RangeSet::parse(&result, None)?;
755        let mut parts = Vec::new();
756        let trail_len = trailing.len();
757        let power = 10i64.pow(trail_len as u32);
758        let trail_val: i64 = trailing.parse().unwrap_or(0);
759
760        for val in rs.intiter() {
761            let combined = val * power + trail_val;
762            parts.push(format!("{}", combined));
763        }
764        result = parts.join(",");
765    }
766
767    Ok(result)
768}
769
770// ============================================================================
771// Public API: NodeSet
772// ============================================================================
773
774/// A set of cluster node names supporting range notation like `node[1-100]`.
775///
776/// # Examples
777///
778/// ```
779/// use consortium::node_set::NodeSet;
780///
781/// let ns = NodeSet::parse("node[1-5,10]").unwrap();
782/// assert_eq!(ns.len(), 6);
783/// assert!(ns.contains("node3"));
784/// assert!(!ns.contains("node7"));
785/// ```
786#[derive(Debug, Clone)]
787pub struct NodeSet {
788    base: NodeSetBase,
789}
790
791impl NodeSet {
792    /// Create an empty NodeSet.
793    pub fn new() -> Self {
794        Self {
795            base: NodeSetBase::new(),
796        }
797    }
798
799    /// Parse a node set from a string like `"node[1-10,15],other[001-050]"`.
800    pub fn parse(pattern: &str) -> Result<Self> {
801        let base = parse_nodeset_string(pattern, None)?;
802        Ok(Self { base })
803    }
804
805    /// Parse with explicit autostep setting.
806    pub fn parse_with_autostep(pattern: &str, autostep: Option<u32>) -> Result<Self> {
807        let base = parse_nodeset_string(pattern, autostep)?;
808        Ok(Self { base })
809    }
810
811    /// Number of individual nodes in the set.
812    pub fn len(&self) -> usize {
813        self.base.len()
814    }
815
816    /// Whether the set is empty.
817    pub fn is_empty(&self) -> bool {
818        self.base.is_empty()
819    }
820
821    /// Check if a specific node name is in the set.
822    pub fn contains(&self, node: &str) -> bool {
823        self.base.contains(node)
824    }
825
826    /// Update (union) this nodeset with another.
827    pub fn update(&mut self, other: &NodeSet) {
828        self.base.update(&other.base);
829    }
830
831    /// Update from a string pattern.
832    pub fn update_str(&mut self, pattern: &str) -> Result<()> {
833        let other = parse_nodeset_string(pattern, self.base.autostep)?;
834        self.base.update(&other);
835        Ok(())
836    }
837
838    /// Return the union of this nodeset and another.
839    pub fn union(&self, other: &NodeSet) -> NodeSet {
840        let mut copy = self.clone();
841        copy.update(other);
842        copy
843    }
844
845    /// Intersection update: keep only elements in both.
846    pub fn intersection_update(&mut self, other: &NodeSet) {
847        self.base.intersection_update(&other.base);
848    }
849
850    /// Return the intersection of this nodeset and another.
851    pub fn intersection(&self, other: &NodeSet) -> NodeSet {
852        let mut copy = self.clone();
853        copy.intersection_update(other);
854        copy
855    }
856
857    /// Difference update: remove elements of other from self.
858    pub fn difference_update(&mut self, other: &NodeSet) {
859        self.base.difference_update(&other.base);
860    }
861
862    /// Return the difference of this nodeset and another.
863    pub fn difference(&self, other: &NodeSet) -> NodeSet {
864        let mut copy = self.clone();
865        copy.difference_update(other);
866        copy
867    }
868
869    /// Symmetric difference update.
870    pub fn symmetric_difference_update(&mut self, other: &NodeSet) {
871        self.base.symmetric_difference_update(&other.base);
872    }
873
874    /// Return the symmetric difference of this nodeset and another.
875    pub fn symmetric_difference(&self, other: &NodeSet) -> NodeSet {
876        let mut copy = self.clone();
877        copy.symmetric_difference_update(other);
878        copy
879    }
880
881    /// Clear all nodes from this set.
882    pub fn clear(&mut self) {
883        self.base.clear();
884    }
885
886    /// Iterate all individual node names in sorted order.
887    pub fn iter(&self) -> impl Iterator<Item = String> + '_ {
888        self.base.iter().into_iter()
889    }
890
891    /// Split the nodeset into `nbr` sub-nodesets (at most).
892    pub fn split(&self, nbr: usize) -> Vec<NodeSet> {
893        assert!(nbr > 0);
894        let all_nodes: Vec<String> = self.iter().collect();
895        let total = all_nodes.len();
896        let slice_size = total / nbr;
897        let leftover = total % nbr;
898
899        let mut result = Vec::new();
900        let mut begin = 0;
901
902        for i in 0..nbr.min(total) {
903            let length = slice_size + if i < leftover { 1 } else { 0 };
904            let slice = &all_nodes[begin..begin + length];
905            let pattern = slice.join(",");
906            if let Ok(ns) = NodeSet::parse(&pattern) {
907                result.push(ns);
908            }
909            begin += length;
910        }
911
912        result
913    }
914
915    /// Check if this nodeset is a subset of another.
916    pub fn is_subset(&self, other: &NodeSet) -> bool {
917        self.difference(other).is_empty()
918    }
919
920    /// Check if this nodeset is a superset of another.
921    pub fn is_superset(&self, other: &NodeSet) -> bool {
922        other.is_subset(self)
923    }
924}
925
926impl Default for NodeSet {
927    fn default() -> Self {
928        Self::new()
929    }
930}
931
932impl fmt::Display for NodeSet {
933    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
934        write!(f, "{}", self.base)
935    }
936}
937
938impl FromStr for NodeSet {
939    type Err = NodeSetError;
940
941    fn from_str(s: &str) -> Result<Self> {
942        NodeSet::parse(s)
943    }
944}
945
946// ============================================================================
947// Convenience functions
948// ============================================================================
949
950/// Expand a nodeset pattern into a list of individual node names.
951///
952/// ```
953/// use consortium::node_set::expand;
954/// let nodes = expand("node[1-3]").unwrap();
955/// assert_eq!(nodes, vec!["node1", "node2", "node3"]);
956/// ```
957pub fn expand(pat: &str) -> Result<Vec<String>> {
958    let ns = NodeSet::parse(pat)?;
959    Ok(ns.iter().collect())
960}
961
962/// Fold a nodeset pattern: remove duplicates and compress ranges.
963///
964/// ```
965/// use consortium::node_set::fold;
966/// let s = fold("node3,node1,node2").unwrap();
967/// assert_eq!(s, "node[1-3]");
968/// ```
969pub fn fold(pat: &str) -> Result<String> {
970    let ns = NodeSet::parse(pat)?;
971    Ok(format!("{}", ns))
972}
973
974// ============================================================================
975// Tests
976// ============================================================================
977
978#[cfg(test)]
979mod tests {
980    use super::*;
981
982    #[test]
983    fn test_new_is_empty() {
984        let ns = NodeSet::new();
985        assert!(ns.is_empty());
986        assert_eq!(ns.len(), 0);
987    }
988
989    #[test]
990    fn test_parse_simple_node() {
991        let ns = NodeSet::parse("node1").unwrap();
992        assert_eq!(ns.len(), 1);
993        assert!(ns.contains("node1"));
994        assert!(!ns.contains("node2"));
995    }
996
997    #[test]
998    fn test_parse_range() {
999        let ns = NodeSet::parse("node[1-5]").unwrap();
1000        assert_eq!(ns.len(), 5);
1001        for i in 1..=5 {
1002            assert!(ns.contains(&format!("node{}", i)));
1003        }
1004        assert!(!ns.contains("node0"));
1005        assert!(!ns.contains("node6"));
1006    }
1007
1008    #[test]
1009    fn test_parse_multiple_ranges() {
1010        let ns = NodeSet::parse("node[1-3,7-9]").unwrap();
1011        assert_eq!(ns.len(), 6);
1012        assert!(ns.contains("node1"));
1013        assert!(ns.contains("node3"));
1014        assert!(!ns.contains("node4"));
1015        assert!(ns.contains("node7"));
1016        assert!(ns.contains("node9"));
1017    }
1018
1019    #[test]
1020    fn test_parse_padded() {
1021        let ns = NodeSet::parse("node[001-003]").unwrap();
1022        assert_eq!(ns.len(), 3);
1023        assert!(ns.contains("node001"));
1024        assert!(ns.contains("node002"));
1025        assert!(ns.contains("node003"));
1026        let display = format!("{}", ns);
1027        assert!(display.contains("001"));
1028    }
1029
1030    #[test]
1031    fn test_parse_comma_separated() {
1032        let ns = NodeSet::parse("node1,node2,node3").unwrap();
1033        assert_eq!(ns.len(), 3);
1034        assert!(ns.contains("node1"));
1035        assert!(ns.contains("node2"));
1036        assert!(ns.contains("node3"));
1037    }
1038
1039    #[test]
1040    fn test_parse_difference_operator() {
1041        let ns = NodeSet::parse("node[1-10]!node[5-7]").unwrap();
1042        assert_eq!(ns.len(), 7);
1043        assert!(ns.contains("node1"));
1044        assert!(ns.contains("node4"));
1045        assert!(!ns.contains("node5"));
1046        assert!(!ns.contains("node6"));
1047        assert!(!ns.contains("node7"));
1048        assert!(ns.contains("node8"));
1049        assert!(ns.contains("node10"));
1050    }
1051
1052    #[test]
1053    fn test_parse_intersection_operator() {
1054        let ns = NodeSet::parse("node[1-10]&node[5-15]").unwrap();
1055        assert_eq!(ns.len(), 6);
1056        for i in 5..=10 {
1057            assert!(ns.contains(&format!("node{}", i)));
1058        }
1059        assert!(!ns.contains("node1"));
1060        assert!(!ns.contains("node11"));
1061    }
1062
1063    #[test]
1064    fn test_parse_xor_operator() {
1065        let ns = NodeSet::parse("node[1-5]^node[3-7]").unwrap();
1066        assert_eq!(ns.len(), 4);
1067        assert!(ns.contains("node1"));
1068        assert!(ns.contains("node2"));
1069        assert!(!ns.contains("node3"));
1070        assert!(!ns.contains("node5"));
1071        assert!(ns.contains("node6"));
1072        assert!(ns.contains("node7"));
1073    }
1074
1075    #[test]
1076    fn test_multi_pattern() {
1077        let ns = NodeSet::parse("web[1-3],db[1-2]").unwrap();
1078        assert_eq!(ns.len(), 5);
1079        assert!(ns.contains("web1"));
1080        assert!(ns.contains("web3"));
1081        assert!(ns.contains("db1"));
1082        assert!(ns.contains("db2"));
1083    }
1084
1085    #[test]
1086    fn test_display_fold() {
1087        let ns = NodeSet::parse("node3,node1,node2").unwrap();
1088        let display = format!("{}", ns);
1089        assert_eq!(display, "node[1-3]");
1090    }
1091
1092    #[test]
1093    fn test_set_union() {
1094        let a = NodeSet::parse("node[1-3]").unwrap();
1095        let b = NodeSet::parse("node[5-7]").unwrap();
1096        let c = a.union(&b);
1097        assert_eq!(c.len(), 6);
1098    }
1099
1100    #[test]
1101    fn test_set_intersection() {
1102        let a = NodeSet::parse("node[1-10]").unwrap();
1103        let b = NodeSet::parse("node[5-15]").unwrap();
1104        let c = a.intersection(&b);
1105        assert_eq!(c.len(), 6);
1106    }
1107
1108    #[test]
1109    fn test_set_difference() {
1110        let a = NodeSet::parse("node[1-10]").unwrap();
1111        let b = NodeSet::parse("node[5-7]").unwrap();
1112        let c = a.difference(&b);
1113        assert_eq!(c.len(), 7);
1114    }
1115
1116    #[test]
1117    fn test_set_symmetric_difference() {
1118        let a = NodeSet::parse("node[1-5]").unwrap();
1119        let b = NodeSet::parse("node[3-7]").unwrap();
1120        let c = a.symmetric_difference(&b);
1121        assert_eq!(c.len(), 4);
1122    }
1123
1124    #[test]
1125    fn test_iter_sorted() {
1126        let ns = NodeSet::parse("node[3,1,2]").unwrap();
1127        let nodes: Vec<String> = ns.iter().collect();
1128        assert_eq!(nodes, vec!["node1", "node2", "node3"]);
1129    }
1130
1131    #[test]
1132    fn test_non_indexed_node() {
1133        let ns = NodeSet::parse("switch").unwrap();
1134        assert_eq!(ns.len(), 1);
1135        assert!(ns.contains("switch"));
1136    }
1137
1138    #[test]
1139    fn test_expand_function() {
1140        let nodes = expand("node[1-3]").unwrap();
1141        assert_eq!(nodes, vec!["node1", "node2", "node3"]);
1142    }
1143
1144    #[test]
1145    fn test_fold_function() {
1146        let s = fold("node3,node1,node2").unwrap();
1147        assert_eq!(s, "node[1-3]");
1148    }
1149
1150    #[test]
1151    fn test_from_str() {
1152        let ns: NodeSet = "node[1-3]".parse().unwrap();
1153        assert_eq!(ns.len(), 3);
1154    }
1155
1156    #[test]
1157    fn test_update_str() {
1158        let mut ns = NodeSet::parse("node[1-3]").unwrap();
1159        ns.update_str("node[5-7]").unwrap();
1160        assert_eq!(ns.len(), 6);
1161    }
1162
1163    #[test]
1164    fn test_split() {
1165        let ns = NodeSet::parse("node[1-6]").unwrap();
1166        let parts = ns.split(3);
1167        assert_eq!(parts.len(), 3);
1168        assert_eq!(parts[0].len(), 2);
1169        assert_eq!(parts[1].len(), 2);
1170        assert_eq!(parts[2].len(), 2);
1171    }
1172
1173    #[test]
1174    fn test_subset_superset() {
1175        let a = NodeSet::parse("node[1-5]").unwrap();
1176        let b = NodeSet::parse("node[1-10]").unwrap();
1177        assert!(a.is_subset(&b));
1178        assert!(b.is_superset(&a));
1179        assert!(!b.is_subset(&a));
1180    }
1181
1182    #[test]
1183    fn test_multi_dimensional() {
1184        let ns = NodeSet::parse("rack[1-2]-node[1-3]").unwrap();
1185        assert_eq!(ns.len(), 6);
1186        assert!(ns.contains("rack1-node1"));
1187        assert!(ns.contains("rack1-node3"));
1188        assert!(ns.contains("rack2-node1"));
1189        assert!(ns.contains("rack2-node3"));
1190        assert!(!ns.contains("rack3-node1"));
1191    }
1192
1193    #[test]
1194    fn test_empty_parse() {
1195        let ns = NodeSet::parse("").unwrap();
1196        assert!(ns.is_empty());
1197    }
1198
1199    #[test]
1200    fn test_missing_bracket_error() {
1201        let result = NodeSet::parse("node[1-3");
1202        assert!(result.is_err());
1203    }
1204
1205    #[test]
1206    fn test_clear() {
1207        let mut ns = NodeSet::parse("node[1-5]").unwrap();
1208        assert!(!ns.is_empty());
1209        ns.clear();
1210        assert!(ns.is_empty());
1211    }
1212
1213    // ── Dotted hostname / IP address tests ──────────────────────────────
1214
1215    #[test]
1216    fn test_dotted_hostname_single() {
1217        let ns = NodeSet::parse("server1.example.com").unwrap();
1218        assert_eq!(ns.len(), 1);
1219        let nodes: Vec<String> = ns.iter().collect();
1220        assert_eq!(nodes, vec!["server1.example.com"]);
1221        assert_eq!(format!("{ns}"), "server1.example.com");
1222    }
1223
1224    #[test]
1225    fn test_dotted_hostname_multiple() {
1226        let ns = NodeSet::parse("server1.local,server2.local").unwrap();
1227        assert_eq!(ns.len(), 2);
1228        let nodes: Vec<String> = ns.iter().collect();
1229        assert_eq!(nodes, vec!["server1.local", "server2.local"]);
1230    }
1231
1232    #[test]
1233    fn test_ip_address_single() {
1234        let ns = NodeSet::parse("192.168.1.29").unwrap();
1235        assert_eq!(ns.len(), 1);
1236        let nodes: Vec<String> = ns.iter().collect();
1237        assert_eq!(nodes, vec!["192.168.1.29"]);
1238        assert_eq!(format!("{ns}"), "192.168.1.29");
1239    }
1240
1241    #[test]
1242    fn test_ip_address_multiple_same_subnet() {
1243        let ns = NodeSet::parse("192.168.1.3,192.168.1.29").unwrap();
1244        assert_eq!(ns.len(), 2);
1245        let mut nodes: Vec<String> = ns.iter().collect();
1246        nodes.sort();
1247        assert_eq!(nodes, vec!["192.168.1.29", "192.168.1.3"]);
1248        assert_eq!(format!("{ns}"), "192.168.1.[3,29]");
1249    }
1250
1251    #[test]
1252    fn test_ip_address_different_subnets() {
1253        let ns = NodeSet::parse("10.0.0.1,192.168.1.1").unwrap();
1254        assert_eq!(ns.len(), 2);
1255        let mut nodes: Vec<String> = ns.iter().collect();
1256        nodes.sort();
1257        assert_eq!(nodes, vec!["10.0.0.1", "192.168.1.1"]);
1258    }
1259
1260    #[test]
1261    fn test_ip_address_contains() {
1262        let ns = NodeSet::parse("192.168.1.3,192.168.1.29").unwrap();
1263        assert!(ns.contains("192.168.1.3"));
1264        assert!(ns.contains("192.168.1.29"));
1265        assert!(!ns.contains("192.168.1.4"));
1266        assert!(!ns.contains("10.0.0.1"));
1267    }
1268
1269    #[test]
1270    fn test_dotted_hostname_with_numbers() {
1271        // Hostname like "node1.rack2.dc3" has numbers in each component
1272        let ns = NodeSet::parse("node1.rack2.dc3").unwrap();
1273        assert_eq!(ns.len(), 1);
1274        let nodes: Vec<String> = ns.iter().collect();
1275        assert_eq!(nodes, vec!["node1.rack2.dc3"]);
1276    }
1277
1278    #[test]
1279    fn test_mdns_hostname() {
1280        let ns = NodeSet::parse("GN9CFLM92K-MBP.local").unwrap();
1281        assert_eq!(ns.len(), 1);
1282        let nodes: Vec<String> = ns.iter().collect();
1283        assert_eq!(nodes, vec!["GN9CFLM92K-MBP.local"]);
1284    }
1285
1286    #[test]
1287    fn test_mdns_hostname_comma_list() {
1288        let ns = NodeSet::parse("CK2Q9LN7PM-MBA.local,GJHC5VVN49-MBP.local,GN9CFLM92K-MBP.local")
1289            .unwrap();
1290        assert_eq!(ns.len(), 3);
1291        let nodes: Vec<String> = ns.iter().collect();
1292        assert!(nodes.contains(&"CK2Q9LN7PM-MBA.local".to_string()));
1293        assert!(nodes.contains(&"GJHC5VVN49-MBP.local".to_string()));
1294        assert!(nodes.contains(&"GN9CFLM92K-MBP.local".to_string()));
1295    }
1296
1297    #[test]
1298    fn test_thunderbolt_hostname() {
1299        let ns = NodeSet::parse("CK2Q9LN7PM-MBA.tb").unwrap();
1300        assert_eq!(ns.len(), 1);
1301        let nodes: Vec<String> = ns.iter().collect();
1302        assert_eq!(nodes, vec!["CK2Q9LN7PM-MBA.tb"]);
1303    }
1304}