aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorRuben Beltran del Rio <git@r.bdr.sh>2025-04-05 14:43:19 +0200
committerRuben Beltran del Rio <git@r.bdr.sh>2025-04-05 14:43:19 +0200
commit93c0e8ecee430883c62120f9f0b6999acfac4435 (patch)
tree785b9275d9dd525889bfb8cb687a41260ae39b16 /src
parentbb3ec6b31f3c709fbf9f2a12c1708d63a61892ff (diff)
Update, panic less and propagate errors
Diffstat (limited to 'src')
-rw-r--r--src/file_finder.rs26
-rw-r--r--src/file_handler/file_strategies/file.rs32
-rw-r--r--src/file_handler/file_strategies/gemini.rs117
-rw-r--r--src/file_handler/file_strategies/layout.rs29
-rw-r--r--src/file_handler/mod.rs208
-rw-r--r--src/main.rs36
6 files changed, 333 insertions, 115 deletions
diff --git a/src/file_finder.rs b/src/file_finder.rs
index ef57950..08e86c4 100644
--- a/src/file_finder.rs
+++ b/src/file_finder.rs
@@ -9,18 +9,20 @@ pub fn find_files(directory_path: &PathBuf) -> Vec<File> {
fn find_files_recursively(root_path: &PathBuf, directory_path: &PathBuf) -> Vec<File> {
let mut result: Vec<File> = vec![];
let file_handler = FileHandler::default();
- let entries = read_dir(directory_path).unwrap();
- for entry in entries {
- let path = entry.unwrap().path();
- let relative_path = path.strip_prefix(root_path).unwrap();
- if relative_path.starts_with(".git") || relative_path.starts_with(".gitignore") {
- continue;
- }
- if path.is_dir() {
- result.append(&mut find_files_recursively(root_path, &path));
- } else {
- let file_type = file_handler.identify(&path);
- result.push(File { path, file_type });
+ if let Ok(entries) = read_dir(directory_path) {
+ for entry in entries.flatten() {
+ let path = entry.path();
+ if let Ok(relative_path) = path.strip_prefix(root_path) {
+ if relative_path.starts_with(".git") || relative_path.starts_with(".gitignore") {
+ continue;
+ }
+ if path.is_dir() {
+ result.append(&mut find_files_recursively(root_path, &path));
+ } else {
+ let file_type = file_handler.identify(&path);
+ result.push(File { path, file_type });
+ }
+ }
}
}
result
diff --git a/src/file_handler/file_strategies/file.rs b/src/file_handler/file_strategies/file.rs
index 363f712..c31eae4 100644
--- a/src/file_handler/file_strategies/file.rs
+++ b/src/file_handler/file_strategies/file.rs
@@ -1,17 +1,31 @@
pub struct Strategy {}
use std::fs::{copy, create_dir_all};
+use std::io::{Error, ErrorKind, Result};
use std::path::Path;
use crate::file_handler::{File, FileType, Strategy as FileHandlerStrategy};
impl Strategy {
- fn handle(source: &Path, destination: &Path, file: &File) {
- let relative_path = file.path.strip_prefix(source).unwrap();
+ fn handle(source: &Path, destination: &Path, file: &File) -> Result<()> {
+ let relative_path = file.path.strip_prefix(source).map_err(|_| {
+ Error::new(
+ ErrorKind::InvalidData,
+ "Path was not part of source directory.",
+ )
+ })?;
let complete_destination = destination.join(relative_path);
- let destination_parent = complete_destination.parent().unwrap();
- create_dir_all(destination_parent).unwrap();
- copy(&file.path, &complete_destination).unwrap();
+ match complete_destination.parent() {
+ Some(destination_parent) => {
+ create_dir_all(destination_parent)?;
+ copy(&file.path, &complete_destination)?;
+ Ok(())
+ }
+ None => Err(Error::new(
+ ErrorKind::InvalidData,
+ "Destination parent was not readable.",
+ )),
+ }
}
}
@@ -28,12 +42,12 @@ impl FileHandlerStrategy for Strategy {
matches!(file_type, FileType::File)
}
- fn handle_html(&self, source: &Path, destination: &Path, file: &File, _l: &str) {
- Strategy::handle(source, destination, file);
+ fn handle_html(&self, source: &Path, destination: &Path, file: &File, _l: &str) -> Result<()> {
+ Strategy::handle(source, destination, file)
}
- fn handle_gemini(&self, source: &Path, destination: &Path, file: &File) {
- Strategy::handle(source, destination, file);
+ fn handle_gemini(&self, source: &Path, destination: &Path, file: &File) -> Result<()> {
+ Strategy::handle(source, destination, file)
}
}
diff --git a/src/file_handler/file_strategies/gemini.rs b/src/file_handler/file_strategies/gemini.rs
index 5388769..0c7efbb 100644
--- a/src/file_handler/file_strategies/gemini.rs
+++ b/src/file_handler/file_strategies/gemini.rs
@@ -1,7 +1,7 @@
pub struct Strategy {}
use std::fs::{File as IOFile, create_dir_all, read_to_string};
-use std::io::Write;
+use std::io::{Error, ErrorKind, Result, Write};
use std::path::Path;
use crate::file_handler::{File, FileType, Strategy as FileHandlerStrategy};
@@ -17,11 +17,17 @@ impl Strategy {
}
fn get_title(line: &str) -> &str {
- line.split_once("--- title:").unwrap().1
+ match line.split_once("--- title:") {
+ Some(title_line) => title_line.1,
+ None => "",
+ }
}
fn get_description(line: &str) -> &str {
- line.split_once("--- description:").unwrap().1
+ match line.split_once("--- description:") {
+ Some(description_line) => description_line.1,
+ None => "",
+ }
}
}
@@ -41,8 +47,14 @@ impl FileHandlerStrategy for Strategy {
matches!(file_type, FileType::Gemini)
}
- fn handle_html(&self, source: &Path, destination: &Path, file: &File, layout: &str) {
- let gemini_contents = read_to_string(&file.path).unwrap();
+ fn handle_html(
+ &self,
+ source: &Path,
+ destination: &Path,
+ file: &File,
+ layout: &str,
+ ) -> Result<()> {
+ let gemini_contents = read_to_string(&file.path)?;
// Front matter extraction
let lines: Vec<&str> = gemini_contents.split('\n').collect();
@@ -59,12 +71,12 @@ impl FileHandlerStrategy for Strategy {
if Strategy::is_description(line) {
description = Strategy::get_description(line).trim();
lines_found += 1;
- continue;
}
}
}
- let gemini_source = lines[lines_found..].join("\n");
+ let gemini_lines = lines.get(lines_found..).unwrap_or(&[]);
+ let gemini_source = gemini_lines.join("\n");
let content_html = render_html(&parse(&gemini_source[..]));
let generated_html = layout
@@ -72,20 +84,30 @@ impl FileHandlerStrategy for Strategy {
.replace("{{ description }}", description)
.replace("{{ content }}", &content_html[..]);
- let relative_path = file.path.strip_prefix(source).unwrap();
+ let relative_path = file.path.strip_prefix(source).map_err(|_| {
+ Error::new(
+ ErrorKind::InvalidData,
+ "Path was not part of source directory.",
+ )
+ })?;
let mut complete_destination = destination.join(relative_path);
complete_destination.set_extension("html");
- let destination_parent = complete_destination.parent().unwrap();
- create_dir_all(destination_parent).unwrap();
-
- let mut destination_file = IOFile::create(&complete_destination).unwrap();
- destination_file
- .write_all(generated_html.as_bytes())
- .unwrap();
+ match complete_destination.parent() {
+ Some(destination_parent) => {
+ create_dir_all(destination_parent)?;
+ let mut destination_file = IOFile::create(&complete_destination)?;
+ destination_file.write_all(generated_html.as_bytes())?;
+ Ok(())
+ }
+ None => Err(Error::new(
+ ErrorKind::InvalidData,
+ "Destination parent was not readable.",
+ )),
+ }
}
- fn handle_gemini(&self, source: &Path, destination: &Path, file: &File) {
- let gemini_contents = read_to_string(&file.path).unwrap();
+ fn handle_gemini(&self, source: &Path, destination: &Path, file: &File) -> Result<()> {
+ let gemini_contents = read_to_string(&file.path)?;
// Front matter extraction
let lines: Vec<&str> = gemini_contents.split('\n').collect();
@@ -98,22 +120,32 @@ impl FileHandlerStrategy for Strategy {
}
if Strategy::is_description(line) {
lines_found += 1;
- continue;
}
}
}
- let gemini_source = lines[lines_found..].join("\n");
+ let gemini_lines = lines.get(lines_found..).unwrap_or(&[]);
+ let gemini_source = gemini_lines.join("\n");
- let relative_path = file.path.strip_prefix(source).unwrap();
+ let relative_path = file.path.strip_prefix(source).map_err(|_| {
+ Error::new(
+ ErrorKind::InvalidData,
+ "Path was not part of source directory.",
+ )
+ })?;
let complete_destination = destination.join(relative_path);
- let destination_parent = complete_destination.parent().unwrap();
- create_dir_all(destination_parent).unwrap();
-
- let mut destination_file = IOFile::create(&complete_destination).unwrap();
- destination_file
- .write_all(gemini_source.as_bytes())
- .unwrap();
+ match complete_destination.parent() {
+ Some(destination_parent) => {
+ create_dir_all(destination_parent)?;
+ let mut destination_file = IOFile::create(&complete_destination)?;
+ destination_file.write_all(gemini_source.as_bytes())?;
+ Ok(())
+ }
+ None => Err(Error::new(
+ ErrorKind::InvalidData,
+ "Destination parent was not readable.",
+ )),
+ }
}
}
@@ -151,6 +183,11 @@ mod tests {
}
#[test]
+ fn returns_empty_string_on_bad_title_extraction() {
+ assert_eq!(Strategy::get_title("--- mitle: Hello!").trim(), "");
+ }
+
+ #[test]
fn extracts_description() {
assert_eq!(
Strategy::get_description("--- description: What is this?").trim(),
@@ -159,6 +196,14 @@ mod tests {
}
#[test]
+ fn returns_empty_string_on_bad_description_extraction() {
+ assert_eq!(
+ Strategy::get_description("--- mescription: NOOO!").trim(),
+ ""
+ );
+ }
+
+ #[test]
fn identifies_gemini_file() {
let test_dir = setup_test_dir();
create_test_file(&test_dir.join("test.gmi"), "");
@@ -229,7 +274,11 @@ Hello world
file_type: FileType::Gemini,
};
- strategy.handle_html(&source_dir, &output_dir, &file, layout);
+ assert!(
+ strategy
+ .handle_html(&source_dir, &output_dir, &file, layout)
+ .is_ok()
+ );
let html_output = output_dir.join("test.html");
assert!(html_output.exists());
@@ -274,7 +323,11 @@ Hello world
file_type: FileType::Gemini,
};
- strategy.handle_gemini(&source_dir, &output_dir, &file);
+ assert!(
+ strategy
+ .handle_gemini(&source_dir, &output_dir, &file)
+ .is_ok()
+ );
let gemini_output = output_dir.join("test.gmi");
assert!(gemini_output.exists());
@@ -319,7 +372,11 @@ Hello world
file_type: FileType::Gemini,
};
- strategy.handle_html(&source_dir, &output_dir, &file, layout);
+ assert!(
+ strategy
+ .handle_html(&source_dir, &output_dir, &file, layout)
+ .is_ok()
+ );
let html_output = output_dir.join("nested/test.html");
assert!(html_output.exists());
diff --git a/src/file_handler/file_strategies/layout.rs b/src/file_handler/file_strategies/layout.rs
index 9a60f12..45ef665 100644
--- a/src/file_handler/file_strategies/layout.rs
+++ b/src/file_handler/file_strategies/layout.rs
@@ -1,5 +1,6 @@
pub struct Strategy {}
+use std::io::Result;
use std::path::Path;
use crate::file_handler::{File, FileType, Strategy as FileHandlerStrategy};
@@ -19,8 +20,12 @@ impl FileHandlerStrategy for Strategy {
// We don't implement handling for layout, as we assume there's only one
// and it got handled before.
- fn handle_html(&self, _s: &Path, _d: &Path, _f: &File, _l: &str) {}
- fn handle_gemini(&self, _s: &Path, _d: &Path, _f: &File) {}
+ fn handle_html(&self, _s: &Path, _d: &Path, _f: &File, _l: &str) -> Result<()> {
+ Ok(())
+ }
+ fn handle_gemini(&self, _s: &Path, _d: &Path, _f: &File) -> Result<()> {
+ Ok(())
+ }
}
#[cfg(test)]
@@ -101,11 +106,15 @@ mod tests {
file_type: FileType::Layout,
};
- strategy.handle_html(
- &PathBuf::from("source"),
- &PathBuf::from("dest"),
- &file,
- "layout content",
+ assert!(
+ strategy
+ .handle_html(
+ &PathBuf::from("source"),
+ &PathBuf::from("dest"),
+ &file,
+ "layout content",
+ )
+ .is_ok()
);
}
@@ -118,6 +127,10 @@ mod tests {
};
// Should not panic
- strategy.handle_gemini(&PathBuf::from("source"), &PathBuf::from("dest"), &file);
+ assert!(
+ strategy
+ .handle_gemini(&PathBuf::from("source"), &PathBuf::from("dest"), &file)
+ .is_ok()
+ );
}
}
diff --git a/src/file_handler/mod.rs b/src/file_handler/mod.rs
index 2bdbb74..f84d0d9 100644
--- a/src/file_handler/mod.rs
+++ b/src/file_handler/mod.rs
@@ -5,6 +5,7 @@ use file_strategies::gemini::Strategy as GeminiStrategy;
use file_strategies::layout::Strategy as LayoutStrategy;
use std::fs::read_to_string;
+use std::io::{Error, ErrorKind, Result};
use std::path::{Path, PathBuf};
pub struct FileHandler {
@@ -35,15 +36,18 @@ impl FileHandler {
FileType::Unknown
}
- pub fn get_layout_or_panic(&mut self, files: &[File]) -> Result<(), &str> {
+ pub fn get_layout(&mut self, files: &[File]) -> Result<()> {
for file in files {
if file.file_type == FileType::Layout {
- let layout_text = read_to_string(&file.path).unwrap();
+ let layout_text = read_to_string(&file.path)?;
self.layout = Some(layout_text);
return Ok(());
}
}
- Err("No layout found. Please ensure there's a _layout.html file at the root")
+ Err(Error::new(
+ ErrorKind::NotFound,
+ "No layout found. Please ensure there's a _layout.html file at the root",
+ ))
}
pub fn handle_all(
@@ -52,10 +56,11 @@ impl FileHandler {
html_destination: &Path,
gemini_destination: &Path,
files: &[File],
- ) {
+ ) -> Result<()> {
for file in files {
- self.handle(source, html_destination, gemini_destination, file);
+ self.handle(source, html_destination, gemini_destination, file)?;
}
+ Ok(())
}
pub fn handle(
@@ -64,18 +69,26 @@ impl FileHandler {
html_destination: &Path,
gemini_destination: &Path,
file: &File,
- ) {
- if let Some(strategy) = self
+ ) -> Result<()> {
+ match self
.strategies
.iter()
.find(|s| s.can_handle(&file.file_type))
{
- let layout = self
- .layout
- .as_ref()
- .expect("Layout should be initialized before handling files");
- strategy.handle_html(source, html_destination, file, layout);
- strategy.handle_gemini(source, gemini_destination, file);
+ Some(strategy) => {
+ let layout = self.layout.as_ref().ok_or_else(|| {
+ Error::new(
+ ErrorKind::NotFound,
+ "Layout should be initialized before handling files",
+ )
+ })?;
+ strategy.handle_html(source, html_destination, file, layout)?;
+ strategy.handle_gemini(source, gemini_destination, file)?;
+ Ok(())
+ }
+
+ // We silently ignore files we can't process.
+ None => Ok(()),
}
}
}
@@ -84,8 +97,14 @@ pub trait Strategy {
fn is(&self, path: &Path) -> bool;
fn identify(&self) -> FileType;
fn can_handle(&self, file_type: &FileType) -> bool;
- fn handle_html(&self, source: &Path, destination: &Path, file: &File, layout: &str);
- fn handle_gemini(&self, source: &Path, destination: &Path, file: &File);
+ fn handle_html(
+ &self,
+ source: &Path,
+ destination: &Path,
+ file: &File,
+ layout: &str,
+ ) -> Result<()>;
+ fn handle_gemini(&self, source: &Path, destination: &Path, file: &File) -> Result<()>;
}
#[derive(Debug, Clone, PartialEq)]
@@ -162,7 +181,7 @@ mod tests {
create_test_internal_file("regular.html", FileType::File),
];
- assert!(handler.get_layout_or_panic(&files).is_ok());
+ assert!(handler.get_layout(&files).is_ok());
}
#[test]
@@ -173,7 +192,7 @@ mod tests {
create_test_internal_file("regular.html", FileType::File),
];
- assert!(handler.get_layout_or_panic(&files).is_err());
+ assert!(handler.get_layout(&files).is_err());
}
// Mock strategy for testing
@@ -195,8 +214,57 @@ mod tests {
&self.file_type == file_type
}
- fn handle_html(&self, _source: &Path, _destination: &Path, _file: &File, _layout: &str) {}
- fn handle_gemini(&self, _source: &Path, _destination: &Path, _file: &File) {}
+ fn handle_html(
+ &self,
+ _source: &Path,
+ _destination: &Path,
+ _file: &File,
+ _layout: &str,
+ ) -> Result<()> {
+ Ok(())
+ }
+ fn handle_gemini(&self, _source: &Path, _destination: &Path, _file: &File) -> Result<()> {
+ Ok(())
+ }
+ }
+
+ // Mock strategy for testing
+ struct ErroringMockStrategy {
+ is_match: bool,
+ file_type: FileType,
+ }
+
+ impl Strategy for ErroringMockStrategy {
+ fn is(&self, _path: &Path) -> bool {
+ self.is_match
+ }
+
+ fn identify(&self) -> FileType {
+ self.file_type.clone()
+ }
+
+ fn can_handle(&self, file_type: &FileType) -> bool {
+ &self.file_type == file_type
+ }
+
+ fn handle_html(
+ &self,
+ _source: &Path,
+ _destination: &Path,
+ _file: &File,
+ _layout: &str,
+ ) -> Result<()> {
+ Err(Error::new(
+ ErrorKind::Other,
+ "Forced to fail by my cruel author.",
+ ))
+ }
+ fn handle_gemini(&self, _source: &Path, _destination: &Path, _file: &File) -> Result<()> {
+ Err(Error::new(
+ ErrorKind::Other,
+ "Forced to fail by my benevolent author.",
+ ))
+ }
}
#[test]
@@ -217,7 +285,48 @@ mod tests {
file_type: FileType::Gemini,
};
assert!(matches!(handler.identify(&path), FileType::Gemini));
- handler.handle(&path, &path, &path, &file);
+ assert!(handler.handle(&path, &path, &path, &file).is_ok());
+ }
+
+ #[test]
+ fn test_failure_propagates_in_handler() {
+ let mock_strategy = ErroringMockStrategy {
+ is_match: true,
+ file_type: FileType::Gemini,
+ };
+
+ let handler = FileHandler {
+ strategies: vec![Box::new(mock_strategy)],
+ layout: Some("None".to_string()),
+ };
+
+ let path = PathBuf::from("test.whatever");
+ let file = File {
+ path: path.clone(),
+ file_type: FileType::Gemini,
+ };
+ assert!(matches!(handler.identify(&path), FileType::Gemini));
+ assert!(handler.handle(&path, &path, &path, &file).is_err());
+ }
+
+ #[test]
+ fn silently_ignore_unhandleable_files() {
+ let mock_strategy = MockStrategy {
+ is_match: false,
+ file_type: FileType::Gemini,
+ };
+
+ let handler = FileHandler {
+ strategies: vec![Box::new(mock_strategy)],
+ layout: Some("None".to_string()),
+ };
+
+ let path = PathBuf::from("test.whatever");
+ let file = File {
+ path: path.clone(),
+ file_type: FileType::Layout,
+ };
+ assert!(handler.handle(&path, &path, &path, &file).is_ok());
}
#[test]
@@ -226,11 +335,15 @@ mod tests {
let files: Vec<File> = vec![];
// Should not panic with empty vector
- handler.handle_all(
- &PathBuf::from("source"),
- &PathBuf::from("output_html"),
- &PathBuf::from("output_gemini"),
- &files,
+ assert!(
+ handler
+ .handle_all(
+ &PathBuf::from("source"),
+ &PathBuf::from("output_html"),
+ &PathBuf::from("output_gemini"),
+ &files,
+ )
+ .is_ok()
);
}
@@ -256,25 +369,32 @@ mod tests {
);
// Should not panic with valid layout
- handler.handle(
- &test_dir,
- &test_dir.join("output_html"),
- &test_dir.join("output_gemini"),
- &file,
+ assert!(
+ handler
+ .handle(
+ &test_dir,
+ &test_dir.join("output_html"),
+ &test_dir.join("output_gemini"),
+ &file,
+ )
+ .is_ok()
);
}
#[test]
- #[should_panic(expected = "Layout should be initialized before handling files")]
fn test_handle_without_layout() {
let handler = FileHandler::default();
let file = create_test_internal_file("test.gmi", FileType::Gemini);
- handler.handle(
- &PathBuf::from("source"),
- &PathBuf::from("output_html"),
- &PathBuf::from("output_gemini"),
- &file,
+ assert!(
+ handler
+ .handle(
+ &PathBuf::from("source"),
+ &PathBuf::from("output_html"),
+ &PathBuf::from("output_gemini"),
+ &file,
+ )
+ .is_err()
);
}
@@ -320,14 +440,18 @@ mod tests {
),
];
- let _ = handler.get_layout_or_panic(&files[1..]);
+ let _ = handler.get_layout(&files[1..]);
// Test with slice
- handler.handle_all(
- &test_dir,
- &test_dir.join("output_html"),
- &test_dir.join("output_gemini"),
- &files[1..], // Test with slice of last three elements
+ assert!(
+ handler
+ .handle_all(
+ &test_dir,
+ &test_dir.join("output_html"),
+ &test_dir.join("output_gemini"),
+ &files[1..], // Test with slice of last three elements
+ )
+ .is_ok()
);
}
}
diff --git a/src/main.rs b/src/main.rs
index 0414288..9dd296a 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -3,16 +3,28 @@ mod file_handler;
use std::env::current_dir;
use std::fs::{create_dir_all, remove_dir_all};
-use std::io::Result;
-use std::process::exit;
+use std::io::{Error, ErrorKind, Result};
use crate::file_finder::find_files;
use crate::file_handler::FileHandler;
fn main() -> Result<()> {
let source = current_dir()?;
- let source_name = source.file_name().unwrap().to_string_lossy();
- let parent = source.parent().unwrap();
+ let source_name = source
+ .file_name()
+ .ok_or_else(|| {
+ Error::new(
+ ErrorKind::InvalidData,
+ "Could not read current directory name.",
+ )
+ })?
+ .to_string_lossy();
+ let parent = source.parent().ok_or_else(|| {
+ Error::new(
+ ErrorKind::InvalidData,
+ "Current directory parent was not readable.",
+ )
+ })?;
let gemini_destination_name = format!("{source_name}_gemini");
let gemini_destination = parent.join(gemini_destination_name);
let html_destination_name = format!("{source_name}_html");
@@ -23,22 +35,18 @@ fn main() -> Result<()> {
// Step 2. Load the layout
let mut file_handler = FileHandler::default();
- match file_handler.get_layout_or_panic(&files) {
- Ok(()) => {}
- Err(error) => {
- eprintln!("{error}");
- exit(1);
- }
- }
+ file_handler.get_layout(&files)?;
// Step 3. Prepare the target priority
let _ = remove_dir_all(&html_destination);
let _ = remove_dir_all(&gemini_destination);
- create_dir_all(&html_destination).expect("Could not create HTML directory.");
- create_dir_all(&gemini_destination).expect("Could not create Gemini directory.");
+ create_dir_all(&html_destination)
+ .map_err(|e| Error::new(e.kind(), "Could not create HTML directory."))?;
+ create_dir_all(&gemini_destination)
+ .map_err(|e| Error::new(e.kind(), "Could not create Gemini directory."))?;
// Step 4. Process all files
- file_handler.handle_all(&source, &html_destination, &gemini_destination, &files);
+ file_handler.handle_all(&source, &html_destination, &gemini_destination, &files)?;
Ok(())
}