Skip to main content

use_biome/
lib.rs

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    /// # Errors
41    /// Returns `BiomeTextError::Empty` when `value` is blank.
42    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    /// # Errors
71    /// Returns `BiomeTextError::Empty` when `value` is blank.
72    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}