change vfs API to something akin to the deleted std provider API

this gives us a _much_ easier time with composing file systems, since you can call `query` on the inner file system and all the magic will be done for you
the overhead is technically slightly higher, but seems to be drowned out by other more expensive stuff; i couldn't measure a difference in my traces
This commit is contained in:
liquidex 2024-11-29 20:03:32 +01:00
parent 5b6d637f44
commit 600651ec16
29 changed files with 418 additions and 611 deletions

View file

@ -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<Edit> {
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::<Content>(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::<EditPath>(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<Edit> {
// 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::<EditPath>(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<Ed
fn fix_one(dir: &dyn Dir, path: &VPath) -> anyhow::Result<Edit> {
if path.extension() == Some("tree") {
let Some(content) = dir.content(path) else {
let Some(content) = vfs::query::<Content>(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<Ed
let mut treehouse = Treehouse::new();
let mut diagnostics = vec![];
let file_id = treehouse.add_file(path.to_owned(), Source::Other(content));
let edit_path = dir.edit_path(path).context("path is not editable")?;
let edit_path = vfs::query::<EditPath>(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() {

View file

@ -73,7 +73,7 @@ struct VfsQuery {
#[instrument(skip(state))]
async fn get_static_file(path: &str, query: &VfsQuery, state: &Server) -> Option<Response> {
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<Arc<Server>>)
.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 =

View file

@ -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>(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) {

View file

@ -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<ImageSize> {
self.pics.get(id).and_then(|path| pics_dir.image_size(path))
self.pics
.get(id)
.and_then(|path| vfs::query::<ImageSize>(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<Syntax, _> = dir
.content(path)
let result: Result<Syntax, _> = vfs::query::<Content>(&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")

View file

@ -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::<Content>(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<Dirs>,
sources: Arc<Sources>,
@ -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<DirEntry> {
fn dir(&self, path: &VPath) -> Vec<VPathBuf> {
// 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<Vec<u8>> {
fn content(&self, path: &VPath) -> Option<Content> {
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<String> {
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));
}
}

View file

@ -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<DirEntry> {
fn entries(&self, path: &VPath) -> Vec<VPathBuf> {
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<Vec<u8>> {
fn content(&self, path: &VPath) -> Option<Content> {
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<String> {
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));
}
}

View file

@ -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<ScopedJson<'reg, 'rc>, 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::<Content>(&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"))
}

View file

@ -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::<ImageSize>(&self.renderer.dirs.emoji, vpath)
{
write!(
out,
r#" width="{}" height="{}""#,

View file

@ -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::<Content>(&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<Treehouse> {
.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::<Content>(&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)

View file

@ -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::<Content>(&dirs.static_, vpath))
.and_then(|c| c.string().ok())
.ok_or(InvalidTemplate),
_ => Err(InvalidTemplate),
}

View file

@ -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<DirEntry>;
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<Vec<u8>>;
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<String>;
pub fn query<'a, T>(dir: &'a (impl Dir + ?Sized), path: &VPath) -> Option<T>
where
T: 'static + Fork,
{
let mut slot = TaggedOption::<'a, tags::Value<T>>(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<ImageSize> {
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<VPathBuf> {
None
pub fn provide<T>(&mut self, f: impl FnOnce() -> T)
where
T: 'static + Fork,
{
if let Some(result @ TaggedOption(None)) = self.erased.downcast_mut::<tags::Value<T>>() {
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<EditPath> {
None
pub fn try_provide<T>(&mut self, f: impl FnOnce() -> Option<T>)
where
T: 'static + Fork,
{
if let Some(result @ TaggedOption(None)) = self.erased.downcast_mut::<tags::Value<T>>() {
result.0 = f();
}
}
}
mod tags {
use std::marker::PhantomData;
pub trait Type<'a>: Sized + 'static {
type Reified: 'a;
}
pub struct Value<T>(PhantomData<T>)
where
T: 'static;
impl<T> Type<'_> for Value<T>
where
T: 'static,
{
type Reified = T;
}
}
#[repr(transparent)]
struct TaggedOption<'a, I: tags::Type<'a>>(Option<I::Reified>);
#[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::<I>()
}
}
impl<'a> dyn Erased<'a> + 'a {
fn downcast_mut<I>(&mut self) -> Option<&mut TaggedOption<'a, I>>
where
I: tags::Type<'a>,
{
if self.tag_id() == TypeId::of::<I>() {
// SAFETY: Just checked whether we're pointing to an I.
Some(unsafe { &mut *(self as *mut Self).cast::<TaggedOption<'a, I>>() })
} else {
None
}
}
}
@ -129,28 +172,8 @@ impl<T> Dir for &T
where
T: Dir,
{
fn dir(&self, path: &VPath) -> Vec<DirEntry> {
(**self).dir(path)
}
fn content(&self, path: &VPath) -> Option<Vec<u8>> {
(**self).content(path)
}
fn content_version(&self, path: &VPath) -> Option<String> {
(**self).content_version(path)
}
fn image_size(&self, path: &VPath) -> Option<ImageSize> {
(**self).image_size(path)
}
fn anchor(&self, path: &VPath) -> Option<VPathBuf> {
(**self).anchor(path)
}
fn edit_path(&self, path: &VPath) -> Option<EditPath> {
(**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<DirEntry> {
self.arc.dir(path)
}
fn content(&self, path: &VPath) -> Option<Vec<u8>> {
self.arc.content(path)
}
fn content_version(&self, path: &VPath) -> Option<String> {
self.arc.content_version(path)
}
fn image_size(&self, path: &VPath) -> Option<ImageSize> {
self.arc.image_size(path)
}
fn anchor(&self, path: &VPath) -> Option<VPathBuf> {
self.arc.anchor(path)
}
fn edit_path(&self, path: &VPath) -> Option<EditPath> {
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<VPathBuf>);
/// Byte content in an entry.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Content {
bytes: Vec<u8>,
}
/// 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<u8>) -> Self {
Self { bytes }
}
pub fn bytes(self) -> Vec<u8> {
self.bytes
}
pub fn string(self) -> Result<String, FromUtf8Error> {
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<VPathBuf> {
query::<Entries>(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<String> {
let anchor = dir.anchor(path)?;
if let Some(version) = dir.content_version(path) {
Some(format!("{}/{anchor}?v={version}", site))
let anchor = query::<Anchor>(dir, path)?;
if let Some(version) = query::<ContentVersion>(dir, path) {
Some(format!("{}/{}?v={}", site, anchor.path, version.string))
} else {
Some(format!("{}/{anchor}", site))
Some(format!("{}/{}", site, anchor.path))
}
}

View file

@ -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<T> {
inner: T,
@ -17,28 +17,12 @@ impl<T> Dir for Anchored<T>
where
T: Dir,
{
fn dir(&self, path: &VPath) -> Vec<DirEntry> {
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<Vec<u8>> {
self.inner.content(path)
}
fn content_version(&self, path: &VPath) -> Option<String> {
self.inner.content_version(path)
}
fn image_size(&self, path: &VPath) -> Option<ImageSize> {
self.inner.image_size(path)
}
fn anchor(&self, path: &VPath) -> Option<VPathBuf> {
Some(self.at.join(path))
}
fn edit_path(&self, path: &VPath) -> Option<EditPath> {
self.inner.edit_path(path)
self.inner.query(path, query);
}
}

View file

@ -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<Vec<u8>> {
pub async fn content(&self, path: &VPath) -> Option<Content> {
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::<Content>(&this.inner, &path))
.await
.unwrap()
}

View file

@ -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<T> {
parent: T,
@ -13,42 +13,34 @@ impl<T> Cd<T> {
}
}
impl<T> Cd<T>
where
T: Dir,
{
fn dir(&self, path: &VPath) -> Vec<VPathBuf> {
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<T> Dir for Cd<T>
where
T: Dir,
{
fn dir(&self, path: &VPath) -> Vec<DirEntry> {
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<String> {
self.parent.content_version(&self.path.join(path))
}
fn content(&self, path: &VPath) -> Option<Vec<u8>> {
self.parent.content(&self.path.join(path))
}
fn image_size(&self, path: &VPath) -> Option<ImageSize> {
self.parent.image_size(&self.path.join(path))
}
fn anchor(&self, path: &VPath) -> Option<VPathBuf> {
self.parent.anchor(&self.path.join(path))
}
fn edit_path(&self, path: &VPath) -> Option<EditPath> {
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);
}
}

View file

@ -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<T> {
inner: T,
cache: DashMap<VPathBuf, Vec<u8>>,
cache: DashMap<VPathBuf, Content>,
}
impl<T> ContentCache<T> {
@ -39,41 +39,32 @@ where
}
}
impl<T> Dir for ContentCache<T>
impl<T> ContentCache<T>
where
T: Dir,
{
fn dir(&self, path: &VPath) -> Vec<DirEntry> {
self.inner.dir(path)
}
#[instrument(name = "ContentCache::content", skip(self))]
fn content(&self, path: &VPath) -> Option<Vec<u8>> {
fn content(&self, path: &VPath) -> Option<Content> {
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::<Content>(&self.inner, path);
if let Some(content) = &content {
self.cache.insert(path.to_owned(), content.clone());
}
content
})
}
}
fn content_version(&self, path: &VPath) -> Option<String> {
self.inner.content_version(path)
}
impl<T> Dir for ContentCache<T>
where
T: Dir,
{
fn query(&self, path: &VPath, query: &mut Query) {
query.try_provide(|| self.content(path));
fn image_size(&self, path: &VPath) -> Option<ImageSize> {
self.inner.image_size(path)
}
fn anchor(&self, path: &VPath) -> Option<VPathBuf> {
self.inner.anchor(path)
}
fn edit_path(&self, path: &VPath) -> Option<EditPath> {
self.inner.edit_path(path)
self.inner.query(path, query);
}
}

View file

@ -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<T> {
inner: T,
cache: DashMap<VPathBuf, String>,
cache: DashMap<VPathBuf, ContentVersion>,
}
impl<T> Blake3ContentVersionCache<T> {
@ -19,26 +19,20 @@ impl<T> Blake3ContentVersionCache<T> {
}
}
impl<T> Dir for Blake3ContentVersionCache<T>
impl<T> Blake3ContentVersionCache<T>
where
T: Dir,
{
fn dir(&self, path: &VPath) -> Vec<DirEntry> {
self.inner.dir(path)
}
fn content(&self, path: &VPath) -> Option<Vec<u8>> {
self.inner.content(path)
}
#[instrument(name = "Blake3ContentVersionCache::content_version", skip(self))]
fn content_version(&self, path: &VPath) -> Option<String> {
fn content_version(&self, path: &VPath) -> Option<ContentVersion> {
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::<Content>(&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<ImageSize> {
self.inner.image_size(path)
}
impl<T> Dir for Blake3ContentVersionCache<T>
where
T: Dir,
{
fn query(&self, path: &VPath, query: &mut Query) {
query.try_provide(|| self.content_version(path));
fn anchor(&self, path: &VPath) -> Option<VPathBuf> {
self.inner.anchor(path)
}
fn edit_path(&self, path: &VPath) -> Option<EditPath> {
self.inner.edit_path(path)
self.inner.query(path, query);
}
}

View file

@ -1,18 +0,0 @@
use super::{Dir, DirEntry, VPath};
#[derive(Debug)]
pub struct EmptyEntry;
impl Dir for EmptyEntry {
fn dir(&self, _path: &VPath) -> Vec<DirEntry> {
vec![]
}
fn content_version(&self, _path: &VPath) -> Option<String> {
None
}
fn content(&self, _path: &VPath) -> Option<Vec<u8>> {
None
}
}

View file

@ -1,6 +1,6 @@
use std::fmt;
use super::{DirEntry, Dir, VPath};
use super::{Content, Dir, Query, VPath};
pub struct BufferedFile {
pub content: Vec<u8>,
@ -13,20 +13,9 @@ impl BufferedFile {
}
impl Dir for BufferedFile {
fn dir(&self, _path: &VPath) -> Vec<DirEntry> {
vec![]
}
fn content_version(&self, _path: &VPath) -> Option<String> {
// TODO: StaticFile should _probably_ calculate a content_version.
None
}
fn content(&self, path: &VPath) -> Option<Vec<u8>> {
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()));
}
}
}

View file

@ -1,6 +1,6 @@
use core::fmt;
use super::{Dir, DirEntry, EditPath, ImageSize, VPath, VPathBuf};
use super::{Dir, Query, VPath};
pub struct HtmlCanonicalize<T> {
inner: T,
@ -16,33 +16,13 @@ impl<T> Dir for HtmlCanonicalize<T>
where
T: Dir,
{
fn dir(&self, path: &VPath) -> Vec<DirEntry> {
self.inner.dir(path)
}
fn content(&self, path: &VPath) -> Option<Vec<u8>> {
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<String> {
self.inner.content_version(path)
}
fn image_size(&self, path: &VPath) -> Option<ImageSize> {
self.inner.image_size(path)
}
fn anchor(&self, path: &VPath) -> Option<VPathBuf> {
self.inner.anchor(path)
}
fn edit_path(&self, path: &VPath) -> Option<EditPath> {
self.inner.edit_path(path)
self.inner.query(&path, query);
}
}

View file

@ -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<T> {
inner: T,
@ -28,8 +28,8 @@ where
{
fn compute_image_size(&self, path: &VPath) -> anyhow::Result<Option<ImageSize>> {
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::<Content>(&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<T> Dir for ImageSizeCache<T>
where
T: Dir,
{
fn dir(&self, path: &VPath) -> Vec<DirEntry> {
self.inner.dir(path)
}
fn content(&self, path: &VPath) -> Option<Vec<u8>> {
self.inner.content(path)
}
fn content_version(&self, path: &VPath) -> Option<String> {
self.inner.content_version(path)
}
#[instrument("ImageSizeCache::image_size", skip(self))]
fn image_size(&self, path: &VPath) -> Option<ImageSize> {
@ -73,13 +56,16 @@ where
image_size
})
}
}
fn anchor(&self, path: &VPath) -> Option<VPathBuf> {
self.inner.anchor(path)
}
impl<T> Dir for ImageSizeCache<T>
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<EditPath> {
self.inner.edit_path(path)
self.inner.query(path, query);
}
}

View file

@ -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<String, DynDir>,
@ -55,6 +55,23 @@ impl MemDir {
Resolved::None
}
fn dir(&self, path: &VPath) -> Vec<VPathBuf> {
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<DirEntry> {
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<String> {
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<Vec<u8>> {
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<ImageSize> {
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<VPathBuf> {
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<EditPath> {
match self.resolve(path) {
Resolved::MountPoint {
fs,
fs_path: _,
subpath,
} => fs.edit_path(subpath),
Resolved::Root | Resolved::None => None,
} => fs.query(subpath, query),
}
}
}

View file

@ -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<DirEntry> {
let mut dir = self.base.dir(path);
dir.append(&mut self.overlay.dir(path));
fn dir(&self, path: &VPath) -> Vec<VPathBuf> {
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<Vec<u8>> {
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<String> {
self.overlay
.content_version(path)
.or_else(|| self.base.content_version(path))
}
fn image_size(&self, path: &VPath) -> Option<ImageSize> {
self.overlay
.image_size(path)
.or_else(|| self.base.image_size(path))
}
fn anchor(&self, path: &VPath) -> Option<VPathBuf> {
self.overlay.anchor(path).or_else(|| self.base.anchor(path))
}
fn edit_path(&self, path: &VPath) -> Option<EditPath> {
self.overlay
.edit_path(path)
.or_else(|| self.base.edit_path(path))
self.overlay.query(path, query);
self.base.query(path, query);
}
}

View file

@ -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<DirEntry> {
fn entries(&self, vpath: &VPath) -> Vec<VPathBuf> {
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<String> {
None
}
#[instrument("PhysicalDir::content", skip(self))]
fn content(&self, path: &VPath) -> Option<Vec<u8>> {
fn content(&self, path: &VPath) -> Option<Content> {
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<EditPath> {
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));
}
}

View file

@ -1,5 +1,4 @@
mod cd;
mod empty;
mod file;
mod mount_points;
mod physical;

View file

@ -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::<Content>(&inner, VPath::new("test1.txt")).map(Content::bytes),
query::<Content>(&outer, VPath::new("inner/test1.txt")).map(Content::bytes)
);
}

View file

@ -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());
}

View file

@ -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::<Content>(&vfs, VPath::ROOT)
.map(|c| c.bytes())
.as_deref(),
Some(b"hewwo :3".as_slice()),
);
}

View file

@ -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::<Content>(&vfs, VPath::new("file1.txt"))
.map(Content::bytes)
.as_deref(),
Some(HEWWO)
);
assert_eq!(
query::<Content>(&vfs, VPath::new("file2.txt"))
.map(Content::bytes)
.as_deref(),
Some(FWOOFEE)
);
assert_eq!(
vfs.content(VPath::new("inner/file3.txt")).as_deref(),
query::<Content>(&vfs, VPath::new("inner/file3.txt"))
.map(Content::bytes)
.as_deref(),
Some(BOOP)
);
}

View file

@ -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::<Content>(&vfs, VPath::new("test.txt")).map(Content::bytes);
assert_eq!(content.as_deref(), Some(b"hewwo :3\n".as_slice()));
}