1use 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#[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#[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#[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#[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 pub source: std::io::Error,
64 pub path: PathBuf,
66}
67
68impl ReadFileError {
69 pub fn new(source: std::io::Error, path: PathBuf) -> Self {
71 Self { source, path }
72 }
73}
74
75#[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 pub source: std::io::Error,
85 pub path: PathBuf,
87}
88
89impl WriteFileError {
90 pub fn new(source: std::io::Error, path: PathBuf) -> Self {
92 Self { source, path }
93 }
94}
95
96#[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 pub source: std::io::Error,
106 pub path: PathBuf,
108}
109
110impl OpenFileError {
111 pub fn new(source: std::io::Error, path: PathBuf) -> Self {
113 Self { source, path }
114 }
115}
116
117#[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#[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#[derive(Debug, Error, Diagnostic)]
137#[error(transparent)]
138#[diagnostic(transparent)]
139pub enum CollectErrorSource {
140 ReadDirectory(#[from] ReadDirectoryError),
142 IterDirectory(#[from] IterDirectoryError),
144}
145
146#[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 #[source]
156 #[diagnostic_source]
157 pub source: CollectErrorSource,
158 pub path: PathBuf,
160}
161
162impl CollectError {
163 pub fn new(source: CollectErrorSource, path: PathBuf) -> Self {
165 Self { source, path }
166 }
167
168 pub fn read_directory(error: ReadDirectoryError, path: PathBuf) -> Self {
170 Self::new(error.into(), path)
171 }
172
173 pub fn iter_directory(error: IterDirectoryError, path: PathBuf) -> Self {
175 Self::new(error.into(), path)
176 }
177
178 pub fn new_read_directory(error: std::io::Error, path: PathBuf) -> Self {
180 Self::read_directory(ReadDirectoryError(error), path)
181 }
182
183 pub fn new_iter_directory(error: std::io::Error, path: PathBuf) -> Self {
185 Self::iter_directory(IterDirectoryError(error), path)
186 }
187}
188
189#[derive(Debug, Error, Diagnostic)]
191#[error(transparent)]
192#[diagnostic(transparent)]
193pub enum BuildErrorSource {
194 BuildTitle(#[from] BuildTitleError),
196 BuildFragment(#[from] BuildFragmentError),
198 Collect(#[from] CollectError),
200}
201
202#[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 #[source]
212 #[diagnostic_source]
213 pub source: BuildErrorSource,
214}
215
216impl BuildError {
217 pub fn new(source: BuildErrorSource) -> Self {
219 Self { source }
220 }
221
222 pub fn build_title(error: BuildTitleError) -> Self {
224 Self::new(error.into())
225 }
226
227 pub fn build_fragment(error: BuildFragmentError) -> Self {
229 Self::new(error.into())
230 }
231
232 pub fn collect(error: CollectError) -> Self {
234 Self::new(error.into())
235 }
236
237 pub fn new_build_title(error: RenderError) -> Self {
239 Self::build_title(BuildTitleError(error))
240 }
241
242 pub fn new_build_fragment(error: RenderError) -> Self {
244 Self::build_fragment(BuildFragmentError(error))
245 }
246}
247
248#[derive(Debug, Error, Diagnostic)]
250#[error(transparent)]
251#[diagnostic(transparent)]
252pub enum WriteErrorSource {
253 OpenFile(#[from] OpenFileError),
255 ReadFile(#[from] ReadFileError),
257 Build(#[from] BuildError),
259 WriteFile(#[from] WriteFileError),
261}
262
263#[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 #[source]
273 #[diagnostic_source]
274 pub source: WriteErrorSource,
275}
276
277impl WriteError {
278 pub fn new(source: WriteErrorSource) -> Self {
280 Self { source }
281 }
282
283 pub fn open_file(error: OpenFileError) -> Self {
285 Self::new(error.into())
286 }
287
288 pub fn read_file(error: ReadFileError) -> Self {
290 Self::new(error.into())
291 }
292
293 pub fn build(error: BuildError) -> Self {
295 Self::new(error.into())
296 }
297
298 pub fn write_file(error: WriteFileError) -> Self {
300 Self::new(error.into())
301 }
302
303 pub fn new_open_file(error: std::io::Error, path: PathBuf) -> Self {
305 Self::open_file(OpenFileError::new(error, path))
306 }
307
308 pub fn new_read_file(error: std::io::Error, path: PathBuf) -> Self {
310 Self::read_file(ReadFileError::new(error, path))
311 }
312
313 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#[derive(Debug, Clone)]
351pub struct Builder<'b> {
352 pub context: Context<'b>,
354 pub config: Config<'b>,
356 pub date: Date,
358 pub renderer: Handlebars<'b>,
360}
361
362pub const TITLE: &str = "title";
364
365pub const FRAGMENT: &str = "fragment";
367
368impl<'b> Builder<'b> {
369 pub fn from_workspace(workspace: Workspace<'b>, date: Date) -> Result<Self, InitError> {
375 Self::new(workspace.context, workspace.config, date)
376 }
377
378 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 pub fn context(&self) -> &Context<'_> {
423 &self.context
424 }
425
426 pub fn config(&self) -> &Config<'_> {
428 &self.config
429 }
430
431 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 pub fn preview(&self) -> Result<(), BuildError> {
506 let string = self.build()?;
507
508 println!("{string}");
509
510 Ok(())
511 }
512
513 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(§ions)
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 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 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 pub fn build_section_title<S: AsRef<str>>(&self, title: S) -> String {
568 self.build_section_title_str(title.as_ref())
569 }
570
571 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 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 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 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 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 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 pub fn wrap<S: AsRef<str>>(&self, string: S) -> String {
677 self.wrap_str(string.as_ref())
678 }
679
680 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 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 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()) .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 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 pub fn level_heading(&self, level: Level) -> String {
771 heading(self.config.indents.heading, level)
772 }
773
774 pub fn entry_heading(&self) -> String {
776 self.level_heading(self.config.levels.entry)
777 }
778
779 pub fn section_heading(&self) -> String {
781 self.level_heading(self.config.levels.section)
782 }
783}