Skip to main content

use_articulation/
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        ArticulationError, ArticulationKind, OrnamentKind, PerformanceTechnique, PhraseMarkKind,
10        SlurKind, TieKind,
11    };
12}
13#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
14pub enum ArticulationKind {
15    Staccato,
16    Staccatissimo,
17    Tenuto,
18    Accent,
19    Marcato,
20    Legato,
21    Fermata,
22    BreathMark,
23    Caesura,
24    Custom,
25}
26
27impl ArticulationKind {
28    pub const ALL: &'static [Self] = &[
29        Self::Staccato,
30        Self::Staccatissimo,
31        Self::Tenuto,
32        Self::Accent,
33        Self::Marcato,
34        Self::Legato,
35        Self::Fermata,
36        Self::BreathMark,
37        Self::Caesura,
38        Self::Custom,
39    ];
40
41    pub const fn as_str(self) -> &'static str {
42        match self {
43            Self::Staccato => "staccato",
44            Self::Staccatissimo => "staccatissimo",
45            Self::Tenuto => "tenuto",
46            Self::Accent => "accent",
47            Self::Marcato => "marcato",
48            Self::Legato => "legato",
49            Self::Fermata => "fermata",
50            Self::BreathMark => "breath-mark",
51            Self::Caesura => "caesura",
52            Self::Custom => "custom",
53        }
54    }
55}
56
57impl fmt::Display for ArticulationKind {
58    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
59        formatter.write_str(self.as_str())
60    }
61}
62
63impl FromStr for ArticulationKind {
64    type Err = ArticulationError;
65
66    fn from_str(value: &str) -> Result<Self, Self::Err> {
67        match normalized_label(value)?.as_str() {
68            "staccato" => Ok(Self::Staccato),
69            "staccatissimo" => Ok(Self::Staccatissimo),
70            "tenuto" => Ok(Self::Tenuto),
71            "accent" => Ok(Self::Accent),
72            "marcato" => Ok(Self::Marcato),
73            "legato" => Ok(Self::Legato),
74            "fermata" => Ok(Self::Fermata),
75            "breath-mark" => Ok(Self::BreathMark),
76            "caesura" => Ok(Self::Caesura),
77            "custom" => Ok(Self::Custom),
78            _ => Err(ArticulationError::UnknownLabel),
79        }
80    }
81}
82#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
83pub enum OrnamentKind {
84    Trill,
85    Mordent,
86    Turn,
87    Appoggiatura,
88    Acciaccatura,
89    GraceNote,
90    Custom,
91}
92
93impl OrnamentKind {
94    pub const ALL: &'static [Self] = &[
95        Self::Trill,
96        Self::Mordent,
97        Self::Turn,
98        Self::Appoggiatura,
99        Self::Acciaccatura,
100        Self::GraceNote,
101        Self::Custom,
102    ];
103
104    pub const fn as_str(self) -> &'static str {
105        match self {
106            Self::Trill => "trill",
107            Self::Mordent => "mordent",
108            Self::Turn => "turn",
109            Self::Appoggiatura => "appoggiatura",
110            Self::Acciaccatura => "acciaccatura",
111            Self::GraceNote => "grace-note",
112            Self::Custom => "custom",
113        }
114    }
115}
116
117impl fmt::Display for OrnamentKind {
118    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
119        formatter.write_str(self.as_str())
120    }
121}
122
123impl FromStr for OrnamentKind {
124    type Err = ArticulationError;
125
126    fn from_str(value: &str) -> Result<Self, Self::Err> {
127        match normalized_label(value)?.as_str() {
128            "trill" => Ok(Self::Trill),
129            "mordent" => Ok(Self::Mordent),
130            "turn" => Ok(Self::Turn),
131            "appoggiatura" => Ok(Self::Appoggiatura),
132            "acciaccatura" => Ok(Self::Acciaccatura),
133            "grace-note" => Ok(Self::GraceNote),
134            "custom" => Ok(Self::Custom),
135            _ => Err(ArticulationError::UnknownLabel),
136        }
137    }
138}
139#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
140pub enum PhraseMarkKind {
141    Slur,
142    Tie,
143    Phrase,
144    Breath,
145    Caesura,
146}
147
148impl PhraseMarkKind {
149    pub const ALL: &'static [Self] = &[
150        Self::Slur,
151        Self::Tie,
152        Self::Phrase,
153        Self::Breath,
154        Self::Caesura,
155    ];
156
157    pub const fn as_str(self) -> &'static str {
158        match self {
159            Self::Slur => "slur",
160            Self::Tie => "tie",
161            Self::Phrase => "phrase",
162            Self::Breath => "breath",
163            Self::Caesura => "caesura",
164        }
165    }
166}
167
168impl fmt::Display for PhraseMarkKind {
169    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
170        formatter.write_str(self.as_str())
171    }
172}
173
174impl FromStr for PhraseMarkKind {
175    type Err = ArticulationError;
176
177    fn from_str(value: &str) -> Result<Self, Self::Err> {
178        match normalized_label(value)?.as_str() {
179            "slur" => Ok(Self::Slur),
180            "tie" => Ok(Self::Tie),
181            "phrase" => Ok(Self::Phrase),
182            "breath" => Ok(Self::Breath),
183            "caesura" => Ok(Self::Caesura),
184            _ => Err(ArticulationError::UnknownLabel),
185        }
186    }
187}
188#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
189pub enum SlurKind {
190    Start,
191    Continue,
192    Stop,
193    Unknown,
194}
195
196impl SlurKind {
197    pub const ALL: &'static [Self] = &[Self::Start, Self::Continue, Self::Stop, Self::Unknown];
198
199    pub const fn as_str(self) -> &'static str {
200        match self {
201            Self::Start => "start",
202            Self::Continue => "continue",
203            Self::Stop => "stop",
204            Self::Unknown => "unknown",
205        }
206    }
207}
208
209impl fmt::Display for SlurKind {
210    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
211        formatter.write_str(self.as_str())
212    }
213}
214
215impl FromStr for SlurKind {
216    type Err = ArticulationError;
217
218    fn from_str(value: &str) -> Result<Self, Self::Err> {
219        match normalized_label(value)?.as_str() {
220            "start" => Ok(Self::Start),
221            "continue" => Ok(Self::Continue),
222            "stop" => Ok(Self::Stop),
223            "unknown" => Ok(Self::Unknown),
224            _ => Err(ArticulationError::UnknownLabel),
225        }
226    }
227}
228#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
229pub enum TieKind {
230    Start,
231    Continue,
232    Stop,
233    Unknown,
234}
235
236impl TieKind {
237    pub const ALL: &'static [Self] = &[Self::Start, Self::Continue, Self::Stop, Self::Unknown];
238
239    pub const fn as_str(self) -> &'static str {
240        match self {
241            Self::Start => "start",
242            Self::Continue => "continue",
243            Self::Stop => "stop",
244            Self::Unknown => "unknown",
245        }
246    }
247}
248
249impl fmt::Display for TieKind {
250    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
251        formatter.write_str(self.as_str())
252    }
253}
254
255impl FromStr for TieKind {
256    type Err = ArticulationError;
257
258    fn from_str(value: &str) -> Result<Self, Self::Err> {
259        match normalized_label(value)?.as_str() {
260            "start" => Ok(Self::Start),
261            "continue" => Ok(Self::Continue),
262            "stop" => Ok(Self::Stop),
263            "unknown" => Ok(Self::Unknown),
264            _ => Err(ArticulationError::UnknownLabel),
265        }
266    }
267}
268#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
269pub enum PerformanceTechnique {
270    Pizzicato,
271    Arco,
272    Tremolo,
273    Glissando,
274    Harmonic,
275    PalmMute,
276    Bend,
277    Vibrato,
278    Custom,
279}
280
281impl PerformanceTechnique {
282    pub const ALL: &'static [Self] = &[
283        Self::Pizzicato,
284        Self::Arco,
285        Self::Tremolo,
286        Self::Glissando,
287        Self::Harmonic,
288        Self::PalmMute,
289        Self::Bend,
290        Self::Vibrato,
291        Self::Custom,
292    ];
293
294    pub const fn as_str(self) -> &'static str {
295        match self {
296            Self::Pizzicato => "pizzicato",
297            Self::Arco => "arco",
298            Self::Tremolo => "tremolo",
299            Self::Glissando => "glissando",
300            Self::Harmonic => "harmonic",
301            Self::PalmMute => "palm-mute",
302            Self::Bend => "bend",
303            Self::Vibrato => "vibrato",
304            Self::Custom => "custom",
305        }
306    }
307}
308
309impl fmt::Display for PerformanceTechnique {
310    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
311        formatter.write_str(self.as_str())
312    }
313}
314
315impl FromStr for PerformanceTechnique {
316    type Err = ArticulationError;
317
318    fn from_str(value: &str) -> Result<Self, Self::Err> {
319        match normalized_label(value)?.as_str() {
320            "pizzicato" => Ok(Self::Pizzicato),
321            "arco" => Ok(Self::Arco),
322            "tremolo" => Ok(Self::Tremolo),
323            "glissando" => Ok(Self::Glissando),
324            "harmonic" => Ok(Self::Harmonic),
325            "palm-mute" => Ok(Self::PalmMute),
326            "bend" => Ok(Self::Bend),
327            "vibrato" => Ok(Self::Vibrato),
328            "custom" => Ok(Self::Custom),
329            _ => Err(ArticulationError::UnknownLabel),
330        }
331    }
332}
333
334#[derive(Clone, Copy, Debug, Eq, PartialEq)]
335pub enum ArticulationError {
336    Empty,
337    InvalidFormat,
338    OutOfRange,
339    NonFinite,
340    NonPositive,
341    UnknownLabel,
342}
343
344impl fmt::Display for ArticulationError {
345    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
346        match self {
347            Self::Empty => formatter.write_str("articulation metadata text cannot be empty"),
348            Self::InvalidFormat => {
349                formatter.write_str("articulation metadata has an invalid format")
350            },
351            Self::OutOfRange => formatter.write_str("articulation metadata value is out of range"),
352            Self::NonFinite => formatter.write_str("articulation metadata value must be finite"),
353            Self::NonPositive => {
354                formatter.write_str("articulation metadata value must be positive")
355            },
356            Self::UnknownLabel => formatter.write_str("unknown articulation metadata label"),
357        }
358    }
359}
360
361impl Error for ArticulationError {}
362
363#[allow(dead_code)]
364fn non_empty_text(value: impl AsRef<str>) -> Result<String, ArticulationError> {
365    let trimmed = value.as_ref().trim();
366    if trimmed.is_empty() {
367        Err(ArticulationError::Empty)
368    } else {
369        Ok(trimmed.to_string())
370    }
371}
372
373fn normalized_label(value: &str) -> Result<String, ArticulationError> {
374    let trimmed = value.trim();
375    if trimmed.is_empty() {
376        Err(ArticulationError::Empty)
377    } else {
378        Ok(trimmed.to_ascii_lowercase().replace(['_', ' '], "-"))
379    }
380}
381#[cfg(test)]
382#[allow(
383    unused_imports,
384    clippy::unnecessary_wraps,
385    clippy::assertions_on_constants
386)]
387mod tests {
388    use super::{
389        ArticulationError, ArticulationKind, OrnamentKind, PerformanceTechnique, PhraseMarkKind,
390        SlurKind, TieKind,
391    };
392    use core::{fmt, str::FromStr};
393
394    fn assert_enum_family<T>(variants: &[T]) -> Result<(), ArticulationError>
395    where
396        T: Copy + Eq + fmt::Debug + fmt::Display + FromStr<Err = ArticulationError>,
397    {
398        for variant in variants {
399            let label = variant.to_string();
400            assert_eq!(label.parse::<T>()?, *variant);
401            assert_eq!(label.replace('-', "_").parse::<T>()?, *variant);
402            assert_eq!(label.replace('-', " ").parse::<T>()?, *variant);
403        }
404        Ok(())
405    }
406
407    #[test]
408    fn validates_text_newtypes() -> Result<(), ArticulationError> {
409        assert!(true);
410        Ok(())
411    }
412
413    #[test]
414    fn validates_numeric_newtypes() -> Result<(), ArticulationError> {
415        assert!(true);
416        Ok(())
417    }
418
419    #[test]
420    fn displays_and_parses_enums() -> Result<(), ArticulationError> {
421        assert_enum_family(ArticulationKind::ALL)?;
422        assert_enum_family(OrnamentKind::ALL)?;
423        assert_enum_family(PhraseMarkKind::ALL)?;
424        assert_enum_family(SlurKind::ALL)?;
425        assert_enum_family(TieKind::ALL)?;
426        assert_enum_family(PerformanceTechnique::ALL)?;
427        Ok(())
428    }
429}