aboutsummaryrefslogtreecommitdiff
path: root/src/file_handler
diff options
context:
space:
mode:
Diffstat (limited to 'src/file_handler')
-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
4 files changed, 297 insertions, 89 deletions
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()
);
}
}