Skip to main content

use_residue/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7/// Error returned by residue vocabulary constructors.
8#[derive(Clone, Copy, Debug, Eq, PartialEq)]
9pub enum ResidueError {
10    /// The supplied residue symbol text was empty.
11    EmptySymbol,
12    /// The supplied residue symbol text contained more than one character.
13    MultipleSymbols,
14}
15
16impl fmt::Display for ResidueError {
17    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
18        match self {
19            Self::EmptySymbol => formatter.write_str("residue symbol cannot be empty"),
20            Self::MultipleSymbols => formatter.write_str("residue symbol must be one character"),
21        }
22    }
23}
24
25impl Error for ResidueError {}
26
27/// A descriptive kind for a biological residue.
28#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
29pub enum ResidueKind {
30    /// Nucleotide residue.
31    Nucleotide,
32    /// Amino-acid residue.
33    AminoAcid,
34    /// Gap residue, commonly represented as `-`.
35    Gap,
36    /// Ambiguous residue symbol.
37    Ambiguous,
38    /// Unknown residue kind.
39    Unknown,
40    /// Domain-specific residue kind.
41    Custom(String),
42}
43
44impl fmt::Display for ResidueKind {
45    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
46        match self {
47            Self::Nucleotide => formatter.write_str("nucleotide"),
48            Self::AminoAcid => formatter.write_str("amino-acid"),
49            Self::Gap => formatter.write_str("gap"),
50            Self::Ambiguous => formatter.write_str("ambiguous"),
51            Self::Unknown => formatter.write_str("unknown"),
52            Self::Custom(kind) => formatter.write_str(kind),
53        }
54    }
55}
56
57impl FromStr for ResidueKind {
58    type Err = core::convert::Infallible;
59
60    fn from_str(value: &str) -> Result<Self, Self::Err> {
61        let kind = match value.trim().to_ascii_lowercase().as_str() {
62            "nucleotide" => Self::Nucleotide,
63            "amino-acid" | "amino_acid" | "amino acid" => Self::AminoAcid,
64            "gap" => Self::Gap,
65            "ambiguous" => Self::Ambiguous,
66            "unknown" | "" => Self::Unknown,
67            _ => Self::Custom(value.to_string()),
68        };
69
70        Ok(kind)
71    }
72}
73
74/// A single residue symbol.
75#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
76pub struct ResidueSymbol(char);
77
78impl ResidueSymbol {
79    /// Creates a residue symbol from a single character.
80    #[must_use]
81    pub const fn from_char(value: char) -> Self {
82        Self(value)
83    }
84
85    /// Creates a residue symbol from text containing exactly one character.
86    ///
87    /// # Errors
88    ///
89    /// Returns [`ResidueError::EmptySymbol`] for empty text and
90    /// [`ResidueError::MultipleSymbols`] for text with more than one character.
91    pub fn new(value: impl AsRef<str>) -> Result<Self, ResidueError> {
92        let mut chars = value.as_ref().chars();
93        let Some(symbol) = chars.next() else {
94            return Err(ResidueError::EmptySymbol);
95        };
96
97        if chars.next().is_some() {
98            Err(ResidueError::MultipleSymbols)
99        } else {
100            Ok(Self(symbol))
101        }
102    }
103
104    /// Returns the residue symbol character.
105    #[must_use]
106    pub const fn as_char(self) -> char {
107        self.0
108    }
109}
110
111impl fmt::Display for ResidueSymbol {
112    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
113        write!(formatter, "{}", self.0)
114    }
115}
116
117impl FromStr for ResidueSymbol {
118    type Err = ResidueError;
119
120    fn from_str(value: &str) -> Result<Self, Self::Err> {
121        Self::new(value)
122    }
123}
124
125/// A single residue symbol with a descriptive kind.
126#[derive(Clone, Debug, Eq, PartialEq)]
127pub struct Residue {
128    symbol: ResidueSymbol,
129    kind: ResidueKind,
130}
131
132impl Residue {
133    /// Creates a residue from a symbol and kind.
134    #[must_use]
135    pub const fn new(symbol: char, kind: ResidueKind) -> Self {
136        Self {
137            symbol: ResidueSymbol::from_char(symbol),
138            kind,
139        }
140    }
141
142    /// Creates a gap residue represented by `-`.
143    #[must_use]
144    pub const fn gap() -> Self {
145        Self::new('-', ResidueKind::Gap)
146    }
147
148    /// Creates an ambiguous residue with the supplied symbol.
149    #[must_use]
150    pub const fn ambiguous(symbol: char) -> Self {
151        Self::new(symbol, ResidueKind::Ambiguous)
152    }
153
154    /// Returns the residue symbol.
155    #[must_use]
156    pub const fn symbol(&self) -> ResidueSymbol {
157        self.symbol
158    }
159
160    /// Returns the descriptive residue kind.
161    #[must_use]
162    pub const fn kind(&self) -> &ResidueKind {
163        &self.kind
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::{Residue, ResidueError, ResidueKind, ResidueSymbol};
170    use core::str::FromStr;
171
172    #[test]
173    fn creates_valid_residue_symbol() {
174        let symbol = ResidueSymbol::new("A").expect("valid symbol");
175
176        assert_eq!(symbol.as_char(), 'A');
177        assert_eq!(symbol.to_string(), "A");
178    }
179
180    #[test]
181    fn rejects_empty_or_multiple_symbol_text() {
182        assert_eq!(ResidueSymbol::new(""), Err(ResidueError::EmptySymbol));
183        assert_eq!(ResidueSymbol::new("AC"), Err(ResidueError::MultipleSymbols));
184    }
185
186    #[test]
187    fn creates_gap_residue() {
188        let residue = Residue::gap();
189
190        assert_eq!(residue.symbol().as_char(), '-');
191        assert_eq!(residue.kind(), &ResidueKind::Gap);
192    }
193
194    #[test]
195    fn creates_ambiguous_residue() {
196        let residue = Residue::ambiguous('N');
197
198        assert_eq!(residue.symbol().as_char(), 'N');
199        assert_eq!(residue.kind(), &ResidueKind::Ambiguous);
200    }
201
202    #[test]
203    fn residue_kind_displays_and_parses() {
204        assert_eq!(ResidueKind::AminoAcid.to_string(), "amino-acid");
205        assert_eq!(ResidueKind::from_str("gap"), Ok(ResidueKind::Gap));
206    }
207
208    #[test]
209    fn supports_custom_residue_kind() {
210        assert_eq!(
211            ResidueKind::from_str("modified"),
212            Ok(ResidueKind::Custom("modified".into()))
213        );
214    }
215}