ukebox 0.9.1

A ukulele chord toolbox in Rust
Documentation
use std::slice::Iter;
use std::str::FromStr;

use crate::Chord;

#[derive(Debug, PartialEq, Eq)]
pub struct ChordSequence {
    chords: Vec<Chord>,
}

impl ChordSequence {
    pub fn chords(&self) -> Iter<'_, Chord> {
        self.chords.iter()
    }

    pub fn transpose(&self, semitones: i8) -> ChordSequence {
        let chords = self.chords().map(|c| c.transpose(semitones)).collect();
        Self { chords }
    }
}

impl FromStr for ChordSequence {
    type Err = &'static str;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let res: Result<Vec<_>, _> = s.split_whitespace().map(Chord::from_str).collect();

        if let Ok(chords) = res {
            return Ok(Self { chords });
        }

        Err("Could not parse chord sequence")
    }
}

#[cfg(test)]
mod tests {
    use rstest::rstest;

    use super::*;

    #[rstest(
        chord_seq,
        chords,
        case("", &[]),
        case("C", &["C"]),
        case("C F G", &["C", "F", "G"]),
        case("Dsus2 Am7 C#", &["Dsus2", "Am7", "C#"]),
    )]
    fn test_from_str(chord_seq: ChordSequence, chords: &[&str]) {
        let chords1: Vec<Chord> = chord_seq.chords().cloned().collect();
        let chords2: Vec<Chord> = chords.iter().map(|c| Chord::from_str(c).unwrap()).collect();
        assert_eq!(chords1, chords2);
    }

    #[rstest(chord_seq, case("Z"), case("A Z"))]
    fn test_from_str_fail(chord_seq: &str) {
        assert!(ChordSequence::from_str(chord_seq).is_err())
    }

    #[rstest(
        chord_seq1,
        semitones,
        chord_seq2,
        case("", 0, ""),
        case("C F G", 0, "C F G"),
        case("C F G", 1, "C# F# G#"),
        case("C F G", -1, "B E Gb"),
        case("C F G", 12, "C F G"),
    )]
    fn test_transpose(chord_seq1: ChordSequence, semitones: i8, chord_seq2: ChordSequence) {
        assert_eq!(chord_seq1.transpose(semitones), chord_seq2);
    }
}