1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7fn normalized_token(value: &str) -> String {
8 value.trim().to_ascii_lowercase().replace(['_', ' '], "-")
9}
10
11fn non_empty_text(value: impl AsRef<str>) -> Result<String, BiomeTextError> {
12 let trimmed = value.as_ref().trim();
13
14 if trimmed.is_empty() {
15 Err(BiomeTextError::Empty)
16 } else {
17 Ok(trimmed.to_string())
18 }
19}
20
21#[derive(Clone, Copy, Debug, Eq, PartialEq)]
22pub enum BiomeTextError {
23 Empty,
24}
25
26impl fmt::Display for BiomeTextError {
27 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
28 match self {
29 Self::Empty => formatter.write_str("biome text cannot be empty"),
30 }
31 }
32}
33
34impl Error for BiomeTextError {}
35
36#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
37pub struct BiomeName(String);
38
39impl BiomeName {
40 pub fn new(value: impl AsRef<str>) -> Result<Self, BiomeTextError> {
43 non_empty_text(value).map(Self)
44 }
45
46 #[must_use]
47 pub fn as_str(&self) -> &str {
48 &self.0
49 }
50}
51
52impl fmt::Display for BiomeName {
53 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
54 formatter.write_str(self.as_str())
55 }
56}
57
58impl FromStr for BiomeName {
59 type Err = BiomeTextError;
60
61 fn from_str(value: &str) -> Result<Self, Self::Err> {
62 Self::new(value)
63 }
64}
65
66#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
67pub struct BiomeClimate(String);
68
69impl BiomeClimate {
70 pub fn new(value: impl AsRef<str>) -> Result<Self, BiomeTextError> {
73 non_empty_text(value).map(Self)
74 }
75
76 #[must_use]
77 pub fn as_str(&self) -> &str {
78 &self.0
79 }
80}
81
82impl fmt::Display for BiomeClimate {
83 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
84 formatter.write_str(self.as_str())
85 }
86}
87
88impl FromStr for BiomeClimate {
89 type Err = BiomeTextError;
90
91 fn from_str(value: &str) -> Result<Self, Self::Err> {
92 Self::new(value)
93 }
94}
95
96#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
97pub enum BiomeKind {
98 TropicalRainforest,
99 TemperateForest,
100 BorealForest,
101 Grassland,
102 Savanna,
103 Desert,
104 Tundra,
105 Mediterranean,
106 Wetland,
107 Freshwater,
108 Marine,
109 Unknown,
110 Custom(String),
111}
112
113impl fmt::Display for BiomeKind {
114 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
115 formatter.write_str(match self {
116 Self::TropicalRainforest => "tropical-rainforest",
117 Self::TemperateForest => "temperate-forest",
118 Self::BorealForest => "boreal-forest",
119 Self::Grassland => "grassland",
120 Self::Savanna => "savanna",
121 Self::Desert => "desert",
122 Self::Tundra => "tundra",
123 Self::Mediterranean => "mediterranean",
124 Self::Wetland => "wetland",
125 Self::Freshwater => "freshwater",
126 Self::Marine => "marine",
127 Self::Unknown => "unknown",
128 Self::Custom(value) => value.as_str(),
129 })
130 }
131}
132
133impl FromStr for BiomeKind {
134 type Err = BiomeKindParseError;
135
136 fn from_str(value: &str) -> Result<Self, Self::Err> {
137 let trimmed = value.trim();
138
139 if trimmed.is_empty() {
140 return Err(BiomeKindParseError::Empty);
141 }
142
143 Ok(match normalized_token(trimmed).as_str() {
144 "tropical-rainforest" => Self::TropicalRainforest,
145 "temperate-forest" => Self::TemperateForest,
146 "boreal-forest" => Self::BorealForest,
147 "grassland" => Self::Grassland,
148 "savanna" => Self::Savanna,
149 "desert" => Self::Desert,
150 "tundra" => Self::Tundra,
151 "mediterranean" => Self::Mediterranean,
152 "wetland" => Self::Wetland,
153 "freshwater" => Self::Freshwater,
154 "marine" => Self::Marine,
155 "unknown" => Self::Unknown,
156 _ => Self::Custom(trimmed.to_string()),
157 })
158 }
159}
160
161#[derive(Clone, Copy, Debug, Eq, PartialEq)]
162pub enum BiomeKindParseError {
163 Empty,
164}
165
166impl fmt::Display for BiomeKindParseError {
167 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
168 match self {
169 Self::Empty => formatter.write_str("biome kind cannot be empty"),
170 }
171 }
172}
173
174impl Error for BiomeKindParseError {}
175
176#[cfg(test)]
177mod tests {
178 use super::{BiomeClimate, BiomeKind, BiomeName, BiomeTextError};
179
180 #[test]
181 fn valid_biome_name() -> Result<(), BiomeTextError> {
182 let name = BiomeName::new("coastal upwelling")?;
183
184 assert_eq!(name.as_str(), "coastal upwelling");
185 Ok(())
186 }
187
188 #[test]
189 fn empty_biome_name_rejected() {
190 assert_eq!(BiomeName::new(" "), Err(BiomeTextError::Empty));
191 }
192
193 #[test]
194 fn biome_kind_display_parse() {
195 assert_eq!("marine".parse::<BiomeKind>(), Ok(BiomeKind::Marine));
196 assert_eq!(BiomeKind::TemperateForest.to_string(), "temperate-forest");
197 }
198
199 #[test]
200 fn custom_biome_kind() {
201 assert_eq!(
202 "fog-desert".parse::<BiomeKind>(),
203 Ok(BiomeKind::Custom("fog-desert".to_string()))
204 );
205 }
206
207 #[test]
208 fn biome_climate_construction() -> Result<(), BiomeTextError> {
209 let climate = BiomeClimate::new("cool and wet")?;
210
211 assert_eq!(climate.to_string(), "cool and wet");
212 Ok(())
213 }
214}