Skip to main content

use_feature/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6use use_annotation::AnnotationSet;
7use use_genomic_range::GenomicRange;
8
9fn non_empty_text(value: impl AsRef<str>) -> Result<String, FeatureValueError> {
10    let trimmed = value.as_ref().trim();
11
12    if trimmed.is_empty() {
13        Err(FeatureValueError::Empty)
14    } else {
15        Ok(trimmed.to_string())
16    }
17}
18
19/// Error returned by feature vocabulary constructors.
20#[derive(Clone, Copy, Debug, Eq, PartialEq)]
21pub enum FeatureValueError {
22    /// The supplied value was empty after trimming surrounding whitespace.
23    Empty,
24}
25
26impl fmt::Display for FeatureValueError {
27    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
28        match self {
29            Self::Empty => formatter.write_str("feature value cannot be empty"),
30        }
31    }
32}
33
34impl Error for FeatureValueError {}
35
36/// A non-empty feature identifier.
37#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
38pub struct FeatureId(String);
39
40impl FeatureId {
41    /// Creates a feature identifier from non-empty text.
42    ///
43    /// # Errors
44    ///
45    /// Returns [`FeatureValueError::Empty`] when the trimmed identifier is empty.
46    pub fn new(value: impl AsRef<str>) -> Result<Self, FeatureValueError> {
47        non_empty_text(value).map(Self)
48    }
49
50    /// Returns the identifier text.
51    #[must_use]
52    pub fn as_str(&self) -> &str {
53        &self.0
54    }
55}
56
57impl fmt::Display for FeatureId {
58    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
59        formatter.write_str(self.as_str())
60    }
61}
62
63impl FromStr for FeatureId {
64    type Err = FeatureValueError;
65
66    fn from_str(value: &str) -> Result<Self, Self::Err> {
67        Self::new(value)
68    }
69}
70
71/// A non-empty feature name.
72#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
73pub struct FeatureName(String);
74
75impl FeatureName {
76    /// Creates a feature name from non-empty text.
77    ///
78    /// # Errors
79    ///
80    /// Returns [`FeatureValueError::Empty`] when the trimmed name is empty.
81    pub fn new(value: impl AsRef<str>) -> Result<Self, FeatureValueError> {
82        non_empty_text(value).map(Self)
83    }
84
85    /// Returns the feature name text.
86    #[must_use]
87    pub fn as_str(&self) -> &str {
88        &self.0
89    }
90}
91
92impl fmt::Display for FeatureName {
93    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
94        formatter.write_str(self.as_str())
95    }
96}
97
98impl FromStr for FeatureName {
99    type Err = FeatureValueError;
100
101    fn from_str(value: &str) -> Result<Self, Self::Err> {
102        Self::new(value)
103    }
104}
105
106/// A descriptive feature kind.
107#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
108pub enum FeatureKind {
109    /// Gene feature.
110    Gene,
111    /// Exon feature.
112    Exon,
113    /// Intron feature.
114    Intron,
115    /// Coding sequence feature.
116    Cds,
117    /// Untranslated region feature.
118    Utr,
119    /// Promoter feature.
120    Promoter,
121    /// Enhancer feature.
122    Enhancer,
123    /// Repeat feature.
124    Repeat,
125    /// Motif feature.
126    Motif,
127    /// Variant feature label.
128    Variant,
129    /// Generic region feature.
130    Region,
131    /// Unknown feature kind.
132    Unknown,
133    /// Domain-specific feature kind.
134    Custom(String),
135}
136
137impl fmt::Display for FeatureKind {
138    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
139        match self {
140            Self::Gene => formatter.write_str("gene"),
141            Self::Exon => formatter.write_str("exon"),
142            Self::Intron => formatter.write_str("intron"),
143            Self::Cds => formatter.write_str("cds"),
144            Self::Utr => formatter.write_str("utr"),
145            Self::Promoter => formatter.write_str("promoter"),
146            Self::Enhancer => formatter.write_str("enhancer"),
147            Self::Repeat => formatter.write_str("repeat"),
148            Self::Motif => formatter.write_str("motif"),
149            Self::Variant => formatter.write_str("variant"),
150            Self::Region => formatter.write_str("region"),
151            Self::Unknown => formatter.write_str("unknown"),
152            Self::Custom(kind) => formatter.write_str(kind),
153        }
154    }
155}
156
157impl FromStr for FeatureKind {
158    type Err = core::convert::Infallible;
159
160    fn from_str(value: &str) -> Result<Self, Self::Err> {
161        let kind = match value.trim().to_ascii_lowercase().as_str() {
162            "gene" => Self::Gene,
163            "exon" => Self::Exon,
164            "intron" => Self::Intron,
165            "cds" => Self::Cds,
166            "utr" => Self::Utr,
167            "promoter" => Self::Promoter,
168            "enhancer" => Self::Enhancer,
169            "repeat" => Self::Repeat,
170            "motif" => Self::Motif,
171            "variant" => Self::Variant,
172            "region" => Self::Region,
173            "unknown" | "" => Self::Unknown,
174            _ => Self::Custom(value.to_string()),
175        };
176
177        Ok(kind)
178    }
179}
180
181/// A sequence or genomic feature with optional range and deterministic attributes.
182#[derive(Clone, Debug, Eq, PartialEq)]
183pub struct SequenceFeature {
184    kind: FeatureKind,
185    name: FeatureName,
186    id: Option<FeatureId>,
187    range: Option<GenomicRange>,
188    attributes: AnnotationSet,
189}
190
191impl SequenceFeature {
192    /// Creates a sequence feature.
193    #[must_use]
194    pub const fn new(kind: FeatureKind, name: FeatureName) -> Self {
195        Self {
196            kind,
197            name,
198            id: None,
199            range: None,
200            attributes: AnnotationSet::new(),
201        }
202    }
203
204    /// Sets the optional feature identifier.
205    #[must_use]
206    pub fn with_id(mut self, id: FeatureId) -> Self {
207        self.id = Some(id);
208        self
209    }
210
211    /// Sets the optional genomic range.
212    #[must_use]
213    pub fn with_range(mut self, range: GenomicRange) -> Self {
214        self.range = Some(range);
215        self
216    }
217
218    /// Sets deterministic feature attributes.
219    #[must_use]
220    pub fn with_attributes(mut self, attributes: AnnotationSet) -> Self {
221        self.attributes = attributes;
222        self
223    }
224
225    /// Returns the feature kind.
226    #[must_use]
227    pub const fn kind(&self) -> &FeatureKind {
228        &self.kind
229    }
230
231    /// Returns the feature name.
232    #[must_use]
233    pub const fn name(&self) -> &FeatureName {
234        &self.name
235    }
236
237    /// Returns the optional feature identifier.
238    #[must_use]
239    pub const fn id(&self) -> Option<&FeatureId> {
240        self.id.as_ref()
241    }
242
243    /// Returns the optional genomic range.
244    #[must_use]
245    pub const fn range(&self) -> Option<&GenomicRange> {
246        self.range.as_ref()
247    }
248
249    /// Returns deterministic feature attributes.
250    #[must_use]
251    pub const fn attributes(&self) -> &AnnotationSet {
252        &self.attributes
253    }
254}
255
256#[cfg(test)]
257mod tests {
258    use super::{FeatureKind, FeatureName, FeatureValueError, SequenceFeature};
259    use core::str::FromStr;
260    use use_genomic_range::{GenomicPosition, GenomicRange};
261
262    #[test]
263    fn creates_valid_feature_name() {
264        let name = FeatureName::new("BRCA1 region").expect("valid feature name");
265
266        assert_eq!(name.as_str(), "BRCA1 region");
267    }
268
269    #[test]
270    fn rejects_empty_feature_name() {
271        assert_eq!(FeatureName::new(" "), Err(FeatureValueError::Empty));
272    }
273
274    #[test]
275    fn feature_kind_displays_and_parses() {
276        assert_eq!(FeatureKind::Cds.to_string(), "cds");
277        assert_eq!(FeatureKind::from_str("enhancer"), Ok(FeatureKind::Enhancer));
278    }
279
280    #[test]
281    fn supports_custom_feature_kind() {
282        assert_eq!(
283            FeatureKind::from_str("operator"),
284            Ok(FeatureKind::Custom("operator".into()))
285        );
286    }
287
288    #[test]
289    fn creates_feature_with_genomic_range() {
290        let range = GenomicRange::new(GenomicPosition::new(10), GenomicPosition::new(20))
291            .expect("valid range");
292        let feature = SequenceFeature::new(
293            FeatureKind::Gene,
294            FeatureName::new("BRCA1 region").expect("valid feature name"),
295        )
296        .with_range(range.clone());
297
298        assert_eq!(feature.kind(), &FeatureKind::Gene);
299        assert_eq!(feature.range(), Some(&range));
300    }
301}