use std::{
borrow::Cow,
fs::{read_dir, File},
io::{read_to_string, Write},
iter::{once, repeat},
path::PathBuf,
};
use handlebars::{no_escape, Handlebars, RenderError, TemplateError};
use itertools::Itertools;
use miette::Diagnostic;
use serde::Serialize;
use textwrap::{fill, Options as WrapOptions, WordSeparator, WordSplitter};
use thiserror::Error;
use time::Date;
use crate::{
config::{Config, Level},
context::Context,
fragment::{is_valid_path, Fragment, Fragments, Sections},
load::load,
workspace::Workspace,
};
#[derive(Debug, Error, Diagnostic)]
#[error("failed to initialize the renderer")]
#[diagnostic(
code(changelogging::builder::init),
help("make sure the formats configuration is valid")
)]
pub struct InitError(#[from] pub TemplateError);
#[derive(Debug, Error, Diagnostic)]
#[error("failed to build the title")]
#[diagnostic(
code(changelogging::builder::build_title),
help("make sure the formats configuration is valid")
)]
pub struct BuildTitleError(#[from] pub RenderError);
#[derive(Debug, Error, Diagnostic)]
#[error("failed to build the fragment")]
#[diagnostic(
code(changelogging::builder::build_fragment),
help("make sure the formats configuration is valid")
)]
pub struct BuildFragmentError(#[from] pub RenderError);
#[derive(Debug, Error, Diagnostic)]
#[error("failed to read from `{path}`")]
#[diagnostic(
code(changelogging::builder::read_file),
help("check whether the file exists and is accessible")
)]
pub struct ReadFileError {
pub source: std::io::Error,
pub path: PathBuf,
}
impl ReadFileError {
pub fn new(source: std::io::Error, path: PathBuf) -> Self {
Self { source, path }
}
}
#[derive(Debug, Error, Diagnostic)]
#[error("failed to write to `{path}`")]
#[diagnostic(
code(changelogging::builder::write_file),
help("check whether the file exists and is accessible")
)]
pub struct WriteFileError {
pub source: std::io::Error,
pub path: PathBuf,
}
impl WriteFileError {
pub fn new(source: std::io::Error, path: PathBuf) -> Self {
Self { source, path }
}
}
#[derive(Debug, Error, Diagnostic)]
#[error("failed to open `{path}`")]
#[diagnostic(
code(changelogging::builder::open_file),
help("check whether the file exists and is accessible")
)]
pub struct OpenFileError {
pub source: std::io::Error,
pub path: PathBuf,
}
impl OpenFileError {
pub fn new(source: std::io::Error, path: PathBuf) -> Self {
Self { source, path }
}
}
#[derive(Debug, Error, Diagnostic)]
#[error("failed to read directory")]
#[diagnostic(
code(changelogging::builder::read_directory),
help("make sure the directory is accessible")
)]
pub struct ReadDirectoryError(#[from] std::io::Error);
#[derive(Debug, Error, Diagnostic)]
#[error("failed to iterate directory")]
#[diagnostic(
code(changelogging::builder::iter_directory),
help("make sure the directory is accessible")
)]
pub struct IterDirectoryError(#[from] std::io::Error);
#[derive(Debug, Error, Diagnostic)]
#[error(transparent)]
#[diagnostic(transparent)]
pub enum CollectErrorSource {
ReadDirectory(#[from] ReadDirectoryError),
IterDirectory(#[from] IterDirectoryError),
}
#[derive(Debug, Error, Diagnostic)]
#[error("failed to collect from `{path}`")]
#[diagnostic(
code(changelogging::builder::collect),
help("make sure the directory is accessible")
)]
pub struct CollectError {
#[source]
#[diagnostic_source]
pub source: CollectErrorSource,
pub path: PathBuf,
}
impl CollectError {
pub fn new(source: CollectErrorSource, path: PathBuf) -> Self {
Self { source, path }
}
pub fn read_directory(error: ReadDirectoryError, path: PathBuf) -> Self {
Self::new(error.into(), path)
}
pub fn iter_directory(error: IterDirectoryError, path: PathBuf) -> Self {
Self::new(error.into(), path)
}
pub fn new_read_directory(error: std::io::Error, path: PathBuf) -> Self {
Self::read_directory(ReadDirectoryError(error), path)
}
pub fn new_iter_directory(error: std::io::Error, path: PathBuf) -> Self {
Self::iter_directory(IterDirectoryError(error), path)
}
}
#[derive(Debug, Error, Diagnostic)]
#[error(transparent)]
#[diagnostic(transparent)]
pub enum BuildErrorSource {
BuildTitle(#[from] BuildTitleError),
BuildFragment(#[from] BuildFragmentError),
Collect(#[from] CollectError),
}
#[derive(Debug, Error, Diagnostic)]
#[error("failed to build")]
#[diagnostic(
code(changelogging::builder::build),
help("see the report for more information")
)]
pub struct BuildError {
#[source]
#[diagnostic_source]
pub source: BuildErrorSource,
}
impl BuildError {
pub fn new(source: BuildErrorSource) -> Self {
Self { source }
}
pub fn build_title(error: BuildTitleError) -> Self {
Self::new(error.into())
}
pub fn build_fragment(error: BuildFragmentError) -> Self {
Self::new(error.into())
}
pub fn collect(error: CollectError) -> Self {
Self::new(error.into())
}
pub fn new_build_title(error: RenderError) -> Self {
Self::build_title(BuildTitleError(error))
}
pub fn new_build_fragment(error: RenderError) -> Self {
Self::build_fragment(BuildFragmentError(error))
}
}
#[derive(Debug, Error, Diagnostic)]
#[error(transparent)]
#[diagnostic(transparent)]
pub enum WriteErrorSource {
OpenFile(#[from] OpenFileError),
ReadFile(#[from] ReadFileError),
Build(#[from] BuildError),
WriteFile(#[from] WriteFileError),
}
#[derive(Debug, Error, Diagnostic)]
#[error("failed to write")]
#[diagnostic(
code(changelogging::builder::write),
help("see the report for more information")
)]
pub struct WriteError {
#[source]
#[diagnostic_source]
pub source: WriteErrorSource,
}
impl WriteError {
pub fn new(source: WriteErrorSource) -> Self {
Self { source }
}
pub fn open_file(error: OpenFileError) -> Self {
Self::new(error.into())
}
pub fn read_file(error: ReadFileError) -> Self {
Self::new(error.into())
}
pub fn build(error: BuildError) -> Self {
Self::new(error.into())
}
pub fn write_file(error: WriteFileError) -> Self {
Self::new(error.into())
}
pub fn new_open_file(error: std::io::Error, path: PathBuf) -> Self {
Self::open_file(OpenFileError::new(error, path))
}
pub fn new_read_file(error: std::io::Error, path: PathBuf) -> Self {
Self::read_file(ReadFileError::new(error, path))
}
pub fn new_write_file(error: std::io::Error, path: PathBuf) -> Self {
Self::write_file(WriteFileError::new(error, path))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
struct RenderTitleData<'t> {
#[serde(flatten)]
context: &'t Context<'t>,
date: Cow<'t, str>,
}
impl<'t> RenderTitleData<'t> {
fn new(context: &'t Context<'_>, date: Date) -> Self {
Self {
context,
date: Cow::Owned(date.to_string()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
struct RenderFragmentData<'f> {
#[serde(flatten)]
context: &'f Context<'f>,
#[serde(flatten)]
fragment: &'f Fragment<'f>,
}
impl<'f> RenderFragmentData<'f> {
fn new(context: &'f Context<'_>, fragment: &'f Fragment<'_>) -> Self {
Self { context, fragment }
}
}
#[derive(Debug, Clone)]
pub struct Builder<'b> {
pub context: Context<'b>,
pub config: Config<'b>,
pub date: Date,
pub renderer: Handlebars<'b>,
}
pub const TITLE: &str = "title";
pub const FRAGMENT: &str = "fragment";
impl<'b> Builder<'b> {
pub fn from_workspace(workspace: Workspace<'b>, date: Date) -> Result<Self, InitError> {
Self::new(workspace.context, workspace.config, date)
}
pub fn new(context: Context<'b>, config: Config<'b>, date: Date) -> Result<Self, InitError> {
let mut renderer = Handlebars::new();
let formats = config.formats();
renderer.set_strict_mode(true);
renderer.register_escape_fn(no_escape);
renderer.register_template_string(TITLE, formats.title.as_ref())?;
renderer.register_template_string(FRAGMENT, formats.fragment.as_ref())?;
Ok(Self {
context,
config,
date,
renderer,
})
}
}
const SPACE: char = ' ';
const NEW_LINE: char = '\n';
const DOUBLE_NEW_LINE: &str = "\n\n";
const NO_SIGNIFICANT_CHANGES: &str = "No significant changes.";
fn heading(character: char, level: Level) -> String {
repeat(character)
.take(level.into())
.chain(once(SPACE))
.collect()
}
fn indent(character: char) -> String {
once(character).chain(once(SPACE)).collect()
}
impl Builder<'_> {
pub fn context(&self) -> &Context<'_> {
&self.context
}
pub fn config(&self) -> &Config<'_> {
&self.config
}
pub fn write(&self) -> Result<(), WriteError> {
let entry = self.build().map_err(WriteError::build)?;
let path = self.config.paths.output.as_ref();
let file = File::options()
.read(true)
.open(path)
.map_err(|error| WriteError::new_open_file(error, path.to_owned()))?;
let contents = read_to_string(file)
.map_err(|error| WriteError::new_read_file(error, path.to_owned()))?;
let mut file = File::options()
.create(true)
.write(true)
.truncate(true)
.open(path)
.map_err(|error| WriteError::new_open_file(error, path.to_owned()))?;
let start = self.config.start.as_ref();
let mut string = String::new();
if let Some((before, after)) = contents.split_once(start) {
string.push_str(before);
string.push_str(start);
string.push_str(DOUBLE_NEW_LINE);
string.push_str(&entry);
string.push(NEW_LINE);
let trimmed = after.trim_start();
if !trimmed.is_empty() {
string.push(NEW_LINE);
string.push_str(trimmed);
}
} else {
string.push_str(&entry);
string.push(NEW_LINE);
let trimmed = contents.trim_start();
if !trimmed.is_empty() {
string.push(NEW_LINE);
string.push_str(trimmed);
}
};
write!(file, "{string}")
.map_err(|error| WriteError::new_write_file(error, path.to_owned()))?;
Ok(())
}
pub fn preview(&self) -> Result<(), BuildError> {
let string = self.build()?;
println!("{string}");
Ok(())
}
pub fn build(&self) -> Result<String, BuildError> {
let mut string = self.build_title().map_err(BuildError::build_title)?;
string.push_str(DOUBLE_NEW_LINE);
let sections = self.collect().map_err(BuildError::collect)?;
let built = self
.build_sections(§ions)
.map_err(BuildError::build_fragment)?;
let contents = if built.is_empty() {
NO_SIGNIFICANT_CHANGES
} else {
&built
};
string.push_str(contents);
Ok(string)
}
pub fn build_title(&self) -> Result<String, BuildTitleError> {
let mut string = self.entry_heading();
let title = self.render_title()?;
string.push_str(&title);
Ok(string)
}
pub fn build_section_title_str(&self, title: &str) -> String {
let mut string = self.section_heading();
string.push_str(title);
string
}
pub fn build_section_title<S: AsRef<str>>(&self, title: S) -> String {
self.build_section_title_str(title.as_ref())
}
pub fn build_fragment(&self, fragment: &Fragment<'_>) -> Result<String, BuildFragmentError> {
let string = self.render_fragment(fragment)?;
Ok(self.wrap(string))
}
pub fn build_fragments(&self, fragments: &Fragments<'_>) -> Result<String, BuildFragmentError> {
let string = fragments
.iter()
.map(|fragment| self.build_fragment(fragment))
.process_results(|iterator| iterator.into_iter().join(DOUBLE_NEW_LINE))?;
Ok(string)
}
pub fn build_section_str(
&self,
title: &str,
fragments: &Fragments<'_>,
) -> Result<String, BuildFragmentError> {
let mut string = self.build_section_title(title);
let built = self.build_fragments(fragments)?;
string.push_str(DOUBLE_NEW_LINE);
string.push_str(&built);
Ok(string)
}
pub fn build_section<S: AsRef<str>>(
&self,
title: S,
fragments: &Fragments<'_>,
) -> Result<String, BuildFragmentError> {
self.build_section_str(title.as_ref(), fragments)
}
pub fn build_sections(&self, sections: &Sections<'_>) -> Result<String, BuildFragmentError> {
let types = self.config.types_with_defaults();
let string = self
.config
.order
.iter()
.filter_map(|name| types.get(name).zip(sections.get(name)))
.map(|(title, fragments)| self.build_section(title, fragments))
.process_results(|iterator| iterator.into_iter().join(DOUBLE_NEW_LINE))?;
Ok(string)
}
pub fn wrap_str(&self, string: &str) -> String {
let initial_indent = indent(self.config.indents.bullet);
let subsequent_indent = indent(SPACE);
let options = WrapOptions::new(self.config.wrap.get())
.break_words(false)
.word_separator(WordSeparator::AsciiSpace)
.word_splitter(WordSplitter::NoHyphenation)
.initial_indent(&initial_indent)
.subsequent_indent(&subsequent_indent);
fill(string, options)
}
pub fn wrap<S: AsRef<str>>(&self, string: S) -> String {
self.wrap_str(string.as_ref())
}
pub fn render_title(&self) -> Result<String, RenderError> {
let data = RenderTitleData::new(self.context(), self.date);
self.renderer.render(TITLE, &data)
}
pub fn render_fragment(&self, fragment: &Fragment<'_>) -> Result<String, RenderError> {
if fragment.partial.id.is_integer() {
let data = RenderFragmentData::new(self.context(), fragment);
self.renderer.render(FRAGMENT, &data)
} else {
Ok(fragment.content.as_ref().to_owned())
}
}
pub fn collect(&self) -> Result<Sections<'_>, CollectError> {
let directory = self.config.paths.directory.as_ref();
let mut sections = Sections::new();
read_dir(directory)
.map_err(|error| CollectError::new_read_directory(error, directory.to_owned()))?
.map(|result| {
result
.map(|entry| entry.path())
.map_err(|error| CollectError::new_iter_directory(error, directory.to_owned()))
})
.process_results(|iterator| {
iterator
.into_iter()
.filter_map(|path| load::<Fragment<'_>, _>(path).ok()) .for_each(|fragment| {
sections
.entry(fragment.partial.type_name.clone())
.or_default()
.push(fragment);
});
})?;
sections.values_mut().for_each(|section| section.sort());
Ok(sections)
}
pub fn collect_paths(&self) -> Result<Vec<PathBuf>, CollectError> {
let directory = self.config.paths.directory.as_ref();
read_dir(directory)
.map_err(|error| CollectError::new_read_directory(error, directory.to_owned()))?
.map(|result| {
result
.map(|entry| entry.path())
.map_err(|error| CollectError::new_iter_directory(error, directory.to_owned()))
})
.process_results(|iterator| {
iterator
.into_iter()
.filter(|path| is_valid_path(path))
.collect()
})
}
pub fn level_heading(&self, level: Level) -> String {
heading(self.config.indents.heading, level)
}
pub fn entry_heading(&self) -> String {
self.level_heading(self.config.levels.entry)
}
pub fn section_heading(&self) -> String {
self.level_heading(self.config.levels.section)
}
}