git-disjoint 0.13.1

A tool to batch commits by issue into GitHub PRs
Documentation
use std::{error::Error, fmt::Display, str::FromStr};

use crate::pull_request_message::IGNORE_MARKER;

#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub struct PullRequestMetadata {
    pub title: String,
    pub body: String,
}

#[derive(Debug)]
#[non_exhaustive]
pub struct FromStrError {
    kind: FromStrErrorKind,
}

impl Display for FromStrError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match &self.kind {
            FromStrErrorKind::EmptyPullRequestMessage => {
                write!(f, "pull request metadata is empty")
            }
        }
    }
}

impl Error for FromStrError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match &self.kind {
            FromStrErrorKind::EmptyPullRequestMessage => None,
        }
    }
}

#[derive(Debug)]
pub enum FromStrErrorKind {
    #[non_exhaustive]
    EmptyPullRequestMessage,
}

impl From<FromStrErrorKind> for FromStrError {
    fn from(kind: FromStrErrorKind) -> Self {
        Self { kind }
    }
}

impl FromStr for PullRequestMetadata {
    type Err = FromStrError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        if s.is_empty() {
            return Err(FromStrErrorKind::EmptyPullRequestMessage)?;
        }

        let mut iterator = s.lines();
        let title = iterator.next().unwrap_or_default().trim().to_owned();
        let body = iterator
            .take_while(|line| line != &IGNORE_MARKER)
            .collect::<Vec<_>>()
            .join("\n")
            .trim()
            .to_owned();

        Ok(Self { title, body })
    }
}

#[cfg(test)]
mod test {
    use super::PullRequestMetadata;
    use crate::pull_request_message::IGNORE_MARKER;

    #[test]
    fn parse_empty_string_returns_error() {
        let result = "".parse::<PullRequestMetadata>();
        assert!(result.is_err());
    }

    #[test]
    fn parse_title_only() {
        let result = "My PR title".parse::<PullRequestMetadata>().unwrap();
        assert_eq!(result.title, "My PR title");
        assert_eq!(result.body, "");
    }

    #[test]
    fn parse_title_with_body() {
        let result = "Title\n\nBody paragraph"
            .parse::<PullRequestMetadata>()
            .unwrap();
        assert_eq!(result.title, "Title");
        assert_eq!(result.body, "Body paragraph");
    }

    #[test]
    fn parse_strips_content_after_ignore_marker() {
        let input = format!("Title\n\nBody\n{IGNORE_MARKER}\nshould be ignored");
        let result = input.parse::<PullRequestMetadata>().unwrap();
        assert_eq!(result.title, "Title");
        assert_eq!(result.body, "Body");
    }

    #[test]
    fn parse_trims_whitespace() {
        let result = "  Title with spaces  \n\n  Body with spaces  "
            .parse::<PullRequestMetadata>()
            .unwrap();
        assert_eq!(result.title, "Title with spaces");
        assert_eq!(result.body, "Body with spaces");
    }
}