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}