Skip to content

Commit

Permalink
Merge pull request #4 from aliev/chore/errors-handling-refactoring
Browse files Browse the repository at this point in the history
Update dependencies and enhance error handling
  • Loading branch information
aliev authored Jan 18, 2025
2 parents 61c3b25 + 19f7435 commit f4825ad
Show file tree
Hide file tree
Showing 11 changed files with 103 additions and 112 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ serde_derive = "1.0"
serde_yaml = "0.9"
url = "2.5"
dialoguer = { version = "0.10.0", features = ["fuzzy-select"] }
anyhow = { version = "1.0.95" }

[dev-dependencies]
tempfile = "3.15"
3 changes: 1 addition & 2 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,8 +221,7 @@ pub fn run(args: Args) -> Result<()> {

// Process template files
for dir_entry in WalkDir::new(&template_root) {
let raw_entry = dir_entry.map_err(|e| Error::TemplateError(e.to_string()))?;
let template_entry = raw_entry.path().to_path_buf();
let template_entry = dir_entry?.path().to_path_buf();
match processor.process(&template_entry) {
Ok(file_operation) => {
let user_confirmed_overwrite = match &file_operation {
Expand Down
11 changes: 5 additions & 6 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::error::{Error, Result};
use crate::ioutils::path_to_str;
use crate::renderer::TemplateRenderer;
use indexmap::IndexMap;
use serde::Deserialize;
Expand Down Expand Up @@ -68,24 +69,22 @@ impl Config {
if let Ok(contents) = std::fs::read_to_string(path) {
if let Ok(config) = serde_json::from_str(&contents) {
return Some(config);
} else if let Ok(config) = serde_yaml::from_str(&contents) {
return Some(config);
}
}
None
}

pub fn load_config<P: AsRef<Path>>(template_root: P) -> Result<Config> {
let template_root = template_root.as_ref().to_path_buf();
let template_dir = template_root
.to_str()
.to_owned()
.ok_or_else(|| Error::TemplateSourceInvalidError)?
.to_string();
let template_dir = path_to_str(&template_root)?.to_string();
for config_file in CONFIG_LIST.iter() {
if let Some(config) = Config::from_file(template_root.join(config_file)) {
return Ok(config);
}
}
Err(Error::ConfigError { template_dir, config_files: CONFIG_LIST.join(", ") })
Err(Error::ConfigNotFound { template_dir, config_files: CONFIG_LIST.join(", ") })
}
}

Expand Down
27 changes: 7 additions & 20 deletions src/dialoguer.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
use crate::{
config::Question,
error::{Error, Result},
};
use crate::{config::Question, error::Result};

use dialoguer::{Confirm, Input, MultiSelect, Password, Select};

pub fn confirm(skip: bool, prompt: String) -> Result<bool> {
if skip {
return Ok(true);
}
Confirm::new().with_prompt(prompt).default(false).interact().map_err(Error::IoError)
Ok(Confirm::new().with_prompt(prompt).default(false).interact()?)
}

pub fn prompt_multiple_choice(
Expand All @@ -34,8 +31,7 @@ pub fn prompt_multiple_choice(
.with_prompt(prompt)
.items(&choices)
.defaults(&defaults)
.interact()
.map_err(Error::IoError)?;
.interact()?;

let selected: Vec<serde_json::Value> =
indices.iter().map(|&i| serde_json::Value::String(choices[i].clone())).collect();
Expand All @@ -48,11 +44,7 @@ pub fn prompt_boolean(
prompt: String,
) -> Result<serde_json::Value> {
let default_value = default_value.as_bool().unwrap();
let result = Confirm::new()
.with_prompt(prompt)
.default(default_value)
.interact()
.map_err(Error::IoError)?;
let result = Confirm::new().with_prompt(prompt).default(default_value).interact()?;

Ok(serde_json::Value::Bool(result))
}
Expand All @@ -72,8 +64,7 @@ pub fn prompt_single_choice(
.with_prompt(prompt)
.default(default_value)
.items(&choices)
.interact()
.map_err(Error::IoError)?;
.interact()?;

Ok(serde_json::Value::String(choices[selection].clone()))
}
Expand Down Expand Up @@ -104,13 +95,9 @@ pub fn prompt_text(
);
}

password.interact().map_err(Error::IoError)?
password.interact()?
} else {
Input::new()
.with_prompt(&prompt)
.default(default_str)
.interact_text()
.map_err(Error::IoError)?
Input::new().with_prompt(&prompt).default(default_str).interact_text()?
};

Ok(serde_json::Value::String(input))
Expand Down
44 changes: 12 additions & 32 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ pub enum Error {
#[error("IO error: {0}.")]
IoError(#[from] std::io::Error),

#[error("Failed to parse config file.")]
ConfigParseError,

#[error("Failed to parse .bakerignore file. Original error: {0}")]
GlobSetParseError(#[from] globset::Error),

Expand All @@ -18,48 +15,31 @@ pub enum Error {
#[error("Failed to render. Original error: {0}")]
MinijinjaError(#[from] minijinja::Error),

#[error("Template error: {0}.")]
TemplateError(String),

#[error("No configuration file found in '{template_dir}'. Tried: {config_files}.")]
ConfigError { template_dir: String, config_files: String },
#[error("Failed to extract dir entry. Original error: {0}")]
WalkdirError(#[from] walkdir::Error),

/// When the Hook has executed but finished with an error.
#[error("Hook execution failed with status: {status}")]
HookExecutionError { status: ExitStatus },
#[error(
"Configuration file not found. Searched in '{template_dir}' for: {config_files}"
)]
ConfigNotFound { template_dir: String, config_files: String },

/// Represents validation failures in user input or data
#[error("Validation error: {0}.")]
ValidationError(String),

/// Represents errors in processing .bakerignore files
#[error("BakerIgnore error: {0}.")]
BakerIgnoreError(String),
#[error("Hook script '{script}' failed with exit code: {status}")]
HookExecutionError { script: String, status: ExitStatus },

#[error("Cannot proceed: output directory '{output_dir}' already exists. Use --force to overwrite it.")]
OutputDirectoryExistsError { output_dir: String },
#[error("Cannot proceed: template directory '{template_dir}' does not exist.")]
TemplateDoesNotExistsError { template_dir: String },
#[error("Cannot proceed: invalid type of template source.")]
TemplateSourceInvalidError,

#[error("Cannot process the source path: '{source_path}'. Original error: {e}")]
ProcessError { source_path: String, e: String },

#[error(transparent)]
Other(#[from] anyhow::Error),
}

/// Convenience type alias for Results with BakerError as the error type.
///
/// # Type Parameters
/// * `T` - The type of the success value
pub type Result<T> = std::result::Result<T, Error>;
pub type Result<T, E = Error> = core::result::Result<T, E>;

/// Default error handler that prints the error and exits the program.
///
/// # Arguments
/// * `err` - The BakerError to handle
///
/// # Behavior
/// Prints the error message to stderr and exits with status code 1
pub fn default_error_handler(err: Error) {
eprintln!("{}", err);
std::process::exit(1);
Expand Down
22 changes: 12 additions & 10 deletions src/hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use std::process::{ChildStdout, Command, Stdio};

use crate::dialoguer::confirm;
use crate::error::{Error, Result};
use crate::ioutils::path_to_str;

/// Structure representing data passed to hook scripts.
///
Expand Down Expand Up @@ -72,11 +73,10 @@ pub fn run_hook<P: AsRef<Path>>(
) -> Result<Option<ChildStdout>> {
let script_path = script_path.as_ref();

let output = Output {
template_dir: template_dir.as_ref().to_str().unwrap(),
output_dir: output_dir.as_ref().to_str().unwrap(),
answers,
};
let template_dir = path_to_str(&template_dir)?;
let output_dir = path_to_str(&output_dir)?;

let output = Output { template_dir, output_dir, answers };

let output_data = serde_json::to_vec(&output).unwrap();

Expand All @@ -88,20 +88,22 @@ pub fn run_hook<P: AsRef<Path>>(
.stdin(Stdio::piped())
.stdout(if is_piped_stdout { Stdio::piped() } else { Stdio::inherit() })
.stderr(Stdio::inherit())
.spawn()
.map_err(Error::IoError)?;
.spawn()?;

// Write context to stdin
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(&output_data).map_err(Error::IoError)?;
stdin.write_all(&output_data)?;
stdin.write_all(b"\n")?;
}

// Wait for the process to complete
let status = child.wait().map_err(Error::IoError)?;
let status = child.wait()?;

if !status.success() {
return Err(Error::HookExecutionError { status });
return Err(Error::HookExecutionError {
script: script_path.display().to_string(),
status,
});
}

Ok(child.stdout)
Expand Down
19 changes: 9 additions & 10 deletions src/ignore.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::error::{Error, Result};
use crate::{error::Result, ioutils::path_to_str};
use globset::{Glob, GlobSet, GlobSetBuilder};
use log::debug;
use std::{fs::read_to_string, path::Path};
Expand Down Expand Up @@ -31,26 +31,25 @@ pub fn parse_bakerignore_file<P: AsRef<Path>>(template_root: P) -> Result<GlobSe

// Add default patterns first
for pattern in DEFAULT_IGNORE_PATTERNS {
builder.add(
Glob::new(template_root.join(pattern).to_str().unwrap())
.map_err(Error::GlobSetParseError)?,
);
let path_to_ignored_pattern = template_root.join(pattern);
let path_str = path_to_str(&path_to_ignored_pattern)?;
builder.add(Glob::new(path_str)?);
}

// Then add patterns from .bakerignore if it exists
if let Ok(contents) = read_to_string(bakerignore_path) {
for line in contents.lines() {
let line = line.trim();
let path_to_ignored_pattern = template_root.join(line);
let path_str = path_to_str(&path_to_ignored_pattern)?;

if !line.is_empty() && !line.starts_with('#') {
builder.add(
Glob::new(template_root.join(line).to_str().unwrap())
.map_err(Error::GlobSetParseError)?,
);
builder.add(Glob::new(path_str)?);
}
}
} else {
debug!("No .bakerignore file found, using default patterns.");
}

builder.build().map_err(Error::GlobSetParseError)
Ok(builder.build()?)
}
46 changes: 42 additions & 4 deletions src/ioutils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ pub fn get_output_dir<P: AsRef<Path>>(output_dir: P, force: bool) -> Result<Path

pub fn create_dir_all<P: AsRef<Path>>(dest_path: P) -> Result<()> {
let dest_path = dest_path.as_ref();
std::fs::create_dir_all(dest_path).map_err(Error::IoError)
Ok(std::fs::create_dir_all(dest_path)?)
}

pub fn write_file<P: AsRef<Path>>(content: &str, dest_path: P) -> Result<()> {
Expand All @@ -30,7 +30,7 @@ pub fn write_file<P: AsRef<Path>>(content: &str, dest_path: P) -> Result<()> {
if let Some(parent) = abs_path.parent() {
create_dir_all(parent)?;
}
std::fs::write(abs_path, content).map_err(Error::IoError)
Ok(std::fs::write(abs_path, content)?)
}

pub fn copy_file<P: AsRef<Path>>(source_path: P, dest_path: P) -> Result<()> {
Expand All @@ -46,7 +46,7 @@ pub fn copy_file<P: AsRef<Path>>(source_path: P, dest_path: P) -> Result<()> {
if let Some(parent) = abs_dest.parent() {
create_dir_all(parent)?;
}
std::fs::copy(source_path, abs_dest).map(|_| ()).map_err(Error::IoError)
Ok(std::fs::copy(source_path, abs_dest).map(|_| ())?)
}

pub fn parse_string_to_json(
Expand All @@ -63,6 +63,44 @@ pub fn parse_string_to_json(

pub fn read_from(mut reader: impl std::io::Read) -> Result<String> {
let mut buf = String::new();
reader.read_to_string(&mut buf).map_err(Error::IoError)?;
reader.read_to_string(&mut buf)?;
Ok(buf)
}

/// Converts a path to a string slice, returning an error if the path contains invalid Unicode characters.
///
/// # Arguments
/// * `path` - A reference to a type that can be converted to a [`Path`]
///
/// # Returns
/// * `Ok(&str)` - A string slice representing the path
/// * `Err(Error)` - If the path contains invalid Unicode characters
///
/// # Examples
/// ```
/// use std::path::Path;
/// use std::ffi::OsStr;
/// use std::os::unix::ffi::OsStrExt;
/// use baker::ioutils::path_to_str;
///
/// let valid_path = Path::new("/tmp/test.txt");
/// let str_path = path_to_str(valid_path).unwrap();
/// assert_eq!(str_path, "/tmp/test.txt");
///
/// // Path with invalid Unicode will return an error
/// let invalid_bytes = vec![0x2F, 0x74, 0x6D, 0x70, 0xFF, 0xFF]; // "/tmp��"
/// let invalid_path = Path::new(OsStr::from_bytes(&invalid_bytes));
/// assert!(path_to_str(invalid_path).is_err());
/// ```
///
/// # Errors
/// Returns an error if the path contains any invalid Unicode characters
///
pub fn path_to_str<P: AsRef<Path> + ?Sized>(path: &P) -> Result<&str> {
Ok(path.as_ref().to_str().ok_or_else(|| {
anyhow::anyhow!(
"Path '{}' contains invalid Unicode characters",
path.as_ref().display()
)
})?)
}
2 changes: 1 addition & 1 deletion src/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ impl<S: AsRef<str>> TemplateLoader for GitLoader<S> {
format!("Directory '{}' already exists. Replace it?", repo_name),
)?;
if response {
fs::remove_dir_all(&clone_path).map_err(Error::IoError)?;
fs::remove_dir_all(&clone_path)?;
} else {
debug!("Using existing directory '{}'.", clone_path.display());
return Ok(clone_path);
Expand Down
Loading

0 comments on commit f4825ad

Please sign in to comment.