diff --git a/crates/treehouse/src/cli/fix.rs b/crates/treehouse/src/cli/fix.rs index c1eb6c3..c019754 100644 --- a/crates/treehouse/src/cli/fix.rs +++ b/crates/treehouse/src/cli/fix.rs @@ -8,7 +8,7 @@ use treehouse_format::ast::Branch; use crate::{ parse::{self, parse_toml_with_diagnostics, parse_tree_with_diagnostics}, state::{report_diagnostics, FileId, Source, Treehouse}, - vfs::{self, Dir, Edit, VPath}, + vfs::{self, Content, Dir, Edit, EditPath, VPath}, }; use super::{FixAllArgs, FixArgs}; @@ -138,17 +138,15 @@ pub fn fix_file_cli(fix_args: FixArgs, root: &dyn Dir) -> anyhow::Result { let file = if &*fix_args.file == VPath::new("-") { std::io::read_to_string(std::io::stdin().lock()).context("cannot read file from stdin")? } else { - String::from_utf8( - root.content(&fix_args.file) - .ok_or_else(|| anyhow!("cannot read file to fix"))?, - ) - .context("input file has invalid UTF-8")? + vfs::query::(root, &fix_args.file) + .ok_or_else(|| anyhow!("cannot read file to fix"))? + .string()? }; let mut treehouse = Treehouse::new(); let mut diagnostics = vec![]; let file_id = treehouse.add_file(fix_args.file.clone(), Source::Other(file)); - let edit_path = root.edit_path(&fix_args.file).ok_or_else(|| { + let edit_path = vfs::query::(root, &fix_args.file).ok_or_else(|| { anyhow!( "{} is not an editable file (perhaps it is not in a persistent path?)", fix_args.file @@ -161,9 +159,10 @@ pub fn fix_file_cli(fix_args: FixArgs, root: &dyn Dir) -> anyhow::Result { // Try to write the backup first. If writing that fails, bail out without overwriting // the source file. if let Some(backup_path) = fix_args.backup { - let backup_edit_path = root.edit_path(&backup_path).ok_or_else(|| { - anyhow!("backup file {backup_path} is not an editable file") - })?; + let backup_edit_path = + vfs::query::(root, &backup_path).ok_or_else(|| { + anyhow!("backup file {backup_path} is not an editable file") + })?; Edit::Seq(vec![ Edit::Write( backup_edit_path, @@ -190,7 +189,7 @@ pub fn fix_all_cli(fix_all_args: FixAllArgs, dir: &dyn Dir) -> anyhow::Result anyhow::Result { if path.extension() == Some("tree") { - let Some(content) = dir.content(path) else { + let Some(content) = vfs::query::(dir, path).map(Content::bytes) else { return Ok(Edit::NoOp); }; let content = String::from_utf8(content).context("file is not valid UTF-8")?; @@ -198,7 +197,7 @@ pub fn fix_all_cli(fix_all_args: FixAllArgs, dir: &dyn Dir) -> anyhow::Result(dir, path).context("path is not editable")?; if let Ok(fixed) = fix_file(&mut treehouse, &mut diagnostics, file_id) { if fixed != treehouse.source(file_id).input() { diff --git a/crates/treehouse/src/cli/serve.rs b/crates/treehouse/src/cli/serve.rs index 5a5b8ec..9db00fc 100644 --- a/crates/treehouse/src/cli/serve.rs +++ b/crates/treehouse/src/cli/serve.rs @@ -73,7 +73,7 @@ struct VfsQuery { #[instrument(skip(state))] async fn get_static_file(path: &str, query: &VfsQuery, state: &Server) -> Option { let vpath = VPath::try_new(path).ok()?; - let content = state.target.content(vpath).await?; + let content = state.target.content(vpath).await.map(|c| c.bytes())?; let mut response = content.into_response(); if let Some(content_type) = vpath.extension().and_then(get_content_type) { @@ -108,7 +108,7 @@ async fn vfs_entry( async fn system_page(target: &AsyncDir, path: &VPath, status_code: StatusCode) -> Response { if let Some(content) = target.content(path).await { - (status_code, Html(content)).into_response() + (status_code, Html(content.bytes())).into_response() } else { ( StatusCode::INTERNAL_SERVER_ERROR, @@ -152,7 +152,7 @@ async fn branch(RawQuery(named_id): RawQuery, State(state): State>) .target .content(tree_path) .await - .and_then(|s| String::from_utf8(s).ok()) + .and_then(|c| c.string().ok()) { let branch_markup = input[branch.content.clone()].trim(); let mut per_page_metadata = diff --git a/crates/treehouse/src/cli/wc.rs b/crates/treehouse/src/cli/wc.rs index 27ad3a6..b210225 100644 --- a/crates/treehouse/src/cli/wc.rs +++ b/crates/treehouse/src/cli/wc.rs @@ -5,7 +5,7 @@ use treehouse_format::ast::{Branch, Roots}; use crate::{ parse::parse_tree_with_diagnostics, state::{report_diagnostics, Source, Treehouse}, - vfs::{self, Dir, VPath}, + vfs::{self, Content, Dir, VPath}, }; use super::WcArgs; @@ -43,9 +43,8 @@ pub fn wc_cli(content_dir: &dyn Dir, mut wc_args: WcArgs) -> anyhow::Result<()> let mut total = 0; for path in &wc_args.paths { - if let Some(content) = content_dir - .content(path) - .and_then(|b| String::from_utf8(b).ok()) + if let Some(content) = + vfs::query::(content_dir, path).and_then(|b| b.string().ok()) { let file_id = treehouse.add_file(path.clone(), Source::Other(content.clone())); match parse_tree_with_diagnostics(file_id, &content) { diff --git a/crates/treehouse/src/config.rs b/crates/treehouse/src/config.rs index 885d6ec..46f5555 100644 --- a/crates/treehouse/src/config.rs +++ b/crates/treehouse/src/config.rs @@ -14,7 +14,7 @@ use crate::{ Syntax, }, import_map::ImportRoot, - vfs::{self, Dir, DynDir, ImageSize, VPath, VPathBuf}, + vfs::{self, Content, Dir, DynDir, ImageSize, VPath, VPathBuf}, }; #[derive(Debug, Clone, Deserialize, Serialize)] @@ -167,7 +167,9 @@ impl Config { } pub fn pic_size(&self, pics_dir: &dyn Dir, id: &str) -> Option { - self.pics.get(id).and_then(|path| pics_dir.image_size(path)) + self.pics + .get(id) + .and_then(|path| vfs::query::(pics_dir, path)) } /// Loads all syntax definition files. @@ -188,12 +190,9 @@ impl Config { .file_stem() .expect("syntax file name should have a stem due to the .json extension"); - let result: Result = dir - .content(path) + let result: Result = vfs::query::(&dir, path) .ok_or_else(|| anyhow!("syntax .json is not a file")) - .and_then(|b| { - String::from_utf8(b).context("syntax .json contains invalid UTF-8") - }) + .and_then(|b| b.string().context("syntax .json contains invalid UTF-8")) .and_then(|s| { let _span = info_span!("Config::load_syntaxes::parse").entered(); serde_json::from_str(&s).context("could not deserialize syntax file") diff --git a/crates/treehouse/src/generate.rs b/crates/treehouse/src/generate.rs index fc8d1c0..e250e8e 100644 --- a/crates/treehouse/src/generate.rs +++ b/crates/treehouse/src/generate.rs @@ -19,8 +19,8 @@ use crate::{ fun::seasons::Season, sources::Sources, vfs::{ - self, Cd, ContentCache, Dir, DirEntry, DynDir, HtmlCanonicalize, MemDir, Overlay, ToDynDir, - VPath, VPathBuf, + self, Cd, Content, ContentCache, Dir, DynDir, Entries, HtmlCanonicalize, MemDir, Overlay, + ToDynDir, VPath, VPathBuf, }, }; @@ -46,6 +46,36 @@ impl<'a> BaseTemplateData<'a> { } } +fn create_handlebars(site: &str, static_: DynDir) -> Handlebars<'static> { + let mut handlebars = Handlebars::new(); + + handlebars_helper!(cat: |a: String, b: String| a + &b); + + handlebars.register_helper("cat", Box::new(cat)); + handlebars.register_helper("asset", Box::new(DirHelper::new(site, static_.clone()))); + handlebars.register_helper( + "include_static", + Box::new(IncludeStaticHelper::new(static_)), + ); + + handlebars +} + +#[instrument(skip(handlebars))] +fn load_templates(handlebars: &mut Handlebars, dir: &dyn Dir) { + vfs::walk_dir_rec(dir, VPath::ROOT, &mut |path| { + if path.extension() == Some("hbs") { + if let Some(content) = vfs::query::(dir, path).and_then(|c| c.string().ok()) { + let _span = info_span!("register_template", ?path).entered(); + if let Err(err) = handlebars.register_template_string(path.as_str(), content) { + error!("in template: {err}"); + } + } + } + ControlFlow::Continue(()) + }); +} + struct TreehouseDir { dirs: Arc, sources: Arc, @@ -67,41 +97,9 @@ impl TreehouseDir { dir_index, } } -} -fn create_handlebars(site: &str, static_: DynDir) -> Handlebars<'static> { - let mut handlebars = Handlebars::new(); - - handlebars_helper!(cat: |a: String, b: String| a + &b); - - handlebars.register_helper("cat", Box::new(cat)); - handlebars.register_helper("asset", Box::new(DirHelper::new(site, static_.clone()))); - handlebars.register_helper( - "include_static", - Box::new(IncludeStaticHelper::new(static_)), - ); - - handlebars -} - -#[instrument(skip(handlebars))] -fn load_templates(handlebars: &mut Handlebars, dir: &dyn Dir) { - vfs::walk_dir_rec(dir, VPath::ROOT, &mut |path| { - if path.extension() == Some("hbs") { - if let Some(content) = dir.content(path).and_then(|b| String::from_utf8(b).ok()) { - let _span = info_span!("register_template", ?path).entered(); - if let Err(err) = handlebars.register_template_string(path.as_str(), content) { - error!("in template: {err}"); - } - } - } - ControlFlow::Continue(()) - }); -} - -impl Dir for TreehouseDir { #[instrument("TreehouseDir::dir", skip(self))] - fn dir(&self, path: &VPath) -> Vec { + fn dir(&self, path: &VPath) -> Vec { // NOTE: This does not include simple templates, because that's not really needed right now. let mut index = &self.dir_index; @@ -118,14 +116,12 @@ impl Dir for TreehouseDir { index .children .values() - .map(|child| DirEntry { - path: child.full_path.clone(), - }) + .map(|child| child.full_path.clone()) .collect() } #[instrument("TreehouseDir::content", skip(self))] - fn content(&self, path: &VPath) -> Option> { + fn content(&self, path: &VPath) -> Option { let path = if path.is_root() { VPath::new_const("index") } else { @@ -137,28 +133,34 @@ impl Dir for TreehouseDir { .files_by_tree_path .get(path) .map(|&file_id| { - tree::generate_or_error(&self.sources, &self.dirs, &self.handlebars, file_id).into() + Content::new( + tree::generate_or_error(&self.sources, &self.dirs, &self.handlebars, file_id) + .into(), + ) }) .or_else(|| { if path.file_name().is_some_and(|s| !s.starts_with('_')) { let template_name = path.with_extension("hbs"); if self.handlebars.has_template(template_name.as_str()) { - return Some( + return Some(Content::new( simple_template::generate_or_error( &self.sources, &self.handlebars, template_name.as_str(), ) .into(), - ); + )); } } None }) } +} - fn content_version(&self, _path: &VPath) -> Option { - None +impl Dir for TreehouseDir { + fn query(&self, path: &VPath, query: &mut vfs::Query) { + query.provide(|| Entries(self.dir(path))); + query.try_provide(|| self.content(path)); } } diff --git a/crates/treehouse/src/generate/atom.rs b/crates/treehouse/src/generate/atom.rs index 0b3d16c..9f0d9c5 100644 --- a/crates/treehouse/src/generate/atom.rs +++ b/crates/treehouse/src/generate/atom.rs @@ -13,7 +13,7 @@ use crate::{ sources::Sources, state::FileId, tree::SemaBranchId, - vfs::{Dir, DirEntry, VPath, VPathBuf}, + vfs::{self, Content, Dir, Entries, VPath, VPathBuf}, }; use super::BaseTemplateData; @@ -36,25 +36,21 @@ impl FeedDir { handlebars, } } -} -impl Dir for FeedDir { - fn dir(&self, path: &VPath) -> Vec { + fn entries(&self, path: &VPath) -> Vec { if path == VPath::ROOT { self.sources .treehouse .feeds_by_name .keys() - .map(|name| DirEntry { - path: VPathBuf::new(format!("{name}.atom")), - }) + .map(|name| VPathBuf::new(format!("{name}.atom"))) .collect() } else { vec![] } } - fn content(&self, path: &VPath) -> Option> { + fn content(&self, path: &VPath) -> Option { info!("{path}"); if path.extension() == Some("atom") { let feed_name = path.with_extension("").to_string(); @@ -63,15 +59,21 @@ impl Dir for FeedDir { .feeds_by_name .get(&feed_name) .map(|file_id| { - generate_or_error(&self.sources, &self.dirs, &self.handlebars, *file_id).into() + Content::new( + generate_or_error(&self.sources, &self.dirs, &self.handlebars, *file_id) + .into(), + ) }) } else { None } } +} - fn content_version(&self, _path: &VPath) -> Option { - None +impl Dir for FeedDir { + fn query(&self, path: &VPath, query: &mut vfs::Query) { + query.provide(|| Entries(self.entries(path))); + query.try_provide(|| self.content(path)); } } diff --git a/crates/treehouse/src/generate/include_static_helper.rs b/crates/treehouse/src/generate/include_static_helper.rs index 8181b30..56aa860 100644 --- a/crates/treehouse/src/generate/include_static_helper.rs +++ b/crates/treehouse/src/generate/include_static_helper.rs @@ -1,7 +1,7 @@ use handlebars::{Context, Handlebars, Helper, HelperDef, RenderContext, RenderError, ScopedJson}; use serde_json::Value; -use crate::vfs::{DynDir, VPath}; +use crate::vfs::{self, Content, DynDir, VPath}; pub struct IncludeStaticHelper { dir: DynDir, @@ -23,13 +23,11 @@ impl HelperDef for IncludeStaticHelper { ) -> Result, RenderError> { if let Some(path) = h.param(0).and_then(|v| v.value().as_str()) { let vpath = VPath::try_new(path).map_err(|e| RenderError::new(e.to_string()))?; - let url = String::from_utf8( - self.dir - .content(vpath) - .ok_or_else(|| RenderError::new("file does not exist"))?, - ) - .map_err(|_| RenderError::new("included file does not contain UTF-8 text"))?; - Ok(ScopedJson::Derived(Value::String(url))) + let content = vfs::query::(&self.dir, vpath) + .ok_or_else(|| RenderError::new("file does not exist"))? + .string() + .map_err(|_| RenderError::new("included file does not contain UTF-8 text"))?; + Ok(ScopedJson::Derived(Value::String(content))) } else { Err(RenderError::new("missing path string")) } diff --git a/crates/treehouse/src/html/djot.rs b/crates/treehouse/src/html/djot.rs index 94bedd2..0f45673 100644 --- a/crates/treehouse/src/html/djot.rs +++ b/crates/treehouse/src/html/djot.rs @@ -21,6 +21,7 @@ use crate::dirs::Dirs; use crate::state::FileId; use crate::state::Treehouse; use crate::vfs; +use crate::vfs::ImageSize; use super::highlight::highlight; @@ -584,7 +585,9 @@ impl<'a> Writer<'a> { write_attr(&url, out); out.push('"'); - if let Some(image_size) = self.renderer.dirs.emoji.image_size(vpath) { + if let Some(image_size) = + vfs::query::(&self.renderer.dirs.emoji, vpath) + { write!( out, r#" width="{}" height="{}""#, diff --git a/crates/treehouse/src/sources.rs b/crates/treehouse/src/sources.rs index a60faa5..56feac4 100644 --- a/crates/treehouse/src/sources.rs +++ b/crates/treehouse/src/sources.rs @@ -12,7 +12,7 @@ use crate::{ parse::parse_tree_with_diagnostics, state::{report_diagnostics, Source, Treehouse}, tree::SemaRoots, - vfs::{self, Cd, VPath, VPathBuf}, + vfs::{self, Cd, Content, VPath, VPathBuf}, }; pub struct Sources { @@ -27,10 +27,8 @@ impl Sources { let config = { let _span = info_span!("load_config").entered(); let mut config: Config = toml_edit::de::from_str( - &dirs - .root - .content(VPath::new("treehouse.toml")) - .map(String::from_utf8) + &vfs::query::(&dirs.root, VPath::new_const("treehouse.toml")) + .map(Content::string) .ok_or_else(|| anyhow!("config file does not exist"))??, ) .context("failed to deserialize config")?; @@ -88,9 +86,8 @@ fn load_trees(config: &Config, dirs: &Dirs) -> anyhow::Result { .into_par_iter() .zip(&file_ids) .flat_map(|(path, &file_id)| { - dirs.content - .content(&path) - .and_then(|b| String::from_utf8(b).ok()) + vfs::query::(&dirs.content, &path) + .and_then(|c| c.string().ok()) .map(|input| { let parse_result = parse_tree_with_diagnostics(file_id, &input); (path, file_id, input, parse_result) diff --git a/crates/treehouse/src/tree/mini_template.rs b/crates/treehouse/src/tree/mini_template.rs index accea1b..aa2f66b 100644 --- a/crates/treehouse/src/tree/mini_template.rs +++ b/crates/treehouse/src/tree/mini_template.rs @@ -12,7 +12,7 @@ use crate::{ dirs::Dirs, html::EscapeHtml, state::Treehouse, - vfs::{Dir, VPath}, + vfs::{self, Content, VPath}, }; struct Lexer<'a> { @@ -206,8 +206,8 @@ impl Renderer<'_> { "pic" => Ok(config.pic_url(&*dirs.pic, arguments)), "include_static" => VPath::try_new(arguments) .ok() - .and_then(|vpath| dirs.static_.content(vpath)) - .and_then(|content| String::from_utf8(content).ok()) + .and_then(|vpath| vfs::query::(&dirs.static_, vpath)) + .and_then(|c| c.string().ok()) .ok_or(InvalidTemplate), _ => Err(InvalidTemplate), } diff --git a/crates/treehouse/src/vfs.rs b/crates/treehouse/src/vfs.rs index 4e2e538..d121274 100644 --- a/crates/treehouse/src/vfs.rs +++ b/crates/treehouse/src/vfs.rs @@ -24,7 +24,6 @@ //! //! In-memory directories can be composed using the following primitives: //! -//! - [`EmptyEntry`] - has no metadata whatsoever. //! - [`BufferedFile`] - root path content is the provided byte vector. //! - [`MemDir`] - a [`Dir`] containing a single level of other [`Dir`]s inside. //! @@ -44,8 +43,10 @@ //! [`VPath`] also has an owned version, [`VPathBuf`]. use std::{ + any::TypeId, fmt::{self, Debug}, ops::{ControlFlow, Deref}, + string::FromUtf8Error, sync::Arc, }; @@ -55,7 +56,6 @@ mod cd; mod content_cache; mod content_version_cache; mod edit; -mod empty; mod file; mod html_canonicalize; mod image_size_cache; @@ -69,7 +69,6 @@ pub use cd::*; pub use content_cache::*; pub use content_version_cache::*; pub use edit::*; -pub use empty::*; pub use file::*; pub use html_canonicalize::*; pub use image_size_cache::*; @@ -78,50 +77,94 @@ pub use overlay::*; pub use path::*; pub use physical::*; -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub struct DirEntry { - pub path: VPathBuf, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub struct ImageSize { - pub width: u32, - pub height: u32, -} - pub trait Dir: Debug { - /// List all entries under the provided path. - fn dir(&self, path: &VPath) -> Vec; + fn query(&self, path: &VPath, query: &mut Query); +} - /// Return the byte content of the entry at the given path. - fn content(&self, path: &VPath) -> Option>; +pub trait Fork {} - /// Get a string signifying the current version of the provided path's content. - /// If the content changes, the version must also change. - /// - /// Returns None if there is no content or no version string is available. - fn content_version(&self, path: &VPath) -> Option; +pub fn query<'a, T>(dir: &'a (impl Dir + ?Sized), path: &VPath) -> Option +where + T: 'static + Fork, +{ + let mut slot = TaggedOption::<'a, tags::Value>(None); + dir.query(path, Query::new(&mut slot)); + slot.0 +} - /// Returns the size of the image at the given path, or `None` if the entry is not an image - /// (or its size cannot be known.) - fn image_size(&self, _path: &VPath) -> Option { - None +#[repr(transparent)] +pub struct Query<'a> { + erased: dyn Erased<'a> + 'a, +} + +impl<'a> Query<'a> { + fn new<'b>(erased: &'b mut (dyn Erased<'a> + 'a)) -> &'b mut Query<'a> { + unsafe { &mut *(erased as *mut dyn Erased<'a> as *mut Query<'a>) } } - /// Returns a path relative to `config.site` indicating where the file will be available - /// once served. - /// - /// May return `None` if the file is not served. - fn anchor(&self, _path: &VPath) -> Option { - None + pub fn provide(&mut self, f: impl FnOnce() -> T) + where + T: 'static + Fork, + { + if let Some(result @ TaggedOption(None)) = self.erased.downcast_mut::>() { + result.0 = Some(f()); + } } - /// If a file can be written persistently, returns an [`EditPath`] representing the file in - /// persistent storage. - /// - /// An edit path can then be made into an [`Edit`]. - fn edit_path(&self, _path: &VPath) -> Option { - None + pub fn try_provide(&mut self, f: impl FnOnce() -> Option) + where + T: 'static + Fork, + { + if let Some(result @ TaggedOption(None)) = self.erased.downcast_mut::>() { + result.0 = f(); + } + } +} + +mod tags { + use std::marker::PhantomData; + + pub trait Type<'a>: Sized + 'static { + type Reified: 'a; + } + + pub struct Value(PhantomData) + where + T: 'static; + + impl Type<'_> for Value + where + T: 'static, + { + type Reified = T; + } +} + +#[repr(transparent)] +struct TaggedOption<'a, I: tags::Type<'a>>(Option); + +#[expect(clippy::missing_safety_doc)] +unsafe trait Erased<'a>: 'a { + fn tag_id(&self) -> TypeId; +} + +unsafe impl<'a, I: tags::Type<'a>> Erased<'a> for TaggedOption<'a, I> { + fn tag_id(&self) -> TypeId { + TypeId::of::() + } +} + +impl<'a> dyn Erased<'a> + 'a { + fn downcast_mut(&mut self) -> Option<&mut TaggedOption<'a, I>> + where + I: tags::Type<'a>, + { + if self.tag_id() == TypeId::of::() { + // SAFETY: Just checked whether we're pointing to an I. + Some(unsafe { &mut *(self as *mut Self).cast::>() }) + } else { + None + } } } @@ -129,28 +172,8 @@ impl Dir for &T where T: Dir, { - fn dir(&self, path: &VPath) -> Vec { - (**self).dir(path) - } - - fn content(&self, path: &VPath) -> Option> { - (**self).content(path) - } - - fn content_version(&self, path: &VPath) -> Option { - (**self).content_version(path) - } - - fn image_size(&self, path: &VPath) -> Option { - (**self).image_size(path) - } - - fn anchor(&self, path: &VPath) -> Option { - (**self).anchor(path) - } - - fn edit_path(&self, path: &VPath) -> Option { - (**self).edit_path(path) + fn query(&self, path: &VPath, query: &mut Query) { + (**self).query(path, query) } } @@ -160,28 +183,8 @@ pub struct DynDir { } impl Dir for DynDir { - fn dir(&self, path: &VPath) -> Vec { - self.arc.dir(path) - } - - fn content(&self, path: &VPath) -> Option> { - self.arc.content(path) - } - - fn content_version(&self, path: &VPath) -> Option { - self.arc.content_version(path) - } - - fn image_size(&self, path: &VPath) -> Option { - self.arc.image_size(path) - } - - fn anchor(&self, path: &VPath) -> Option { - self.arc.anchor(path) - } - - fn edit_path(&self, path: &VPath) -> Option { - self.arc.edit_path(path) + fn query(&self, path: &VPath, query: &mut Query) { + self.arc.query(path, query); } } @@ -229,21 +232,75 @@ where } } +/// List of child entries under a directory. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct Entries(pub Vec); + +/// Byte content in an entry. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct Content { + bytes: Vec, +} + +/// Abstract version of an entry. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct ContentVersion { + pub string: String, +} + +/// Path relative to `config.site` indicating where the file will be available once served. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct Anchor { + pub path: VPathBuf, +} + +/// Size of image entries. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct ImageSize { + pub width: u32, + pub height: u32, +} + +impl Content { + pub fn new(bytes: Vec) -> Self { + Self { bytes } + } + + pub fn bytes(self) -> Vec { + self.bytes + } + + pub fn string(self) -> Result { + String::from_utf8(self.bytes()) + } +} + +impl Fork for Entries {} +impl Fork for Content {} +impl Fork for ContentVersion {} +impl Fork for Anchor {} +impl Fork for ImageSize {} +impl Fork for EditPath {} + +pub fn entries(dir: &dyn Dir, path: &VPath) -> Vec { + query::(dir, path).map(|e| e.0).unwrap_or_default() +} + pub fn walk_dir_rec(dir: &dyn Dir, path: &VPath, f: &mut dyn FnMut(&VPath) -> ControlFlow<(), ()>) { - for entry in dir.dir(path) { - match f(&entry.path) { + for entry in entries(dir, path) { + match f(&entry) { ControlFlow::Continue(_) => (), ControlFlow::Break(_) => return, } - walk_dir_rec(dir, &entry.path, f); + walk_dir_rec(dir, &entry, f); } } pub fn url(site: &str, dir: &dyn Dir, path: &VPath) -> Option { - let anchor = dir.anchor(path)?; - if let Some(version) = dir.content_version(path) { - Some(format!("{}/{anchor}?v={version}", site)) + let anchor = query::(dir, path)?; + if let Some(version) = query::(dir, path) { + Some(format!("{}/{}?v={}", site, anchor.path, version.string)) } else { - Some(format!("{}/{anchor}", site)) + Some(format!("{}/{}", site, anchor.path)) } } diff --git a/crates/treehouse/src/vfs/anchored.rs b/crates/treehouse/src/vfs/anchored.rs index 6af0fe4..7bf6bf3 100644 --- a/crates/treehouse/src/vfs/anchored.rs +++ b/crates/treehouse/src/vfs/anchored.rs @@ -1,6 +1,6 @@ use std::fmt; -use super::{Dir, DirEntry, EditPath, ImageSize, VPath, VPathBuf}; +use super::{Anchor, Dir, Query, VPath, VPathBuf}; pub struct Anchored { inner: T, @@ -17,28 +17,12 @@ impl Dir for Anchored where T: Dir, { - fn dir(&self, path: &VPath) -> Vec { - self.inner.dir(path) - } + fn query(&self, path: &VPath, query: &mut Query) { + query.provide(|| Anchor { + path: self.at.join(path), + }); - fn content(&self, path: &VPath) -> Option> { - self.inner.content(path) - } - - fn content_version(&self, path: &VPath) -> Option { - self.inner.content_version(path) - } - - fn image_size(&self, path: &VPath) -> Option { - self.inner.image_size(path) - } - - fn anchor(&self, path: &VPath) -> Option { - Some(self.at.join(path)) - } - - fn edit_path(&self, path: &VPath) -> Option { - self.inner.edit_path(path) + self.inner.query(path, query); } } diff --git a/crates/treehouse/src/vfs/asynch.rs b/crates/treehouse/src/vfs/asynch.rs index acad193..faaf61b 100644 --- a/crates/treehouse/src/vfs/asynch.rs +++ b/crates/treehouse/src/vfs/asynch.rs @@ -1,4 +1,4 @@ -use super::{Dir, DynDir, VPath}; +use super::{query, Content, DynDir, VPath}; #[derive(Debug, Clone)] pub struct AsyncDir { @@ -10,13 +10,13 @@ impl AsyncDir { Self { inner } } - pub async fn content(&self, path: &VPath) -> Option> { + pub async fn content(&self, path: &VPath) -> Option { let this = self.clone(); let path = path.to_owned(); // NOTE: Performance impact of spawning a blocking task may be a bit high in case // we add caching. // Measure throughput here. - tokio::task::spawn_blocking(move || this.inner.content(&path)) + tokio::task::spawn_blocking(move || query::(&this.inner, &path)) .await .unwrap() } diff --git a/crates/treehouse/src/vfs/cd.rs b/crates/treehouse/src/vfs/cd.rs index baa61aa..bb55e82 100644 --- a/crates/treehouse/src/vfs/cd.rs +++ b/crates/treehouse/src/vfs/cd.rs @@ -1,6 +1,6 @@ use std::fmt; -use super::{Dir, DirEntry, EditPath, ImageSize, VPath, VPathBuf}; +use super::{entries, Dir, Entries, Query, VPath, VPathBuf}; pub struct Cd { parent: T, @@ -13,42 +13,34 @@ impl Cd { } } +impl Cd +where + T: Dir, +{ + fn dir(&self, path: &VPath) -> Vec { + entries(&self.parent, &self.path.join(path)) + .into_iter() + .map(|entry| { + entry + .strip_prefix(&self.path) + .expect("all entries must be anchored within `self.path`") + .to_owned() + }) + .collect() + } +} + impl Dir for Cd where T: Dir, { - fn dir(&self, path: &VPath) -> Vec { - self.parent - .dir(&self.path.join(path)) - .into_iter() - .map(|entry| DirEntry { - path: entry - .path - .strip_prefix(&self.path) - .expect("all entries must be anchored within `self.path`") - .to_owned(), - }) - .collect() - } + fn query(&self, path: &VPath, query: &mut Query) { + // The only query that meaningfully needs to return something else is `dir`, which must + // be modified to strip prefixes off of the parent's returned paths. + query.provide(|| Entries(self.dir(path))); - fn content_version(&self, path: &VPath) -> Option { - self.parent.content_version(&self.path.join(path)) - } - - fn content(&self, path: &VPath) -> Option> { - self.parent.content(&self.path.join(path)) - } - - fn image_size(&self, path: &VPath) -> Option { - self.parent.image_size(&self.path.join(path)) - } - - fn anchor(&self, path: &VPath) -> Option { - self.parent.anchor(&self.path.join(path)) - } - - fn edit_path(&self, path: &VPath) -> Option { - self.parent.edit_path(&self.path.join(path)) + // Other queries can run unmodified, only passing them the right path. + self.parent.query(&self.path.join(path), query); } } diff --git a/crates/treehouse/src/vfs/content_cache.rs b/crates/treehouse/src/vfs/content_cache.rs index 03fd41e..3858985 100644 --- a/crates/treehouse/src/vfs/content_cache.rs +++ b/crates/treehouse/src/vfs/content_cache.rs @@ -7,11 +7,11 @@ use dashmap::DashMap; use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use tracing::{info_span, instrument}; -use super::{walk_dir_rec, Dir, DirEntry, EditPath, ImageSize, VPath, VPathBuf}; +use super::{query, walk_dir_rec, Content, Dir, Query, VPath, VPathBuf}; pub struct ContentCache { inner: T, - cache: DashMap>, + cache: DashMap, } impl ContentCache { @@ -39,41 +39,32 @@ where } } -impl Dir for ContentCache +impl ContentCache where T: Dir, { - fn dir(&self, path: &VPath) -> Vec { - self.inner.dir(path) - } - #[instrument(name = "ContentCache::content", skip(self))] - fn content(&self, path: &VPath) -> Option> { + fn content(&self, path: &VPath) -> Option { self.cache.get(path).map(|x| x.clone()).or_else(|| { let _span = info_span!("cache_miss").entered(); - let content = self.inner.content(path); + let content = query::(&self.inner, path); if let Some(content) = &content { self.cache.insert(path.to_owned(), content.clone()); } content }) } +} - fn content_version(&self, path: &VPath) -> Option { - self.inner.content_version(path) - } +impl Dir for ContentCache +where + T: Dir, +{ + fn query(&self, path: &VPath, query: &mut Query) { + query.try_provide(|| self.content(path)); - fn image_size(&self, path: &VPath) -> Option { - self.inner.image_size(path) - } - - fn anchor(&self, path: &VPath) -> Option { - self.inner.anchor(path) - } - - fn edit_path(&self, path: &VPath) -> Option { - self.inner.edit_path(path) + self.inner.query(path, query); } } diff --git a/crates/treehouse/src/vfs/content_version_cache.rs b/crates/treehouse/src/vfs/content_version_cache.rs index 0dfaae5..f953e2a 100644 --- a/crates/treehouse/src/vfs/content_version_cache.rs +++ b/crates/treehouse/src/vfs/content_version_cache.rs @@ -3,11 +3,11 @@ use std::fmt::{self, Debug}; use dashmap::DashMap; use tracing::{info_span, instrument}; -use super::{Dir, DirEntry, EditPath, ImageSize, VPath, VPathBuf}; +use super::{query, Content, ContentVersion, Dir, Query, VPath, VPathBuf}; pub struct Blake3ContentVersionCache { inner: T, - cache: DashMap, + cache: DashMap, } impl Blake3ContentVersionCache { @@ -19,26 +19,20 @@ impl Blake3ContentVersionCache { } } -impl Dir for Blake3ContentVersionCache +impl Blake3ContentVersionCache where T: Dir, { - fn dir(&self, path: &VPath) -> Vec { - self.inner.dir(path) - } - - fn content(&self, path: &VPath) -> Option> { - self.inner.content(path) - } - #[instrument(name = "Blake3ContentVersionCache::content_version", skip(self))] - fn content_version(&self, path: &VPath) -> Option { + fn content_version(&self, path: &VPath) -> Option { self.cache.get(path).map(|x| x.clone()).or_else(|| { let _span = info_span!("cache_miss").entered(); - let version = self.inner.content(path).map(|content| { - let hash = blake3::hash(&content).to_hex(); - format!("b3-{}", &hash[0..8]) + let version = query::(&self.inner, path).map(|content| { + let hash = blake3::hash(&content.bytes()).to_hex(); + ContentVersion { + string: format!("b3-{}", &hash[0..8]), + } }); if let Some(version) = &version { self.cache.insert(path.to_owned(), version.clone()); @@ -46,17 +40,16 @@ where version }) } +} - fn image_size(&self, path: &VPath) -> Option { - self.inner.image_size(path) - } +impl Dir for Blake3ContentVersionCache +where + T: Dir, +{ + fn query(&self, path: &VPath, query: &mut Query) { + query.try_provide(|| self.content_version(path)); - fn anchor(&self, path: &VPath) -> Option { - self.inner.anchor(path) - } - - fn edit_path(&self, path: &VPath) -> Option { - self.inner.edit_path(path) + self.inner.query(path, query); } } diff --git a/crates/treehouse/src/vfs/empty.rs b/crates/treehouse/src/vfs/empty.rs deleted file mode 100644 index aa15d7e..0000000 --- a/crates/treehouse/src/vfs/empty.rs +++ /dev/null @@ -1,18 +0,0 @@ -use super::{Dir, DirEntry, VPath}; - -#[derive(Debug)] -pub struct EmptyEntry; - -impl Dir for EmptyEntry { - fn dir(&self, _path: &VPath) -> Vec { - vec![] - } - - fn content_version(&self, _path: &VPath) -> Option { - None - } - - fn content(&self, _path: &VPath) -> Option> { - None - } -} diff --git a/crates/treehouse/src/vfs/file.rs b/crates/treehouse/src/vfs/file.rs index cb033cb..19fa3cd 100644 --- a/crates/treehouse/src/vfs/file.rs +++ b/crates/treehouse/src/vfs/file.rs @@ -1,6 +1,6 @@ use std::fmt; -use super::{DirEntry, Dir, VPath}; +use super::{Content, Dir, Query, VPath}; pub struct BufferedFile { pub content: Vec, @@ -13,20 +13,9 @@ impl BufferedFile { } impl Dir for BufferedFile { - fn dir(&self, _path: &VPath) -> Vec { - vec![] - } - - fn content_version(&self, _path: &VPath) -> Option { - // TODO: StaticFile should _probably_ calculate a content_version. - None - } - - fn content(&self, path: &VPath) -> Option> { + fn query(&self, path: &VPath, query: &mut Query) { if path == VPath::ROOT { - Some(self.content.clone()) - } else { - None + query.provide(|| Content::new(self.content.clone())); } } } diff --git a/crates/treehouse/src/vfs/html_canonicalize.rs b/crates/treehouse/src/vfs/html_canonicalize.rs index 109bfe6..fb3ee54 100644 --- a/crates/treehouse/src/vfs/html_canonicalize.rs +++ b/crates/treehouse/src/vfs/html_canonicalize.rs @@ -1,6 +1,6 @@ use core::fmt; -use super::{Dir, DirEntry, EditPath, ImageSize, VPath, VPathBuf}; +use super::{Dir, Query, VPath}; pub struct HtmlCanonicalize { inner: T, @@ -16,33 +16,13 @@ impl Dir for HtmlCanonicalize where T: Dir, { - fn dir(&self, path: &VPath) -> Vec { - self.inner.dir(path) - } - - fn content(&self, path: &VPath) -> Option> { + fn query(&self, path: &VPath, query: &mut Query) { let mut path = path.to_owned(); if path.extension() == Some("html") { path.set_extension(""); } - self.inner.content(&path) - } - - fn content_version(&self, path: &VPath) -> Option { - self.inner.content_version(path) - } - - fn image_size(&self, path: &VPath) -> Option { - self.inner.image_size(path) - } - - fn anchor(&self, path: &VPath) -> Option { - self.inner.anchor(path) - } - - fn edit_path(&self, path: &VPath) -> Option { - self.inner.edit_path(path) + self.inner.query(&path, query); } } diff --git a/crates/treehouse/src/vfs/image_size_cache.rs b/crates/treehouse/src/vfs/image_size_cache.rs index 2f33264..48bb5f7 100644 --- a/crates/treehouse/src/vfs/image_size_cache.rs +++ b/crates/treehouse/src/vfs/image_size_cache.rs @@ -6,7 +6,7 @@ use tracing::{info_span, instrument, warn}; use crate::config; -use super::{Dir, DirEntry, EditPath, ImageSize, VPath, VPathBuf}; +use super::{query, Content, Dir, ImageSize, Query, VPath, VPathBuf}; pub struct ImageSizeCache { inner: T, @@ -28,8 +28,8 @@ where { fn compute_image_size(&self, path: &VPath) -> anyhow::Result> { if path.extension().is_some_and(config::is_image_file) { - if let Some(content) = self.content(path) { - let reader = image::ImageReader::new(Cursor::new(content)) + if let Some(content) = query::(&self.inner, path) { + let reader = image::ImageReader::new(Cursor::new(content.bytes())) .with_guessed_format() .context("cannot guess image format")?; let (width, height) = reader.into_dimensions()?; @@ -39,23 +39,6 @@ where Ok(None) } -} - -impl Dir for ImageSizeCache -where - T: Dir, -{ - fn dir(&self, path: &VPath) -> Vec { - self.inner.dir(path) - } - - fn content(&self, path: &VPath) -> Option> { - self.inner.content(path) - } - - fn content_version(&self, path: &VPath) -> Option { - self.inner.content_version(path) - } #[instrument("ImageSizeCache::image_size", skip(self))] fn image_size(&self, path: &VPath) -> Option { @@ -73,13 +56,16 @@ where image_size }) } +} - fn anchor(&self, path: &VPath) -> Option { - self.inner.anchor(path) - } +impl Dir for ImageSizeCache +where + T: Dir, +{ + fn query(&self, path: &VPath, query: &mut Query) { + query.try_provide(|| self.image_size(path)); - fn edit_path(&self, path: &VPath) -> Option { - self.inner.edit_path(path) + self.inner.query(path, query); } } diff --git a/crates/treehouse/src/vfs/mem_dir.rs b/crates/treehouse/src/vfs/mem_dir.rs index a799749..eab7b3f 100644 --- a/crates/treehouse/src/vfs/mem_dir.rs +++ b/crates/treehouse/src/vfs/mem_dir.rs @@ -1,6 +1,6 @@ use std::{collections::HashMap, fmt}; -use super::{Dir, DirEntry, DynDir, EditPath, ImageSize, VPath, VPathBuf}; +use super::{entries, Dir, DynDir, Entries, Query, VPath, VPathBuf}; pub struct MemDir { mount_points: HashMap, @@ -55,6 +55,23 @@ impl MemDir { Resolved::None } + + fn dir(&self, path: &VPath) -> Vec { + match self.resolve(path) { + Resolved::Root => self.mount_points.keys().map(VPathBuf::new).collect(), + + Resolved::MountPoint { + fs, + fs_path, + subpath, + } => entries(fs, subpath) + .into_iter() + .map(|path| fs_path.join(&path)) + .collect(), + + Resolved::None => vec![], + } + } } impl Default for MemDir { @@ -64,82 +81,16 @@ impl Default for MemDir { } impl Dir for MemDir { - fn dir(&self, path: &VPath) -> Vec { - match self.resolve(path) { - Resolved::Root => self - .mount_points - .keys() - .map(|name| DirEntry { - path: VPathBuf::new(name), - }) - .collect(), - Resolved::MountPoint { - fs, - fs_path, - subpath, - } => fs - .dir(subpath) - .into_iter() - .map(|entry| DirEntry { - path: fs_path.join(&entry.path), - }) - .collect(), - Resolved::None => vec![], - } - } + fn query(&self, path: &VPath, query: &mut Query) { + query.provide(|| Entries(self.dir(path))); - fn content_version(&self, path: &VPath) -> Option { match self.resolve(path) { + Resolved::Root | Resolved::None => (), Resolved::MountPoint { fs, fs_path: _, subpath, - } => fs.content_version(subpath), - Resolved::Root | Resolved::None => None, - } - } - - fn content(&self, path: &VPath) -> Option> { - match self.resolve(path) { - Resolved::MountPoint { - fs, - fs_path: _, - subpath, - } => fs.content(subpath), - Resolved::Root | Resolved::None => None, - } - } - - fn image_size(&self, path: &VPath) -> Option { - match self.resolve(path) { - Resolved::MountPoint { - fs, - fs_path: _, - subpath, - } => fs.image_size(subpath), - Resolved::Root | Resolved::None => None, - } - } - - fn anchor(&self, path: &VPath) -> Option { - match self.resolve(path) { - Resolved::MountPoint { - fs, - fs_path: _, - subpath, - } => fs.anchor(subpath), - Resolved::Root | Resolved::None => None, - } - } - - fn edit_path(&self, path: &VPath) -> Option { - match self.resolve(path) { - Resolved::MountPoint { - fs, - fs_path: _, - subpath, - } => fs.edit_path(subpath), - Resolved::Root | Resolved::None => None, + } => fs.query(subpath, query), } } } diff --git a/crates/treehouse/src/vfs/overlay.rs b/crates/treehouse/src/vfs/overlay.rs index 33d6b55..13907ed 100644 --- a/crates/treehouse/src/vfs/overlay.rs +++ b/crates/treehouse/src/vfs/overlay.rs @@ -2,7 +2,7 @@ use std::fmt; use tracing::instrument; -use super::{Dir, DirEntry, DynDir, EditPath, ImageSize, VPath, VPathBuf}; +use super::{entries, Dir, DynDir, Entries, Query, VPath, VPathBuf}; pub struct Overlay { base: DynDir, @@ -13,44 +13,23 @@ impl Overlay { pub fn new(base: DynDir, overlay: DynDir) -> Self { Self { base, overlay } } -} -impl Dir for Overlay { #[instrument("Overlay::dir", skip(self))] - fn dir(&self, path: &VPath) -> Vec { - let mut dir = self.base.dir(path); - dir.append(&mut self.overlay.dir(path)); + fn dir(&self, path: &VPath) -> Vec { + let mut dir = entries(&self.base, path); + dir.append(&mut entries(&self.overlay, path)); dir.sort(); dir.dedup(); dir } +} - fn content(&self, path: &VPath) -> Option> { - self.overlay - .content(path) - .or_else(|| self.base.content(path)) - } +impl Dir for Overlay { + fn query(&self, path: &VPath, query: &mut Query) { + query.provide(|| Entries(self.dir(path))); - fn content_version(&self, path: &VPath) -> Option { - self.overlay - .content_version(path) - .or_else(|| self.base.content_version(path)) - } - - fn image_size(&self, path: &VPath) -> Option { - self.overlay - .image_size(path) - .or_else(|| self.base.image_size(path)) - } - - fn anchor(&self, path: &VPath) -> Option { - self.overlay.anchor(path).or_else(|| self.base.anchor(path)) - } - - fn edit_path(&self, path: &VPath) -> Option { - self.overlay - .edit_path(path) - .or_else(|| self.base.edit_path(path)) + self.overlay.query(path, query); + self.base.query(path, query); } } diff --git a/crates/treehouse/src/vfs/physical.rs b/crates/treehouse/src/vfs/physical.rs index d4af780..4835f47 100644 --- a/crates/treehouse/src/vfs/physical.rs +++ b/crates/treehouse/src/vfs/physical.rs @@ -2,7 +2,7 @@ use std::path::{Path, PathBuf}; use tracing::{error, instrument}; -use super::{Dir, DirEntry, EditPath, VPath, VPathBuf}; +use super::{Content, Dir, EditPath, Entries, Query, VPath, VPathBuf}; #[derive(Debug, Clone)] pub struct PhysicalDir { @@ -13,11 +13,8 @@ impl PhysicalDir { pub fn new(root: PathBuf) -> Self { Self { root } } -} -impl Dir for PhysicalDir { - #[instrument("PhysicalDir::dir", skip(self))] - fn dir(&self, vpath: &VPath) -> Vec { + fn entries(&self, vpath: &VPath) -> Vec { let physical = self.root.join(physical_path(vpath)); if !physical.is_dir() { return vec![]; @@ -47,7 +44,7 @@ impl Dir for PhysicalDir { error!("{self:?} error with vpath for {path_str:?}: {err:?}"); }) .ok()?; - Some(DirEntry { path: vpath_buf }) + Some(vpath_buf) }) }) .collect(), @@ -60,21 +57,26 @@ impl Dir for PhysicalDir { } } - fn content_version(&self, _path: &VPath) -> Option { - None - } - #[instrument("PhysicalDir::content", skip(self))] - fn content(&self, path: &VPath) -> Option> { + fn content(&self, path: &VPath) -> Option { std::fs::read(self.root.join(physical_path(path))) .inspect_err(|err| error!("{self:?} cannot read file at vpath {path:?}: {err:?}",)) .ok() + .map(Content::new) } - fn edit_path(&self, path: &VPath) -> Option { - Some(EditPath { + fn edit_path(&self, path: &VPath) -> EditPath { + EditPath { path: self.root.join(physical_path(path)), - }) + } + } +} + +impl Dir for PhysicalDir { + fn query(&self, path: &VPath, query: &mut Query) { + query.provide(|| Entries(self.entries(path))); + query.try_provide(|| self.content(path)); + query.provide(|| self.edit_path(path)); } } diff --git a/crates/treehouse/tests/it/vfs.rs b/crates/treehouse/tests/it/vfs.rs index 9a35d7d..ba500b3 100644 --- a/crates/treehouse/tests/it/vfs.rs +++ b/crates/treehouse/tests/it/vfs.rs @@ -1,5 +1,4 @@ mod cd; -mod empty; mod file; mod mount_points; mod physical; diff --git a/crates/treehouse/tests/it/vfs/cd.rs b/crates/treehouse/tests/it/vfs/cd.rs index eb45e9c..cad2583 100644 --- a/crates/treehouse/tests/it/vfs/cd.rs +++ b/crates/treehouse/tests/it/vfs/cd.rs @@ -1,4 +1,6 @@ -use treehouse::vfs::{BufferedFile, Cd, Dir, DirEntry, MemDir, ToDynDir, VPath, VPathBuf}; +use treehouse::vfs::{ + entries, query, BufferedFile, Cd, Content, MemDir, ToDynDir, VPath, VPathBuf, +}; const HEWWO: &[u8] = b"hewwo :3"; const FWOOFEE: &[u8] = b"fwoofee -w-"; @@ -27,20 +29,14 @@ fn dir1() { let outer = vfs(); let inner = Cd::new(outer, VPathBuf::new("inner")); - let mut dir = inner.dir(VPath::ROOT); + let mut dir = entries(&inner, VPath::ROOT); dir.sort(); assert_eq!( dir, vec![ - DirEntry { - path: VPathBuf::new("file1.txt"), - }, - DirEntry { - path: VPathBuf::new("file2.txt"), - }, - DirEntry { - path: VPathBuf::new("innermost"), - } + VPathBuf::new("file1.txt"), + VPathBuf::new("file2.txt"), + VPathBuf::new("innermost"), ] ); } @@ -50,25 +46,9 @@ fn dir2() { let outer = vfs(); let innermost = Cd::new(&outer, VPathBuf::new("inner/innermost")); - let mut dir = innermost.dir(VPath::ROOT); + let mut dir = entries(&innermost, VPath::ROOT); dir.sort(); - assert_eq!( - dir, - vec![DirEntry { - path: VPathBuf::new("file3.txt"), - },] - ); -} - -#[test] -fn content_version() { - let outer = vfs(); - let inner = Cd::new(&outer, VPathBuf::new("inner")); - - assert_eq!( - inner.content_version(VPath::new("test1.txt")), - outer.content_version(VPath::new("inner/test1.txt")) - ); + assert_eq!(dir, vec![VPathBuf::new("file3.txt")]); } #[test] @@ -77,7 +57,7 @@ fn content() { let inner = Cd::new(&outer, VPathBuf::new("inner")); assert_eq!( - inner.content(VPath::new("test1.txt")), - outer.content(VPath::new("inner/test1.txt")) + query::(&inner, VPath::new("test1.txt")).map(Content::bytes), + query::(&outer, VPath::new("inner/test1.txt")).map(Content::bytes) ); } diff --git a/crates/treehouse/tests/it/vfs/empty.rs b/crates/treehouse/tests/it/vfs/empty.rs deleted file mode 100644 index d3e6a70..0000000 --- a/crates/treehouse/tests/it/vfs/empty.rs +++ /dev/null @@ -1,16 +0,0 @@ -use treehouse::vfs::{Dir, EmptyEntry, VPath}; - -#[test] -fn dir() { - assert!(EmptyEntry.dir(VPath::ROOT).is_empty()); -} - -#[test] -fn content_version() { - assert!(EmptyEntry.content_version(VPath::ROOT).is_none()); -} - -#[test] -fn content() { - assert!(EmptyEntry.content(VPath::ROOT).is_none()); -} diff --git a/crates/treehouse/tests/it/vfs/file.rs b/crates/treehouse/tests/it/vfs/file.rs index b0f5429..077b51a 100644 --- a/crates/treehouse/tests/it/vfs/file.rs +++ b/crates/treehouse/tests/it/vfs/file.rs @@ -1,4 +1,4 @@ -use treehouse::vfs::{BufferedFile, Dir, VPath}; +use treehouse::vfs::{entries, query, BufferedFile, Content, VPath}; fn vfs() -> BufferedFile { BufferedFile::new(b"hewwo :3".to_vec()) @@ -7,23 +7,16 @@ fn vfs() -> BufferedFile { #[test] fn dir() { let vfs = vfs(); - assert!(vfs.dir(VPath::ROOT).is_empty()); -} - -#[test] -fn content_version() { - let vfs = vfs(); - assert!( - vfs.content_version(VPath::ROOT).is_none(), - "content_version is not implemented for BufferedFile for now" - ); + assert!(entries(&vfs, VPath::ROOT).is_empty()); } #[test] fn content() { let vfs = vfs(); assert_eq!( - vfs.content(VPath::ROOT).as_deref(), + query::(&vfs, VPath::ROOT) + .map(|c| c.bytes()) + .as_deref(), Some(b"hewwo :3".as_slice()), ); } diff --git a/crates/treehouse/tests/it/vfs/mount_points.rs b/crates/treehouse/tests/it/vfs/mount_points.rs index 22a1a59..f08b956 100644 --- a/crates/treehouse/tests/it/vfs/mount_points.rs +++ b/crates/treehouse/tests/it/vfs/mount_points.rs @@ -1,4 +1,6 @@ -use treehouse::vfs::{BufferedFile, Dir, DirEntry, MemDir, ToDynDir, VPath, VPathBuf}; +use treehouse::vfs::{ + entries, query, BufferedFile, Content, Dir, MemDir, ToDynDir, VPath, VPathBuf, +}; const HEWWO: &[u8] = b"hewwo :3"; const FWOOFEE: &[u8] = b"fwoofee -w-"; @@ -23,52 +25,22 @@ fn vfs() -> MemDir { fn dir() { let vfs = vfs(); - let mut dir = vfs.dir(VPath::new("")); + let mut dir = entries(&vfs, VPath::ROOT); dir.sort(); assert_eq!( dir, vec![ - DirEntry { - path: VPathBuf::new("file1.txt"), - }, - DirEntry { - path: VPathBuf::new("file2.txt"), - }, - DirEntry { - path: VPathBuf::new("inner"), - } + VPathBuf::new("file1.txt"), + VPathBuf::new("file2.txt"), + VPathBuf::new("inner"), ] ); - assert!(vfs.dir(VPath::new("file1.txt")).is_empty()); - assert!(vfs.dir(VPath::new("file2.txt")).is_empty()); + assert!(entries(&vfs, VPath::new("file1.txt")).is_empty()); + assert!(entries(&vfs, VPath::new("file2.txt")).is_empty()); assert_eq!( - vfs.dir(VPath::new("inner")), - vec![DirEntry { - path: VPathBuf::new("inner/file3.txt") - }] - ); -} - -#[test] -fn content_version() { - let vfs = vfs(); - - let file1 = BufferedFile::new(HEWWO.to_vec()); - let file2 = BufferedFile::new(FWOOFEE.to_vec()); - let file3 = BufferedFile::new(BOOP.to_vec()); - - assert_eq!( - vfs.content_version(VPath::new("file1.txt")), - file1.content_version(VPath::ROOT) - ); - assert_eq!( - vfs.content_version(VPath::new("file2.txt")), - file2.content_version(VPath::ROOT) - ); - assert_eq!( - vfs.content_version(VPath::new("inner/file3.txt")), - file3.content_version(VPath::ROOT) + entries(&vfs, VPath::new("inner")), + vec![VPathBuf::new("inner/file3.txt")] ); } @@ -76,13 +48,22 @@ fn content_version() { fn content() { let vfs = vfs(); - assert_eq!(vfs.content(VPath::new("file1.txt")).as_deref(), Some(HEWWO)); assert_eq!( - vfs.content(VPath::new("file2.txt")).as_deref(), + query::(&vfs, VPath::new("file1.txt")) + .map(Content::bytes) + .as_deref(), + Some(HEWWO) + ); + assert_eq!( + query::(&vfs, VPath::new("file2.txt")) + .map(Content::bytes) + .as_deref(), Some(FWOOFEE) ); assert_eq!( - vfs.content(VPath::new("inner/file3.txt")).as_deref(), + query::(&vfs, VPath::new("inner/file3.txt")) + .map(Content::bytes) + .as_deref(), Some(BOOP) ); } diff --git a/crates/treehouse/tests/it/vfs/physical.rs b/crates/treehouse/tests/it/vfs/physical.rs index 0eb6a10..ead0d98 100644 --- a/crates/treehouse/tests/it/vfs/physical.rs +++ b/crates/treehouse/tests/it/vfs/physical.rs @@ -1,6 +1,6 @@ use std::path::Path; -use treehouse::vfs::{DirEntry, PhysicalDir, Dir, VPath, VPathBuf}; +use treehouse::vfs::{entries, query, Content, PhysicalDir, VPath, VPathBuf}; fn vfs() -> PhysicalDir { let root = Path::new("tests/it/vfs_physical").to_path_buf(); @@ -10,28 +10,13 @@ fn vfs() -> PhysicalDir { #[test] fn dir() { let vfs = vfs(); - let dir = vfs.dir(VPath::ROOT); - assert_eq!( - &dir[..], - &[DirEntry { - path: VPathBuf::new("test.txt"), - }] - ); -} - -#[test] -fn content_version() { - let vfs = vfs(); - let content_version = vfs.content_version(VPath::new("test.txt")); - assert_eq!( - content_version, None, - "content_version remains unimplemented for now" - ); + let dir = entries(&vfs, VPath::ROOT); + assert_eq!(&dir[..], &[VPathBuf::new("test.txt")]); } #[test] fn content() { let vfs = vfs(); - let content = vfs.content(VPath::new("test.txt")); + let content = query::(&vfs, VPath::new("test.txt")).map(Content::bytes); assert_eq!(content.as_deref(), Some(b"hewwo :3\n".as_slice())); }