ed_journals/modules/exobiology/models/
variant.rs1use 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 #[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}