Skip to main content

omics_coordinate/
contig.rs

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