Skip to main content

gxfkit_core/
attributes.rs

1//! Parsing of the GFF3 9th column (`key=value;key=value`).
2//!
3//! Values are kept as borrowed slices of the source line where possible. We
4//! preserve insertion order because downstream serialization (GTF) wants a
5//! stable, reproducible attribute order for diffing against AGAT.
6
7/// An ordered list of GFF3 attribute key/value pairs.
8///
9/// GFF3 allows multi-valued attributes (comma-separated). We keep the raw value
10/// string as-is; callers that care about multiplicity (e.g. `Parent`) can split
11/// on commas themselves.
12#[derive(Debug, Default, Clone, PartialEq, Eq)]
13pub struct Attributes {
14    pub pairs: Vec<(String, String)>,
15}
16
17impl Attributes {
18    /// Parse a GFF3 attribute column. Empty / "." columns yield no pairs.
19    pub fn parse_gff3(col: &str) -> Self {
20        let mut pairs = Vec::new();
21        if col == "." || col.is_empty() {
22            return Self { pairs };
23        }
24        for field in col.split(';') {
25            let field = field.trim();
26            if field.is_empty() {
27                continue;
28            }
29            match field.split_once('=') {
30                Some((k, v)) => pairs.push((k.trim().to_string(), v.to_string())),
31                // Tolerate a bare token with no '=' rather than dropping it.
32                None => pairs.push((field.to_string(), String::new())),
33            }
34        }
35        Self { pairs }
36    }
37
38    pub fn get(&self, key: &str) -> Option<&str> {
39        self.pairs
40            .iter()
41            .find(|(k, _)| k == key)
42            .map(|(_, v)| v.as_str())
43    }
44
45    /// First value of a potentially multi-valued attribute (split on comma).
46    pub fn first(&self, key: &str) -> Option<&str> {
47        self.get(key).map(|v| v.split(',').next().unwrap_or(v))
48    }
49
50    pub fn is_empty(&self) -> bool {
51        self.pairs.is_empty()
52    }
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58
59    #[test]
60    fn parses_basic() {
61        let a = Attributes::parse_gff3("ID=gene1;Name=BRCA2;Parent=x,y");
62        assert_eq!(a.get("ID"), Some("gene1"));
63        assert_eq!(a.first("Parent"), Some("x"));
64        assert_eq!(a.get("missing"), None);
65    }
66
67    #[test]
68    fn handles_dot_and_empty() {
69        assert!(Attributes::parse_gff3(".").is_empty());
70        assert!(Attributes::parse_gff3("").is_empty());
71    }
72}