chart-file-parser 0.4.6

Parser for .chart files
Documentation
use std::fmt::Display;

use nom::{bytes::complete::tag, combinator::map_res, multi::many1, sequence::preceded, IResult};

use crate::{
    components::{curlied, spaced},
    song_property::SongProperty,
};

#[derive(Debug, PartialEq, Eq)]
pub(crate) struct Song<'a> {
    resolution: u32,
    properties: Vec<SongProperty<'a>>,
}

impl<'a> Song<'a> {
    #[must_use]
    pub(crate) fn new(resolution: u32, properties: Vec<SongProperty<'a>>) -> Self {
        Self {
            resolution,
            properties,
        }
    }

    pub(crate) fn multiply(&mut self, factor: u32) {
        self.resolution *= factor;
    }

    #[inline]
    pub(crate) fn parse(input: &str) -> IResult<&str, Song> {
        map_res(
            preceded(
                spaced(tag("[Song]")),
                curlied(many1(spaced(SongProperty::parse))),
            ),
            Song::try_from,
        )(input)
    }
}

impl<'a> TryFrom<Vec<SongProperty<'a>>> for Song<'a> {
    type Error = &'static str;

    fn try_from(value: Vec<SongProperty<'a>>) -> Result<Self, Self::Error> {
        let resolution_entry = value
            .iter()
            .find(|x| x.name() == "Resolution")
            .ok_or("resolution not found")?;
        let resolution = resolution_entry
            .value()
            .parse::<u32>()
            .map_err(|_| "invalid resolution")?;
        let properties = value
            .into_iter()
            .filter(|x| x.name() != "Resolution")
            .collect();
        Ok(Self::new(resolution, properties))
    }
}

impl<'a> Display for Song<'a> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let resolution_string = self.resolution.to_string();
        write!(
            f,
            "[Song]\n{{\n{}{}}}",
            SongProperty::new("Resolution", &resolution_string),
            self.properties
                .iter()
                .map(SongProperty::to_string)
                .collect::<String>()
        )
    }
}

#[cfg(test)]
mod tests {
    #![allow(clippy::unwrap_used)]
    use super::*;

    #[test]
    fn test_song() {
        Song::parse(
            r#"[Song]
{
  Name = "Second Sight"
  Artist = "Adagio"
  Charter = "Peddy"
  Album = "Sanctus Ignis"
  Year = ", 2001"
  Offset = 0
  Resolution = 192
  Player2 = bass
  Difficulty = 0
  PreviewStart = 0
  PreviewEnd = 0
  Genre = "Neoclassical Metal"
  MediaType = "cd"
  MusicStream = "song.ogg"
}"#,
        )
        .unwrap();
    }
}