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, AlignmentValueError> {
8 let trimmed = value.as_ref().trim();
9
10 if trimmed.is_empty() {
11 Err(AlignmentValueError::Empty)
12 } else {
13 Ok(value.as_ref().to_string())
14 }
15}
16
17#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19pub enum AlignmentValueError {
20 Empty,
22}
23
24impl fmt::Display for AlignmentValueError {
25 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
26 match self {
27 Self::Empty => formatter.write_str("alignment value cannot be empty"),
28 }
29 }
30}
31
32impl Error for AlignmentValueError {}
33
34#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
36pub struct AlignmentId(String);
37
38impl AlignmentId {
39 pub fn new(value: impl AsRef<str>) -> Result<Self, AlignmentValueError> {
45 non_empty_text(value).map(Self)
46 }
47
48 #[must_use]
50 pub fn as_str(&self) -> &str {
51 &self.0
52 }
53}
54
55impl fmt::Display for AlignmentId {
56 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
57 formatter.write_str(self.as_str())
58 }
59}
60
61impl FromStr for AlignmentId {
62 type Err = AlignmentValueError;
63
64 fn from_str(value: &str) -> Result<Self, Self::Err> {
65 Self::new(value)
66 }
67}
68
69#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
71pub enum AlignmentKind {
72 Pairwise,
74 Multiple,
76 Local,
78 Global,
80 SemiGlobal,
82 Unknown,
84 Custom(String),
86}
87
88impl fmt::Display for AlignmentKind {
89 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
90 match self {
91 Self::Pairwise => formatter.write_str("pairwise"),
92 Self::Multiple => formatter.write_str("multiple"),
93 Self::Local => formatter.write_str("local"),
94 Self::Global => formatter.write_str("global"),
95 Self::SemiGlobal => formatter.write_str("semi-global"),
96 Self::Unknown => formatter.write_str("unknown"),
97 Self::Custom(kind) => formatter.write_str(kind),
98 }
99 }
100}
101
102impl FromStr for AlignmentKind {
103 type Err = core::convert::Infallible;
104
105 fn from_str(value: &str) -> Result<Self, Self::Err> {
106 let kind = match value.trim().to_ascii_lowercase().as_str() {
107 "pairwise" => Self::Pairwise,
108 "multiple" => Self::Multiple,
109 "local" => Self::Local,
110 "global" => Self::Global,
111 "semi-global" | "semiglobal" | "semi_global" => Self::SemiGlobal,
112 "unknown" | "" => Self::Unknown,
113 _ => Self::Custom(value.to_string()),
114 };
115
116 Ok(kind)
117 }
118}
119
120#[derive(Clone, Copy, Debug, PartialEq)]
122pub struct AlignmentScore(f64);
123
124impl AlignmentScore {
125 #[must_use]
127 pub const fn new(value: f64) -> Self {
128 Self(value)
129 }
130
131 #[must_use]
133 pub const fn value(self) -> f64 {
134 self.0
135 }
136}
137
138#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
140pub struct AlignedSequence(String);
141
142impl AlignedSequence {
143 pub fn new(value: impl AsRef<str>) -> Result<Self, AlignmentValueError> {
149 non_empty_text(value).map(Self)
150 }
151
152 #[must_use]
154 pub fn as_str(&self) -> &str {
155 &self.0
156 }
157
158 #[must_use]
160 pub fn aligned_len(&self) -> usize {
161 self.0.chars().count()
162 }
163
164 #[must_use]
166 pub const fn is_empty(&self) -> bool {
167 self.0.is_empty()
168 }
169}
170
171impl fmt::Display for AlignedSequence {
172 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
173 formatter.write_str(self.as_str())
174 }
175}
176
177impl FromStr for AlignedSequence {
178 type Err = AlignmentValueError;
179
180 fn from_str(value: &str) -> Result<Self, Self::Err> {
181 Self::new(value)
182 }
183}
184
185#[derive(Clone, Debug, PartialEq)]
187pub struct AlignmentSummary {
188 kind: AlignmentKind,
189 score: Option<AlignmentScore>,
190 sequences: Vec<AlignedSequence>,
191}
192
193impl AlignmentSummary {
194 #[must_use]
196 pub const fn new(kind: AlignmentKind) -> Self {
197 Self {
198 kind,
199 score: None,
200 sequences: Vec::new(),
201 }
202 }
203
204 #[must_use]
206 pub const fn with_score(mut self, score: AlignmentScore) -> Self {
207 self.score = Some(score);
208 self
209 }
210
211 #[must_use]
213 pub fn with_sequence(mut self, sequence: AlignedSequence) -> Self {
214 self.sequences.push(sequence);
215 self
216 }
217
218 #[must_use]
220 pub const fn kind(&self) -> &AlignmentKind {
221 &self.kind
222 }
223
224 #[must_use]
226 pub const fn score(&self) -> Option<AlignmentScore> {
227 self.score
228 }
229
230 #[must_use]
232 pub fn sequences(&self) -> &[AlignedSequence] {
233 &self.sequences
234 }
235
236 #[must_use]
238 pub const fn sequence_count(&self) -> usize {
239 self.sequences.len()
240 }
241}
242
243#[cfg(test)]
244mod tests {
245 use super::{
246 AlignedSequence, AlignmentKind, AlignmentScore, AlignmentSummary, AlignmentValueError,
247 };
248 use core::str::FromStr;
249
250 #[test]
251 fn alignment_kind_displays_and_parses() {
252 assert_eq!(AlignmentKind::SemiGlobal.to_string(), "semi-global");
253 assert_eq!(
254 AlignmentKind::from_str("pairwise"),
255 Ok(AlignmentKind::Pairwise)
256 );
257 }
258
259 #[test]
260 fn creates_valid_aligned_sequence() {
261 let sequence = AlignedSequence::new("ACG-T").expect("valid aligned sequence");
262
263 assert_eq!(sequence.as_str(), "ACG-T");
264 }
265
266 #[test]
267 fn aligned_length_helper_counts_symbols() {
268 let sequence = AlignedSequence::new("ACG-T").expect("valid aligned sequence");
269
270 assert_eq!(sequence.aligned_len(), 5);
271 }
272
273 #[test]
274 fn rejects_empty_aligned_sequence() {
275 assert_eq!(AlignedSequence::new(" "), Err(AlignmentValueError::Empty));
276 }
277
278 #[test]
279 fn constructs_alignment_score() {
280 let score = AlignmentScore::new(42.5);
281
282 assert!((score.value() - 42.5).abs() < f64::EPSILON);
283 }
284
285 #[test]
286 fn supports_custom_alignment_kind() {
287 assert_eq!(
288 AlignmentKind::from_str("chain"),
289 Ok(AlignmentKind::Custom("chain".into()))
290 );
291 }
292
293 #[test]
294 fn alignment_summary_stores_metadata_only() {
295 let summary = AlignmentSummary::new(AlignmentKind::Pairwise)
296 .with_score(AlignmentScore::new(1.0))
297 .with_sequence(AlignedSequence::new("A-C").expect("valid aligned sequence"));
298
299 assert_eq!(summary.kind(), &AlignmentKind::Pairwise);
300 assert_eq!(summary.score(), Some(AlignmentScore::new(1.0)));
301 assert_eq!(summary.sequence_count(), 1);
302 }
303}