changelogging/
builder.rs

1//! Building changelogs from fragments.
2
3use std::{
4    borrow::Cow,
5    fs::{read_dir, File},
6    io::{read_to_string, Write},
7    iter::{once, repeat},
8    path::PathBuf,
9};
10
11use handlebars::{no_escape, Handlebars, RenderError, TemplateError};
12use itertools::Itertools;
13use miette::Diagnostic;
14use serde::Serialize;
15use textwrap::{fill, Options as WrapOptions, WordSeparator, WordSplitter};
16use thiserror::Error;
17use time::Date;
18
19use crate::{
20    config::{Config, Level},
21    context::Context,
22    fragment::{is_valid_path, Fragment, Fragments, Sections},
23    load::load,
24    workspace::Workspace,
25};
26
27/// Represents errors that can occur during builder initialization.
28#[derive(Debug, Error, Diagnostic)]
29#[error("failed to initialize the renderer")]
30#[diagnostic(
31    code(changelogging::builder::init),
32    help("make sure the formats configuration is valid")
33)]
34pub struct InitError(#[from] pub TemplateError);
35
36/// Represents errors that can occur when building titles.
37#[derive(Debug, Error, Diagnostic)]
38#[error("failed to build the title")]
39#[diagnostic(
40    code(changelogging::builder::build_title),
41    help("make sure the formats configuration is valid")
42)]
43pub struct BuildTitleError(#[from] pub RenderError);
44
45/// Represents errors that can occur when building fragments.
46#[derive(Debug, Error, Diagnostic)]
47#[error("failed to build the fragment")]
48#[diagnostic(
49    code(changelogging::builder::build_fragment),
50    help("make sure the formats configuration is valid")
51)]
52pub struct BuildFragmentError(#[from] pub RenderError);
53
54/// Represents errors that can occur when reading from files.
55#[derive(Debug, Error, Diagnostic)]
56#[error("failed to read from `{path}`")]
57#[diagnostic(
58    code(changelogging::builder::read_file),
59    help("check whether the file exists and is accessible")
60)]
61pub struct ReadFileError {
62    /// The underlying I/O error.
63    pub source: std::io::Error,
64    /// The path provided.
65    pub path: PathBuf,
66}
67
68impl ReadFileError {
69    /// Constructs [`Self`].
70    pub fn new(source: std::io::Error, path: PathBuf) -> Self {
71        Self { source, path }
72    }
73}
74
75/// Represents errors that can occur when writing to files.
76#[derive(Debug, Error, Diagnostic)]
77#[error("failed to write to `{path}`")]
78#[diagnostic(
79    code(changelogging::builder::write_file),
80    help("check whether the file exists and is accessible")
81)]
82pub struct WriteFileError {
83    /// The underlying I/O error.
84    pub source: std::io::Error,
85    /// The path provided.
86    pub path: PathBuf,
87}
88
89impl WriteFileError {
90    /// Constructs [`Self`].
91    pub fn new(source: std::io::Error, path: PathBuf) -> Self {
92        Self { source, path }
93    }
94}
95
96/// Represents errors that can occur when opening files.
97#[derive(Debug, Error, Diagnostic)]
98#[error("failed to open `{path}`")]
99#[diagnostic(
100    code(changelogging::builder::open_file),
101    help("check whether the file exists and is accessible")
102)]
103pub struct OpenFileError {
104    /// The underlying I/O error.
105    pub source: std::io::Error,
106    /// The path provided.
107    pub path: PathBuf,
108}
109
110impl OpenFileError {
111    /// Constructs [`Self`].
112    pub fn new(source: std::io::Error, path: PathBuf) -> Self {
113        Self { source, path }
114    }
115}
116
117/// Represents errors that can occur when reading directories.
118#[derive(Debug, Error, Diagnostic)]
119#[error("failed to read directory")]
120#[diagnostic(
121    code(changelogging::builder::read_directory),
122    help("make sure the directory is accessible")
123)]
124pub struct ReadDirectoryError(#[from] std::io::Error);
125
126/// Represents errors that can occur during iterating over directories.
127#[derive(Debug, Error, Diagnostic)]
128#[error("failed to iterate directory")]
129#[diagnostic(
130    code(changelogging::builder::iter_directory),
131    help("make sure the directory is accessible")
132)]
133pub struct IterDirectoryError(#[from] std::io::Error);
134
135/// Represents sources of errors that can occur during fragment collection.
136#[derive(Debug, Error, Diagnostic)]
137#[error(transparent)]
138#[diagnostic(transparent)]
139pub enum CollectErrorSource {
140    /// Read directory errors.
141    ReadDirectory(#[from] ReadDirectoryError),
142    /// Iterate directory errors.
143    IterDirectory(#[from] IterDirectoryError),
144}
145
146/// Represents errors that can occur during fragment collection.
147#[derive(Debug, Error, Diagnostic)]
148#[error("failed to collect from `{path}`")]
149#[diagnostic(
150    code(changelogging::builder::collect),
151    help("make sure the directory is accessible")
152)]
153pub struct CollectError {
154    /// The source of this error.
155    #[source]
156    #[diagnostic_source]
157    pub source: CollectErrorSource,
158    /// The path provided.
159    pub path: PathBuf,
160}
161
162impl CollectError {
163    /// Constructs [`Self`].
164    pub fn new(source: CollectErrorSource, path: PathBuf) -> Self {
165        Self { source, path }
166    }
167
168    /// Constructs [`Self`] from [`ReadDirectoryError`].
169    pub fn read_directory(error: ReadDirectoryError, path: PathBuf) -> Self {
170        Self::new(error.into(), path)
171    }
172
173    /// Constructs [`Self`] from [`IterDirectoryError`].
174    pub fn iter_directory(error: IterDirectoryError, path: PathBuf) -> Self {
175        Self::new(error.into(), path)
176    }
177
178    /// Constructs [`ReadDirectoryError`] and constructs [`Self`] from it.
179    pub fn new_read_directory(error: std::io::Error, path: PathBuf) -> Self {
180        Self::read_directory(ReadDirectoryError(error), path)
181    }
182
183    /// Constructs [`IterDirectoryError`] and constructs [`Self`] from it.
184    pub fn new_iter_directory(error: std::io::Error, path: PathBuf) -> Self {
185        Self::iter_directory(IterDirectoryError(error), path)
186    }
187}
188
189/// Represents sources of errors that can occur when building.
190#[derive(Debug, Error, Diagnostic)]
191#[error(transparent)]
192#[diagnostic(transparent)]
193pub enum BuildErrorSource {
194    /// Build title errors.
195    BuildTitle(#[from] BuildTitleError),
196    /// Build fragment errors.
197    BuildFragment(#[from] BuildFragmentError),
198    /// Collect errors.
199    Collect(#[from] CollectError),
200}
201
202/// Represents errors that can occur when building.
203#[derive(Debug, Error, Diagnostic)]
204#[error("failed to build")]
205#[diagnostic(
206    code(changelogging::builder::build),
207    help("see the report for more information")
208)]
209pub struct BuildError {
210    /// The source of this error.
211    #[source]
212    #[diagnostic_source]
213    pub source: BuildErrorSource,
214}
215
216impl BuildError {
217    /// Constructs [`Self`].
218    pub fn new(source: BuildErrorSource) -> Self {
219        Self { source }
220    }
221
222    /// Constructs [`Self`] from [`BuildTitleError`].
223    pub fn build_title(error: BuildTitleError) -> Self {
224        Self::new(error.into())
225    }
226
227    /// Constructs [`Self`] from [`BuildFragmentError`].
228    pub fn build_fragment(error: BuildFragmentError) -> Self {
229        Self::new(error.into())
230    }
231
232    /// Constructs [`Self`] from [`CollectError`].
233    pub fn collect(error: CollectError) -> Self {
234        Self::new(error.into())
235    }
236
237    /// Constructs [`BuildTitleError`] and constructs [`Self`] from it.
238    pub fn new_build_title(error: RenderError) -> Self {
239        Self::build_title(BuildTitleError(error))
240    }
241
242    /// Constructs [`BuildFragmentError`] and constructs [`Self`] from it.
243    pub fn new_build_fragment(error: RenderError) -> Self {
244        Self::build_fragment(BuildFragmentError(error))
245    }
246}
247
248/// Represents sources of errors that can occur when writing entries.
249#[derive(Debug, Error, Diagnostic)]
250#[error(transparent)]
251#[diagnostic(transparent)]
252pub enum WriteErrorSource {
253    /// Open file errors.
254    OpenFile(#[from] OpenFileError),
255    /// Read file errors.
256    ReadFile(#[from] ReadFileError),
257    /// Build errors.
258    Build(#[from] BuildError),
259    /// Write file errors.
260    WriteFile(#[from] WriteFileError),
261}
262
263/// Represents errors that can occur when writing entries.
264#[derive(Debug, Error, Diagnostic)]
265#[error("failed to write")]
266#[diagnostic(
267    code(changelogging::builder::write),
268    help("see the report for more information")
269)]
270pub struct WriteError {
271    /// The source of this error.
272    #[source]
273    #[diagnostic_source]
274    pub source: WriteErrorSource,
275}
276
277impl WriteError {
278    /// Constructs [`Self`].
279    pub fn new(source: WriteErrorSource) -> Self {
280        Self { source }
281    }
282
283    /// Constructs [`Self`] from [`OpenFileError`].
284    pub fn open_file(error: OpenFileError) -> Self {
285        Self::new(error.into())
286    }
287
288    /// Constructs [`Self`] from [`ReadFileError`].
289    pub fn read_file(error: ReadFileError) -> Self {
290        Self::new(error.into())
291    }
292
293    /// Constructs [`Self`] from [`BuildError`].
294    pub fn build(error: BuildError) -> Self {
295        Self::new(error.into())
296    }
297
298    /// Constructs [`Self`] from [`WriteFileError`].
299    pub fn write_file(error: WriteFileError) -> Self {
300        Self::new(error.into())
301    }
302
303    /// Constructs [`OpenFileError`] and constructs [`Self`] from it.
304    pub fn new_open_file(error: std::io::Error, path: PathBuf) -> Self {
305        Self::open_file(OpenFileError::new(error, path))
306    }
307
308    /// Constructs [`ReadFileError`] and constructs [`Self`] from it.
309    pub fn new_read_file(error: std::io::Error, path: PathBuf) -> Self {
310        Self::read_file(ReadFileError::new(error, path))
311    }
312
313    /// Constructs [`WriteFileError`] and constructs [`Self`] from it.
314    pub fn new_write_file(error: std::io::Error, path: PathBuf) -> Self {
315        Self::write_file(WriteFileError::new(error, path))
316    }
317}
318
319#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
320struct RenderTitleData<'t> {
321    #[serde(flatten)]
322    context: &'t Context<'t>,
323    date: Cow<'t, str>,
324}
325
326impl<'t> RenderTitleData<'t> {
327    fn new(context: &'t Context<'_>, date: Date) -> Self {
328        Self {
329            context,
330            date: Cow::Owned(date.to_string()),
331        }
332    }
333}
334
335#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
336struct RenderFragmentData<'f> {
337    #[serde(flatten)]
338    context: &'f Context<'f>,
339    #[serde(flatten)]
340    fragment: &'f Fragment<'f>,
341}
342
343impl<'f> RenderFragmentData<'f> {
344    fn new(context: &'f Context<'_>, fragment: &'f Fragment<'_>) -> Self {
345        Self { context, fragment }
346    }
347}
348
349/// Represents changelog builders.
350#[derive(Debug, Clone)]
351pub struct Builder<'b> {
352    /// The context of the project.
353    pub context: Context<'b>,
354    /// The configuration to use.
355    pub config: Config<'b>,
356    /// The date to use.
357    pub date: Date,
358    /// The renderer to use.
359    pub renderer: Handlebars<'b>,
360}
361
362/// The `title` literal.
363pub const TITLE: &str = "title";
364
365/// The `fragment` literal.
366pub const FRAGMENT: &str = "fragment";
367
368impl<'b> Builder<'b> {
369    /// Constructs [`Self`] from [`Workspace`].
370    ///
371    /// # Errors
372    ///
373    /// Returns [`InitError`] if initializing the renderer fails.
374    pub fn from_workspace(workspace: Workspace<'b>, date: Date) -> Result<Self, InitError> {
375        Self::new(workspace.context, workspace.config, date)
376    }
377
378    /// Constructs [`Self`].
379    ///
380    /// # Errors
381    ///
382    /// Returns [`InitError`] if initializing the renderer fails.
383    pub fn new(context: Context<'b>, config: Config<'b>, date: Date) -> Result<Self, InitError> {
384        let mut renderer = Handlebars::new();
385
386        let formats = config.formats();
387
388        renderer.set_strict_mode(true);
389
390        renderer.register_escape_fn(no_escape);
391
392        renderer.register_template_string(TITLE, formats.title.as_ref())?;
393        renderer.register_template_string(FRAGMENT, formats.fragment.as_ref())?;
394
395        Ok(Self {
396            context,
397            config,
398            date,
399            renderer,
400        })
401    }
402}
403
404const SPACE: char = ' ';
405const NEW_LINE: char = '\n';
406const DOUBLE_NEW_LINE: &str = "\n\n";
407const NO_SIGNIFICANT_CHANGES: &str = "No significant changes.";
408
409fn heading(character: char, level: Level) -> String {
410    repeat(character)
411        .take(level.into())
412        .chain(once(SPACE))
413        .collect()
414}
415
416fn indent(character: char) -> String {
417    once(character).chain(once(SPACE)).collect()
418}
419
420impl Builder<'_> {
421    /// Returns [`Context`] reference.
422    pub fn context(&self) -> &Context<'_> {
423        &self.context
424    }
425
426    /// Returns [`Config`] reference.
427    pub fn config(&self) -> &Config<'_> {
428        &self.config
429    }
430
431    // BUILDING
432
433    /// Builds entries and writes them to the changelog.
434    ///
435    /// # Errors
436    ///
437    /// Returns [`WriteError`] when building fails, as well as when I/O operations fail.
438    pub fn write(&self) -> Result<(), WriteError> {
439        let entry = self.build().map_err(WriteError::build)?;
440
441        let path = self.config.paths.output.as_ref();
442
443        let file = File::options()
444            .read(true)
445            .open(path)
446            .map_err(|error| WriteError::new_open_file(error, path.to_owned()))?;
447
448        let contents = read_to_string(file)
449            .map_err(|error| WriteError::new_read_file(error, path.to_owned()))?;
450
451        let mut file = File::options()
452            .create(true)
453            .write(true)
454            .truncate(true)
455            .open(path)
456            .map_err(|error| WriteError::new_open_file(error, path.to_owned()))?;
457
458        let start = self.config.start.as_ref();
459
460        let mut string = String::new();
461
462        if let Some((before, after)) = contents.split_once(start) {
463            string.push_str(before);
464
465            string.push_str(start);
466
467            string.push_str(DOUBLE_NEW_LINE);
468
469            string.push_str(&entry);
470
471            string.push(NEW_LINE);
472
473            let trimmed = after.trim_start();
474
475            if !trimmed.is_empty() {
476                string.push(NEW_LINE);
477
478                string.push_str(trimmed);
479            }
480        } else {
481            string.push_str(&entry);
482
483            string.push(NEW_LINE);
484
485            let trimmed = contents.trim_start();
486
487            if !trimmed.is_empty() {
488                string.push(NEW_LINE);
489
490                string.push_str(trimmed);
491            }
492        };
493
494        write!(file, "{string}")
495            .map_err(|error| WriteError::new_write_file(error, path.to_owned()))?;
496
497        Ok(())
498    }
499
500    /// Builds and previews (prints) entries.
501    ///
502    /// # Errors
503    ///
504    /// Returns [`BuildError`] when building fails.
505    pub fn preview(&self) -> Result<(), BuildError> {
506        let string = self.build()?;
507
508        println!("{string}");
509
510        Ok(())
511    }
512
513    /// Builds and returns entries.
514    ///
515    /// # Errors
516    ///
517    /// Returns [`BuildError`] when rendering titles and fragments or collecting fragments fails.
518    pub fn build(&self) -> Result<String, BuildError> {
519        let mut string = self.build_title().map_err(BuildError::build_title)?;
520
521        string.push_str(DOUBLE_NEW_LINE);
522
523        let sections = self.collect().map_err(BuildError::collect)?;
524
525        let built = self
526            .build_sections(&sections)
527            .map_err(BuildError::build_fragment)?;
528
529        let contents = if built.is_empty() {
530            NO_SIGNIFICANT_CHANGES
531        } else {
532            &built
533        };
534
535        string.push_str(contents);
536
537        Ok(string)
538    }
539
540    /// Builds entry titles.
541    ///
542    /// # Errors
543    ///
544    /// Returns [`BuildTitleError`] when rendering fails.
545    pub fn build_title(&self) -> Result<String, BuildTitleError> {
546        let mut string = self.entry_heading();
547
548        let title = self.render_title()?;
549
550        string.push_str(&title);
551
552        Ok(string)
553    }
554
555    /// Builds section titles.
556    pub fn build_section_title_str(&self, title: &str) -> String {
557        let mut string = self.section_heading();
558
559        string.push_str(title);
560
561        string
562    }
563
564    /// Similar to [`build_section_title_str`], except the input is [`AsRef<str>`].
565    ///
566    /// [`build_section_title_str`]: Self::build_section_title_str
567    pub fn build_section_title<S: AsRef<str>>(&self, title: S) -> String {
568        self.build_section_title_str(title.as_ref())
569    }
570
571    /// Builds fragments.
572    ///
573    /// # Errors
574    ///
575    /// Returns [`BuildFragmentError`] when rendering fails.
576    pub fn build_fragment(&self, fragment: &Fragment<'_>) -> Result<String, BuildFragmentError> {
577        let string = self.render_fragment(fragment)?;
578
579        Ok(self.wrap(string))
580    }
581
582    /// Builds multiple fragments and joins them together.
583    ///
584    /// # Errors
585    ///
586    /// Returns [`BuildFragmentError`] when building any of the fragments fails.
587    pub fn build_fragments(&self, fragments: &Fragments<'_>) -> Result<String, BuildFragmentError> {
588        let string = fragments
589            .iter()
590            .map(|fragment| self.build_fragment(fragment))
591            .process_results(|iterator| iterator.into_iter().join(DOUBLE_NEW_LINE))?;
592
593        Ok(string)
594    }
595
596    /// Builds sections.
597    ///
598    /// This is essentially the same as calling [`build_section_title`] and [`build_fragments`],
599    /// joining the results together.
600    ///
601    /// # Errors
602    ///
603    /// Returns [`BuildFragmentError`] when building any of the fragments fails.
604    ///
605    /// [`build_section_title`]: Self::build_section_title
606    /// [`build_fragments`]: Self::build_fragments
607    pub fn build_section_str(
608        &self,
609        title: &str,
610        fragments: &Fragments<'_>,
611    ) -> Result<String, BuildFragmentError> {
612        let mut string = self.build_section_title(title);
613
614        let built = self.build_fragments(fragments)?;
615
616        string.push_str(DOUBLE_NEW_LINE);
617        string.push_str(&built);
618
619        Ok(string)
620    }
621
622    /// Similar to [`build_section_str`], except the input is [`AsRef<str>`].
623    ///
624    /// # Errors
625    ///
626    /// Returns [`BuildFragmentError`] when building any of the fragments fails.
627    ///
628    /// [`build_section_str`]: Self::build_section_str
629    pub fn build_section<S: AsRef<str>>(
630        &self,
631        title: S,
632        fragments: &Fragments<'_>,
633    ) -> Result<String, BuildFragmentError> {
634        self.build_section_str(title.as_ref(), fragments)
635    }
636
637    /// Builds multiple sections and joins them together.
638    ///
639    /// # Errors
640    ///
641    /// Returns [`BuildFragmentError`] when building any of the sections fails.
642    pub fn build_sections(&self, sections: &Sections<'_>) -> Result<String, BuildFragmentError> {
643        let types = self.config.types_with_defaults();
644
645        let string = self
646            .config
647            .order
648            .iter()
649            .filter_map(|name| types.get(name).zip(sections.get(name)))
650            .map(|(title, fragments)| self.build_section(title, fragments))
651            .process_results(|iterator| iterator.into_iter().join(DOUBLE_NEW_LINE))?;
652
653        Ok(string)
654    }
655
656    // WRAPPING
657
658    /// Wraps the given string.
659    pub fn wrap_str(&self, string: &str) -> String {
660        let initial_indent = indent(self.config.indents.bullet);
661        let subsequent_indent = indent(SPACE);
662
663        let options = WrapOptions::new(self.config.wrap.get())
664            .break_words(false)
665            .word_separator(WordSeparator::AsciiSpace)
666            .word_splitter(WordSplitter::NoHyphenation)
667            .initial_indent(&initial_indent)
668            .subsequent_indent(&subsequent_indent);
669
670        fill(string, options)
671    }
672
673    /// Similar to [`wrap_str`], except the input is [`AsRef<str>`].
674    ///
675    /// [`wrap_str`]: Self::wrap_str
676    pub fn wrap<S: AsRef<str>>(&self, string: S) -> String {
677        self.wrap_str(string.as_ref())
678    }
679
680    // RENDERING
681
682    /// Renders entry titles.
683    ///
684    /// # Errors
685    ///
686    /// Returns [`RenderError`] if rendering the title fails.
687    pub fn render_title(&self) -> Result<String, RenderError> {
688        let data = RenderTitleData::new(self.context(), self.date);
689
690        self.renderer.render(TITLE, &data)
691    }
692
693    /// Renders fragments.
694    ///
695    /// # Errors
696    ///
697    /// Returns [`RenderError`] if rendering the given fragment fails.
698    pub fn render_fragment(&self, fragment: &Fragment<'_>) -> Result<String, RenderError> {
699        if fragment.partial.id.is_integer() {
700            let data = RenderFragmentData::new(self.context(), fragment);
701
702            self.renderer.render(FRAGMENT, &data)
703        } else {
704            Ok(fragment.content.as_ref().to_owned())
705        }
706    }
707
708    // COLLECTING
709
710    /// Collects fragments into sections.
711    ///
712    /// # Errors
713    ///
714    /// Returns [`CollectError`] when reading or iterating the fragments directory fails.
715    pub fn collect(&self) -> Result<Sections<'_>, CollectError> {
716        let directory = self.config.paths.directory.as_ref();
717
718        let mut sections = Sections::new();
719
720        read_dir(directory)
721            .map_err(|error| CollectError::new_read_directory(error, directory.to_owned()))?
722            .map(|result| {
723                result
724                    .map(|entry| entry.path())
725                    .map_err(|error| CollectError::new_iter_directory(error, directory.to_owned()))
726            })
727            .process_results(|iterator| {
728                iterator
729                    .into_iter()
730                    .filter_map(|path| load::<Fragment<'_>, _>(path).ok()) // ignore errors
731                    .for_each(|fragment| {
732                        sections
733                            .entry(fragment.partial.type_name.clone())
734                            .or_default()
735                            .push(fragment);
736                    });
737            })?;
738
739        sections.values_mut().for_each(|section| section.sort());
740
741        Ok(sections)
742    }
743
744    /// Collects paths to fragments.
745    ///
746    /// # Errors
747    ///
748    /// Returns [`CollectError`] if reading or iterating the fragments directory fails.
749    pub fn collect_paths(&self) -> Result<Vec<PathBuf>, CollectError> {
750        let directory = self.config.paths.directory.as_ref();
751
752        read_dir(directory)
753            .map_err(|error| CollectError::new_read_directory(error, directory.to_owned()))?
754            .map(|result| {
755                result
756                    .map(|entry| entry.path())
757                    .map_err(|error| CollectError::new_iter_directory(error, directory.to_owned()))
758            })
759            .process_results(|iterator| {
760                iterator
761                    .into_iter()
762                    .filter(|path| is_valid_path(path))
763                    .collect()
764            })
765    }
766
767    // HEADING
768
769    /// Constructs headings for the given level.
770    pub fn level_heading(&self, level: Level) -> String {
771        heading(self.config.indents.heading, level)
772    }
773
774    /// Constructs entry headings.
775    pub fn entry_heading(&self) -> String {
776        self.level_heading(self.config.levels.entry)
777    }
778
779    /// Constructs section headings.
780    pub fn section_heading(&self) -> String {
781        self.level_heading(self.config.levels.section)
782    }
783}