From 371ab3db018705cc008fb9a8bd4fb5e636154b6b Mon Sep 17 00:00:00 2001 From: Nikita Tikhonov Date: Sun, 25 Aug 2024 15:24:48 +0300 Subject: [PATCH] Implement unlinked fragments, improve the codebase. --- README.md | 14 +++ src/app.rs | 24 ++--- src/builder.rs | 131 +++++++++++++++---------- src/commands/build.rs | 34 +++---- src/commands/create.rs | 52 +++++----- src/commands/preview.rs | 18 ++-- src/config.rs | 16 +-- src/date.rs | 21 ++-- src/discover.rs | 34 +++---- src/fragment.rs | 212 ++++++++++++++++++++++++++++------------ src/git.rs | 4 +- src/init.rs | 9 +- src/lib.rs | 1 + src/load.rs | 31 ++++++ src/workspace.rs | 64 +++++------- 15 files changed, 405 insertions(+), 260 deletions(-) create mode 100644 src/load.rs diff --git a/README.md b/README.md index 9569f64..d238a56 100644 --- a/README.md +++ b/README.md @@ -92,12 +92,22 @@ $ changelogging create --content "Added cool features!" 13.feature.md $ changelogging create --content "Fixed annoying bugs!" 64.fix.md ``` +There are also *unlinked* fragments, which have non-integer IDs: + +```console +$ changelogging create --content "Fixed security issues!" ~issue.security.md +``` + And finally, preview the changelog entry! ```console $ changelogging preview ## 0.4.4 (YYYY-MM-DD) +### Security + +- Fixed security issues! + ### Features - Added cool features! (#13) @@ -133,6 +143,10 @@ $ cat CHANGELOG.md ## 0.4.4 (YYYY-MM-DD) +### Security + +- Fixed security issues! + ### Features - Added cool features! (#13) diff --git a/src/app.rs b/src/app.rs index 7639ea2..c420620 100644 --- a/src/app.rs +++ b/src/app.rs @@ -10,7 +10,8 @@ use crate::{ commands::{build::build, create::create, preview::preview}, discover::discover, init::init, - workspace::{load, Workspace}, + load::load, + workspace::Workspace, }; /// Represents global options of `changelogging`. @@ -146,27 +147,26 @@ impl App { pub fn run(self) -> Result<(), Error> { let globals = self.globals; - init(globals.directory).map_err(|error| Error::init(error))?; + init(globals.directory).map_err(Error::init)?; - let workspace = match globals.config { - Some(path) => load(path).map_err(|error| Error::workspace(error))?, - None => discover().map_err(|error| Error::discover(error))?, + let workspace = if let Some(path) = globals.config { + load(path).map_err(Error::workspace)? + } else { + discover().map_err(Error::discover)? }; if let Some(command) = self.command { match command { Command::Build(build) => { - build.run(workspace).map_err(|error| Error::build(error))? + build.run(workspace).map_err(Error::build)?; + } + Command::Preview(preview) => { + preview.run(workspace).map_err(Error::preview)?; } - Command::Preview(preview) => preview - .run(workspace) - .map_err(|error| Error::preview(error))?, Command::Create(create) => { let directory = workspace.config.paths.directory; - create - .run(directory) - .map_err(|error| Error::create(error))?; + create.run(directory).map_err(Error::create)?; } } }; diff --git a/src/builder.rs b/src/builder.rs index bce4185..9fe7d39 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -5,7 +5,7 @@ use std::{ fs::{read_dir, File}, io::{read_to_string, Write}, iter::{once, repeat}, - path::{Path, PathBuf}, + path::PathBuf, }; use handlebars::{no_escape, Handlebars, RenderError, TemplateError}; @@ -20,6 +20,7 @@ use crate::{ config::{Config, Level}, context::Context, fragment::{is_valid_path, Fragment, Fragments, Sections}, + load::load, workspace::Workspace, }; @@ -66,9 +67,7 @@ pub struct ReadFileError { impl ReadFileError { /// Constructs [`Self`]. - pub fn new>(source: std::io::Error, path: P) -> Self { - let path = path.as_ref().to_owned(); - + pub fn new(source: std::io::Error, path: PathBuf) -> Self { Self { source, path } } } @@ -89,9 +88,7 @@ pub struct WriteFileError { impl WriteFileError { /// Constructs [`Self`]. - pub fn new>(source: std::io::Error, path: P) -> Self { - let path = path.as_ref().to_owned(); - + pub fn new(source: std::io::Error, path: PathBuf) -> Self { Self { source, path } } } @@ -112,9 +109,7 @@ pub struct OpenFileError { impl OpenFileError { /// Constructs [`Self`]. - pub fn new>(source: std::io::Error, path: P) -> Self { - let path = path.as_ref().to_owned(); - + pub fn new(source: std::io::Error, path: PathBuf) -> Self { Self { source, path } } } @@ -166,29 +161,27 @@ pub struct CollectError { impl CollectError { /// Constructs [`Self`]. - pub fn new>(source: CollectErrorSource, path: P) -> Self { - let path = path.as_ref().to_owned(); - + pub fn new(source: CollectErrorSource, path: PathBuf) -> Self { Self { source, path } } /// Constructs [`Self`] from [`ReadDirectoryError`]. - pub fn read_directory>(source: ReadDirectoryError, path: P) -> Self { + pub fn read_directory(source: ReadDirectoryError, path: PathBuf) -> Self { Self::new(source.into(), path) } /// Constructs [`Self`] from [`IterDirectoryError`]. - pub fn iter_directory>(source: IterDirectoryError, path: P) -> Self { + pub fn iter_directory(source: IterDirectoryError, path: PathBuf) -> Self { Self::new(source.into(), path) } /// Constructs [`ReadDirectoryError`] and constructs [`Self`] from it. - pub fn new_read_directory>(source: std::io::Error, path: P) -> Self { + pub fn new_read_directory(source: std::io::Error, path: PathBuf) -> Self { Self::read_directory(ReadDirectoryError(source), path) } /// Constructs [`IterDirectoryError`] and constructs [`Self`] from it. - pub fn new_iter_directory>(source: std::io::Error, path: P) -> Self { + pub fn new_iter_directory(source: std::io::Error, path: PathBuf) -> Self { Self::iter_directory(IterDirectoryError(source), path) } } @@ -308,17 +301,17 @@ impl WriteError { } /// Constructs [`OpenFileError`] and constructs [`Self`] from it. - pub fn new_open_file>(source: std::io::Error, path: P) -> Self { + pub fn new_open_file(source: std::io::Error, path: PathBuf) -> Self { Self::open_file(OpenFileError::new(source, path)) } /// Constructs [`ReadFileError`] and constructs [`Self`] from it. - pub fn new_read_file>(source: std::io::Error, path: P) -> Self { + pub fn new_read_file(source: std::io::Error, path: PathBuf) -> Self { Self::read_file(ReadFileError::new(source, path)) } /// Constructs [`WriteFileError`] and constructs [`Self`] from it. - pub fn new_write_file>(source: std::io::Error, path: P) -> Self { + pub fn new_write_file(source: std::io::Error, path: PathBuf) -> Self { Self::write_file(WriteFileError::new(source, path)) } } @@ -334,7 +327,7 @@ impl<'t> RenderTitleData<'t> { fn new(context: &'t Context<'_>, date: Date) -> Self { Self { context, - date: date.to_string().into(), + date: Cow::Owned(date.to_string()), } } } @@ -390,7 +383,7 @@ impl<'b> Builder<'b> { pub fn new(context: Context<'b>, config: Config<'b>, date: Date) -> Result { let mut renderer = Handlebars::new(); - let formats = config.formats_ref(); + let formats = config.formats(); renderer.set_strict_mode(true); @@ -426,12 +419,12 @@ fn indent(character: char) -> String { impl Builder<'_> { /// Returns [`Context`] reference. - pub fn context_ref(&self) -> &Context<'_> { + pub fn context(&self) -> &Context<'_> { &self.context } /// Returns [`Config`] reference. - pub fn config_ref(&self) -> &Config<'_> { + pub fn config(&self) -> &Config<'_> { &self.config } @@ -443,24 +436,24 @@ impl Builder<'_> { /// /// Returns [`WriteError`] when building fails, as well as when I/O operations fail. pub fn write(&self) -> Result<(), WriteError> { - let entry = self.build().map_err(|error| WriteError::build(error))?; + 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))?; + .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))?; + 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))?; + .map_err(|error| WriteError::new_open_file(error, path.to_owned()))?; let start = self.config.start.as_ref(); @@ -473,7 +466,7 @@ impl Builder<'_> { string.push_str(DOUBLE_NEW_LINE); - string.push_str(entry.as_ref()); + string.push_str(&entry); string.push(NEW_LINE); @@ -485,7 +478,7 @@ impl Builder<'_> { string.push_str(trimmed); } } else { - string.push_str(entry.as_ref()); + string.push_str(&entry); string.push(NEW_LINE); @@ -498,7 +491,8 @@ impl Builder<'_> { } }; - write!(file, "{string}").map_err(|error| WriteError::new_write_file(error, path))?; + write!(file, "{string}") + .map_err(|error| WriteError::new_write_file(error, path.to_owned()))?; Ok(()) } @@ -528,7 +522,7 @@ impl Builder<'_> { string.push_str(DOUBLE_NEW_LINE); - let sections = self.collect().map_err(|error| BuildError::collect(error))?; + let sections = self.collect().map_err(BuildError::collect)?; let built = self .build_sections(§ions) @@ -537,7 +531,7 @@ impl Builder<'_> { let contents = if built.is_empty() { NO_SIGNIFICANT_CHANGES } else { - built.as_ref() + &built }; string.push_str(contents); @@ -555,20 +549,27 @@ impl Builder<'_> { let title = self.render_title()?; - string.push_str(title.as_ref()); + string.push_str(&title); Ok(string) } /// Builds section titles. - pub fn build_section_title>(&self, title: S) -> String { + pub fn build_section_title_str(&self, title: &str) -> String { let mut string = self.section_heading(); - string.push_str(title.as_ref()); + string.push_str(title); string } + /// Similar to [`build_section_title_str`], except the input is [`AsRef`]. + /// + /// [`build_section_title_str`]: Self::build_section_title_str + pub fn build_section_title>(&self, title: S) -> String { + self.build_section_title_str(title.as_ref()) + } + /// Builds fragments. /// /// # Errors @@ -605,9 +606,9 @@ impl Builder<'_> { /// /// [`build_section_title`]: Self::build_section_title /// [`build_fragments`]: Self::build_fragments - pub fn build_section>( + pub fn build_section_str( &self, - title: S, + title: &str, fragments: &Fragments<'_>, ) -> Result { let mut string = self.build_section_title(title); @@ -615,11 +616,24 @@ impl Builder<'_> { let built = self.build_fragments(fragments)?; string.push_str(DOUBLE_NEW_LINE); - string.push_str(built.as_ref()); + string.push_str(&built); Ok(string) } + /// Similar to [`build_section_str`], except the input is [`AsRef`]. + /// + /// # Errors + /// + /// Returns [`BuildFragmentError`] when building any of the fragments fails. + pub fn build_section>( + &self, + title: S, + fragments: &Fragments<'_>, + ) -> Result { + self.build_section_str(title.as_ref(), fragments) + } + /// Builds multiple sections and joins them together. /// /// # Errors @@ -642,18 +656,25 @@ impl Builder<'_> { // WRAPPING /// Wraps the given string. - pub fn wrap>(&self, string: S) -> 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.into()) + let options = WrapOptions::new(self.config.wrap.get()) .break_words(false) .word_separator(WordSeparator::AsciiSpace) .word_splitter(WordSplitter::NoHyphenation) - .initial_indent(initial_indent.as_ref()) - .subsequent_indent(subsequent_indent.as_ref()); + .initial_indent(&initial_indent) + .subsequent_indent(&subsequent_indent); - fill(string.as_ref(), options) + fill(string, options) + } + + /// Similar to [`wrap_str`], except the input is [`AsRef`]. + /// + /// [`wrap_str`]: Self::wrap_str + pub fn wrap>(&self, string: S) -> String { + self.wrap_str(string.as_ref()) } // RENDERING @@ -664,7 +685,7 @@ impl Builder<'_> { /// /// Returns [`RenderError`] if rendering the title fails. pub fn render_title(&self) -> Result { - let data = RenderTitleData::new(self.context_ref(), self.date); + let data = RenderTitleData::new(self.context(), self.date); self.renderer.render(TITLE, &data) } @@ -675,9 +696,13 @@ impl Builder<'_> { /// /// Returns [`RenderError`] if rendering the given fragment fails. pub fn render_fragment(&self, fragment: &Fragment<'_>) -> Result { - let data = RenderFragmentData::new(self.context_ref(), fragment); + if fragment.partial.id.is_integer() { + let data = RenderFragmentData::new(self.context(), fragment); - self.renderer.render(FRAGMENT, &data) + self.renderer.render(FRAGMENT, &data) + } else { + Ok(fragment.content.as_ref().to_owned()) + } } // COLLECTING @@ -693,16 +718,16 @@ impl Builder<'_> { let mut sections = Sections::new(); read_dir(directory) - .map_err(|error| CollectError::new_read_directory(error, 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)) + .map_err(|error| CollectError::new_iter_directory(error, directory.to_owned())) }) .process_results(|iterator| { iterator .into_iter() - .filter_map(|path| Fragment::load(path).ok()) // ignore errors + .filter_map(|path| load::, _>(path).ok()) // ignore errors .for_each(|fragment| { sections .entry(fragment.partial.type_name.clone()) @@ -725,11 +750,11 @@ impl Builder<'_> { let directory = self.config.paths.directory.as_ref(); read_dir(directory) - .map_err(|error| CollectError::new_read_directory(error, 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)) + .map_err(|error| CollectError::new_iter_directory(error, directory.to_owned())) }) .process_results(|iterator| { iterator diff --git a/src/commands/build.rs b/src/commands/build.rs index 1622983..ebf3109 100644 --- a/src/commands/build.rs +++ b/src/commands/build.rs @@ -54,30 +54,30 @@ impl Error { /// Constructs [`Self`] from [`Error`]. /// /// [`Error`]: crate::date::Error - pub fn date(source: crate::date::Error) -> Self { - Self::new(source.into()) + pub fn date(error: crate::date::Error) -> Self { + Self::new(error.into()) } /// Constructs [`Self`] from [`InitError`]. - pub fn init(source: InitError) -> Self { - Self::new(source.into()) + pub fn init(error: InitError) -> Self { + Self::new(error.into()) } /// Constructs [`Self`] from [`WriteError`]. - pub fn write(source: WriteError) -> Self { - Self::new(source.into()) + pub fn write(error: WriteError) -> Self { + Self::new(error.into()) } /// Constructs [`Self`] from [`CollectError`]. - pub fn collect(source: CollectError) -> Self { - Self::new(source.into()) + pub fn collect(error: CollectError) -> Self { + Self::new(error.into()) } /// Constructs [`Self`] from [`Error`]. /// /// [`Error`]: crate::git::Error - pub fn git(source: crate::git::Error) -> Self { - Self::new(source.into()) + pub fn git(error: crate::git::Error) -> Self { + Self::new(error.into()) } } @@ -94,26 +94,24 @@ pub fn build>( remove: bool, ) -> Result<(), Error> { let date = match date { - Some(string) => parse(string).map_err(|error| Error::date(error))?, + Some(string) => parse(string).map_err(Error::date)?, None => today(), }; - let builder = Builder::from_workspace(workspace, date).map_err(|error| Error::init(error))?; + let builder = Builder::from_workspace(workspace, date).map_err(Error::init)?; - builder.write().map_err(|error| Error::write(error))?; + builder.write().map_err(Error::write)?; if stage { let path = builder.config.paths.output.as_ref(); - git::add(once(path)).map_err(|error| Error::git(error))?; + git::add(once(path)).map_err(Error::git)?; } if remove { - let paths = builder - .collect_paths() - .map_err(|error| Error::collect(error))?; + let paths = builder.collect_paths().map_err(Error::collect)?; - git::remove(paths).map_err(|error| Error::git(error))?; + git::remove(paths).map_err(Error::git)?; } Ok(()) diff --git a/src/commands/create.rs b/src/commands/create.rs index c427d00..677a159 100644 --- a/src/commands/create.rs +++ b/src/commands/create.rs @@ -80,52 +80,50 @@ pub struct Error { impl Error { /// Constructs [`Self`]. - pub fn new>(source: ErrorSource, path: P) -> Self { - let path = path.as_ref().to_owned(); - + pub fn new(source: ErrorSource, path: PathBuf) -> Self { Self { source, path } } /// Constructs [`Self`] from [`ParseError`]. - pub fn parse>(source: ParseError, path: P) -> Self { - Self::new(source.into(), path) + pub fn parse(error: ParseError, path: PathBuf) -> Self { + Self::new(error.into(), path) } /// Constructs [`Self`] from [`OpenError`]. - pub fn open>(source: OpenError, path: P) -> Self { - Self::new(source.into(), path) + pub fn open(error: OpenError, path: PathBuf) -> Self { + Self::new(error.into(), path) } /// Constructs [`Self`] from [`WriteError`]. - pub fn write>(source: WriteError, path: P) -> Self { - Self::new(source.into(), path) + pub fn write(error: WriteError, path: PathBuf) -> Self { + Self::new(error.into(), path) } /// Constructs [`Self`] from [`EditError`]. - pub fn edit>(source: EditError, path: P) -> Self { - Self::new(source.into(), path) + pub fn edit(error: EditError, path: PathBuf) -> Self { + Self::new(error.into(), path) } /// Constructs [`Self`] from [`Error`]. /// /// [`Error`]: crate::git::Error - pub fn git>(source: crate::git::Error, path: P) -> Self { - Self::new(source.into(), path) + pub fn git(error: crate::git::Error, path: PathBuf) -> Self { + Self::new(error.into(), path) } /// Constructs [`OpenError`] and constructs [`Self`] from it. - pub fn new_open>(source: std::io::Error, path: P) -> Self { - Self::open(OpenError(source), path) + pub fn new_open(error: std::io::Error, path: PathBuf) -> Self { + Self::open(OpenError(error), path) } /// Constructs [`WriteError`] and constructs [`Self`] from it. - pub fn new_write>(source: std::io::Error, path: P) -> Self { - Self::write(WriteError(source), path) + pub fn new_write(error: std::io::Error, path: PathBuf) -> Self { + Self::write(WriteError(error), path) } /// Constructs [`EditError`] and constructs [`Self`] from it. - pub fn new_edit>(source: std::io::Error, path: P) -> Self { - Self::edit(EditError(source), path) + pub fn new_edit(error: std::io::Error, path: PathBuf) -> Self { + Self::edit(EditError(error), path) } } @@ -147,28 +145,26 @@ pub fn create, S: AsRef, C: AsRef>( ) -> Result<(), Error> { let name = name.as_ref(); - let joined = directory.as_ref().join(name); - - let path = joined.as_path(); + let path = directory.as_ref().join(name); - validate(name).map_err(|error| Error::parse(error, path))?; + validate(name).map_err(|error| Error::parse(error, path.clone()))?; let mut file = File::options() .create_new(true) .write(true) - .open(path) - .map_err(|error| Error::new_open(error, path))?; + .open(&path) + .map_err(|error| Error::new_open(error, path.clone()))?; let string = content.as_ref().map_or(PLACEHOLDER, |slice| slice.as_ref()); - writeln!(file, "{string}").map_err(|error| Error::new_write(error, path))?; + writeln!(file, "{string}").map_err(|error| Error::new_write(error, path.clone()))?; if edit { - edit_file(path).map_err(|error| Error::new_edit(error, path))?; + edit_file(&path).map_err(|error| Error::new_edit(error, path.clone()))?; } if add { - git::add(once(path)).map_err(|error| Error::git(error, path))?; + git::add(once(&path)).map_err(|error| Error::git(error, path.clone()))?; } Ok(()) diff --git a/src/commands/preview.rs b/src/commands/preview.rs index 52d2d43..83f8dfb 100644 --- a/src/commands/preview.rs +++ b/src/commands/preview.rs @@ -47,18 +47,18 @@ impl Error { /// Constructs [`Self`] from [`Error`]. /// /// [`Error`]: crate::date::Error - pub fn date(source: crate::date::Error) -> Self { - Self::new(source.into()) + pub fn date(error: crate::date::Error) -> Self { + Self::new(error.into()) } /// Constructs [`Self`] from [`InitError`]. - pub fn init(source: InitError) -> Self { - Self::new(source.into()) + pub fn init(error: InitError) -> Self { + Self::new(error.into()) } /// Constructs [`Self`] from [`BuildError`]. - pub fn build(source: BuildError) -> Self { - Self::new(source.into()) + pub fn build(error: BuildError) -> Self { + Self::new(error.into()) } } @@ -69,13 +69,13 @@ impl Error { /// Returns [`struct@Error`] if parsing the date, initializing the builder or previewing fails. pub fn preview>(workspace: Workspace<'_>, date: Option) -> Result<(), Error> { let date = match date { - Some(string) => parse(string).map_err(|error| Error::date(error))?, + Some(string) => parse(string).map_err(Error::date)?, None => today(), }; - let builder = Builder::from_workspace(workspace, date).map_err(|error| Error::init(error))?; + let builder = Builder::from_workspace(workspace, date).map_err(Error::init)?; - builder.preview().map_err(|error| Error::build(error))?; + builder.preview().map_err(Error::build)?; Ok(()) } diff --git a/src/config.rs b/src/config.rs index 00a528b..5ce7084 100644 --- a/src/config.rs +++ b/src/config.rs @@ -288,7 +288,7 @@ pub fn default_order() -> Vec<&'static str> { } fn into_order(vec: Vec<&str>) -> Order<'_> { - vec.into_iter().map(|name| name.into()).collect() + vec.into_iter().map(Cow::Borrowed).collect() } /// Specifies the mapping of types to their titles. @@ -346,7 +346,7 @@ pub fn default_types() -> HashMap<&'static str, &'static str> { fn into_types<'t>(hash_map: HashMap<&'t str, &'t str>) -> Types<'t> { hash_map .into_iter() - .map(|(name, title)| (name.into(), title.into())) + .map(|(name, title)| (Cow::Borrowed(name), Cow::Borrowed(title))) .collect() } @@ -383,32 +383,32 @@ impl Default for Config<'_> { impl Config<'_> { /// Returns [`Paths`] reference. - pub fn paths_ref(&self) -> &Paths<'_> { + pub fn paths(&self) -> &Paths<'_> { &self.paths } /// Returns [`Levels`] reference. - pub fn levels_ref(&self) -> &Levels { + pub fn levels(&self) -> &Levels { &self.levels } /// Returns [`Indents`] reference. - pub fn indents_ref(&self) -> &Indents { + pub fn indents(&self) -> &Indents { &self.indents } /// Returns [`Formats`] reference. - pub fn formats_ref(&self) -> &Formats<'_> { + pub fn formats(&self) -> &Formats<'_> { &self.formats } /// Returns [`Order`] reference. - pub fn order_ref(&self) -> &Order<'_> { + pub fn order(&self) -> &Order<'_> { &self.order } /// Returns [`Types`] reference. - pub fn types_ref(&self) -> &Types<'_> { + pub fn types(&self) -> &Types<'_> { &self.types } } diff --git a/src/date.rs b/src/date.rs index c988ccf..41cff8f 100644 --- a/src/date.rs +++ b/src/date.rs @@ -8,7 +8,7 @@ use time::{macros::format_description, Date, OffsetDateTime}; /// Represents errors that can occur when parsing dates. #[derive(Debug, Error, Diagnostic)] -#[error("failed to parse `{string}`")] +#[error("failed to parse `{string}` into date")] #[diagnostic( code(changelogging::date::date), help("dates must be in `[year]-[month]-[day]` (aka `YYYY-MM-DD`) format") @@ -20,9 +20,7 @@ pub struct Error { impl Error { /// Constructs [`Self`]. - pub fn new>(string: S) -> Self { - let string = string.as_ref().to_owned(); - + pub fn new(string: String) -> Self { Self { string } } } @@ -37,10 +35,17 @@ pub fn today() -> Date { /// # Errors /// /// Returns [`struct@Error`] on invalid dates. -pub fn parse>(string: S) -> Result { - let string = string.as_ref(); - +pub fn parse_str(string: &str) -> Result { let description = format_description!("[year]-[month]-[day]"); - Date::parse(string, description).map_err(|_| Error::new(string)) + Date::parse(string, description).map_err(|_| Error::new(string.to_owned())) +} + +/// Similar to [`parse_str`], except the input is [`AsRef`]. +/// +/// # Errors +/// +/// Returns [`struct@Error`] on invalid dates. +pub fn parse>(string: S) -> Result { + parse_str(string.as_ref()) } diff --git a/src/discover.rs b/src/discover.rs index a67fe19..4af7def 100644 --- a/src/discover.rs +++ b/src/discover.rs @@ -1,14 +1,14 @@ //! Discovering workspaces. -use std::{ - env::current_dir, - path::{Path, PathBuf}, -}; +use std::{env::current_dir, path::PathBuf}; use miette::Diagnostic; use thiserror::Error; -use crate::workspace::{PyProject, Workspace}; +use crate::{ + load::load, + workspace::{PyProject, Workspace}, +}; /// Represents errors that can occur when fetching the current directory fails. #[derive(Debug, Error, Diagnostic)] @@ -35,9 +35,7 @@ pub struct ExistenceError { impl ExistenceError { /// Constructs [`Self`]. - pub fn new>(source: std::io::Error, path: P) -> Self { - let path = path.as_ref().to_owned(); - + pub fn new(source: std::io::Error, path: PathBuf) -> Self { Self { source, path } } } @@ -56,9 +54,7 @@ pub struct NotFoundError { impl NotFoundError { /// Constructs [`Self`]. - pub fn new>(directory: D) -> Self { - let directory = directory.as_ref().to_owned(); - + pub fn new(directory: PathBuf) -> Self { Self { directory } } } @@ -126,12 +122,12 @@ impl Error { } /// Constructs [`ExistenceError`] and constructs [`Self`] from it. - pub fn new_existence>(source: std::io::Error, path: P) -> Self { + pub fn new_existence(source: std::io::Error, path: PathBuf) -> Self { Self::existence(ExistenceError::new(source, path)) } /// Constructs [`NotFoundError`] and constructs [`Self`] from it. - pub fn new_not_found>(directory: D) -> Self { + pub fn new_not_found(directory: PathBuf) -> Self { Self::not_found(NotFoundError::new(directory)) } } @@ -154,7 +150,7 @@ pub const PYPROJECT: &str = "pyproject.toml"; /// Returns [`struct@Error`] if fetching the current directory, checking the existence /// or loading the workspace fails. Also returned when no workspace can be found. pub fn discover() -> Result, Error> { - let mut path = current_dir().map_err(|error| Error::new_current_directory(error))?; + let mut path = current_dir().map_err(Error::new_current_directory)?; // try `changelogging.toml` @@ -162,9 +158,9 @@ pub fn discover() -> Result, Error> { if path .try_exists() - .map_err(|error| Error::new_existence(error, path.as_path()))? + .map_err(|error| Error::new_existence(error, path.clone()))? { - let workspace = Workspace::load(path.as_path()).map_err(|error| Error::workspace(error))?; + let workspace = load(path.as_path()).map_err(Error::workspace)?; return Ok(workspace); } @@ -177,11 +173,11 @@ pub fn discover() -> Result, Error> { if path .try_exists() - .map_err(|error| Error::new_existence(error, path.as_path()))? + .map_err(|error| Error::new_existence(error, path.clone()))? { - let pyproject = PyProject::load(path.as_path()).map_err(|error| Error::workspace(error))?; + let pyproject: PyProject<'_> = load(path.as_path()).map_err(Error::workspace)?; - if let Some(workspace) = pyproject.tool.and_then(|tools| tools.changelogging) { + if let Some(workspace) = pyproject.into_workspace() { return Ok(workspace); } } diff --git a/src/fragment.rs b/src/fragment.rs index 4ada870..a2fb439 100644 --- a/src/fragment.rs +++ b/src/fragment.rs @@ -4,7 +4,6 @@ use std::{ borrow::Cow, collections::HashMap, fs::read_to_string, - num::ParseIntError, path::{Path, PathBuf}, str::FromStr, }; @@ -13,17 +12,86 @@ use miette::Diagnostic; use serde::{Deserialize, Serialize}; use thiserror::Error; -/// Represents IDs of fragments. -pub type FragmentId = u32; +use crate::load::Load; + +/// Represents integer IDs of fragments. +pub type Integer = u32; + +/// Represents fragment IDs. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +#[serde(untagged)] +pub enum Id<'i> { + /// Integer fragment ID. + Integer(Integer), + /// String fragment ID. + String(Cow<'i, str>), +} + +impl<'i> Id<'i> { + /// Constructs [`Self`] from [`Integer`]. + pub fn integer(value: Integer) -> Self { + Self::Integer(value) + } + + /// Constructs [`Self`] from [`String`]. + pub fn owned(string: String) -> Self { + Self::String(Cow::Owned(string)) + } + + /// Constructs [`Self`] from [`str`]. + pub fn borrowed(string: &'i str) -> Self { + Self::String(Cow::Borrowed(string)) + } +} + +impl Id<'_> { + /// Checks if [`Self`] is [`Integer`]. + pub fn is_integer(&self) -> bool { + matches!(self, Self::Integer(_)) + } + + /// Checks if [`Self`] is [`String`]. + pub fn is_string(&self) -> bool { + matches!(self, Self::String(_)) + } +} + +impl FromStr for Id<'_> { + type Err = InvalidIdError; + + fn from_str(string: &str) -> Result { + if let Some(stripped) = string.strip_prefix(STRING_PREFIX) { + Ok(Self::owned(stripped.to_owned())) + } else { + string + .parse() + .map(Self::integer) + .map_err(|_| Self::Err::new(string.to_owned())) + } + } +} /// Represents errors that can occur when parsing fragment IDs. #[derive(Debug, Error, Diagnostic)] -#[error("invalid ID")] +#[error("failed to parse `{string}` into fragment ID")] #[diagnostic( code(changelogging::fragment::invalid_id), - help("fragment IDs are integers") + help("fragment IDs are either integers or strings in the `{STRING_PREFIX}string` form") )] -pub struct InvalidIdError(#[from] pub ParseIntError); +pub struct InvalidIdError { + /// The string that could not be parsed into any valid ID. + pub string: String, +} + +impl InvalidIdError { + /// Constructs [`Self`]. + pub fn new(string: String) -> Self { + Self { string } + } +} + +/// The prefix used for non-integer fragment IDs. +pub const STRING_PREFIX: char = '~'; /// Represents errors that can occur when there are not enough parts to parse. #[derive(Debug, Error, Diagnostic)] @@ -34,7 +102,7 @@ pub struct InvalidIdError(#[from] pub ParseIntError); )] pub struct UnexpectedEofError; -/// Represents sources of errors that can occur while parsing into [`PartialFragment`]. +/// Represents sources of errors that can occur while parsing into [`Partial`]. #[derive(Debug, Error, Diagnostic)] #[error(transparent)] #[diagnostic(transparent)] @@ -45,7 +113,7 @@ pub enum ParseErrorSource { UnexpectedEof(#[from] UnexpectedEofError), } -/// Represents errors that can occur while parsing into [`PartialFragment`]. +/// Represents errors that can occur while parsing into [`Partial`]. #[derive(Debug, Error, Diagnostic)] #[error("failed to parse `{name}`")] #[diagnostic( @@ -63,52 +131,60 @@ pub struct ParseError { impl ParseError { /// Constructs [`Self`]. - pub fn new>(source: ParseErrorSource, name: S) -> Self { - let name = name.as_ref().to_owned(); - + pub fn new(source: ParseErrorSource, name: String) -> Self { Self { source, name } } /// Constructs [`Self`] from [`InvalidIdError`]. - pub fn invalid_id>(source: InvalidIdError, name: S) -> Self { - Self::new(source.into(), name) + pub fn invalid_id(error: InvalidIdError, name: String) -> Self { + Self::new(error.into(), name) } /// Constructs [`Self`] from [`UnexpectedEofError`]. - pub fn unexpected_eof>(source: UnexpectedEofError, name: S) -> Self { - Self::new(source.into(), name) + pub fn unexpected_eof(error: UnexpectedEofError, name: String) -> Self { + Self::new(error.into(), name) } /// Constructs [`InvalidIdError`] and constructs [`Self`] from it. - pub fn new_invalid_id>(source: ParseIntError, name: S) -> Self { - Self::invalid_id(InvalidIdError(source), name) + pub fn new_invalid_id(string: String, name: String) -> Self { + Self::invalid_id(InvalidIdError::new(string), name) } /// Constructs [`UnexpectedEofError`] and constructs [`Self`] from it. - pub fn new_unexpected_eof>(name: S) -> Self { + pub fn new_unexpected_eof(name: String) -> Self { Self::unexpected_eof(UnexpectedEofError, name) } } /// Represents partial fragments. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] -pub struct PartialFragment<'p> { +pub struct Partial<'p> { /// The ID of the fragment. - pub id: FragmentId, + pub id: Id<'p>, /// The type of the fragment. pub type_name: Cow<'p, str>, } -impl<'p> PartialFragment<'p> { +impl<'p> Partial<'p> { /// Constructs [`Self`]. - pub fn new(id: FragmentId, type_name: Cow<'p, str>) -> Self { + pub fn new(id: Id<'p>, type_name: Cow<'p, str>) -> Self { Self { id, type_name } } + + /// Constructs [`Self`] with the owned type. + pub fn owned(id: Id<'p>, type_name: String) -> Self { + Self::new(id, Cow::Owned(type_name)) + } + + /// Constructs [`Self`] with the borrowed type. + pub fn borrowed(id: Id<'p>, type_name: &'p str) -> Self { + Self::new(id, Cow::Borrowed(type_name)) + } } const DOT: char = '.'; -impl FromStr for PartialFragment<'_> { +impl FromStr for Partial<'_> { type Err = ParseError; fn from_str(name: &str) -> Result { @@ -116,16 +192,16 @@ impl FromStr for PartialFragment<'_> { let id = split .next() - .ok_or_else(|| ParseError::new_unexpected_eof(name))? + .ok_or_else(|| ParseError::new_unexpected_eof(name.to_owned()))? .parse() - .map_err(|error| ParseError::new_invalid_id(error, name))?; + .map_err(|error| ParseError::invalid_id(error, name.to_owned()))?; let type_name = split .next() - .ok_or_else(|| ParseError::new_unexpected_eof(name))? + .ok_or_else(|| ParseError::new_unexpected_eof(name.to_owned()))? .to_owned(); - Ok(Self::new(id, type_name.into())) + Ok(Self::owned(id, type_name)) } } @@ -136,26 +212,33 @@ impl FromStr for PartialFragment<'_> { /// # Errors /// /// Returns [`ParseError`] if `string` is invalid. -pub fn validate>(string: S) -> Result<(), ParseError> { - let _check: PartialFragment<'_> = string.as_ref().parse()?; +pub fn validate_str(string: &str) -> Result<(), ParseError> { + let _: Partial<'_> = string.parse()?; Ok(()) } -/// Checks if the `string` represents some partial fragment. +/// Similar to [`validate_str`], except the input is [`AsRef`] +/// +/// # Errors /// -/// This function is equivalent to using [`validate`] and checking that the result is [`Ok`]. -pub fn is_valid>(string: S) -> bool { - validate(string).is_ok() +/// Returns [`ParseError`] if `string` is invalid. +pub fn validate>(string: S) -> Result<(), ParseError> { + validate_str(string.as_ref()) } /// Checks if the [`path_name`] of the given path represents some partial fragment. -pub fn is_valid_path>(path: P) -> bool { - path_name(path.as_ref()) - .filter(|name| is_valid(name)) +pub fn is_valid_path_ref(path: &Path) -> bool { + path_name(path) + .filter(|name| validate_str(name).is_ok()) .is_some() } +/// Similar to [`is_valid_path_ref`], except the input is [`AsRef`]. +pub fn is_valid_path>(path: P) -> bool { + is_valid_path_ref(path.as_ref()) +} + /// Returns the [`file_name`] of the given path if it is valid UTF-8. /// /// [`file_name`]: std::path::Path::file_name @@ -212,35 +295,33 @@ pub struct Error { impl Error { /// Constructs [`Self`]. - pub fn new>(source: ErrorSource, path: P) -> Self { - let path = path.as_ref().to_owned(); - + pub fn new(source: ErrorSource, path: PathBuf) -> Self { Self { source, path } } /// Constructs [`Self`] from [`InvalidUtf8Error`]. - pub fn invalid_utf8>(source: InvalidUtf8Error, path: P) -> Self { - Self::new(source.into(), path) + pub fn invalid_utf8(error: InvalidUtf8Error, path: PathBuf) -> Self { + Self::new(error.into(), path) } /// Constructs [`Self`] from [`ParseError`]. - pub fn parse>(source: ParseError, path: P) -> Self { - Self::new(source.into(), path) + pub fn parse(error: ParseError, path: PathBuf) -> Self { + Self::new(error.into(), path) } /// Constructs [`Self`] from [`ReadError`]. - pub fn read>(source: ReadError, path: P) -> Self { - Self::new(source.into(), path) + pub fn read(error: ReadError, path: PathBuf) -> Self { + Self::new(error.into(), path) } /// Constructs [`InvalidUtf8Error`] and constructs [`Self`] from it. - pub fn new_invalid_utf8>(path: P) -> Self { - Self::new(InvalidUtf8Error.into(), path) + pub fn new_invalid_utf8(path: PathBuf) -> Self { + Self::invalid_utf8(InvalidUtf8Error, path) } /// Constructs [`ReadError`] and constructs [`Self`] from it. - pub fn new_read>(source: std::io::Error, path: P) -> Self { - Self::new(ReadError(source).into(), path) + pub fn new_read(error: std::io::Error, path: PathBuf) -> Self { + Self::read(ReadError(error), path) } } @@ -251,33 +332,42 @@ pub struct Fragment<'f> { /// /// This field is flattened during (de)serialization. #[serde(flatten)] - pub partial: PartialFragment<'f>, + pub partial: Partial<'f>, /// The fragment content. pub content: Cow<'f, str>, } impl<'f> Fragment<'f> { /// Constructs [`Self`]. - pub fn new(partial: PartialFragment<'f>, content: Cow<'f, str>) -> Self { + pub fn new(partial: Partial<'f>, content: Cow<'f, str>) -> Self { Self { partial, content } } + + /// Constructs [`Self`] with the owned content. + pub fn owned(partial: Partial<'f>, content: String) -> Self { + Self::new(partial, Cow::Owned(content)) + } + + /// Constructs [`Self`] with the borrowed content. + pub fn borrowed(partial: Partial<'f>, content: &'f str) -> Self { + Self::new(partial, Cow::Borrowed(content)) + } } -impl Fragment<'_> { - /// Loads [`Self`] from the given path. - /// - /// # Errors - /// - /// Returns [`struct@Error`] when reading the file contents or parsing the name fails. - pub fn load>(path: P) -> Result { +impl Load for Fragment<'_> { + type Error = Error; + + fn load>(path: P) -> Result { let path = path.as_ref(); - let name = path_name(path).ok_or_else(|| Error::new_invalid_utf8(path))?; + let name = path_name(path).ok_or_else(|| Error::new_invalid_utf8(path.to_owned()))?; - let info = name.parse().map_err(|error| Error::parse(error, path))?; + let info = name + .parse() + .map_err(|error| Error::parse(error, path.to_owned()))?; let content = read_to_string(path) - .map_err(|error| Error::new_read(error, path))? + .map_err(|error| Error::new_read(error, path.to_owned()))? .trim() .to_owned(); diff --git a/src/git.rs b/src/git.rs index 83d6f0f..70e59b5 100644 --- a/src/git.rs +++ b/src/git.rs @@ -40,7 +40,7 @@ pub fn add, I: IntoIterator>(iterator: I) -> Result, I: IntoIterator>(iterator: I) -> Result< command.arg(path.as_ref()); } - command.status().map_err(|error| error.into()) + command.status().map_err(Into::into) } diff --git a/src/init.rs b/src/init.rs index 6cbc4a9..92a9aa7 100644 --- a/src/init.rs +++ b/src/init.rs @@ -26,9 +26,7 @@ pub struct ChangeCurrentDirectoryError { impl ChangeCurrentDirectoryError { /// Constructs [`Self`]. - pub fn new>(source: std::io::Error, path: P) -> Self { - let path = path.as_ref().to_owned(); - + pub fn new(source: std::io::Error, path: PathBuf) -> Self { Self { source, path } } } @@ -68,7 +66,7 @@ impl Error { } /// Constructs [`ChangeCurrentDirectoryError`] and constructs [`Self`] from it. - pub fn new_change_current_directory>(source: std::io::Error, path: P) -> Self { + pub fn new_change_current_directory(source: std::io::Error, path: PathBuf) -> Self { Self::change_current_directory(ChangeCurrentDirectoryError::new(source, path)) } } @@ -82,7 +80,8 @@ pub fn init>(directory: Option) -> Result<(), Error> { if let Some(path) = directory { let path = path.as_ref(); - set_current_dir(path).map_err(|error| Error::new_change_current_directory(error, path))?; + set_current_dir(path) + .map_err(|error| Error::new_change_current_directory(error, path.to_owned()))?; }; Ok(()) diff --git a/src/lib.rs b/src/lib.rs index 08b34ae..c944337 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -164,4 +164,5 @@ pub mod discover; pub mod fragment; pub mod git; pub mod init; +pub mod load; pub mod workspace; diff --git a/src/load.rs b/src/load.rs new file mode 100644 index 0000000..f1ea236 --- /dev/null +++ b/src/load.rs @@ -0,0 +1,31 @@ +//! Loading values from paths. + +use std::path::Path; + +/// Loading values from paths. +pub trait Load: Sized { + /// The associated error type returned from [`load`] on failure. + /// + /// [`load`]: Self::load + type Error; + + /// Loads the value of this type from the given path. + /// + /// # Errors + /// + /// Returns [`Error`] when loading fails. + /// + /// [`Error`]: Self::Error + fn load>(path: P) -> Result; +} + +/// Loads the value of the type given from the given path. +/// +/// # Errors +/// +/// Returns [`Error`] when loading fails. +/// +/// [`Error`]: Load::Error +pub fn load>(path: P) -> Result { + L::load(path) +} diff --git a/src/workspace.rs b/src/workspace.rs index 6a8c11f..72ea3c4 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -16,7 +16,7 @@ use miette::Diagnostic; use serde::{Deserialize, Serialize}; use thiserror::Error; -use crate::{config::Config, context::Context}; +use crate::{config::Config, context::Context, load::Load}; /// Represents errors that can occur when reading files. #[derive(Debug, Error, Diagnostic)] @@ -65,29 +65,27 @@ pub struct Error { impl Error { /// Constructs [`Self`]. - pub fn new>(source: ErrorSource, path: P) -> Self { - let path = path.as_ref().to_owned(); - + pub fn new(source: ErrorSource, path: PathBuf) -> Self { Self { source, path } } /// Constructs [`Self`] from [`ReadError`]. - pub fn read>(source: ReadError, path: P) -> Self { + pub fn read(source: ReadError, path: PathBuf) -> Self { Self::new(source.into(), path) } /// Constructs [`Self`] from [`ParseError`]. - pub fn parse>(source: ParseError, path: P) -> Self { + pub fn parse(source: ParseError, path: PathBuf) -> Self { Self::new(source.into(), path) } /// Constructs [`ReadError`] and constructs [`Self`] from it. - pub fn new_read>(source: std::io::Error, path: P) -> Self { + pub fn new_read(source: std::io::Error, path: PathBuf) -> Self { Self::read(ReadError(source), path) } /// Constructs [`ParseError`] and constructs [`Self`] from it. - pub fn new_parse>(source: toml::de::Error, path: P) -> Self { + pub fn new_parse(source: toml::de::Error, path: PathBuf) -> Self { Self::parse(ParseError(source), path) } } @@ -111,35 +109,22 @@ impl<'w> Workspace<'w> { } } -impl Workspace<'_> { - /// Loads [`Self`] from the given path. - /// - /// # Errors - /// - /// Returns [`struct@Error`] if reading the file or parsing TOML fails. - pub fn load>(path: P) -> Result { +impl Load for Workspace<'_> { + type Error = Error; + + fn load>(path: P) -> Result { let path = path.as_ref(); - let string = read_to_string(path).map_err(|error| Error::new_read(error, path))?; + let string = + read_to_string(path).map_err(|error| Error::new_read(error, path.to_owned()))?; let workspace = - toml::from_str(string.as_ref()).map_err(|error| Error::new_parse(error, path))?; + toml::from_str(&string).map_err(|error| Error::new_parse(error, path.to_owned()))?; Ok(workspace) } } -/// Calls the [`load`] method of [`Workspace`] on the path provided. -/// -/// # Errors -/// -/// Returns [`struct@Error`] when loading fails. -/// -/// [`load`]: Workspace::load -pub fn load>(path: P) -> Result, Error> { - Workspace::load(path) -} - /// Represents `tool` sections in `pyproject.toml` files. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Tools<'t> { @@ -154,20 +139,25 @@ pub struct PyProject<'p> { pub tool: Option>, } -impl PyProject<'_> { - /// Loads [`Self`] from the given path. - /// - /// # Errors - /// - /// Returns [`struct@Error`] if reading the file or parsing TOML fails. - pub fn load>(path: P) -> Result { +impl Load for PyProject<'_> { + type Error = Error; + + fn load>(path: P) -> Result { let path = path.as_ref(); - let string = read_to_string(path).map_err(|error| Error::new_read(error, path))?; + let string = + read_to_string(path).map_err(|error| Error::new_read(error, path.to_owned()))?; let workspace = - toml::from_str(string.as_ref()).map_err(|error| Error::new_parse(error, path))?; + toml::from_str(&string).map_err(|error| Error::new_parse(error, path.to_owned()))?; Ok(workspace) } } + +impl<'p> PyProject<'p> { + /// Converts [`Self`] to [`Workspace`], provided that the `tool.changelogging` table is present. + pub fn into_workspace(self) -> Option> { + self.tool.and_then(|tools| tools.changelogging) + } +}