Skip to main content

ed_journals/modules/exobiology/models/
variant.rs

1use std::fmt::{Display, Formatter};
2use std::str::FromStr;
3
4use lazy_static::lazy_static;
5use regex::Regex;
6use serde::{Deserialize, Serialize};
7use thiserror::Error;
8
9use crate::deserialize_in_order_impl;
10use crate::exobiology::models::species::SpeciesError;
11use crate::exobiology::Genus;
12use crate::modules::exobiology::{
13    Species, VariantColor, VariantColorError, VariantSource, VariantSourceError,
14};
15
16#[derive(Debug, Serialize, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
17pub struct Variant {
18    pub species: Species,
19    pub color: VariantColor,
20}
21
22impl Variant {
23    /// Whether the current variant is unknown.
24    #[cfg(feature = "allow-unknown")]
25    #[cfg_attr(docsrs, doc(cfg(feature = "allow-unknown")))]
26    pub fn is_unknown(&self) -> bool {
27        matches!(self.species, Species::Unknown(_)) || matches!(self.color, VariantColor::Unknown)
28    }
29
30    pub fn genus(&self) -> Genus {
31        self.species.genus()
32    }
33}
34
35#[derive(Debug, Error)]
36pub enum VariantError {
37    #[error("Failed to parse species: {0}")]
38    FailedToParseSpecies(#[from] SpeciesError),
39
40    #[error(transparent)]
41    VariantSourceError(#[from] VariantSourceError),
42
43    #[error(transparent)]
44    VariantColorError(#[from] VariantColorError),
45
46    #[error("Failed to parse variant: '{0}'")]
47    FailedToParse(String),
48}
49
50lazy_static! {
51    static ref VARIANT_REGEX: Regex =
52        Regex::new(r#"^(\$[cC]odex_[eE]nt_([a-zA-Z]+)_(\d+))_([a-zA-Z]+)(_[nN]ame;)?$"#).unwrap();
53}
54
55impl FromStr for Variant {
56    type Err = VariantError;
57
58    fn from_str(s: &str) -> Result<Self, Self::Err> {
59        match Species::from_str(s) {
60            #[cfg(feature = "allow-unknown")]
61            Ok(species) if species.is_unknown() => {}
62            Ok(species) => {
63                return Ok(Variant {
64                    species,
65                    color: VariantColor::None,
66                })
67            }
68            Err(_) => {}
69        }
70
71        let Some(captures) = VARIANT_REGEX.captures(s) else {
72            return Err(VariantError::FailedToParse(s.to_string()));
73        };
74
75        let species = captures
76            .get(1)
77            .expect("Should have been captured already")
78            .as_str();
79
80        let species = format!("{species}_Name;").parse()?;
81
82        let variant_source: VariantSource = captures
83            .get(4)
84            .expect("Should have been captured already")
85            .as_str()
86            .parse()?;
87
88        let color = (&species, &variant_source).try_into()?;
89
90        Ok(Variant { species, color })
91    }
92}
93
94#[derive(Deserialize)]
95struct VariantInput {
96    pub species: Species,
97    pub color: VariantColor,
98}
99
100impl From<VariantInput> for Variant {
101    fn from(value: VariantInput) -> Self {
102        Variant {
103            species: value.species,
104            color: value.color,
105        }
106    }
107}
108
109deserialize_in_order_impl!(
110    Variant =>
111        A # String,
112        B ! VariantInput,
113);
114
115impl Display for Variant {
116    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
117        if let VariantColor::None = self.color {
118            return self.species.fmt(f);
119        }
120
121        write!(f, "{} - {}", self.species, self.color)
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use std::str::FromStr;
128
129    use crate::modules::exobiology::{Species, Variant, VariantColor};
130
131    #[test]
132    fn variant_test_cases_are_processed_correctly() {
133        let test_cases = [
134            (
135                "$Codex_Ent_Tussocks_01_F_Name;",
136                Variant {
137                    species: Species::TussockPennata,
138                    color: VariantColor::Yellow,
139                },
140            ),
141            (
142                "$Codex_Ent_Stratum_07_T_Name;",
143                Variant {
144                    species: Species::StratumTectonicas,
145                    color: VariantColor::Grey,
146                },
147            ),
148            (
149                "$Codex_Ent_Recepta_03_Yttrium_Name;",
150                Variant {
151                    species: Species::ReceptaConditivus,
152                    color: VariantColor::Green,
153                },
154            ),
155            (
156                "$Codex_Ent_Fonticulus_02_M_Name;",
157                Variant {
158                    species: Species::FonticuluaCampestris,
159                    color: VariantColor::Amethyst,
160                },
161            ),
162            (
163                "$Codex_Ent_Bacterial_05_Tellurium_Name;",
164                Variant {
165                    species: Species::BacteriumVesicula,
166                    color: VariantColor::Red,
167                },
168            ),
169            (
170                "$codex_ent_aleoids_01_a_name;",
171                Variant {
172                    species: Species::AleoidaArcus,
173                    color: VariantColor::Green,
174                },
175            ),
176        ];
177
178        for (case, expected) in test_cases {
179            let result = Variant::from_str(case);
180
181            if result.is_err() {
182                dbg!(&case, &result);
183            }
184
185            assert!(result.is_ok());
186            assert_eq!(result.unwrap(), expected);
187        }
188    }
189
190    #[test]
191    fn variants_test_file_entries_all_parse() {
192        let content = include_str!("zz_variants.txt");
193        let lines = content.lines();
194
195        for line in lines {
196            if line.starts_with('#') {
197                continue;
198            }
199
200            dbg!(&line);
201            let result = Variant::from_str(line);
202
203            dbg!(&result);
204            assert!(result.is_ok());
205        }
206    }
207
208    #[test]
209    fn variants_datadump_test_file_entries_all_parse() {
210        let content = include_str!("zz_datamined_variants.txt");
211        let lines = content.lines();
212
213        for line in lines {
214            if line.starts_with('#') {
215                continue;
216            }
217
218            dbg!(&line);
219            let result = Variant::from_str(line);
220
221            dbg!(&result);
222            assert!(result.is_ok());
223        }
224    }
225}