1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
use std::{
borrow::Cow,
convert::TryFrom,
hash::{Hash, Hasher},
};
use miette::Diagnostic;
use thiserror::Error;
use crate::{body::Body, Fragment};
/// A [`Trailer`] you might see a in a [`CommitMessage`], for example
/// 'Co-authored-by: Billie Thompson <billie@example.com>'
#[derive(Debug, Clone, Eq, Ord, PartialOrd)]
pub struct Trailer<'a> {
key: Cow<'a, str>,
value: Cow<'a, str>,
}
impl<'a> Trailer<'a> {
/// Create a new [`Trailer`]
///
/// This creates a new element that represents the sort of [`Trailers`] you
/// get at the end of commits
///
/// For example there's `Co-authored-by`, `Relates-to`, and `Signed-off-by`
///
/// # Example
///
/// ```
/// use std::convert::TryFrom;
///
/// use mit_commit::{Body, Trailer};
/// assert_eq!(
/// Trailer::new("Co-authored-by".into(), "#124".into()),
/// Trailer::try_from(Body::from("Co-authored-by: #124"))
/// .expect("There should have been a trailer in that body component")
/// )
/// ```
#[must_use]
pub const fn new(key: Cow<'a, str>, value: Cow<'a, str>) -> Trailer<'a> {
Self { key, value }
}
/// Get the key of the [`Trailer`]
///
/// # Example
///
/// ```
/// use std::convert::TryFrom;
///
/// use mit_commit::{Body, Trailer};
/// assert_eq!(
/// Trailer::new("Co-authored-by".into(), "#124".into()).get_key(),
/// "Co-authored-by"
/// )
/// ```
#[must_use]
pub fn get_key(&self) -> String {
format!("{}", self.key)
}
/// Get the value of the [`Trailer`]
///
/// # Example
///
/// ```
/// use std::convert::TryFrom;
///
/// use mit_commit::{Body, Trailer};
/// assert_eq!(
/// Trailer::new("Co-authored-by".into(), "#124".into()).get_value(),
/// "#124"
/// )
/// ```
#[must_use]
pub fn get_value(&self) -> String {
self.value.to_string()
}
}
impl<'a> PartialEq for Trailer<'a> {
fn eq(&self, other: &Self) -> bool {
self.key == other.key && self.value.trim_end() == other.value.trim_end()
}
}
impl<'a> Hash for Trailer<'a> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.key.hash(state);
self.value.trim_end().hash(state);
}
}
impl<'a> From<Trailer<'a>> for String {
fn from(trailer: Trailer<'_>) -> Self {
format!("{}: {}", trailer.key, trailer.value)
}
}
impl<'a> From<Trailer<'a>> for Fragment<'a> {
fn from(trailer: Trailer<'_>) -> Self {
let trailer: String = trailer.into();
Body::from(trailer).into()
}
}
impl<'a> TryFrom<Body<'a>> for Trailer<'a> {
type Error = Error;
fn try_from(body: Body<'a>) -> Result<Trailer<'a>, Self::Error> {
let content: String = body.clone().into();
let mut value_and_key = content.split(": ").map(ToString::to_string);
let key: String = value_and_key
.next()
.ok_or_else(|| Error::new_not_a_trailer(&body))?;
let value: String = value_and_key
.next()
.ok_or_else(|| Error::new_not_a_trailer(&body))?;
Ok(Trailer::new(key.into(), value.into()))
}
}
/// Errors in parsing potential trailers
#[derive(Error, Debug, Diagnostic)]
pub enum Error {
/// When the given fragment is not a trailer
#[error("not a trailer")]
#[diagnostic(url(docsrs), code(mit_commit::trailer::error::not_atrailer))]
NotATrailer(
#[source_code] String,
#[label("no colon in body line")] (usize, usize),
),
}
impl Error {
fn new_not_a_trailer(body: &Body<'_>) -> Self {
let text: String = body.clone().into();
Self::NotATrailer(text.clone(), (0, text.len()))
}
}