use std::fmt;
use std::ops::Deref;
use std::str::FromStr;
use nom::error::VerboseError;
use crate::parser::parse;
use crate::{Error, ErrorKind};
const BREAKING_PHRASE: &str = "BREAKING CHANGE";
const BREAKING_ARROW: &str = "BREAKING-CHANGE";
#[derive(Clone, Debug)]
pub struct Commit<'a> {
ty: Type<'a>,
scope: Option<Scope<'a>>,
description: &'a str,
body: Option<&'a str>,
breaking: bool,
footers: Vec<Footer<'a>>,
}
impl<'a> Commit<'a> {
pub fn parse(string: &'a str) -> Result<Self, Error> {
let (ty, scope, breaking, description, body, footers) =
parse::<VerboseError<&'a str>>(string).map_err(|err| Error::with_nom(string, err))?;
let breaking = breaking.is_some()
|| footers
.iter()
.any(|(k, _, _)| k == &BREAKING_PHRASE || k == &BREAKING_ARROW);
let footers: Result<Vec<_>, Error> = footers
.into_iter()
.map(|(k, s, v)| Ok(Footer::new(FooterToken::new_unchecked(k), s.parse()?, v)))
.collect();
let footers = footers?;
Ok(Self {
ty: Type::new_unchecked(ty),
scope: scope.map(Scope::new_unchecked),
description: description,
body: body,
breaking,
footers,
})
}
pub fn type_(&self) -> Type<'a> {
self.ty
}
pub fn scope(&self) -> Option<Scope<'a>> {
self.scope
}
pub fn description(&self) -> &'a str {
self.description
}
pub fn body(&self) -> Option<&'a str> {
self.body
}
pub fn breaking(&self) -> bool {
self.breaking
}
pub fn footers(&self) -> &[Footer<'_>] {
&self.footers
}
}
impl fmt::Display for Commit<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.type_().as_str())?;
if let Some(scope) = &self.scope() {
f.write_fmt(format_args!("({})", scope))?;
}
f.write_fmt(format_args!(": {}", &self.description()))?;
if let Some(body) = &self.body() {
f.write_fmt(format_args!("\n\n{}", body))?;
}
for t in self.footers() {
write!(f, "\n\n{}{}{}", t.token(), t.separator(), t.value())?;
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub struct Footer<'a> {
token: FooterToken<'a>,
sep: FooterSeparator,
value: &'a str,
}
impl<'a> Footer<'a> {
pub const fn new(token: FooterToken<'a>, sep: FooterSeparator, value: &'a str) -> Self {
Self { token, sep, value }
}
pub const fn token(&self) -> FooterToken<'a> {
self.token
}
pub const fn separator(&self) -> FooterSeparator {
self.sep
}
pub const fn value(&self) -> &'a str {
self.value
}
pub fn breaking(&self) -> bool {
self.token.breaking()
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub enum FooterSeparator {
ColonSpace,
SpacePound,
#[doc(hidden)]
__NonExhaustive,
}
impl FooterSeparator {
pub fn as_str(self) -> &'static str {
match self {
FooterSeparator::ColonSpace => ": ",
FooterSeparator::SpacePound => " #",
FooterSeparator::__NonExhaustive => unreachable!(),
}
}
}
impl Deref for FooterSeparator {
type Target = str;
fn deref(&self) -> &Self::Target {
self.as_str()
}
}
impl PartialEq<&'_ str> for FooterSeparator {
fn eq(&self, other: &&str) -> bool {
self.as_str() == *other
}
}
impl fmt::Display for FooterSeparator {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self)
}
}
impl FromStr for FooterSeparator {
type Err = Error;
fn from_str(sep: &str) -> Result<Self, Self::Err> {
match sep {
": " => Ok(FooterSeparator::ColonSpace),
" #" => Ok(FooterSeparator::SpacePound),
_ => {
Err(Error::new(ErrorKind::InvalidFooter)
.set_context(Box::new(format!("{:?}", sep))))
}
}
}
}
macro_rules! unicase_components {
($($ty:ident),+) => (
$(
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct $ty<'a>(unicase::UniCase<&'a str>);
impl<'a> $ty<'a> {
pub const fn new_unchecked(value: &'a str) -> Self {
$ty(unicase::UniCase::unicode(value))
}
pub fn as_str(&self) -> &'a str {
&self.0.into_inner()
}
}
impl Deref for $ty<'_> {
type Target = str;
fn deref(&self) -> &Self::Target {
self.as_str()
}
}
impl PartialEq<&'_ str> for $ty<'_> {
fn eq(&self, other: &&str) -> bool {
*self == $ty::new_unchecked(*other)
}
}
impl fmt::Display for $ty<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
)+
)
}
unicase_components![Type, Scope, FooterToken];
impl<'a> Type<'a> {
pub fn parse(sep: &'a str) -> Result<Self, Error> {
let (i, t) = crate::parser::type_(sep).map_err(|err| Error::with_nom(sep, err))?;
if !i.is_empty() {
return Err(Error::new(ErrorKind::InvalidFormat));
}
Ok(Type::new_unchecked(t))
}
}
impl<'a> Scope<'a> {
pub fn parse(sep: &'a str) -> Result<Self, Error> {
let (i, t) = crate::parser::scope(sep).map_err(|err| Error::with_nom(sep, err))?;
if !i.is_empty() {
return Err(Error::new(ErrorKind::InvalidScope));
}
Ok(Scope::new_unchecked(t))
}
}
impl<'a> FooterToken<'a> {
pub fn parse(sep: &'a str) -> Result<Self, Error> {
let (i, t) = crate::parser::footer_token(sep).map_err(|err| Error::with_nom(sep, err))?;
if !i.is_empty() {
return Err(Error::new(ErrorKind::InvalidScope));
}
Ok(FooterToken::new_unchecked(t))
}
pub fn breaking(&self) -> bool {
self == &BREAKING_PHRASE || self == &BREAKING_ARROW
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::ErrorKind;
use indoc::indoc;
#[test]
fn test_valid_simple_commit() {
let commit = Commit::parse("type(my scope): hello world").unwrap();
assert_eq!(commit.type_(), "type");
assert_eq!(commit.scope().unwrap(), "my scope");
assert_eq!(commit.description(), "hello world");
}
#[test]
fn test_breaking_change() {
let commit = Commit::parse("feat!: this is a breaking change").unwrap();
assert_eq!(crate::FEAT, commit.type_());
assert!(commit.breaking());
let commit = Commit::parse(indoc!(
"feat: message
BREAKING CHANGE: breaking change"
))
.unwrap();
assert_eq!(crate::FEAT, commit.type_());
assert_eq!(
"breaking change",
&*commit.footers().get(0).unwrap().value()
);
assert!(commit.breaking());
let commit = Commit::parse(indoc!(
"fix: message
BREAKING-CHANGE: it's broken"
))
.unwrap();
assert_eq!(crate::FIX, commit.type_());
assert_eq!("it's broken", &*commit.footers().get(0).unwrap().value());
assert!(commit.breaking());
}
#[test]
fn test_valid_complex_commit() {
let commit = indoc! {"
chore: improve changelog readability
Change date notation from YYYY-MM-DD to YYYY.MM.DD to make it a tiny bit
easier to parse while reading.
BREAKING CHANGE: Just kidding!
"};
let commit = Commit::parse(commit).unwrap();
assert_eq!(crate::CHORE, commit.type_());
assert_eq!(None, commit.scope());
assert_eq!("improve changelog readability", commit.description());
assert_eq!(
Some(indoc!(
"Change date notation from YYYY-MM-DD to YYYY.MM.DD to make it a tiny bit
easier to parse while reading."
)),
commit.body()
);
assert_eq!("Just kidding!", &*commit.footers().get(0).unwrap().value());
}
#[test]
fn test_missing_type() {
let err = Commit::parse("").unwrap_err();
assert_eq!(ErrorKind::MissingType, err.kind());
}
}