use std::{
path::{Path, PathBuf},
str::FromStr,
};
use toml::value::Datetime;
use url::Url;
use super::{blog_post::BlogPost, Draft, DraftError};
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct DraftBuilder {
base_url: Url,
root: PathBuf,
path_or_file: Vec<PathBuf>,
minimum_date: Datetime,
allow_draft: bool,
}
#[cfg(test)]
impl DraftBuilder {
pub(crate) fn base_url(&self) -> &Url {
&self.base_url
}
pub(crate) fn root(&self) -> &PathBuf {
&self.root
}
}
impl DraftBuilder {
pub fn new(base_url: Url, root: Option<&PathBuf>) -> Self {
let root = if let Some(r) = root {
PathBuf::from(r)
} else {
PathBuf::new().join(".")
};
DraftBuilder {
base_url,
root,
path_or_file: Vec::new(),
minimum_date: super::today(),
allow_draft: false,
}
}
pub fn add_path_or_file<P: Into<PathBuf>>(
&mut self,
path_or_file: P,
) -> Result<&mut Self, DraftError> {
self.path_or_file.push(path_or_file.into());
Ok(self)
}
fn add_blog_posts(
path: &PathBuf,
min_date: Datetime,
allow_draft: bool,
base_url: &Url,
www_src_root: &Path,
) -> Result<Vec<BlogPost>, DraftError> {
use walkdir::WalkDir;
let mut blog_paths = Vec::new();
let walk_dir = www_src_root.join(path);
log::trace!("Walking directory at {}", walk_dir.display());
for entry in WalkDir::new(walk_dir).into_iter().flatten() {
log::debug!("Entry found: {entry:?}");
if entry.path().extension().unwrap_or_default() == "md" {
if let Some(file_name) = entry.path().file_name() {
if file_name.to_string_lossy().starts_with('_') {
log::debug!(
"Skipping file starting with '_': {}",
entry.path().display()
);
continue;
}
}
let entry_as_path = entry.into_path();
let Ok(path_to_blog) = entry_as_path.strip_prefix(www_src_root) else {
println!("Failed to strip prefix from `{}`", entry_as_path.display());
continue;
};
blog_paths.push(path_to_blog.to_path_buf());
}
}
log::trace!("Found blog paths:\n{blog_paths:#?}");
let mut blog_posts = Vec::new();
for blog_path in blog_paths {
match BlogPost::new(&blog_path, min_date, allow_draft, base_url, www_src_root) {
Ok(bp) => blog_posts.push(bp),
Err(e) => {
log::warn!("`{}` excluded because `{e}`", blog_path.display());
continue;
}
}
}
Ok(blog_posts)
}
pub fn with_minimum_date(&mut self, minimum_date: &str) -> Result<&mut Self, DraftError> {
let minimum_date = Datetime::from_str(minimum_date)?;
self.minimum_date = minimum_date;
Ok(self)
}
pub fn with_allow_draft(&mut self, allow_draft: bool) -> &mut Self {
self.allow_draft = allow_draft;
self
}
pub async fn build(&mut self) -> Result<Draft, DraftError> {
let mut blog_posts = Vec::new();
if self.path_or_file.is_empty() {
log::debug!("Add default path to the path list as it is empty.");
self.path_or_file = vec![crate::util::default_blog_dir()];
}
log::trace!("Paths and files to processes: {:?}", self.path_or_file);
for path in self.path_or_file.iter() {
let mut vec_fm = DraftBuilder::add_blog_posts(
path,
self.minimum_date,
self.allow_draft,
&self.base_url,
&self.root,
)?;
blog_posts.append(&mut vec_fm);
}
if blog_posts.is_empty() {
log::warn!("No blog posts found");
return Err(DraftError::BlogPostListEmpty);
}
Ok(Draft {
blog_posts,
bsky_store: crate::util::default_bluesky_dir(),
referrer_store: crate::util::default_referrer_dir(),
base_url: self.base_url.clone(),
root: self.root.to_path_buf(),
})
}
}
#[cfg(test)]
mod tests {
use std::{
error::Error,
io,
path::{Path, PathBuf},
};
use toml::value::Date;
use url::Url;
use super::*;
use crate::draft::today;
fn gdb() -> DraftBuilder {
let base_url = Url::parse("https://www.example.com/").unwrap();
Draft::builder(base_url, None)
}
fn ged(year: u16, month: u8, day: u8) -> Option<Date> {
Some(Date { year, month, day })
}
#[test]
fn test_future_capacity_too_large_display() {
let error = DraftError::FutureCapacityTooLarge;
assert_eq!(error.to_string(), "Future capacity is too large");
}
#[test]
fn test_future_capacity_too_large_debug() {
let error = DraftError::FutureCapacityTooLarge;
let debug_str = format!("{error:?}");
assert!(debug_str.contains("FutureCapacityTooLarge"));
}
#[test]
fn test_path_not_found_display() {
let path = "/some/missing/path".to_string();
let error = DraftError::PathNotFound(path.clone());
assert_eq!(error.to_string(), format!("path not found: `{path}`"));
}
#[test]
fn test_path_not_found_with_various_paths() {
let test_paths = vec![
"/absolute/unix/path",
"relative/path",
"C:\\Windows\\Path",
"",
"path with spaces",
"path/with/unicode/🦀",
];
for path in test_paths {
let error = DraftError::PathNotFound(path.to_string());
let expected = format!("path not found: `{path}`");
assert_eq!(error.to_string(), expected);
}
}
#[test]
fn test_file_extension_invalid_display() {
let file_path = "document.txt".to_string();
let expected_ext = ".md".to_string();
let error = DraftError::FileExtensionInvalid(file_path.clone(), expected_ext.clone());
assert_eq!(
error.to_string(),
format!("file extension invalid (must be `{expected_ext}`): {file_path}")
);
}
#[test]
fn test_file_extension_invalid_various_combinations() {
let test_cases = vec![
("file.txt", ".md"),
("document.doc", ".markdown"),
("image.jpg", ".png"),
("no_extension", ".md"),
("file.TAR.GZ", ".zip"),
];
for (file, expected) in test_cases {
let error = DraftError::FileExtensionInvalid(file.to_string(), expected.to_string());
let expected_msg = format!("file extension invalid (must be `{expected}`): {file}");
assert_eq!(error.to_string(), expected_msg);
}
}
#[test]
fn test_blog_post_list_empty_display() {
let error = DraftError::BlogPostListEmpty;
assert_eq!(error.to_string(), "blog post list is empty");
}
#[test]
fn test_qualified_blog_post_list_empty_display() {
let error = DraftError::QualifiedBlogPostListEmpty;
assert_eq!(
error.to_string(),
"blog post list is empty after qualifications have been applied"
);
}
#[test]
fn test_io_error_from_conversion() {
let io_error = io::Error::new(io::ErrorKind::NotFound, "File not found");
let draft_error: DraftError = io_error.into();
match draft_error {
DraftError::Io(inner_error) => {
assert_eq!(inner_error.kind(), io::ErrorKind::NotFound);
assert_eq!(inner_error.to_string(), "File not found");
}
_ => panic!("Expected DraftError::Io variant"),
}
}
#[test]
fn test_io_error_display() {
let io_error = io::Error::new(io::ErrorKind::PermissionDenied, "Access denied");
let draft_error = DraftError::Io(io_error);
let error_string = draft_error.to_string();
assert!(error_string.contains("io error:"));
assert!(error_string.contains("Access denied"));
}
#[test]
fn test_io_error_various_kinds() {
let io_errors = vec![
io::Error::new(io::ErrorKind::NotFound, "Not found"),
io::Error::new(io::ErrorKind::PermissionDenied, "Permission denied"),
io::Error::new(io::ErrorKind::ConnectionRefused, "Connection refused"),
io::Error::new(io::ErrorKind::InvalidData, "Invalid data"),
];
for io_error in io_errors {
let expected_msg = io_error.to_string();
let draft_error = DraftError::Io(io_error);
assert!(draft_error.to_string().contains("io error:"));
assert!(draft_error.to_string().contains(&expected_msg));
}
}
#[test]
fn test_url_parse_error_from_conversion() {
let url_result = url::Url::parse("not-a-valid-url");
assert!(url_result.is_err());
let url_error = url_result.unwrap_err();
let draft_error: DraftError = url_error.into();
match draft_error {
DraftError::UrlParse(_) => {
}
_ => panic!("Expected DraftError::UrlParse variant"),
}
}
#[test]
fn test_url_parse_error_display() {
let url_result = url::Url::parse("h://invalid::url");
let url_error = url_result.unwrap_err();
let draft_error = DraftError::UrlParse(url_error);
let error_string = draft_error.to_string();
assert!(error_string.contains("URL parse error:"));
}
#[test]
fn test_toml_datetime_parse_error_from_conversion() {
let datetime_result = "invalid-datetime".parse::<toml::value::Datetime>();
assert!(datetime_result.is_err());
let datetime_error = datetime_result.unwrap_err();
let draft_error: DraftError = datetime_error.into();
match draft_error {
DraftError::TomlDatetimeParse(_) => {
}
_ => panic!("Expected DraftError::TomlDatetimeParse variant"),
}
}
#[test]
fn test_toml_datetime_parse_error_display() {
let datetime_result = "not-a-date".parse::<toml::value::Datetime>();
let datetime_error = datetime_result.unwrap_err();
let draft_error = DraftError::TomlDatetimeParse(datetime_error);
let error_string = draft_error.to_string();
assert!(error_string.contains("TOML datetime parse error:"));
}
#[test]
fn test_error_trait_implementation() {
let error = DraftError::BlogPostListEmpty;
let _: &dyn Error = &error;
assert!(error.source().is_none());
}
#[test]
fn test_error_trait_with_source() {
let io_error = io::Error::new(io::ErrorKind::NotFound, "Test error");
let draft_error = DraftError::Io(io_error);
let source = draft_error.source();
assert!(source.is_some());
let source_error = source.unwrap();
assert!(source_error.is::<io::Error>());
}
#[test]
fn test_debug_implementation() {
let error = DraftError::PathNotFound("test/path".to_string());
let debug_output = format!("{error:?}");
assert!(debug_output.contains("PathNotFound"));
assert!(debug_output.contains("test/path"));
}
#[test]
fn test_equality_for_simple_variants() {
let error1 = DraftError::FutureCapacityTooLarge;
let error2 = DraftError::FutureCapacityTooLarge;
assert_eq!(error1.to_string(), error2.to_string());
assert_eq!(format!("{error1:?}"), format!("{:?}", error2));
}
#[test]
fn test_error_message_consistency() {
let errors = vec![
DraftError::FutureCapacityTooLarge,
DraftError::PathNotFound("test".to_string()),
DraftError::FileExtensionInvalid("file.txt".to_string(), ".md".to_string()),
DraftError::BlogPostListEmpty,
DraftError::QualifiedBlogPostListEmpty,
];
for error in errors {
let message = error.to_string();
assert!(!message.is_empty());
assert!(!message.ends_with('.'));
assert!(!message.ends_with('!'));
}
}
#[test]
fn test_from_trait_implementations() {
let io_error = io::Error::new(io::ErrorKind::NotFound, "test");
let draft_error: DraftError = DraftError::from(io_error);
assert!(matches!(draft_error, DraftError::Io(_)));
let url_error = url::Url::parse("invalid").unwrap_err();
let draft_error: DraftError = DraftError::from(url_error);
assert!(matches!(draft_error, DraftError::UrlParse(_)));
let datetime_error = "invalid".parse::<toml::value::Datetime>().unwrap_err();
let draft_error: DraftError = DraftError::from(datetime_error);
assert!(matches!(draft_error, DraftError::TomlDatetimeParse(_)));
}
#[test]
fn test_non_exhaustive_attribute() {
let error = DraftError::BlogPostListEmpty;
#[allow(unreachable_patterns)]
let true_branch = match error {
DraftError::FutureCapacityTooLarge => false,
DraftError::PathNotFound(_) => false,
DraftError::FileExtensionInvalid(_, _) => false,
DraftError::BlogPostListEmpty => true,
DraftError::QualifiedBlogPostListEmpty => false,
DraftError::Io(_) => false,
DraftError::UrlParse(_) => false,
DraftError::TomlDatetimeParse(_) => false,
_ => false,
};
assert!(true_branch);
}
#[test]
fn test_add_path_or_file_with_string_literal() {
let mut builder = gdb();
let result = builder.add_path_or_file("test/path.txt");
assert!(result.is_ok());
assert_eq!(builder.path_or_file.len(), 1);
assert_eq!(builder.path_or_file[0], PathBuf::from("test/path.txt"));
}
#[test]
fn test_add_path_or_file_with_string() {
let mut builder = gdb();
let path_string = String::from("/home/user/document.md");
let result = builder.add_path_or_file(path_string);
assert!(result.is_ok());
assert_eq!(builder.path_or_file.len(), 1);
assert_eq!(
builder.path_or_file[0],
PathBuf::from("/home/user/document.md")
);
}
#[test]
fn test_add_path_or_file_with_pathbuf() {
let mut builder = gdb();
let path_buf = PathBuf::from("src/main.rs");
let expected_path = path_buf.clone();
let result = builder.add_path_or_file(path_buf);
assert!(result.is_ok());
assert_eq!(builder.path_or_file.len(), 1);
assert_eq!(builder.path_or_file[0], expected_path);
}
#[test]
fn test_add_path_or_file_with_path_ref() {
let mut builder = gdb();
let path = Path::new("config/settings.json");
let result = builder.add_path_or_file(path);
assert!(result.is_ok());
assert_eq!(builder.path_or_file.len(), 1);
assert_eq!(
builder.path_or_file[0],
PathBuf::from("config/settings.json")
);
}
#[test]
fn test_add_path_or_file_multiple_paths() {
let mut builder = gdb();
builder.add_path_or_file("file1.txt").unwrap();
builder.add_path_or_file("file2.txt").unwrap();
builder.add_path_or_file("dir/file3.txt").unwrap();
assert_eq!(builder.path_or_file.len(), 3);
assert_eq!(builder.path_or_file[0], PathBuf::from("file1.txt"));
assert_eq!(builder.path_or_file[1], PathBuf::from("file2.txt"));
assert_eq!(builder.path_or_file[2], PathBuf::from("dir/file3.txt"));
}
#[test]
fn test_add_path_or_file_returns_mutable_reference() {
let mut builder = gdb();
let result = builder.add_path_or_file("test.txt");
assert!(result.is_ok());
let builder_ref = result.unwrap();
let second_result = builder_ref.add_path_or_file("test2.txt");
assert!(second_result.is_ok());
assert_eq!(builder.path_or_file.len(), 2);
}
#[test]
fn test_add_path_or_file_method_chaining() {
let mut builder = gdb();
let result = builder
.add_path_or_file("path1.txt")
.and_then(|b| b.add_path_or_file("path2.txt"))
.and_then(|b| b.add_path_or_file("path3.txt"));
assert!(result.is_ok());
assert_eq!(builder.path_or_file.len(), 3);
assert_eq!(builder.path_or_file[0], PathBuf::from("path1.txt"));
assert_eq!(builder.path_or_file[1], PathBuf::from("path2.txt"));
assert_eq!(builder.path_or_file[2], PathBuf::from("path3.txt"));
}
#[test]
fn test_add_path_or_file_empty_path() {
let mut builder = gdb();
let result = builder.add_path_or_file("");
assert!(result.is_ok());
assert_eq!(builder.path_or_file.len(), 1);
assert_eq!(builder.path_or_file[0], PathBuf::from(""));
}
#[test]
fn test_add_path_or_file_absolute_paths() {
let mut builder = gdb();
#[cfg(unix)]
let absolute_path = "/usr/local/bin/tool";
#[cfg(windows)]
let absolute_path = r"C:\Program Files\Tool\tool.exe";
let result = builder.add_path_or_file(absolute_path);
assert!(result.is_ok());
assert_eq!(builder.path_or_file.len(), 1);
assert_eq!(builder.path_or_file[0], PathBuf::from(absolute_path));
}
#[test]
fn test_add_path_or_file_relative_paths() {
let mut builder = gdb();
let relative_paths = vec![
"./current_dir/file.txt",
"../parent_dir/file.txt",
"../../grandparent/file.txt",
"subdir/file.txt",
];
for path in &relative_paths {
builder.add_path_or_file(*path).unwrap();
}
assert_eq!(builder.path_or_file.len(), relative_paths.len());
for (i, expected_path) in relative_paths.iter().enumerate() {
assert_eq!(builder.path_or_file[i], PathBuf::from(expected_path));
}
}
#[test]
fn test_add_path_or_file_special_characters() {
let mut builder = gdb();
let special_paths = vec![
"file with spaces.txt",
"file-with-dashes.txt",
"file_with_underscores.txt",
"file.with.dots.txt",
"file[with]brackets.txt",
];
for path in &special_paths {
let result = builder.add_path_or_file(*path);
assert!(result.is_ok(), "Failed to add path: {path}");
}
assert_eq!(builder.path_or_file.len(), special_paths.len());
for (i, expected_path) in special_paths.iter().enumerate() {
assert_eq!(builder.path_or_file[i], PathBuf::from(expected_path));
}
}
#[test]
fn test_add_path_or_file_preserves_order() {
let mut builder = gdb();
let paths_in_order = vec!["first.txt", "second.txt", "third.txt", "fourth.txt"];
for path in &paths_in_order {
builder.add_path_or_file(*path).unwrap();
}
for (i, expected_path) in paths_in_order.iter().enumerate() {
assert_eq!(builder.path_or_file[i], PathBuf::from(expected_path));
}
}
#[test]
fn test_add_path_or_file_duplicate_paths() {
let mut builder = gdb();
builder.add_path_or_file("duplicate.txt").unwrap();
builder.add_path_or_file("duplicate.txt").unwrap();
builder.add_path_or_file("duplicate.txt").unwrap();
assert_eq!(builder.path_or_file.len(), 3);
for path in &builder.path_or_file {
assert_eq!(*path, PathBuf::from("duplicate.txt"));
}
}
#[test]
fn test_add_path_or_file_mixed_types_in_sequence() {
let mut builder = gdb();
builder.add_path_or_file("string_literal.txt").unwrap();
builder
.add_path_or_file(String::from("owned_string.txt"))
.unwrap();
builder
.add_path_or_file(PathBuf::from("pathbuf.txt"))
.unwrap();
builder.add_path_or_file(Path::new("path_ref.txt")).unwrap();
assert_eq!(builder.path_or_file.len(), 4);
assert_eq!(builder.path_or_file[0], PathBuf::from("string_literal.txt"));
assert_eq!(builder.path_or_file[1], PathBuf::from("owned_string.txt"));
assert_eq!(builder.path_or_file[2], PathBuf::from("pathbuf.txt"));
assert_eq!(builder.path_or_file[3], PathBuf::from("path_ref.txt"));
}
#[test]
fn test_add_path_or_file_builder_state_after_success() {
let mut builder = gdb();
assert_eq!(builder.path_or_file.len(), 0);
builder.add_path_or_file("first.txt").unwrap();
assert_eq!(builder.path_or_file.len(), 1);
builder.add_path_or_file("second.txt").unwrap();
assert_eq!(builder.path_or_file.len(), 2);
assert_eq!(builder.path_or_file[0], PathBuf::from("first.txt"));
assert_eq!(builder.path_or_file[1], PathBuf::from("second.txt"));
}
#[test]
fn test_with_minimum_date_valid_date() {
let mut builder = gdb();
let result = builder.with_minimum_date("2023-12-25");
let expected_date = ged(2023, 12, 25);
assert!(result.is_ok());
assert_eq!(builder.minimum_date.date, expected_date);
}
#[test]
fn test_with_minimum_date_different_valid_formats() {
let mut builder = gdb();
let expected_dates = [
ged(2023, 1, 1),
ged(2024, 2, 29), ged(1999, 12, 31),
ged(2000, 6, 15),
];
let valid_dates = [
"2023-01-01",
"2024-02-29", "1999-12-31",
"2000-06-15",
];
for (i, date) in valid_dates.iter().enumerate() {
let result = builder.with_minimum_date(date);
assert!(result.is_ok(), "Date '{date}' should be valid");
assert_eq!(builder.minimum_date.date, expected_dates[i]);
}
}
#[test]
fn test_with_minimum_date_returns_mutable_reference() {
let mut builder = gdb();
let result = builder.with_minimum_date("2023-05-15").unwrap();
assert_eq!(result.minimum_date.date, ged(2023, 5, 15));
let chained_result = result.with_minimum_date("2023-06-20");
assert!(chained_result.is_ok());
assert_eq!(result.minimum_date.date, ged(2023, 6, 20));
}
#[test]
fn test_with_minimum_date_method_chaining_success() {
let mut builder = gdb();
let result = builder
.with_minimum_date("2023-01-01")
.and_then(|b| b.with_minimum_date("2023-12-31"));
assert!(result.is_ok());
assert_eq!(builder.minimum_date.date, ged(2023, 12, 31));
}
#[test]
fn test_with_minimum_date_empty_string() {
let mut builder = gdb();
let original_date = builder.minimum_date;
let result = builder.with_minimum_date("");
assert!(result.is_err());
assert_eq!(
result.expect_err("expecting failure").to_string(),
"TOML datetime parse error: invalid datetime, expected year or hour".to_string()
);
assert_eq!(builder.minimum_date, original_date);
}
#[test]
fn test_with_minimum_date_invalid_format() {
let mut builder = gdb();
let original_date = builder.minimum_date;
let invalid_dates = vec!["invalid", "2023", "not-a-date", "123", "abcd-ef-gh"];
for invalid_date in invalid_dates {
let result = builder.with_minimum_date(invalid_date);
assert!(result.is_err(), "Date '{invalid_date}' should be invalid");
let valid_error = matches!(
result.expect_err("certain of failure"),
DraftError::TomlDatetimeParse(_)
);
assert!(valid_error);
assert_eq!(builder.minimum_date, original_date);
}
}
#[test]
fn test_with_minimum_date_updates_field() {
let mut builder = gdb();
let initial_date = builder.minimum_date;
assert_eq!(builder.minimum_date, today());
let result = builder.with_minimum_date("2024-03-15");
assert!(result.is_ok());
assert_ne!(builder.minimum_date, initial_date);
assert_eq!(builder.minimum_date.date, ged(2024, 3, 15));
}
#[test]
fn test_with_minimum_date_multiple_successful_calls() {
let mut builder = gdb();
builder.with_minimum_date("2023-01-01").unwrap();
assert_eq!(builder.minimum_date.date, ged(2023, 1, 1));
builder.with_minimum_date("2023-06-15").unwrap();
assert_eq!(builder.minimum_date.date, ged(2023, 6, 15));
builder.with_minimum_date("2023-12-31").unwrap();
assert_eq!(builder.minimum_date.date, ged(2023, 12, 31));
}
#[test]
fn test_with_minimum_date_error_preserves_state() {
let mut builder = gdb();
builder.with_minimum_date("2023-05-20").unwrap();
let valid_date = builder.minimum_date;
let result = builder.with_minimum_date("invalid-date");
assert!(result.is_err());
assert_eq!(builder.minimum_date, valid_date);
assert_eq!(builder.minimum_date.date, ged(2023, 5, 20));
}
#[test]
fn test_with_minimum_date_error_types() {
let mut builder = gdb();
let result = builder.with_minimum_date("");
match result {
Err(DraftError::TomlDatetimeParse(_)) => {
}
_ => panic!("Expected InvalidDateFormat error"),
}
}
#[tokio::test]
async fn test_with_min_date_for_valid_date() {
let min_date = "2025-08-04";
let mut builder = gdb();
let post_res = builder.with_minimum_date(min_date).unwrap().build().await;
let error = post_res.expect_err("Expecting error as incomplete");
assert_eq!("blog post list is empty".to_string(), error.to_string());
}
#[test]
fn test_with_allow_draft_sets_true() {
let mut builder = gdb();
assert!(!builder.allow_draft);
builder.with_allow_draft(true);
assert!(builder.allow_draft);
}
#[test]
fn test_with_allow_draft_sets_false() {
let mut builder = gdb();
builder.allow_draft = true;
builder.with_allow_draft(false);
assert!(!builder.allow_draft);
}
#[test]
fn test_with_allow_draft_returns_mutable_reference() {
let mut builder = gdb();
let result = builder.with_allow_draft(true);
assert!(result.allow_draft);
result.with_allow_draft(false);
assert!(!result.allow_draft);
}
#[test]
fn test_with_allow_draft_method_chaining() {
let mut builder = gdb();
builder
.with_allow_draft(true)
.with_allow_draft(false)
.with_allow_draft(true);
assert!(builder.allow_draft);
}
#[test]
fn test_with_allow_draft_multiple_calls() {
let mut builder = gdb();
builder.with_allow_draft(true);
assert!(builder.allow_draft);
builder.with_allow_draft(false);
assert!(!builder.allow_draft);
builder.with_allow_draft(true);
assert!(builder.allow_draft);
}
#[test]
fn test_with_allow_draft_idempotent() {
let mut builder = gdb();
builder.with_allow_draft(true);
builder.with_allow_draft(true);
builder.with_allow_draft(true);
assert!(builder.allow_draft);
builder.with_allow_draft(false);
builder.with_allow_draft(false);
builder.with_allow_draft(false);
assert!(!builder.allow_draft);
}
#[tokio::test]
async fn test_excludes_files_starting_with_underscore() {
use std::fs;
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let content_dir = temp_dir.path().join("content").join("blog");
fs::create_dir_all(&content_dir).unwrap();
let regular_file = content_dir.join("regular-post.md");
let index_file = content_dir.join("_index.md");
let underscore_file = content_dir.join("_draft.md");
let valid_content = r#"+++
title = "Test Post"
description = "A test post"
date = 2030-01-01
draft = false
[taxonomies]
tags = ["test"]
+++
Test content"#;
fs::write(®ular_file, valid_content).unwrap();
fs::write(&index_file, valid_content).unwrap();
fs::write(&underscore_file, valid_content).unwrap();
let base_url = Url::parse("https://www.example.com/").unwrap();
let mut builder = Draft::builder(base_url, Some(&temp_dir.path().to_path_buf()));
builder
.add_path_or_file("content/blog")
.unwrap()
.with_allow_draft(true);
let result = builder.build().await;
let draft = result.expect("Failed to build draft");
assert_eq!(draft.blog_posts.len(), 1);
assert_eq!(draft.blog_posts[0].title(), "Test Post");
}
}