Skip to main content

omics_coordinate/
contig.rs

1//! Contiguous molecules.
2
3use std::sync::Arc;
4
5use thiserror::Error;
6
7////////////////////////////////////////////////////////////////////////////////////////
8// Errors
9////////////////////////////////////////////////////////////////////////////////////////
10
11/// An error related to a contig.
12#[derive(Error, Debug, PartialEq, Eq)]
13pub enum Error {
14    /// An empty contig name was provided.
15    #[error("contig name cannot be empty")]
16    Empty,
17}
18
19/// A [`Result`](std::result::Result) with an [`Error`](enum@Error).
20pub type Result<T> = std::result::Result<T, Error>;
21
22////////////////////////////////////////////////////////////////////////////////////////
23// Contig
24////////////////////////////////////////////////////////////////////////////////////////
25
26/// A named, contiguous molecule within a genome.
27///
28/// Internally, a contig wraps an `Arc<str>`, making `clone` operations
29/// `O(1)` (an atomic reference count increment rather than a heap
30/// allocation).
31///
32/// Notably, the internal representation of [`Contig`] may change in the future
33/// (though the interface to this type will remain stable with respect to
34/// [semantic versioning](https://semver.org/)).
35///
36/// For a more in-depth discussion on this, please see [this section of the
37/// docs](crate#contigs).
38#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
39pub struct Contig(Arc<str>);
40
41impl Contig {
42    /// Attempts to create a new contig.
43    ///
44    /// Returns an error if the contig name is empty.
45    ///
46    /// # Examples
47    ///
48    /// ```
49    /// use omics_coordinate::Contig;
50    ///
51    /// let contig = Contig::try_new("chr1")?;
52    /// assert_eq!(contig.as_str(), "chr1");
53    ///
54    /// // Empty contig names are rejected
55    /// assert!(Contig::try_new("").is_err());
56    ///
57    /// # Ok::<(), Box<dyn std::error::Error>>(())
58    /// ```
59    pub fn try_new(value: impl Into<String>) -> Result<Self> {
60        let s: String = value.into();
61        if s.is_empty() {
62            return Err(Error::Empty);
63        }
64        Ok(Self(Arc::from(s)))
65    }
66
67    /// Creates a new contig without validating that the name is non-empty.
68    ///
69    /// # Safety
70    ///
71    /// This function does not validate that the contig name is non-empty.
72    /// Creating a contig with an empty name may lead to invalid serialization
73    /// and parsing errors.
74    ///
75    /// # Examples
76    ///
77    /// ```
78    /// use omics_coordinate::Contig;
79    ///
80    /// let contig = Contig::new_unchecked("chr1");
81    /// assert_eq!(contig.as_str(), "chr1");
82    /// ```
83    pub fn new_unchecked(value: impl Into<String>) -> Self {
84        Self(Arc::from(value.into()))
85    }
86
87    /// Returns a reference to the contig name as a string slice.
88    pub fn as_str(&self) -> &str {
89        &self.0
90    }
91}
92
93////////////////////////////////////////////////////////////////////////////////////////
94// Trait implementations
95////////////////////////////////////////////////////////////////////////////////////////
96
97impl std::fmt::Display for Contig {
98    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99        write!(f, "{}", self.0)
100    }
101}
102
103impl std::str::FromStr for Contig {
104    type Err = Error;
105
106    fn from_str(s: &str) -> Result<Self> {
107        Self::try_new(s)
108    }
109}
110
111impl TryFrom<&str> for Contig {
112    type Error = Error;
113
114    fn try_from(value: &str) -> Result<Self> {
115        Self::try_new(value)
116    }
117}
118
119impl TryFrom<String> for Contig {
120    type Error = Error;
121
122    fn try_from(value: String) -> Result<Self> {
123        Self::try_new(value)
124    }
125}
126
127impl std::ops::Deref for Contig {
128    type Target = str;
129
130    fn deref(&self) -> &Self::Target {
131        &self.0
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    #[test]
140    fn try_new_valid() {
141        let contig = Contig::try_new("chr1").expect("valid contig name");
142        assert_eq!(contig.as_str(), "chr1");
143
144        let contig = Contig::try_new("seq0").expect("valid contig name");
145        assert_eq!(contig.as_str(), "seq0");
146
147        let contig = Contig::try_new("X").expect("valid contig name");
148        assert_eq!(contig.as_str(), "X");
149    }
150
151    #[test]
152    fn try_new_empty() {
153        let err = Contig::try_new("").expect_err("empty contig name should fail");
154        assert_eq!(err, Error::Empty);
155        assert_eq!(err.to_string(), "contig name cannot be empty");
156    }
157
158    #[test]
159    fn new_unchecked() {
160        let contig = Contig::new_unchecked("chr1");
161        assert_eq!(contig.as_str(), "chr1");
162
163        // new_unchecked allows empty strings (though not recommended)
164        let contig = Contig::new_unchecked("");
165        assert_eq!(contig.as_str(), "");
166    }
167
168    #[test]
169    fn clone_is_shallow() {
170        let a = Contig::new_unchecked("chr1");
171        let b = a.clone();
172        assert_eq!(a, b);
173        // Both point to the same underlying `str`.
174        assert!(std::ptr::eq(a.as_str(), b.as_str()));
175    }
176
177    #[test]
178    fn parse() {
179        let contig = "chr1".parse::<Contig>().expect("contig to parse");
180        assert_eq!(contig.as_str(), "chr1");
181    }
182
183    #[test]
184    fn parse_empty() {
185        let err = "".parse::<Contig>().expect_err("empty string should fail");
186        assert_eq!(err, Error::Empty);
187    }
188
189    #[test]
190    fn try_from_str() {
191        let contig = Contig::try_from("chr1").expect("valid contig");
192        assert_eq!(contig.as_str(), "chr1");
193
194        let err = Contig::try_from("").expect_err("empty should fail");
195        assert_eq!(err, Error::Empty);
196    }
197
198    #[test]
199    fn try_from_string() {
200        let contig = Contig::try_from(String::from("chr1")).expect("valid contig");
201        assert_eq!(contig.as_str(), "chr1");
202
203        let err = Contig::try_from(String::from("")).expect_err("empty should fail");
204        assert_eq!(err, Error::Empty);
205    }
206}