Parse a change identifier from a string. # Arguments `s` - The identifier string # Returns The parsed `ChangeIdentifier`. # Parsing Rules - If the string is empty or None, returns `Latest` - If the string starts with `#`, parses as sequence number - If the string is purely numeric, parses as sequence number - If the string is exactly 52 base32 characters, parses as `FullHash` - Otherwise, pa
(s: Option<&str>)
| 90 | /// - If the string is exactly 52 base32 characters, parses as `FullHash` |
| 91 | /// - Otherwise, parses as `HashPrefix` |
| 92 | pub fn parse(s: Option<&str>) -> Result<Self, String> { |
| 93 | let s = match s { |
| 94 | None | Some("") => return Ok(ChangeIdentifier::Latest), |
| 95 | Some(s) => s.trim(), |
| 96 | }; |
| 97 | |
| 98 | // Check for @ syntax (latest change or relative reference) |
| 99 | if s == "@" { |
| 100 | return Ok(ChangeIdentifier::Latest); |
| 101 | } |
| 102 | |
| 103 | // Check for @~N syntax (N changes before latest) |
| 104 | if let Some(offset_str) = s.strip_prefix("@~") { |
| 105 | // This would need special handling in the resolve step |
| 106 | // For now, we'll treat it as a relative sequence lookup |
| 107 | // which requires the view's length |
| 108 | return Err(format!( |
| 109 | "Relative reference '@~N' is not yet supported: {}", |
| 110 | s |
| 111 | )); |
| 112 | } |
| 113 | |
| 114 | // Check for sequence number with # prefix |
| 115 | if let Some(num_str) = s.strip_prefix('#') { |
| 116 | return num_str |
| 117 | .parse::<u64>() |
| 118 | .map(ChangeIdentifier::Sequence) |
| 119 | .map_err(|_| format!("Invalid sequence number: {}", num_str)); |
| 120 | } |
| 121 | |
| 122 | // Check for pure numeric (sequence number) |
| 123 | if s.chars().all(|c| c.is_ascii_digit()) { |
| 124 | return s |
| 125 | .parse::<u64>() |
| 126 | .map(ChangeIdentifier::Sequence) |
| 127 | .map_err(|_| format!("Invalid sequence number: {}", s)); |
| 128 | } |
| 129 | |
| 130 | // Check for valid Base32 characters |
| 131 | let upper = s.to_uppercase(); |
| 132 | if !upper |
| 133 | .chars() |
| 134 | .all(|c| "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".contains(c)) |
| 135 | { |
| 136 | return Err(format!( |
| 137 | "Invalid hash characters in '{}'. Hashes use Base32 encoding (A-Z, 2-7).", |
| 138 | s |
| 139 | )); |
| 140 | } |
| 141 | |
| 142 | // Full hash (52 characters) |
| 143 | if upper.len() == 52 { |
| 144 | return Hash::from_base32(upper.as_bytes()) |
| 145 | .map(ChangeIdentifier::FullHash) |
| 146 | .ok_or_else(|| format!("Invalid Base32 hash: {}", s)); |
| 147 | } |
| 148 | |
| 149 | // Hash prefix (4-51 characters) |
no test coverage detected