Skip to main content

use_gene/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7fn non_empty_text(value: impl AsRef<str>) -> Result<String, GeneValueError> {
8    let trimmed = value.as_ref().trim();
9
10    if trimmed.is_empty() {
11        Err(GeneValueError::Empty)
12    } else {
13        Ok(trimmed.to_string())
14    }
15}
16
17/// Error returned when gene vocabulary values are empty.
18#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19pub enum GeneValueError {
20    /// The supplied value was empty after trimming surrounding whitespace.
21    Empty,
22}
23
24impl fmt::Display for GeneValueError {
25    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
26        match self {
27            Self::Empty => formatter.write_str("gene value cannot be empty"),
28        }
29    }
30}
31
32impl Error for GeneValueError {}
33
34/// A stable gene identifier string.
35#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
36pub struct GeneId(String);
37
38impl GeneId {
39    /// Creates a gene identifier from non-empty text.
40    ///
41    /// # Errors
42    ///
43    /// Returns [`GeneValueError::Empty`] when the trimmed identifier is empty.
44    pub fn new(value: impl AsRef<str>) -> Result<Self, GeneValueError> {
45        non_empty_text(value).map(Self)
46    }
47
48    /// Returns the identifier text.
49    #[must_use]
50    pub fn as_str(&self) -> &str {
51        &self.0
52    }
53
54    /// Consumes the identifier and returns the owned string.
55    #[must_use]
56    pub fn into_string(self) -> String {
57        self.0
58    }
59}
60
61impl AsRef<str> for GeneId {
62    fn as_ref(&self) -> &str {
63        self.as_str()
64    }
65}
66
67impl fmt::Display for GeneId {
68    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
69        formatter.write_str(self.as_str())
70    }
71}
72
73impl FromStr for GeneId {
74    type Err = GeneValueError;
75
76    fn from_str(value: &str) -> Result<Self, Self::Err> {
77        Self::new(value)
78    }
79}
80
81/// A non-empty gene symbol that preserves caller casing.
82#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
83pub struct GeneSymbol(String);
84
85impl GeneSymbol {
86    /// Creates a gene symbol from non-empty text.
87    ///
88    /// # Errors
89    ///
90    /// Returns [`GeneValueError::Empty`] when the trimmed symbol is empty.
91    pub fn new(value: impl AsRef<str>) -> Result<Self, GeneValueError> {
92        non_empty_text(value).map(Self)
93    }
94
95    /// Returns the symbol text.
96    #[must_use]
97    pub fn as_str(&self) -> &str {
98        &self.0
99    }
100
101    /// Consumes the symbol and returns the owned string.
102    #[must_use]
103    pub fn into_string(self) -> String {
104        self.0
105    }
106}
107
108impl AsRef<str> for GeneSymbol {
109    fn as_ref(&self) -> &str {
110        self.as_str()
111    }
112}
113
114impl fmt::Display for GeneSymbol {
115    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
116        formatter.write_str(self.as_str())
117    }
118}
119
120impl FromStr for GeneSymbol {
121    type Err = GeneValueError;
122
123    fn from_str(value: &str) -> Result<Self, Self::Err> {
124        Self::new(value)
125    }
126}
127
128/// A non-empty descriptive gene name.
129#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
130pub struct GeneName(String);
131
132impl GeneName {
133    /// Creates a gene name from non-empty text.
134    ///
135    /// # Errors
136    ///
137    /// Returns [`GeneValueError::Empty`] when the trimmed name is empty.
138    pub fn new(value: impl AsRef<str>) -> Result<Self, GeneValueError> {
139        non_empty_text(value).map(Self)
140    }
141
142    /// Returns the gene name text.
143    #[must_use]
144    pub fn as_str(&self) -> &str {
145        &self.0
146    }
147
148    /// Consumes the name and returns the owned string.
149    #[must_use]
150    pub fn into_string(self) -> String {
151        self.0
152    }
153}
154
155impl AsRef<str> for GeneName {
156    fn as_ref(&self) -> &str {
157        self.as_str()
158    }
159}
160
161impl fmt::Display for GeneName {
162    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
163        formatter.write_str(self.as_str())
164    }
165}
166
167impl FromStr for GeneName {
168    type Err = GeneValueError;
169
170    fn from_str(value: &str) -> Result<Self, Self::Err> {
171        Self::new(value)
172    }
173}
174
175/// A non-empty descriptive locus identifier.
176#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
177pub struct Locus(String);
178
179impl Locus {
180    /// Creates a locus from non-empty text.
181    ///
182    /// # Errors
183    ///
184    /// Returns [`GeneValueError::Empty`] when the trimmed locus is empty.
185    pub fn new(value: impl AsRef<str>) -> Result<Self, GeneValueError> {
186        non_empty_text(value).map(Self)
187    }
188
189    /// Returns the locus text.
190    #[must_use]
191    pub fn as_str(&self) -> &str {
192        &self.0
193    }
194
195    /// Consumes the locus and returns the owned string.
196    #[must_use]
197    pub fn into_string(self) -> String {
198        self.0
199    }
200}
201
202impl AsRef<str> for Locus {
203    fn as_ref(&self) -> &str {
204        self.as_str()
205    }
206}
207
208impl fmt::Display for Locus {
209    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
210        formatter.write_str(self.as_str())
211    }
212}
213
214impl FromStr for Locus {
215    type Err = GeneValueError;
216
217    fn from_str(value: &str) -> Result<Self, Self::Err> {
218        Self::new(value)
219    }
220}
221
222/// A non-empty descriptive allele identifier.
223#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
224pub struct Allele(String);
225
226impl Allele {
227    /// Creates an allele from non-empty text.
228    ///
229    /// # Errors
230    ///
231    /// Returns [`GeneValueError::Empty`] when the trimmed allele is empty.
232    pub fn new(value: impl AsRef<str>) -> Result<Self, GeneValueError> {
233        non_empty_text(value).map(Self)
234    }
235
236    /// Returns the allele text.
237    #[must_use]
238    pub fn as_str(&self) -> &str {
239        &self.0
240    }
241
242    /// Consumes the allele and returns the owned string.
243    #[must_use]
244    pub fn into_string(self) -> String {
245        self.0
246    }
247}
248
249impl AsRef<str> for Allele {
250    fn as_ref(&self) -> &str {
251        self.as_str()
252    }
253}
254
255impl fmt::Display for Allele {
256    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
257        formatter.write_str(self.as_str())
258    }
259}
260
261impl FromStr for Allele {
262    type Err = GeneValueError;
263
264    fn from_str(value: &str) -> Result<Self, Self::Err> {
265        Self::new(value)
266    }
267}
268
269/// An ordered descriptive genotype value.
270#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
271pub struct Genotype {
272    alleles: Vec<Allele>,
273}
274
275impl Genotype {
276    /// Creates a genotype from caller-supplied allele order.
277    #[must_use]
278    pub const fn new(alleles: Vec<Allele>) -> Self {
279        Self { alleles }
280    }
281
282    /// Returns alleles in caller-supplied order.
283    #[must_use]
284    pub fn alleles(&self) -> &[Allele] {
285        &self.alleles
286    }
287
288    /// Returns the allele count.
289    #[must_use]
290    pub const fn len(&self) -> usize {
291        self.alleles.len()
292    }
293
294    /// Returns `true` when no alleles are present.
295    #[must_use]
296    pub const fn is_empty(&self) -> bool {
297        self.alleles.is_empty()
298    }
299}
300
301impl From<Vec<Allele>> for Genotype {
302    fn from(alleles: Vec<Allele>) -> Self {
303        Self::new(alleles)
304    }
305}
306
307impl fmt::Display for Genotype {
308    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
309        for (index, allele) in self.alleles.iter().enumerate() {
310            if index > 0 {
311                formatter.write_str("/")?;
312            }
313            write!(formatter, "{allele}")?;
314        }
315        Ok(())
316    }
317}
318
319#[cfg(test)]
320mod tests {
321    use super::{Allele, GeneSymbol, GeneValueError, Genotype, Locus};
322
323    #[test]
324    fn constructs_valid_gene_symbol() -> Result<(), GeneValueError> {
325        let symbol = GeneSymbol::new("BRCA1")?;
326
327        assert_eq!(symbol.as_str(), "BRCA1");
328        assert_eq!(symbol.to_string(), "BRCA1");
329        Ok(())
330    }
331
332    #[test]
333    fn rejects_empty_gene_symbol() {
334        assert_eq!(GeneSymbol::new("   "), Err(GeneValueError::Empty));
335    }
336
337    #[test]
338    fn constructs_valid_locus() -> Result<(), GeneValueError> {
339        let locus = Locus::new("17q21.31")?;
340
341        assert_eq!(locus.to_string(), "17q21.31");
342        Ok(())
343    }
344
345    #[test]
346    fn constructs_valid_allele() -> Result<(), GeneValueError> {
347        let allele = Allele::new("A")?;
348
349        assert_eq!(allele.as_str(), "A");
350        Ok(())
351    }
352
353    #[test]
354    fn constructs_genotype() -> Result<(), GeneValueError> {
355        let genotype = Genotype::new(vec![Allele::new("A")?, Allele::new("a")?]);
356
357        assert_eq!(genotype.len(), 2);
358        assert_eq!(genotype.alleles()[0].as_str(), "A");
359        assert!(!genotype.is_empty());
360        Ok(())
361    }
362
363    #[test]
364    fn displays_genotype() -> Result<(), GeneValueError> {
365        let genotype = Genotype::new(vec![Allele::new("A")?, Allele::new("a")?]);
366
367        assert_eq!(genotype.to_string(), "A/a");
368        Ok(())
369    }
370}