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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
21pub enum FeatureValueError {
22 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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
38pub struct FeatureId(String);
39
40impl FeatureId {
41 pub fn new(value: impl AsRef<str>) -> Result<Self, FeatureValueError> {
47 non_empty_text(value).map(Self)
48 }
49
50 #[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
73pub struct FeatureName(String);
74
75impl FeatureName {
76 pub fn new(value: impl AsRef<str>) -> Result<Self, FeatureValueError> {
82 non_empty_text(value).map(Self)
83 }
84
85 #[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
108pub enum FeatureKind {
109 Gene,
111 Exon,
113 Intron,
115 Cds,
117 Utr,
119 Promoter,
121 Enhancer,
123 Repeat,
125 Motif,
127 Variant,
129 Region,
131 Unknown,
133 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#[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 #[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 #[must_use]
206 pub fn with_id(mut self, id: FeatureId) -> Self {
207 self.id = Some(id);
208 self
209 }
210
211 #[must_use]
213 pub fn with_range(mut self, range: GenomicRange) -> Self {
214 self.range = Some(range);
215 self
216 }
217
218 #[must_use]
220 pub fn with_attributes(mut self, attributes: AnnotationSet) -> Self {
221 self.attributes = attributes;
222 self
223 }
224
225 #[must_use]
227 pub const fn kind(&self) -> &FeatureKind {
228 &self.kind
229 }
230
231 #[must_use]
233 pub const fn name(&self) -> &FeatureName {
234 &self.name
235 }
236
237 #[must_use]
239 pub const fn id(&self) -> Option<&FeatureId> {
240 self.id.as_ref()
241 }
242
243 #[must_use]
245 pub const fn range(&self) -> Option<&GenomicRange> {
246 self.range.as_ref()
247 }
248
249 #[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}