1use std::{
2 borrow::Cow,
3 convert::TryFrom,
4 hash::{Hash, Hasher},
5};
6
7use miette::Diagnostic;
8use thiserror::Error;
9
10use crate::{Fragment, body::Body};
11
12#[derive(Debug, Clone, Eq, Ord, PartialOrd)]
15pub struct Trailer<'a> {
16 key: Cow<'a, str>,
17 value: Cow<'a, str>,
18}
19
20impl<'a> Trailer<'a> {
21 #[must_use]
41 pub const fn new(key: Cow<'a, str>, value: Cow<'a, str>) -> Self {
42 Self { key, value }
43 }
44
45 #[must_use]
59 pub fn get_key(&self) -> String {
60 format!("{}", self.key)
61 }
62
63 #[must_use]
77 pub fn get_value(&self) -> String {
78 self.value.to_string()
79 }
80}
81
82impl PartialEq for Trailer<'_> {
83 fn eq(&self, other: &Self) -> bool {
84 self.key == other.key && self.value.trim_end() == other.value.trim_end()
85 }
86}
87
88impl Hash for Trailer<'_> {
89 fn hash<H: Hasher>(&self, state: &mut H) {
90 self.key.hash(state);
91 self.value.trim_end().hash(state);
92 }
93}
94
95impl From<Trailer<'_>> for String {
96 fn from(trailer: Trailer<'_>) -> Self {
97 format!("{}: {}", trailer.key, trailer.value)
98 }
99}
100
101impl<'a> From<Trailer<'a>> for Fragment<'a> {
102 fn from(trailer: Trailer<'_>) -> Self {
103 let trailer: String = trailer.into();
104 Body::from(trailer).into()
105 }
106}
107
108impl<'a> TryFrom<Body<'a>> for Trailer<'a> {
109 type Error = Error;
110
111 fn try_from(body: Body<'a>) -> Result<Self, Self::Error> {
112 let content: String = body.clone().into();
113 let mut value_and_key = content.split(": ").map(ToString::to_string);
114
115 let key: String = value_and_key
116 .next()
117 .ok_or_else(|| Error::new_not_a_trailer(&body))?;
118
119 let value: String = value_and_key
120 .next()
121 .ok_or_else(|| Error::new_not_a_trailer(&body))?;
122
123 Ok(Trailer::new(key.into(), value.into()))
124 }
125}
126
127#[derive(Error, Debug, Diagnostic)]
129pub enum Error {
130 #[error("not a trailer")]
132 #[diagnostic(url(docsrs), code(mit_commit::trailer::error::not_atrailer))]
133 NotATrailer(
134 #[source_code] String,
135 #[label("no colon in body line")] (usize, usize),
136 ),
137}
138
139impl Error {
140 fn new_not_a_trailer(body: &Body<'_>) -> Self {
141 let text: String = body.clone().into();
142 Self::NotATrailer(text.clone(), (0, text.len()))
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use std::{
149 collections::hash_map::DefaultHasher,
150 convert::TryFrom,
151 hash::{Hash, Hasher},
152 };
153
154 use super::Trailer;
155 use crate::{Fragment, body::Body};
156
157 #[test]
158 fn it_can_tell_me_its_key() {
159 let trailer = Trailer::new("Relates-to".into(), "#128".into());
160
161 assert_eq!(trailer.get_key(), String::from("Relates-to"));
162 }
163
164 #[test]
165 fn it_can_tell_me_its_value() {
166 let trailer = Trailer::new("Relates-to".into(), "#128".into());
167
168 assert_eq!(trailer.get_value(), String::from("#128"));
169 }
170
171 #[test]
172 fn it_does_not_take_trailing_whitespace_into_account_in_equality_checks() {
173 let a = Trailer::new("Relates-to".into(), "#128\n".into());
174 let b = Trailer::new("Relates-to".into(), "#128".into());
175
176 assert_eq!(a, b);
177 }
178
179 #[test]
180 fn it_does_not_match_on_differing_vales() {
181 let a = Trailer::new("Relates-to".into(), "#129".into());
182 let b = Trailer::new("Relates-to".into(), "#128".into());
183
184 assert_ne!(a, b);
185 }
186
187 #[test]
188 fn it_does_not_match_on_differing_names() {
189 let a = Trailer::new("Another".into(), "#128".into());
190 let b = Trailer::new("Relates-to".into(), "#128".into());
191
192 assert_ne!(a, b);
193 }
194
195 #[test]
196 fn it_does_not_take_trailing_whitespace_into_account_in_hashing() {
197 let mut hasher_a = DefaultHasher::new();
198 Trailer::new("Relates-to".into(), "#128\n".into()).hash(&mut hasher_a);
199
200 let mut hasher_b = DefaultHasher::new();
201 Trailer::new("Relates-to".into(), "#128".into()).hash(&mut hasher_b);
202
203 assert_eq!(hasher_a.finish(), hasher_b.finish());
204 }
205
206 #[test]
207 fn it_differing_relates_headers_do_not_match_hashes() {
208 let mut hasher_a = DefaultHasher::new();
209 Trailer::new("Relates".into(), "#128".into()).hash(&mut hasher_a);
210
211 let mut hasher_b = DefaultHasher::new();
212 Trailer::new("Relates-to".into(), "#128".into()).hash(&mut hasher_b);
213
214 assert_ne!(hasher_a.finish(), hasher_b.finish());
215 }
216
217 #[test]
218 fn it_differing_relates_values_do_not_match_hashes() {
219 let mut hasher_a = DefaultHasher::new();
220 Trailer::new("Relates-to".into(), "#129".into()).hash(&mut hasher_a);
221
222 let mut hasher_b = DefaultHasher::new();
223 Trailer::new("Relates-to".into(), "#128".into()).hash(&mut hasher_b);
224
225 assert_ne!(hasher_a.finish(), hasher_b.finish());
226 }
227
228 #[test]
229 fn it_can_give_me_itself_as_a_string() {
230 let trailer = Trailer::new("Relates-to".into(), "#128".into());
231
232 assert_eq!(String::from(trailer), String::from("Relates-to: #128"));
233 }
234
235 #[test]
236 fn can_generate_itself_from_body() {
237 let trailer = Trailer::try_from(Body::from("Relates-to: #128"));
238
239 assert_eq!(
240 String::from(trailer.expect("Could not parse from string")),
241 String::from("Relates-to: #128")
242 );
243 }
244
245 #[test]
246 fn it_preserves_preceding_whitespace() {
247 let trailer = Trailer::try_from(Body::from("Relates-to: #128\n"));
248
249 assert_eq!(
250 String::from(trailer.expect("Could not parse from string")),
251 String::from("Relates-to: #128\n")
252 );
253 }
254
255 #[test]
256 fn can_generate_from_body() {
257 let trailer = Trailer::new("Relates-to".into(), "#128".into());
258 let body: Fragment<'_> = Fragment::from(trailer);
259
260 assert_eq!(body, Fragment::Body(Body::from("Relates-to: #128")));
261 }
262}