bitbelay_report/builder.rs
1//! Builders for [`Report`]s.
2
3use chrono::Local;
4use nonempty::NonEmpty;
5
6use crate::section::Section;
7use crate::section::Test;
8use crate::Report;
9
10/// An error when a required field is missing.
11#[derive(Debug)]
12pub enum MissingError {
13 /// No title was provided to the [`Builder`].
14 Title,
15
16 /// No sections where provided to the [`Builder`].
17 Sections,
18}
19
20impl std::fmt::Display for MissingError {
21 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22 match self {
23 MissingError::Title => write!(f, "title"),
24 MissingError::Sections => write!(f, "sections"),
25 }
26 }
27}
28
29impl std::error::Error for MissingError {}
30
31/// An error when multiple values are provided for a singular field.
32#[derive(Debug)]
33pub enum MultipleError {
34 /// Multiple titles were provided to the [`Builder`].
35 Title,
36}
37
38impl std::fmt::Display for MultipleError {
39 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 match self {
41 MultipleError::Title => write!(f, "title"),
42 }
43 }
44}
45
46impl std::error::Error for MultipleError {}
47
48/// An error related to a [`Builder`].
49#[derive(Debug)]
50pub enum Error {
51 /// A required field was missing from the [`Builder`].
52 Missing(MissingError),
53
54 /// Multiple values were provided for a singular field in the [`Builder`].
55 Multiple(MultipleError),
56}
57
58impl std::fmt::Display for Error {
59 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60 match self {
61 Error::Missing(err) => write!(f, "missing error: {}", err),
62 Error::Multiple(err) => write!(f, "multiple error: {}", err),
63 }
64 }
65}
66
67impl std::error::Error for Error {}
68
69/// A [`Result`](std::result::Result) with an [`Error`].
70type Result<T> = std::result::Result<T, Error>;
71
72/// A builder for a [`Report`].
73#[derive(Debug, Default)]
74pub struct Builder {
75 /// The title.
76 title: Option<String>,
77
78 /// The sections.
79 sections: Option<NonEmpty<Section>>,
80}
81
82impl Builder {
83 /// Sets the title for the [`Builder`].
84 ///
85 /// # Examples
86 ///
87 /// ```
88 /// use bitbelay_report::section::test;
89 /// use bitbelay_report::section::test::module::Result;
90 /// use bitbelay_report::section::test::Module;
91 /// use bitbelay_report::Builder;
92 ///
93 /// let result = test::Builder::default()
94 /// .title("Foo")?
95 /// .description("Bar")?
96 /// .push_module(Module::new(Result::Inconclusive, "Baz", None, None))
97 /// .try_build()?;
98 ///
99 /// let report = Builder::default()
100 /// .title("Hello, world!")?
101 /// .push_test_result(result)
102 /// .try_build()?;
103 ///
104 /// assert_eq!(report.title(), "Hello, world!");
105 ///
106 /// # Ok::<(), Box<dyn std::error::Error>>(())
107 /// ```
108 pub fn title(mut self, title: impl Into<String>) -> Result<Self> {
109 let title = title.into();
110
111 if self.title.is_some() {
112 return Err(Error::Multiple(MultipleError::Title));
113 }
114
115 self.title = Some(title);
116 Ok(self)
117 }
118
119 /// Pushes a [test result section](Test) into the [`Builder`].
120 ///
121 /// # Examples
122 ///
123 /// ```
124 /// use bitbelay_report::section::test;
125 /// use bitbelay_report::section::test::module::Result;
126 /// use bitbelay_report::section::test::Module;
127 /// use bitbelay_report::Builder;
128 ///
129 /// let result = test::Builder::default()
130 /// .title("Foo")?
131 /// .description("Bar")?
132 /// .push_module(Module::new(Result::Inconclusive, "Baz", None, None))
133 /// .try_build()?;
134 ///
135 /// let report = Builder::default()
136 /// .title("Hello, world!")?
137 /// .push_test_result(result.clone())
138 /// .try_build()?;
139 ///
140 /// assert_eq!(report.sections().len(), 1);
141 /// assert_eq!(report.sections().first().as_test_result().unwrap(), &result);
142 ///
143 /// # Ok::<(), Box<dyn std::error::Error>>(())
144 /// ```
145 pub fn push_test_result(mut self, result: Test) -> Self {
146 let section = Section::TestResult(result);
147
148 let sections = match self.sections {
149 Some(mut sections) => {
150 sections.push(section);
151 sections
152 }
153 None => NonEmpty::new(section),
154 };
155
156 self.sections = Some(sections);
157 self
158 }
159
160 /// Consumes `self` and attempts to build a [`Report`].
161 ///
162 /// # Examples
163 ///
164 /// ```
165 /// use bitbelay_report::section::test;
166 /// use bitbelay_report::section::test::module::Result;
167 /// use bitbelay_report::section::test::Module;
168 /// use bitbelay_report::Builder;
169 ///
170 /// let result = test::Builder::default()
171 /// .title("Foo")?
172 /// .description("Bar")?
173 /// .push_module(Module::new(Result::Inconclusive, "Baz", None, None))
174 /// .try_build()?;
175 ///
176 /// let report = Builder::default()
177 /// .title("Hello, world!")?
178 /// .push_test_result(result.clone())
179 /// .try_build()?;
180 ///
181 /// assert_eq!(report.title(), "Hello, world!");
182 /// assert_eq!(report.sections().len(), 1);
183 /// assert_eq!(report.sections().first().as_test_result().unwrap(), &result);
184 ///
185 /// # Ok::<(), Box<dyn std::error::Error>>(())
186 /// ```
187 pub fn try_build(self) -> Result<Report> {
188 let title = self.title.ok_or(Error::Missing(MissingError::Title))?;
189
190 let sections = self
191 .sections
192 .ok_or(Error::Missing(MissingError::Sections))?;
193
194 Ok(Report {
195 title,
196 date: Local::now(),
197 sections,
198 })
199 }
200}