Skip to main content

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