From 8bdfcaa1d8a4051c53e3f27997dedeee949e1b9d Mon Sep 17 00:00:00 2001 From: Nathan McCarty Date: Sat, 18 Mar 2023 16:12:14 -0400 Subject: [PATCH] feat: Read site repository state Add a `read` method to Site, Post, and Page that reads the current state of the site repositor/post/page off of the disk. Change to using btree collections for deterministic ordering, and make Site, Post, and Page `Eq` for testability purposes. --- Cargo.lock | 82 ++++++++- Cargo.toml | 4 + src/site.rs | 352 +++++++++++++++++++++++++++++--------- src/site/config.rs | 4 +- src/site/page.rs | 97 ++++++++++- src/site/post.rs | 97 ++++++++++- tests/read-write-state.rs | 21 +++ 7 files changed, 555 insertions(+), 102 deletions(-) create mode 100644 tests/read-write-state.rs diff --git a/Cargo.lock b/Cargo.lock index 1d5b023..6c0399b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -389,6 +389,15 @@ dependencies = [ "once_cell", ] +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + [[package]] name = "form_urlencoded" version = "1.1.0" @@ -512,6 +521,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + [[package]] name = "io-lifetimes" version = "1.0.6" @@ -519,7 +537,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfa919a82ea574332e2de6e74b4c36e74d41982b335080fa59d4ef31be20fdf3" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.45.0", ] [[package]] @@ -531,7 +549,7 @@ dependencies = [ "hermit-abi", "io-lifetimes", "rustix", - "windows-sys", + "windows-sys 0.45.0", ] [[package]] @@ -849,6 +867,15 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + [[package]] name = "regex" version = "1.7.1" @@ -890,7 +917,16 @@ dependencies = [ "io-lifetimes", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.45.0", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", ] [[package]] @@ -997,8 +1033,10 @@ dependencies = [ "serde", "serde_dhall", "snafu", + "tempfile", "tracing", "tracing-subscriber", + "walkdir", ] [[package]] @@ -1018,6 +1056,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af18f7ae1acd354b992402e9ec5864359d693cd8a79dcbef59f76891701c1e95" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys 0.42.0", +] + [[package]] name = "termcolor" version = "1.2.0" @@ -1229,6 +1280,16 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "walkdir" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.10.0+wasi-snapshot-preview1" @@ -1320,6 +1381,21 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + [[package]] name = "windows-sys" version = "0.45.0" diff --git a/Cargo.toml b/Cargo.toml index 88b40fb..e44eaf6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,3 +17,7 @@ serde_dhall = { version = "0.12.1", default-features = false } snafu = "0.7.4" tracing = "0.1.37" tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } +walkdir = "2.3.3" + +[dev-dependencies] +tempfile = "3.4.0" diff --git a/src/site.rs b/src/site.rs index f532cac..46323ed 100644 --- a/src/site.rs +++ b/src/site.rs @@ -1,14 +1,15 @@ //! Management of on-disk layout of the source of a site use std::{ - collections::HashMap, + collections::{BTreeMap, BTreeSet}, fs::{create_dir_all, File}, io::Write, path::{Path, PathBuf}, }; use snafu::{ensure, ResultExt, Snafu}; -use tracing::{debug, info, info_span}; +use tracing::{debug, info, info_span, warn}; +use walkdir::WalkDir; use self::{config::Config, page::Page, post::Post}; @@ -16,108 +17,37 @@ pub mod config; pub mod page; pub mod post; -/// Error encountered interacting with a [`Site`] -#[derive(Snafu, Debug)] -#[snafu(visibility(pub(crate)))] -pub enum SiteError { - /// Error checking to see if a path exists - #[snafu(display("Error checking if path exists: {:?}", path))] - ExistanceCheck { - /// Path being checked - path: PathBuf, - /// Underlying error - source: std::io::Error, - }, - /// Error creating a directory - #[snafu(display("Error creating directory:: {:?}", path))] - CreateDirectory { - /// Path being checked - path: PathBuf, - /// Underlying error - source: std::io::Error, - }, - /// Site repository dir was not a directory - #[snafu(display("Site repository path was not a directory: {:?}", path))] - NotADirectory { - /// Path being checked - path: PathBuf, - }, - /// Failed to reify the configuration - ConfigReify { - /// The underlying error - #[snafu(source(from(serde_dhall::Error, Box::new)))] - source: Box, - }, - /// Failed to write out the configuration - WriteConfig { - /// The path we tried to write the config to - path: PathBuf, - /// The underlying error - source: std::io::Error, - }, - /// Failed to write out a page stub - WritePageStub { - /// The path we tried to write the page stub to - path: PathBuf, - /// The underlying error - source: std::io::Error, - }, - /// Error writing out a page - #[snafu(display("Error writing out page {:?}", page))] - PageWrite { - /// The page we were trying to write out - page: PathBuf, - /// The underlying error - #[snafu(source(from(SiteError, Box::new)))] - source: Box, - }, - /// Failed to write out a post stub - WritePostStub { - /// The path we tried to write the page stub to - path: PathBuf, - /// The underlying error - source: std::io::Error, - }, - /// Error writing out a post - #[snafu(display("Error writing out post {:?}", post))] - PostWrite { - /// The page we were trying to write out - post: PathBuf, - /// The underlying error - #[snafu(source(from(SiteError, Box::new)))] - source: Box, - }, -} - /// Representation of the on-disk structure of a site -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] pub struct Site { /// Top level configuration pub config: Config, /// Non-post static pages - pub pages: HashMap, + pub pages: BTreeMap, /// Posts - pub posts: HashMap, + pub posts: BTreeMap, /// Static content /// /// Path is relative to `statics` directory - pub statics: Vec, + pub statics: BTreeSet, /// Stylesheets - pub styles: Vec, + /// + /// Path is relative to the styles directory + pub styles: BTreeSet, } impl Default for Site { fn default() -> Self { - let mut pages = HashMap::new(); + let mut pages = BTreeMap::new(); pages.insert("index".into(), Page::default()); - let mut posts = HashMap::new(); + let mut posts = BTreeMap::new(); posts.insert("new-blog-who-this".into(), Post::default()); Self { config: Config::default(), pages, posts, - statics: Vec::new(), - styles: Vec::new(), + statics: BTreeSet::new(), + styles: BTreeSet::new(), } } } @@ -129,7 +59,7 @@ impl Site { /// /// Will return an error if serialization fails, the user does not have write permissions for /// the directory, or any other IO errors occur while writing out the config. - pub fn write(&self, site_dir: impl AsRef) -> Result<(), SiteError> { + pub fn write(&mut self, site_dir: impl AsRef) -> Result<(), SiteError> { let site_dir = site_dir.as_ref(); // Setup logging let span = info_span!("Site::write", ?site_dir); @@ -185,6 +115,8 @@ impl Site { // Touch a .gitkeep let git_keep = statics_dir.join(".gitkeep"); File::create(&git_keep).context(CreateDirectorySnafu { path: &git_keep })?; + // Add the .gitkeep to the state + self.statics.insert(PathBuf::from(".gitkeep")); } // Create the gitignore let git_ignore_path = site_dir.join(".gitignore"); @@ -223,6 +155,7 @@ impl Site { .context(WriteConfigSnafu { path: &default_path, })?; + self.styles.insert(PathBuf::from("default.css")); // index let index_path = styles_dir.join("index.css"); debug!(?index_path, "Copying over index.css"); @@ -231,6 +164,7 @@ impl Site { index .write_all(include_bytes!("../assets/css/index.css")) .context(WriteConfigSnafu { path: &index_path })?; + self.styles.insert(PathBuf::from("index.css")); // post let post_path = styles_dir.join("post.css"); debug!(?post_path, "Copying over post.css"); @@ -238,8 +172,256 @@ impl Site { File::create(&post_path).context(WriteConfigSnafu { path: &post_path })?; post.write_all(include_bytes!("../assets/css/post.css")) .context(WriteConfigSnafu { path: &post_path })?; + self.styles.insert(PathBuf::from("post.css")); } Ok(()) } + + /// Reads out the configuration state for a `Site` from the provided directory + /// + /// # Errors + /// + /// Will return an error if the state of the given site is invalid, or of any other IO errors + /// occur while reading in the state + #[allow(clippy::too_many_lines)] + pub fn read(site_dir: impl AsRef) -> Result { + let site_dir = site_dir.as_ref(); + // Setup the logging + let span = info_span!("Site::read", ?site_dir); + let _enter = span.enter(); + info!("Reading site repository state"); + + // Read the configuration file + let config_path = site_dir.join("config.dhall"); + debug!(?config_path, "Reading main site configuration file"); + let config: Config = serde_dhall::from_file(&config_path) + .parse() + .context(ConfigReadSnafu { path: &config_path })?; + + // Read in the statics + let mut statics = BTreeSet::new(); + let statics_dir = site_dir.join("statics"); + let statics_iter = WalkDir::new(&statics_dir) + .into_iter() + .filter_map(std::result::Result::ok) + .filter(|e| e.path().is_file()); + for static_path in statics_iter { + statics.insert( + static_path + .path() + .strip_prefix(&statics_dir) + .expect("Invalid static?") + .to_owned(), + ); + } + + // Read in the styles + let mut styles = BTreeSet::new(); + let styles_dir = site_dir.join("styles"); + let styles_iter = WalkDir::new(&styles_dir) + .into_iter() + .filter_map(std::result::Result::ok) + .filter(|e| e.path().is_file()); + for style_path in styles_iter { + styles.insert( + style_path + .path() + .strip_prefix(&styles_dir) + .expect("Invalid style?") + .to_owned(), + ); + } + + // Read in the pages + let mut pages = BTreeMap::new(); + let pages_dir = site_dir.join("pages"); + debug!(?pages_dir, "Reading in pages"); + for dir in + std::fs::read_dir(&pages_dir).context(EnumerateDirectorySnafu { path: &pages_dir })? + { + let dir = dir.context(EnumerateDirectorySnafu { path: &pages_dir })?; + let dir_path = dir.path(); + // Make sure it is a directory + if dir + .metadata() + .context(EnumerateDirectorySnafu { path: &pages_dir })? + .is_dir() + { + // Extract the final component + if let Some(page_dir) = dir_path.file_name() { + let page = + Page::read(site_dir, page_dir).context(PageReadSnafu { page: page_dir })?; + pages.insert(PathBuf::from(page_dir), page); + } else { + warn!( + ?dir_path, + "Path in pages directory did not have a final component? ignorning" + ); + } + } else { + warn!( + ?dir_path, + "Item in pages directory was not a directory, ignoring" + ); + } + } + + // Read in the posts + let mut posts = BTreeMap::new(); + let posts_dir = site_dir.join("posts"); + debug!(?posts_dir, "Reading in posts"); + for dir in + std::fs::read_dir(&posts_dir).context(EnumerateDirectorySnafu { path: &posts_dir })? + { + let dir = dir.context(EnumerateDirectorySnafu { path: &posts_dir })?; + let dir_path = dir.path(); + // Make sure it is a directory + if dir + .metadata() + .context(EnumerateDirectorySnafu { path: &posts_dir })? + .is_dir() + { + // Extract the final component + if let Some(post_dir) = dir_path.file_name() { + let post = + Post::read(site_dir, post_dir).context(PostReadSnafu { post: post_dir })?; + posts.insert(PathBuf::from(post_dir), post); + } else { + warn!( + ?dir_path, + "Path in posts directory did not have a final component? ignorning" + ); + } + } else { + warn!( + ?dir_path, + "Item in posts directory was not a directory, ignoring" + ); + } + } + + Ok(Site { + config, + pages, + posts, + statics, + styles, + }) + } +} +/// Error encountered interacting with a [`Site`] +#[derive(Snafu, Debug)] +#[snafu(visibility(pub(crate)))] +pub enum SiteError { + /// Error checking to see if a path exists + #[snafu(display("Error checking if path exists: {:?}", path))] + ExistanceCheck { + /// Path being checked + path: PathBuf, + /// Underlying error + source: std::io::Error, + }, + /// Error creating a directory + #[snafu(display("Error creating directory:: {:?}", path))] + CreateDirectory { + /// Path being checked + path: PathBuf, + /// Underlying error + source: std::io::Error, + }, + /// Site repository dir was not a directory + #[snafu(display("Site repository path was not a directory: {:?}", path))] + NotADirectory { + /// Path being checked + path: PathBuf, + }, + /// Failed to enumerate a directory + #[snafu(display("Failed to enumerate directory: {:?}", path))] + EnumerateDirectory { + /// The directory being enumerated + path: PathBuf, + /// The underlying error + source: std::io::Error, + }, + /// Failed to reify the configuration + ConfigReify { + /// The underlying error + #[snafu(source(from(serde_dhall::Error, Box::new)))] + source: Box, + }, + /// Failed to read the configuration + #[snafu(display("Failed to read configuration at: {:?}", path))] + ConfigRead { + /// The path of the configuration file being read + path: PathBuf, + /// The underlying error + #[snafu(source(from(serde_dhall::Error, Box::new)))] + source: Box, + }, + /// Failed to write out the configuration + WriteConfig { + /// The path we tried to write the config to + path: PathBuf, + /// The underlying error + source: std::io::Error, + }, + /// Failed to write out a page stub + WritePageStub { + /// The path we tried to write the page stub to + path: PathBuf, + /// The underlying error + source: std::io::Error, + }, + /// Error writing out a page + #[snafu(display("Error writing out page {:?}", page))] + PageWrite { + /// The page we were trying to write out + page: PathBuf, + /// The underlying error + #[snafu(source(from(SiteError, Box::new)))] + source: Box, + }, + /// Error reading in a page + #[snafu(display("Error reading in page {:?}", page))] + PageRead { + /// The page we were trying to write out + page: PathBuf, + /// The underlying error + #[snafu(source(from(SiteError, Box::new)))] + source: Box, + }, + /// Failed to write out a post stub + WritePostStub { + /// The path we tried to write the page stub to + path: PathBuf, + /// The underlying error + source: std::io::Error, + }, + /// Error writing out a post + #[snafu(display("Error writing out post {:?}", post))] + PostWrite { + /// The post we were trying to write out + post: PathBuf, + /// The underlying error + #[snafu(source(from(SiteError, Box::new)))] + source: Box, + }, + /// Error reading in a post + #[snafu(display("Error reading in post {:?}", post))] + PostRead { + /// The post we were trying to write out + post: PathBuf, + /// The underlying error + #[snafu(source(from(SiteError, Box::new)))] + source: Box, + }, + /// Duplicate configuration files + #[snafu(display("Duplicate or missing configuration files in {:?}: {:?}", dir, configs))] + DuplicateConfigs { + /// The directory being checked for configs + dir: PathBuf, + /// The list of possible canidates + configs: Vec, + }, } diff --git a/src/site/config.rs b/src/site/config.rs index 8e00ba7..a8ae595 100644 --- a/src/site/config.rs +++ b/src/site/config.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; /// Description of the domain name and related settings for a site -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] pub struct Domain { /// The domain name itself pub domain_name: String, @@ -23,7 +23,7 @@ impl Default for Domain { /// Top level configuration for a site /// /// Describes the file located at `site/config.dhall` -#[derive(Serialize, Deserialize, Debug, Default)] +#[derive(Serialize, Deserialize, Debug, Default, PartialEq, Eq)] pub struct Config { /// Doman name settings pub domain: Domain, diff --git a/src/site/page.rs b/src/site/page.rs index be4f789..352f1f1 100644 --- a/src/site/page.rs +++ b/src/site/page.rs @@ -1,22 +1,25 @@ //! Management of a page use std::{ - fs::{create_dir_all, File}, + borrow::Cow, + ffi::OsStr, + fs::{create_dir_all, read_dir, File}, io::Write, path::{Path, PathBuf}, }; use serde::{Deserialize, Serialize}; use snafu::{ensure, ResultExt}; -use tracing::{debug, info, info_span}; +use tracing::{debug, info, info_span, warn}; use super::{ - ConfigReifySnafu, CreateDirectorySnafu, ExistanceCheckSnafu, NotADirectorySnafu, SiteError, - WriteConfigSnafu, WritePageStubSnafu, + ConfigReadSnafu, ConfigReifySnafu, CreateDirectorySnafu, DuplicateConfigsSnafu, + EnumerateDirectorySnafu, ExistanceCheckSnafu, NotADirectorySnafu, SiteError, WriteConfigSnafu, + WritePageStubSnafu, }; /// Representation of the configuration for a page -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] pub struct PageConfig { /// Title of the page title: String, @@ -37,7 +40,7 @@ impl Default for PageConfig { } /// Representation of a page -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] pub struct Page { /// Configuration for the page pub config: PageConfig, @@ -119,4 +122,86 @@ impl Page { } Ok(()) } + + /// Reads in the configuration for a `Page`, and makes sure the file for the page exists + /// + /// # Errors + /// + /// Will return an error if deserialization fails, the user does not have read permissions for + /// the directory, or any other IO errors occur while reading in the config. + pub fn read( + site_path: impl AsRef, + page_dir: impl AsRef, + ) -> Result { + let page_dir = site_path.as_ref().join("pages").join(page_dir.as_ref()); + // Setup logging + let span = info_span!("Page::read", ?page_dir); + let _enter = span.enter(); + // Get the list of files + let mut file_names = Vec::new(); + for file in read_dir(&page_dir).context(EnumerateDirectorySnafu { path: &page_dir })? { + match file { + Ok(file) => { + let file_path = file.path(); + if file_path.is_file() { + if let Some(file_path) = file_path.file_name() { + file_names.push(PathBuf::from(file_path)); + } else { + warn!(?file_path, "File path has no last part?"); + } + } else { + warn!(?file_path, "Page has illegal subdirectory!"); + } + } + Err(error) => warn!(?error, "Error enumerating page directory"), + } + } + // Find the dhall file + let mut dhall_canidates = file_names + .iter() + .filter(|e| e.extension().map(OsStr::to_string_lossy) == Some(Cow::Borrowed("dhall"))) + .cloned() + .collect::>(); + ensure!( + dhall_canidates.len() == 1, + DuplicateConfigsSnafu { + dir: &page_dir, + configs: dhall_canidates.clone() + } + ); + let config_path = dhall_canidates.remove(0); + debug!(?config_path, "Reading config"); + let real_config_path = page_dir.join(config_path); + let config: PageConfig = + serde_dhall::from_file(&real_config_path) + .parse() + .context(ConfigReadSnafu { + path: &real_config_path, + })?; + + // Find the djot file + let mut djot_canidates = file_names + .iter() + .filter(|e| { + e.extension() + .map(OsStr::to_string_lossy) + .map_or(false, |e| e == "djot" || e == "md") + }) + .cloned() + .collect::>(); + ensure!( + djot_canidates.len() == 1, + DuplicateConfigsSnafu { + dir: &page_dir, + configs: djot_canidates.clone() + } + ); + let djot_path = djot_canidates.remove(0); + debug!(?djot_path, "Found djot file"); + + Ok(Page { + config, + file: djot_path, + }) + } } diff --git a/src/site/post.rs b/src/site/post.rs index d233ac2..3ac7085 100644 --- a/src/site/post.rs +++ b/src/site/post.rs @@ -1,7 +1,9 @@ //! Management of a post use std::{ - fs::{create_dir_all, File}, + borrow::Cow, + ffi::OsStr, + fs::{create_dir_all, read_dir, File}, io::Write, path::{Path, PathBuf}, }; @@ -9,15 +11,16 @@ use std::{ use chrono::{Local, NaiveDate}; use serde::{Deserialize, Serialize}; use snafu::{ensure, ResultExt}; -use tracing::{debug, info, span, Level}; +use tracing::{debug, info, info_span, span, warn, Level}; use super::{ - ConfigReifySnafu, CreateDirectorySnafu, ExistanceCheckSnafu, NotADirectorySnafu, SiteError, - WriteConfigSnafu, WritePostStubSnafu, + ConfigReadSnafu, ConfigReifySnafu, CreateDirectorySnafu, DuplicateConfigsSnafu, + EnumerateDirectorySnafu, ExistanceCheckSnafu, NotADirectorySnafu, SiteError, WriteConfigSnafu, + WritePostStubSnafu, }; /// Representation of the configuration for a post -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] pub struct PostConfig { /// Title of the post title: String, @@ -42,7 +45,7 @@ impl Default for PostConfig { } /// Representation of a post -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] pub struct Post { /// Configuration for the post pub config: PostConfig, @@ -124,4 +127,86 @@ impl Post { } Ok(()) } + + /// Reads in the configuration for a `Post`, and makes sure the file for the post exists + /// + /// # Errors + /// + /// Will return an error if deserialization fails, the user does not have read permissions for + /// the directory, or any other IO errors occur while reading in the config. + pub fn read( + site_path: impl AsRef, + post_dir: impl AsRef, + ) -> Result { + let post_dir = site_path.as_ref().join("posts").join(post_dir.as_ref()); + // Setup logging + let span = info_span!("Post::read", ?post_dir); + let _enter = span.enter(); + // Get the list of files + let mut file_names = Vec::new(); + for file in read_dir(&post_dir).context(EnumerateDirectorySnafu { path: &post_dir })? { + match file { + Ok(file) => { + let file_path = file.path(); + if file_path.is_file() { + if let Some(file_path) = file_path.file_name() { + file_names.push(PathBuf::from(file_path)); + } else { + warn!(?file_path, "File path has no last part?"); + } + } else { + warn!(?file_path, "Post has illegal subdirectory!"); + } + } + Err(error) => warn!(?error, "Error enumerating post directory"), + } + } + // Find the dhall file + let mut dhall_canidates = file_names + .iter() + .filter(|e| e.extension().map(OsStr::to_string_lossy) == Some(Cow::Borrowed("dhall"))) + .cloned() + .collect::>(); + ensure!( + dhall_canidates.len() == 1, + DuplicateConfigsSnafu { + dir: &post_dir, + configs: dhall_canidates.clone() + } + ); + let config_path = dhall_canidates.remove(0); + debug!(?config_path, "Reading config"); + let real_config_path = post_dir.join(config_path); + let config: PostConfig = + serde_dhall::from_file(&real_config_path) + .parse() + .context(ConfigReadSnafu { + path: &real_config_path, + })?; + + // Find the djot file + let mut djot_canidates = file_names + .iter() + .filter(|e| { + e.extension() + .map(OsStr::to_string_lossy) + .map_or(false, |e| e == "djot" || e == "md") + }) + .cloned() + .collect::>(); + ensure!( + djot_canidates.len() == 1, + DuplicateConfigsSnafu { + dir: &post_dir, + configs: djot_canidates.clone() + } + ); + let djot_path = djot_canidates.remove(0); + debug!(?djot_path, "Found djot file"); + + Ok(Post { + config, + file: djot_path, + }) + } } diff --git a/tests/read-write-state.rs b/tests/read-write-state.rs new file mode 100644 index 0000000..ea55a06 --- /dev/null +++ b/tests/read-write-state.rs @@ -0,0 +1,21 @@ +use color_eyre::eyre::{ensure, Context, Result}; +use stranger_site_gen::site::Site; + +/// Test that serializing out a fresh site and reading it back in gives back the same [`Site`] +#[test] +fn write_read_default() -> Result<()> { + // Get a temporary directory + let tempdir = tempfile::tempdir().context("Failed to get temporary directory")?; + let tempdir_path = tempdir.path(); + // Generate our site + let mut site_input = Site::default(); + // Write it out + site_input + .write(tempdir_path) + .context("Failed to write out site")?; + // Read it back in + let site_output = Site::read(tempdir_path).context("Failed to read in site")?; + // Make sure the results are equal + ensure!(site_input == site_output, "Sites were not equal"); + Ok(()) +}