ireal-parser 0.1.0

iReal Pro song parser and manipulation library
Documentation
use regex::Regex;
use std::{fmt, str::FromStr};

use crate::{Error, Progression, Result};

/// Represents a song with an iReal Pro progression.
///
/// Includes information such as the title, composer, style, key signature, and
/// chord progression.
#[derive(Debug, PartialEq, Clone)]
pub struct Song {
    /// Song Title (If starting with 'The' change the title to 'Song Title, The'
    /// for sorting purposes)
    pub title: String,
    /// Composer's LastName FirstName (we put the last name first for sorting
    /// purposes within the app)
    pub composer: String,
    /// Style (A short text description of the style used for sorting in the
    /// app. Medium Swing, Ballad, Pop, Rock...)
    pub style: String,
    /// Key Signature (C, Db, D, Eb, E, F, Gb, G, Ab, A, Bb, B, A-, Bb-, B-, C-,
    /// C#-, D-, Eb-, E-, F-, F#-, G-, G#-)
    pub key_signature: String,
    /// Chord Progression (This is the main part)
    pub progression: Progression,
}

impl fmt::Display for Song {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "irealbook://{}={}={}={}=n={}",
            self.title, self.composer, self.style, self.key_signature, self.progression
        )
    }
}

impl FromStr for Song {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self> {
        parse_irealbook_url(s)
    }
}

/// Parses an iReal Pro URL and returns a [`Song`].
///
/// # Arguments
///
/// * `url` - A string representing an iReal Pro URL.
///
/// # Errors
///
/// Returns an error if the provided URL is invalid or if required fields are missing.
pub fn parse_irealbook_url(url: &str) -> Result<Song> {
    let re = Regex::new(r"irealbook://(.*?)=(.*?)=(.*?)=(.*?)=(.*?)=(.*)").unwrap();
    let Some(captures) = re.captures(url) else {
        return Err(Error::InvalidUrl);
    };

    let title = captures
        .get(1)
        .ok_or(Error::MissingField("Title"))?
        .as_str()
        .to_owned();
    let composer = captures
        .get(2)
        .ok_or(Error::MissingField("Composer"))?
        .as_str()
        .to_owned();
    let style = captures
        .get(3)
        .ok_or(Error::MissingField("Style"))?
        .as_str()
        .to_owned();
    let key_signature = captures
        .get(4)
        .ok_or(Error::MissingField("Key signature"))?
        .as_str()
        .to_owned();
    let progression = captures
        .get(6)
        .ok_or(Error::MissingField("Chord progression"))?
        .as_str()
        .parse()?;

    Ok(Song {
        title,
        composer,
        style,
        key_signature,
        progression,
    })
}