1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7pub mod prelude {
8 pub use crate::{
9 ChurchMode, ModalBrightness, ModeDegree, ModeError, ModeFamily, ModeKind, ModeName,
10 };
11}
12#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
13pub struct ModeName(String);
14
15impl ModeName {
16 pub fn new(value: impl AsRef<str>) -> Result<Self, ModeError> {
17 non_empty_text(value).map(Self)
18 }
19
20 pub fn as_str(&self) -> &str {
21 &self.0
22 }
23
24 pub fn value(&self) -> &str {
25 self.as_str()
26 }
27
28 pub fn into_string(self) -> String {
29 self.0
30 }
31}
32
33impl AsRef<str> for ModeName {
34 fn as_ref(&self) -> &str {
35 self.as_str()
36 }
37}
38
39impl fmt::Display for ModeName {
40 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
41 formatter.write_str(self.as_str())
42 }
43}
44
45impl FromStr for ModeName {
46 type Err = ModeError;
47
48 fn from_str(value: &str) -> Result<Self, Self::Err> {
49 Self::new(value)
50 }
51}
52
53impl TryFrom<&str> for ModeName {
54 type Error = ModeError;
55
56 fn try_from(value: &str) -> Result<Self, Self::Error> {
57 Self::new(value)
58 }
59}
60#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
61pub struct ModeDegree(u8);
62
63impl ModeDegree {
64 pub fn new(value: u8) -> Result<Self, ModeError> {
65 if !(1..=32).contains(&value) {
66 return Err(ModeError::OutOfRange);
67 }
68
69 Ok(Self(value))
70 }
71
72 pub const fn value(self) -> u8 {
73 self.0
74 }
75}
76
77impl fmt::Display for ModeDegree {
78 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
79 self.0.fmt(formatter)
80 }
81}
82
83impl FromStr for ModeDegree {
84 type Err = ModeError;
85
86 fn from_str(value: &str) -> Result<Self, Self::Err> {
87 let parsed = value
88 .trim()
89 .parse::<u8>()
90 .map_err(|_| ModeError::InvalidFormat)?;
91 Self::new(parsed)
92 }
93}
94
95impl TryFrom<u8> for ModeDegree {
96 type Error = ModeError;
97
98 fn try_from(value: u8) -> Result<Self, Self::Error> {
99 Self::new(value)
100 }
101}
102#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
103pub enum ModeKind {
104 Ionian,
105 Dorian,
106 Phrygian,
107 Lydian,
108 Mixolydian,
109 Aeolian,
110 Locrian,
111 Major,
112 Minor,
113 Custom,
114}
115
116impl ModeKind {
117 pub const ALL: &'static [Self] = &[
118 Self::Ionian,
119 Self::Dorian,
120 Self::Phrygian,
121 Self::Lydian,
122 Self::Mixolydian,
123 Self::Aeolian,
124 Self::Locrian,
125 Self::Major,
126 Self::Minor,
127 Self::Custom,
128 ];
129
130 pub const fn as_str(self) -> &'static str {
131 match self {
132 Self::Ionian => "ionian",
133 Self::Dorian => "dorian",
134 Self::Phrygian => "phrygian",
135 Self::Lydian => "lydian",
136 Self::Mixolydian => "mixolydian",
137 Self::Aeolian => "aeolian",
138 Self::Locrian => "locrian",
139 Self::Major => "major",
140 Self::Minor => "minor",
141 Self::Custom => "custom",
142 }
143 }
144}
145
146impl fmt::Display for ModeKind {
147 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
148 formatter.write_str(self.as_str())
149 }
150}
151
152impl FromStr for ModeKind {
153 type Err = ModeError;
154
155 fn from_str(value: &str) -> Result<Self, Self::Err> {
156 match normalized_label(value)?.as_str() {
157 "ionian" => Ok(Self::Ionian),
158 "dorian" => Ok(Self::Dorian),
159 "phrygian" => Ok(Self::Phrygian),
160 "lydian" => Ok(Self::Lydian),
161 "mixolydian" => Ok(Self::Mixolydian),
162 "aeolian" => Ok(Self::Aeolian),
163 "locrian" => Ok(Self::Locrian),
164 "major" => Ok(Self::Major),
165 "minor" => Ok(Self::Minor),
166 "custom" => Ok(Self::Custom),
167 _ => Err(ModeError::UnknownLabel),
168 }
169 }
170}
171#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
172pub enum ChurchMode {
173 Ionian,
174 Dorian,
175 Phrygian,
176 Lydian,
177 Mixolydian,
178 Aeolian,
179 Locrian,
180}
181
182impl ChurchMode {
183 pub const ALL: &'static [Self] = &[
184 Self::Ionian,
185 Self::Dorian,
186 Self::Phrygian,
187 Self::Lydian,
188 Self::Mixolydian,
189 Self::Aeolian,
190 Self::Locrian,
191 ];
192
193 pub const fn as_str(self) -> &'static str {
194 match self {
195 Self::Ionian => "ionian",
196 Self::Dorian => "dorian",
197 Self::Phrygian => "phrygian",
198 Self::Lydian => "lydian",
199 Self::Mixolydian => "mixolydian",
200 Self::Aeolian => "aeolian",
201 Self::Locrian => "locrian",
202 }
203 }
204}
205
206impl fmt::Display for ChurchMode {
207 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
208 formatter.write_str(self.as_str())
209 }
210}
211
212impl FromStr for ChurchMode {
213 type Err = ModeError;
214
215 fn from_str(value: &str) -> Result<Self, Self::Err> {
216 match normalized_label(value)?.as_str() {
217 "ionian" => Ok(Self::Ionian),
218 "dorian" => Ok(Self::Dorian),
219 "phrygian" => Ok(Self::Phrygian),
220 "lydian" => Ok(Self::Lydian),
221 "mixolydian" => Ok(Self::Mixolydian),
222 "aeolian" => Ok(Self::Aeolian),
223 "locrian" => Ok(Self::Locrian),
224 _ => Err(ModeError::UnknownLabel),
225 }
226 }
227}
228#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
229pub enum ModeFamily {
230 Diatonic,
231 MelodicMinor,
232 HarmonicMinor,
233 Pentatonic,
234 Custom,
235}
236
237impl ModeFamily {
238 pub const ALL: &'static [Self] = &[
239 Self::Diatonic,
240 Self::MelodicMinor,
241 Self::HarmonicMinor,
242 Self::Pentatonic,
243 Self::Custom,
244 ];
245
246 pub const fn as_str(self) -> &'static str {
247 match self {
248 Self::Diatonic => "diatonic",
249 Self::MelodicMinor => "melodic-minor",
250 Self::HarmonicMinor => "harmonic-minor",
251 Self::Pentatonic => "pentatonic",
252 Self::Custom => "custom",
253 }
254 }
255}
256
257impl fmt::Display for ModeFamily {
258 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
259 formatter.write_str(self.as_str())
260 }
261}
262
263impl FromStr for ModeFamily {
264 type Err = ModeError;
265
266 fn from_str(value: &str) -> Result<Self, Self::Err> {
267 match normalized_label(value)?.as_str() {
268 "diatonic" => Ok(Self::Diatonic),
269 "melodic-minor" => Ok(Self::MelodicMinor),
270 "harmonic-minor" => Ok(Self::HarmonicMinor),
271 "pentatonic" => Ok(Self::Pentatonic),
272 "custom" => Ok(Self::Custom),
273 _ => Err(ModeError::UnknownLabel),
274 }
275 }
276}
277#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
278pub enum ModalBrightness {
279 VeryDark,
280 Dark,
281 Neutral,
282 Bright,
283 VeryBright,
284 Unknown,
285}
286
287impl ModalBrightness {
288 pub const ALL: &'static [Self] = &[
289 Self::VeryDark,
290 Self::Dark,
291 Self::Neutral,
292 Self::Bright,
293 Self::VeryBright,
294 Self::Unknown,
295 ];
296
297 pub const fn as_str(self) -> &'static str {
298 match self {
299 Self::VeryDark => "very-dark",
300 Self::Dark => "dark",
301 Self::Neutral => "neutral",
302 Self::Bright => "bright",
303 Self::VeryBright => "very-bright",
304 Self::Unknown => "unknown",
305 }
306 }
307}
308
309impl fmt::Display for ModalBrightness {
310 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
311 formatter.write_str(self.as_str())
312 }
313}
314
315impl FromStr for ModalBrightness {
316 type Err = ModeError;
317
318 fn from_str(value: &str) -> Result<Self, Self::Err> {
319 match normalized_label(value)?.as_str() {
320 "very-dark" => Ok(Self::VeryDark),
321 "dark" => Ok(Self::Dark),
322 "neutral" => Ok(Self::Neutral),
323 "bright" => Ok(Self::Bright),
324 "very-bright" => Ok(Self::VeryBright),
325 "unknown" => Ok(Self::Unknown),
326 _ => Err(ModeError::UnknownLabel),
327 }
328 }
329}
330
331#[derive(Clone, Copy, Debug, Eq, PartialEq)]
332pub enum ModeError {
333 Empty,
334 InvalidFormat,
335 OutOfRange,
336 NonFinite,
337 NonPositive,
338 UnknownLabel,
339}
340
341impl fmt::Display for ModeError {
342 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
343 match self {
344 Self::Empty => formatter.write_str("mode metadata text cannot be empty"),
345 Self::InvalidFormat => formatter.write_str("mode metadata has an invalid format"),
346 Self::OutOfRange => formatter.write_str("mode metadata value is out of range"),
347 Self::NonFinite => formatter.write_str("mode metadata value must be finite"),
348 Self::NonPositive => formatter.write_str("mode metadata value must be positive"),
349 Self::UnknownLabel => formatter.write_str("unknown mode metadata label"),
350 }
351 }
352}
353
354impl Error for ModeError {}
355
356#[allow(dead_code)]
357fn non_empty_text(value: impl AsRef<str>) -> Result<String, ModeError> {
358 let trimmed = value.as_ref().trim();
359 if trimmed.is_empty() {
360 Err(ModeError::Empty)
361 } else {
362 Ok(trimmed.to_string())
363 }
364}
365
366fn normalized_label(value: &str) -> Result<String, ModeError> {
367 let trimmed = value.trim();
368 if trimmed.is_empty() {
369 Err(ModeError::Empty)
370 } else {
371 Ok(trimmed.to_ascii_lowercase().replace(['_', ' '], "-"))
372 }
373}
374#[cfg(test)]
375#[allow(
376 unused_imports,
377 clippy::unnecessary_wraps,
378 clippy::assertions_on_constants
379)]
380mod tests {
381 use super::{
382 ChurchMode, ModalBrightness, ModeDegree, ModeError, ModeFamily, ModeKind, ModeName,
383 };
384 use core::{fmt, str::FromStr};
385
386 fn assert_enum_family<T>(variants: &[T]) -> Result<(), ModeError>
387 where
388 T: Copy + Eq + fmt::Debug + fmt::Display + FromStr<Err = ModeError>,
389 {
390 for variant in variants {
391 let label = variant.to_string();
392 assert_eq!(label.parse::<T>()?, *variant);
393 assert_eq!(label.replace('-', "_").parse::<T>()?, *variant);
394 assert_eq!(label.replace('-', " ").parse::<T>()?, *variant);
395 }
396 Ok(())
397 }
398
399 #[test]
400 fn validates_text_newtypes() -> Result<(), ModeError> {
401 let value = ModeName::new(" example-value ")?;
402 assert_eq!(value.as_str(), "example-value");
403 assert_eq!(value.value(), "example-value");
404 assert_eq!(value.to_string(), "example-value");
405 assert_eq!(
406 <ModeName as TryFrom<&str>>::try_from("example-value")?,
407 value
408 );
409 Ok(())
410 }
411
412 #[test]
413 fn validates_numeric_newtypes() -> Result<(), ModeError> {
414 let value = ModeDegree::new(1)?;
415 assert_eq!(value.value(), 1);
416 assert_eq!("1".parse::<ModeDegree>()?, value);
417 assert_eq!(ModeDegree::new(33), Err(ModeError::OutOfRange));
418 Ok(())
419 }
420
421 #[test]
422 fn displays_and_parses_enums() -> Result<(), ModeError> {
423 assert_enum_family(ModeKind::ALL)?;
424 assert_enum_family(ChurchMode::ALL)?;
425 assert_enum_family(ModeFamily::ALL)?;
426 assert_enum_family(ModalBrightness::ALL)?;
427 Ok(())
428 }
429}