Skip to content

Commit f4825ad

Browse files
authored
Merge pull request #4 from aliev/chore/errors-handling-refactoring
Update dependencies and enhance error handling
2 parents 61c3b25 + 19f7435 commit f4825ad

File tree

11 files changed

+103
-112
lines changed

11 files changed

+103
-112
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ serde_derive = "1.0"
2424
serde_yaml = "0.9"
2525
url = "2.5"
2626
dialoguer = { version = "0.10.0", features = ["fuzzy-select"] }
27+
anyhow = { version = "1.0.95" }
2728

2829
[dev-dependencies]
2930
tempfile = "3.15"

src/cli.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,8 +221,7 @@ pub fn run(args: Args) -> Result<()> {
221221

222222
// Process template files
223223
for dir_entry in WalkDir::new(&template_root) {
224-
let raw_entry = dir_entry.map_err(|e| Error::TemplateError(e.to_string()))?;
225-
let template_entry = raw_entry.path().to_path_buf();
224+
let template_entry = dir_entry?.path().to_path_buf();
226225
match processor.process(&template_entry) {
227226
Ok(file_operation) => {
228227
let user_confirmed_overwrite = match &file_operation {

src/config.rs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::error::{Error, Result};
2+
use crate::ioutils::path_to_str;
23
use crate::renderer::TemplateRenderer;
34
use indexmap::IndexMap;
45
use serde::Deserialize;
@@ -68,24 +69,22 @@ impl Config {
6869
if let Ok(contents) = std::fs::read_to_string(path) {
6970
if let Ok(config) = serde_json::from_str(&contents) {
7071
return Some(config);
72+
} else if let Ok(config) = serde_yaml::from_str(&contents) {
73+
return Some(config);
7174
}
7275
}
7376
None
7477
}
7578

7679
pub fn load_config<P: AsRef<Path>>(template_root: P) -> Result<Config> {
7780
let template_root = template_root.as_ref().to_path_buf();
78-
let template_dir = template_root
79-
.to_str()
80-
.to_owned()
81-
.ok_or_else(|| Error::TemplateSourceInvalidError)?
82-
.to_string();
81+
let template_dir = path_to_str(&template_root)?.to_string();
8382
for config_file in CONFIG_LIST.iter() {
8483
if let Some(config) = Config::from_file(template_root.join(config_file)) {
8584
return Ok(config);
8685
}
8786
}
88-
Err(Error::ConfigError { template_dir, config_files: CONFIG_LIST.join(", ") })
87+
Err(Error::ConfigNotFound { template_dir, config_files: CONFIG_LIST.join(", ") })
8988
}
9089
}
9190

src/dialoguer.rs

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
1-
use crate::{
2-
config::Question,
3-
error::{Error, Result},
4-
};
1+
use crate::{config::Question, error::Result};
52

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

85
pub fn confirm(skip: bool, prompt: String) -> Result<bool> {
96
if skip {
107
return Ok(true);
118
}
12-
Confirm::new().with_prompt(prompt).default(false).interact().map_err(Error::IoError)
9+
Ok(Confirm::new().with_prompt(prompt).default(false).interact()?)
1310
}
1411

1512
pub fn prompt_multiple_choice(
@@ -34,8 +31,7 @@ pub fn prompt_multiple_choice(
3431
.with_prompt(prompt)
3532
.items(&choices)
3633
.defaults(&defaults)
37-
.interact()
38-
.map_err(Error::IoError)?;
34+
.interact()?;
3935

4036
let selected: Vec<serde_json::Value> =
4137
indices.iter().map(|&i| serde_json::Value::String(choices[i].clone())).collect();
@@ -48,11 +44,7 @@ pub fn prompt_boolean(
4844
prompt: String,
4945
) -> Result<serde_json::Value> {
5046
let default_value = default_value.as_bool().unwrap();
51-
let result = Confirm::new()
52-
.with_prompt(prompt)
53-
.default(default_value)
54-
.interact()
55-
.map_err(Error::IoError)?;
47+
let result = Confirm::new().with_prompt(prompt).default(default_value).interact()?;
5648

5749
Ok(serde_json::Value::Bool(result))
5850
}
@@ -72,8 +64,7 @@ pub fn prompt_single_choice(
7264
.with_prompt(prompt)
7365
.default(default_value)
7466
.items(&choices)
75-
.interact()
76-
.map_err(Error::IoError)?;
67+
.interact()?;
7768

7869
Ok(serde_json::Value::String(choices[selection].clone()))
7970
}
@@ -104,13 +95,9 @@ pub fn prompt_text(
10495
);
10596
}
10697

107-
password.interact().map_err(Error::IoError)?
98+
password.interact()?
10899
} else {
109-
Input::new()
110-
.with_prompt(&prompt)
111-
.default(default_str)
112-
.interact_text()
113-
.map_err(Error::IoError)?
100+
Input::new().with_prompt(&prompt).default(default_str).interact_text()?
114101
};
115102

116103
Ok(serde_json::Value::String(input))

src/error.rs

Lines changed: 12 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,6 @@ pub enum Error {
66
#[error("IO error: {0}.")]
77
IoError(#[from] std::io::Error),
88

9-
#[error("Failed to parse config file.")]
10-
ConfigParseError,
11-
129
#[error("Failed to parse .bakerignore file. Original error: {0}")]
1310
GlobSetParseError(#[from] globset::Error),
1411

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

21-
#[error("Template error: {0}.")]
22-
TemplateError(String),
23-
24-
#[error("No configuration file found in '{template_dir}'. Tried: {config_files}.")]
25-
ConfigError { template_dir: String, config_files: String },
18+
#[error("Failed to extract dir entry. Original error: {0}")]
19+
WalkdirError(#[from] walkdir::Error),
2620

27-
/// When the Hook has executed but finished with an error.
28-
#[error("Hook execution failed with status: {status}")]
29-
HookExecutionError { status: ExitStatus },
21+
#[error(
22+
"Configuration file not found. Searched in '{template_dir}' for: {config_files}"
23+
)]
24+
ConfigNotFound { template_dir: String, config_files: String },
3025

31-
/// Represents validation failures in user input or data
32-
#[error("Validation error: {0}.")]
33-
ValidationError(String),
34-
35-
/// Represents errors in processing .bakerignore files
36-
#[error("BakerIgnore error: {0}.")]
37-
BakerIgnoreError(String),
26+
#[error("Hook script '{script}' failed with exit code: {status}")]
27+
HookExecutionError { script: String, status: ExitStatus },
3828

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

4634
#[error("Cannot process the source path: '{source_path}'. Original error: {e}")]
4735
ProcessError { source_path: String, e: String },
36+
37+
#[error(transparent)]
38+
Other(#[from] anyhow::Error),
4839
}
4940

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

56-
/// Default error handler that prints the error and exits the program.
57-
///
58-
/// # Arguments
59-
/// * `err` - The BakerError to handle
60-
///
61-
/// # Behavior
62-
/// Prints the error message to stderr and exits with status code 1
6343
pub fn default_error_handler(err: Error) {
6444
eprintln!("{}", err);
6545
std::process::exit(1);

src/hooks.rs

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use std::process::{ChildStdout, Command, Stdio};
55

66
use crate::dialoguer::confirm;
77
use crate::error::{Error, Result};
8+
use crate::ioutils::path_to_str;
89

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

75-
let output = Output {
76-
template_dir: template_dir.as_ref().to_str().unwrap(),
77-
output_dir: output_dir.as_ref().to_str().unwrap(),
78-
answers,
79-
};
76+
let template_dir = path_to_str(&template_dir)?;
77+
let output_dir = path_to_str(&output_dir)?;
78+
79+
let output = Output { template_dir, output_dir, answers };
8080

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

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

9493
// Write context to stdin
9594
if let Some(mut stdin) = child.stdin.take() {
96-
stdin.write_all(&output_data).map_err(Error::IoError)?;
95+
stdin.write_all(&output_data)?;
9796
stdin.write_all(b"\n")?;
9897
}
9998

10099
// Wait for the process to complete
101-
let status = child.wait().map_err(Error::IoError)?;
100+
let status = child.wait()?;
102101

103102
if !status.success() {
104-
return Err(Error::HookExecutionError { status });
103+
return Err(Error::HookExecutionError {
104+
script: script_path.display().to_string(),
105+
status,
106+
});
105107
}
106108

107109
Ok(child.stdout)

src/ignore.rs

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::error::{Error, Result};
1+
use crate::{error::Result, ioutils::path_to_str};
22
use globset::{Glob, GlobSet, GlobSetBuilder};
33
use log::debug;
44
use std::{fs::read_to_string, path::Path};
@@ -31,26 +31,25 @@ pub fn parse_bakerignore_file<P: AsRef<Path>>(template_root: P) -> Result<GlobSe
3131

3232
// Add default patterns first
3333
for pattern in DEFAULT_IGNORE_PATTERNS {
34-
builder.add(
35-
Glob::new(template_root.join(pattern).to_str().unwrap())
36-
.map_err(Error::GlobSetParseError)?,
37-
);
34+
let path_to_ignored_pattern = template_root.join(pattern);
35+
let path_str = path_to_str(&path_to_ignored_pattern)?;
36+
builder.add(Glob::new(path_str)?);
3837
}
3938

4039
// Then add patterns from .bakerignore if it exists
4140
if let Ok(contents) = read_to_string(bakerignore_path) {
4241
for line in contents.lines() {
4342
let line = line.trim();
43+
let path_to_ignored_pattern = template_root.join(line);
44+
let path_str = path_to_str(&path_to_ignored_pattern)?;
45+
4446
if !line.is_empty() && !line.starts_with('#') {
45-
builder.add(
46-
Glob::new(template_root.join(line).to_str().unwrap())
47-
.map_err(Error::GlobSetParseError)?,
48-
);
47+
builder.add(Glob::new(path_str)?);
4948
}
5049
}
5150
} else {
5251
debug!("No .bakerignore file found, using default patterns.");
5352
}
5453

55-
builder.build().map_err(Error::GlobSetParseError)
54+
Ok(builder.build()?)
5655
}

src/ioutils.rs

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ pub fn get_output_dir<P: AsRef<Path>>(output_dir: P, force: bool) -> Result<Path
1515

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

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

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

5252
pub fn parse_string_to_json(
@@ -63,6 +63,44 @@ pub fn parse_string_to_json(
6363

6464
pub fn read_from(mut reader: impl std::io::Read) -> Result<String> {
6565
let mut buf = String::new();
66-
reader.read_to_string(&mut buf).map_err(Error::IoError)?;
66+
reader.read_to_string(&mut buf)?;
6767
Ok(buf)
6868
}
69+
70+
/// Converts a path to a string slice, returning an error if the path contains invalid Unicode characters.
71+
///
72+
/// # Arguments
73+
/// * `path` - A reference to a type that can be converted to a [`Path`]
74+
///
75+
/// # Returns
76+
/// * `Ok(&str)` - A string slice representing the path
77+
/// * `Err(Error)` - If the path contains invalid Unicode characters
78+
///
79+
/// # Examples
80+
/// ```
81+
/// use std::path::Path;
82+
/// use std::ffi::OsStr;
83+
/// use std::os::unix::ffi::OsStrExt;
84+
/// use baker::ioutils::path_to_str;
85+
///
86+
/// let valid_path = Path::new("/tmp/test.txt");
87+
/// let str_path = path_to_str(valid_path).unwrap();
88+
/// assert_eq!(str_path, "/tmp/test.txt");
89+
///
90+
/// // Path with invalid Unicode will return an error
91+
/// let invalid_bytes = vec![0x2F, 0x74, 0x6D, 0x70, 0xFF, 0xFF]; // "/tmp��"
92+
/// let invalid_path = Path::new(OsStr::from_bytes(&invalid_bytes));
93+
/// assert!(path_to_str(invalid_path).is_err());
94+
/// ```
95+
///
96+
/// # Errors
97+
/// Returns an error if the path contains any invalid Unicode characters
98+
///
99+
pub fn path_to_str<P: AsRef<Path> + ?Sized>(path: &P) -> Result<&str> {
100+
Ok(path.as_ref().to_str().ok_or_else(|| {
101+
anyhow::anyhow!(
102+
"Path '{}' contains invalid Unicode characters",
103+
path.as_ref().display()
104+
)
105+
})?)
106+
}

src/loader.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ impl<S: AsRef<str>> TemplateLoader for GitLoader<S> {
144144
format!("Directory '{}' already exists. Replace it?", repo_name),
145145
)?;
146146
if response {
147-
fs::remove_dir_all(&clone_path).map_err(Error::IoError)?;
147+
fs::remove_dir_all(&clone_path)?;
148148
} else {
149149
debug!("Using existing directory '{}'.", clone_path.display());
150150
return Ok(clone_path);

0 commit comments

Comments
 (0)