Skip to main content

use_mode/
lib.rs

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}