1use std::fmt::Display;
2
3use miette::{Diagnostic, LabeledSpan, SourceCode};
4use mit_commit::CommitMessage;
5use thiserror::Error;
6
7use crate::model::code::Code;
8
9#[derive(Error, Debug, Eq, PartialEq, Clone)]
11#[error("{error}")]
12pub struct Problem {
13 error: String,
14 tip: String,
15 code: Code,
16 commit_message: String,
17 labels: Option<Vec<(String, usize, usize)>>,
18 url: Option<String>,
19}
20
21impl Diagnostic for Problem {
22 fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
28 Some(Box::new(format!("{:?}", self.code)))
29 }
30
31 fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
34 Some(Box::new(&self.tip))
35 }
36
37 fn url<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
38 self.url
39 .as_deref()
40 .map(|x| Box::new(x) as Box<dyn Display + 'a>)
41 }
42
43 fn source_code(&self) -> Option<&dyn SourceCode> {
44 if self.commit_message.is_empty() {
45 None
46 } else {
47 Some(&self.commit_message)
48 }
49 }
50
51 fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
52 if self.commit_message.is_empty() {
53 return None;
54 }
55
56 self.labels.as_ref().map(|labels| {
57 Box::new(
58 labels.iter().map(|(label, offset, len)| {
59 LabeledSpan::new(Some(label.clone()), *offset, *len)
60 }),
61 ) as Box<dyn Iterator<Item = LabeledSpan> + '_>
62 })
63 }
64}
65
66impl Problem {
67 #[must_use]
87 pub fn new(
88 error: String,
89 tip: String,
90 code: Code,
91 commit_message: &CommitMessage<'_>,
92 labels: Option<Vec<(String, usize, usize)>>,
93 url: Option<String>,
94 ) -> Self {
95 Self {
96 error,
97 tip,
98 code,
99 commit_message: String::from(commit_message.clone()),
100 labels,
101 url,
102 }
103 }
104
105 #[must_use]
125 pub const fn code(&self) -> &Code {
126 &self.code
127 }
128
129 #[must_use]
153 pub fn commit_message(&self) -> CommitMessage<'_> {
154 self.commit_message.clone().into()
155 }
156
157 #[must_use]
177 pub fn error(&self) -> &str {
178 &self.error
179 }
180
181 #[must_use]
203 pub fn tip(&self) -> &str {
204 &self.tip
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211 use std::option::Option::None;
212
213 use miette::Diagnostic;
214 use mit_commit::CommitMessage;
215
216 #[test]
217 fn test_error_returns_correct_value() {
218 let problem = Problem::new(
219 "Some error".into(),
220 String::new(),
221 Code::NotConventionalCommit,
222 &"".into(),
223 None,
224 None,
225 );
226 assert_eq!(problem.error(), "Some error");
227 }
228
229 #[test]
230 fn test_empty_commit_returns_no_labels() {
231 let problem = Problem::new(
232 String::new(),
233 String::new(),
234 Code::NotConventionalCommit,
235 &"".into(),
236 Some(vec![("String".to_string(), 10_usize, 20_usize)]),
237 None,
238 );
239 assert!(problem.labels().is_none());
240 }
241
242 #[test]
243 fn test_empty_commit_returns_no_source_code() {
244 let problem = Problem::new(
245 String::new(),
246 String::new(),
247 Code::NotConventionalCommit,
248 &"".into(),
249 Some(vec![("String".to_string(), 10_usize, 20_usize)]),
250 None,
251 );
252 assert!(problem.source_code().is_none());
253 }
254
255 #[allow(
256 clippy::needless_pass_by_value,
257 reason = "Cannot be passed by value, not supported by quickcheck"
258 )]
259 #[quickcheck]
260 fn test_error_matches_input(error: String) -> bool {
261 let problem = Problem::new(
262 error.clone(),
263 String::new(),
264 Code::NotConventionalCommit,
265 &CommitMessage::from(""),
266 None,
267 None,
268 );
269 problem.error() == error
270 }
271
272 #[test]
273 fn test_tip_returns_correct_value() {
274 let problem = Problem::new(
275 String::new(),
276 "Some tip".into(),
277 Code::NotConventionalCommit,
278 &"".into(),
279 None,
280 None,
281 );
282 assert_eq!(problem.tip(), "Some tip");
283 }
284
285 #[allow(
286 clippy::needless_pass_by_value,
287 reason = "Cannot be passed by value, not supported by quickcheck"
288 )]
289 #[quickcheck]
290 fn test_tip_matches_input(tip: String) -> bool {
291 let problem = Problem::new(
292 String::new(),
293 tip.to_string(),
294 Code::NotConventionalCommit,
295 &"".into(),
296 None,
297 None,
298 );
299 problem.tip() == tip
300 }
301
302 #[test]
303 fn test_code_returns_correct_value() {
304 let problem = Problem::new(
305 String::new(),
306 String::new(),
307 Code::NotConventionalCommit,
308 &"".into(),
309 None,
310 None,
311 );
312 assert_eq!(problem.code(), &Code::NotConventionalCommit);
313 }
314
315 #[quickcheck]
316 fn test_code_matches_input(code: Code) {
317 let problem = Problem::new(String::new(), String::new(), code, &"".into(), None, None);
318
319 assert_eq!(problem.code(), &code, "Code should match the input value");
320 }
321
322 #[test]
323 fn test_commit_message_returns_correct_value() {
324 let problem = Problem::new(
325 String::new(),
326 String::new(),
327 Code::NotConventionalCommit,
328 &CommitMessage::from("Commit message"),
329 None,
330 None,
331 );
332 assert_eq!(
333 problem.commit_message(),
334 CommitMessage::from("Commit message")
335 );
336 }
337
338 #[quickcheck]
339 fn test_commit_message_matches_input(message: String) {
340 let problem = Problem::new(
341 String::new(),
342 String::new(),
343 Code::NotConventionalCommit,
344 &CommitMessage::from(message.clone()),
345 None,
346 None,
347 );
348 assert_eq!(
349 problem.commit_message(),
350 CommitMessage::from(message),
351 "Commit message should match the input value"
352 );
353 }
354
355 #[test]
356 fn test_labels_return_correct_values() {
357 let problem = Problem::new(
358 String::new(),
359 String::new(),
360 Code::NotConventionalCommit,
361 &CommitMessage::from("Commit message"),
362 Some(vec![("String".to_string(), 10_usize, 20_usize)]),
363 None,
364 );
365 assert_eq!(
366 problem
367 .labels()
368 .unwrap()
369 .map(|x| (x.label().unwrap().to_string(), x.offset(), x.len()))
370 .collect::<Vec<_>>(),
371 vec![("String".to_string(), 10_usize, 20_usize)]
372 );
373 }
374
375 #[quickcheck]
376 fn test_labels_match_input_values(start: usize, offset: usize) {
377 let problem = Problem::new(
378 String::new(),
379 String::new(),
380 Code::NotConventionalCommit,
381 &CommitMessage::from("Commit message"),
382 Some(vec![("String".to_string(), start, offset)]),
383 None,
384 );
385 assert_eq!(
386 problem
387 .labels()
388 .unwrap()
389 .map(|x| (x.label().unwrap().to_string(), x.offset(), x.len()))
390 .collect::<Vec<_>>(),
391 vec![("String".to_string(), start, offset)]
392 );
393 }
394}