9356be4ec0
Adds the `obitaxonomy` crate to parse and validate hierarchical taxonomy paths using a strict `taxonomy:/name@rank/...` syntax. Replaces generic string-based path matching in predicates with structured `TaxPath` and `TaxPattern` types, enforcing explicit anchor constraints and rank-aware semantics. Updates filtering documentation to clarify optional leading slashes and segment-boundary matching rules.
50 lines
1.4 KiB
Rust
50 lines
1.4 KiB
Rust
use std::fmt;
|
|
|
|
use crate::error::TaxError;
|
|
|
|
/// A single node in a taxonomy path: a name and an optional rank.
|
|
///
|
|
/// Neither `name` nor `rank` may contain `@` (reserved separator).
|
|
/// Serialised form: `name` or `name@rank`.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct TaxSegment {
|
|
name: String,
|
|
rank: Option<String>,
|
|
}
|
|
|
|
impl TaxSegment {
|
|
pub fn parse(raw: &str) -> Result<Self, TaxError> {
|
|
let parts: Vec<&str> = raw.splitn(3, '@').collect();
|
|
|
|
let (name_raw, rank_raw) = match parts.as_slice() {
|
|
[name] => (*name, None),
|
|
[name, rank] => (*name, Some(*rank)),
|
|
_ => return Err(TaxError::AmbiguousRank { segment: raw.to_string() }),
|
|
};
|
|
|
|
if name_raw.is_empty() {
|
|
return Err(TaxError::EmptySegmentName);
|
|
}
|
|
|
|
let rank = match rank_raw {
|
|
None => None,
|
|
Some("") => return Err(TaxError::EmptyRankName { segment: raw.to_string() }),
|
|
Some(r) => Some(r.to_string()),
|
|
};
|
|
|
|
Ok(Self { name: name_raw.to_string(), rank })
|
|
}
|
|
|
|
pub fn name(&self) -> &str { &self.name }
|
|
pub fn rank(&self) -> Option<&str> { self.rank.as_deref() }
|
|
}
|
|
|
|
impl fmt::Display for TaxSegment {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match &self.rank {
|
|
None => write!(f, "{}", self.name),
|
|
Some(r) => write!(f, "{}@{}", self.name, r),
|
|
}
|
|
}
|
|
}
|