ireal_parser/
song.rs

1use regex::Regex;
2use std::{fmt, str::FromStr};
3
4use crate::{Error, Progression, Result};
5
6/// Represents a song with an iReal Pro progression.
7///
8/// Includes information such as the title, composer, style, key signature, and
9/// chord progression.
10#[derive(Debug, PartialEq, Clone)]
11pub struct Song {
12    /// Song Title (If starting with 'The' change the title to 'Song Title, The'
13    /// for sorting purposes)
14    pub title: String,
15    /// Composer's LastName FirstName (we put the last name first for sorting
16    /// purposes within the app)
17    pub composer: String,
18    /// Style (A short text description of the style used for sorting in the
19    /// app. Medium Swing, Ballad, Pop, Rock...)
20    pub style: String,
21    /// Key Signature (C, Db, D, Eb, E, F, Gb, G, Ab, A, Bb, B, A-, Bb-, B-, C-,
22    /// C#-, D-, Eb-, E-, F-, F#-, G-, G#-)
23    pub key_signature: String,
24    /// Chord Progression (This is the main part)
25    pub progression: Progression,
26}
27
28impl fmt::Display for Song {
29    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30        write!(
31            f,
32            "irealbook://{}={}={}={}=n={}",
33            self.title, self.composer, self.style, self.key_signature, self.progression
34        )
35    }
36}
37
38impl FromStr for Song {
39    type Err = Error;
40
41    fn from_str(s: &str) -> Result<Self> {
42        parse_irealbook_url(s)
43    }
44}
45
46/// Parses an iReal Pro URL and returns a [`Song`].
47///
48/// # Arguments
49///
50/// * `url` - A string representing an iReal Pro URL.
51///
52/// # Errors
53///
54/// Returns an error if the provided URL is invalid or if required fields are missing.
55pub fn parse_irealbook_url(url: &str) -> Result<Song> {
56    let re = Regex::new(r"irealbook://(.*?)=(.*?)=(.*?)=(.*?)=(.*?)=(.*)").unwrap();
57    let Some(captures) = re.captures(url) else {
58        return Err(Error::InvalidUrl);
59    };
60
61    let title = captures
62        .get(1)
63        .ok_or(Error::MissingField("Title"))?
64        .as_str()
65        .to_owned();
66    let composer = captures
67        .get(2)
68        .ok_or(Error::MissingField("Composer"))?
69        .as_str()
70        .to_owned();
71    let style = captures
72        .get(3)
73        .ok_or(Error::MissingField("Style"))?
74        .as_str()
75        .to_owned();
76    let key_signature = captures
77        .get(4)
78        .ok_or(Error::MissingField("Key signature"))?
79        .as_str()
80        .to_owned();
81    let progression = captures
82        .get(6)
83        .ok_or(Error::MissingField("Chord progression"))?
84        .as_str()
85        .parse()?;
86
87    Ok(Song {
88        title,
89        composer,
90        style,
91        key_signature,
92        progression,
93    })
94}