dobby 0.1.5

A command line tool for automating common development tasks
use markdown::{generate_markdown, tokenize, Block, ListItem, Span};

#[derive(Debug)]
pub(crate) struct Changelog {
    header: Vec<Block>,
    rest: Vec<Block>,
}

impl Changelog {
    pub(crate) fn into_markdown(self) -> String {
        let Self { header, rest } = self;
        let blocks = header.into_iter().chain(rest.into_iter()).collect();
        generate_markdown(blocks)
    }

    pub(crate) fn from_markdown(text: &str) -> Self {
        let blocks = tokenize(text);
        let (header, rest) = parse_header(blocks);

        Self { header, rest }
    }

    pub(crate) fn add_version(self, version: Version) -> Self {
        let Self { header, rest } = self;
        Self {
            header,
            rest: version
                .into_markdown_blocks()
                .into_iter()
                .chain(rest.into_iter())
                .collect(),
        }
    }
}

fn parse_header(mut blocks: Vec<Block>) -> (Vec<Block>, Vec<Block>) {
    let end_index = blocks.iter().enumerate().find_map(|(idx, block)| {
        if matches!(block, Block::Header(_, 2)) {
            Some(idx)
        } else {
            None
        }
    });
    match end_index {
        Some(index) => {
            let rest = blocks.split_off(index);
            (blocks, rest)
        }
        None => (blocks, Vec::new()),
    }
}

#[derive(Clone)]
pub(crate) struct Version {
    pub(crate) title: String,
    pub(crate) fixes: Vec<String>,
    pub(crate) features: Vec<String>,
    pub(crate) breaking_changes: Vec<String>,
}

impl Version {
    fn into_markdown_blocks(self) -> Vec<Block> {
        let headers_size = 4;
        let Self {
            title,
            fixes,
            features,
            breaking_changes,
        } = self;
        let mut blocks = Vec::with_capacity(
            fixes.len() + features.len() + breaking_changes.len() + headers_size,
        );

        blocks.push(header_block(title, 2));
        if !breaking_changes.is_empty() {
            blocks.push(header_block("Breaking Changes".to_string(), 3));
            blocks.push(unordered_list(breaking_changes));
        }
        if !features.is_empty() {
            blocks.push(header_block("Features".to_string(), 3));
            blocks.push(unordered_list(features));
        }
        if !fixes.is_empty() {
            blocks.push(header_block("Fixes".to_string(), 3));
            blocks.push(unordered_list(fixes));
        }
        blocks
    }
}

fn header_block(text: String, level: usize) -> Block {
    Block::Header(vec![Span::Text(text)], level)
}

fn unordered_list(items: Vec<String>) -> Block {
    Block::UnorderedList(
        items
            .into_iter()
            .map(|note| ListItem::Simple(vec![Span::Text(note)]))
            .collect(),
    )
}

#[cfg(test)]
mod tests {
    use markdown::{generate_markdown, Block, ListItem, Span};

    #[test]
    fn changelog_from_markdown() {
        let markdown = r##"
# Changelog
Some details about the keepachangelog format

Sometimes a second paragraph

## 0.1.0 - 2020-12-25
### Features
- Initial version

[link]: some footer details
"##;
        let changelog = super::Changelog::from_markdown(markdown);
        println!("{:#?}", changelog);
        assert_eq!(changelog.header.len(), 3);
        assert_eq!(changelog.rest.len(), 4);
    }

    #[test]
    fn changelog_into_markdown() {
        let expected = r##"# Changelog

Some details about the keepachangelog format

## 0.1.0 - 2020-12-25

### Features

* Initial version

[link]: some footer details"##;
        let changelog = super::Changelog {
            header: vec![
                Block::Header(vec![Span::Text("Changelog".to_string())], 1),
                Block::Paragraph(vec![Span::Text(
                    "Some details about the keepachangelog format".to_string(),
                )]),
            ],
            rest: vec![
                Block::Header(vec![Span::Text("0.1.0 - 2020-12-25".to_string())], 2),
                Block::Header(vec![Span::Text("Features".to_string())], 3),
                Block::UnorderedList(vec![ListItem::Simple(vec![Span::Text(
                    "Initial version".to_string(),
                )])]),
                Block::Paragraph(vec![Span::Text("[link]: some footer details".to_string())]),
            ],
        };
        assert_eq!(changelog.into_markdown(), expected);
    }

    #[test]
    fn changelog_add_version() {
        let markdown = r##"
# Changelog
Some details about the keepachangelog format

Sometimes a second paragraph

## 0.1.0 - 2020-12-25
### Features
- Initial version

[link]: some footer details
"##;
        let changelog = super::Changelog::from_markdown(markdown);
        let version = super::Version {
            title: "0.2.0 - 2020-12-31".to_string(),
            fixes: vec!["Fixed something".to_string()],
            features: vec![],
            breaking_changes: vec![],
        };
        let changelog = changelog.add_version(version.clone());
        assert_eq!(changelog.rest.len(), 7);
        assert_eq!(changelog.rest[0], version.into_markdown_blocks()[0])
    }

    #[test]
    fn version_into_blocks() {
        let version = super::Version {
            title: "0.2.0 - 2020-12-31".to_string(),
            fixes: vec![
                "Fixed something".to_string(),
                "Fixed something else".to_string(),
            ],
            features: vec![
                "Added something".to_string(),
                "Added something else".to_string(),
            ],
            breaking_changes: vec![
                "Broke something".to_string(),
                "Broke something else".to_string(),
            ],
        };
        let expected = r##"## 0.2.0 - 2020-12-31

### Breaking Changes

* Broke something
* Broke something else

### Features

* Added something
* Added something else

### Fixes

* Fixed something
* Fixed something else"##;

        let blocks = version.into_markdown_blocks();
        assert_eq!(generate_markdown(blocks), expected);
    }
}