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}