use crate::constants::{MANIFEST_FILE_NAME, OBJECT_DEFINITION_FILE_NAME};
use crate::file_system_stdlib::NativeFileSystem;
use crate::object_definition::ObjectDefinition;
use crate::site::Site;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use tracing::{debug, warn};
const MARKERS: [&str; 2] = [MANIFEST_FILE_NAME, OBJECT_DEFINITION_FILE_NAME];
pub(crate) struct SiteState {
pub root: PathBuf,
pub site: Option<Site>,
}
impl SiteState {
fn load(root: PathBuf) -> Self {
let fs = NativeFileSystem::new(&root);
let site = match Site::load(&fs, Some("")) {
Ok(site) => Some(site),
Err(err) => {
warn!("{}: could not load site: {err}", root.display());
None
}
};
Self { root, site }
}
pub fn is_manifest(&self, file: &Path) -> bool {
file == self.root.join(MANIFEST_FILE_NAME)
}
pub fn is_definitions(&self, file: &Path) -> bool {
let named = self
.site
.as_ref()
.map(|site| self.root.join(&site.manifest.object_definition_file));
match named {
Some(named) => file == named,
None => file == self.root.join(OBJECT_DEFINITION_FILE_NAME),
}
}
pub fn manifest(&self) -> Option<&crate::manifest::Manifest> {
self.site.as_ref().map(|site| &site.manifest)
}
pub fn definition_for(&self, file: &Path) -> Option<&ObjectDefinition> {
let site = self.site.as_ref()?;
let relative = file
.strip_prefix(&self.root)
.ok()?
.strip_prefix(&site.manifest.objects_dir)
.ok()?;
let mut parts = relative.components();
let first = parts.next()?.as_os_str().to_str()?;
let name = if parts.next().is_some() {
first.to_string()
} else {
first.strip_suffix(".toml")?.to_string()
};
site.object_definitions.get(&name)
}
}
#[derive(Default)]
pub(crate) struct Workspace {
sites: Vec<SiteState>,
resolved: HashMap<PathBuf, Option<usize>>,
}
impl Workspace {
pub fn site_for(&mut self, file: &Path) -> Option<&SiteState> {
let dir = file.parent()?;
let index = match self.resolved.get(dir) {
Some(found) => *found,
None => {
let found = self.find_or_load(dir);
self.resolved.insert(dir.to_path_buf(), found);
found
}
};
index.map(|index| &self.sites[index])
}
fn find_or_load(&mut self, dir: &Path) -> Option<usize> {
let root = site_root(dir)?;
if let Some(index) = self.sites.iter().position(|site| site.root == root) {
return Some(index);
}
debug!("loading site at {}", root.display());
self.sites.push(SiteState::load(root));
Some(self.sites.len() - 1)
}
pub fn reload(&mut self) {
self.sites.clear();
self.resolved.clear();
}
pub fn is_definition_file(&self, file: &Path) -> bool {
file.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| MARKERS.contains(&name))
}
}
fn site_root(dir: &Path) -> Option<PathBuf> {
dir.ancestors()
.find(|candidate| MARKERS.iter().any(|marker| candidate.join(marker).exists()))
.map(Path::to_path_buf)
}
#[cfg(test)]
mod tests {
use super::*;
fn fixture() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/website")
}
#[test]
fn finds_the_site_a_file_belongs_to() {
let mut workspace = Workspace::default();
let site = workspace
.site_for(&fixture().join("pages/index.liquid"))
.expect("no site found");
assert_eq!(site.root, fixture());
assert!(site.site.is_some());
}
#[test]
fn reports_no_site_outside_one() {
let mut workspace = Workspace::default();
assert!(workspace
.site_for(Path::new("/tmp/nowhere/a.liquid"))
.is_none());
}
#[test]
fn maps_object_files_to_their_definitions() {
let mut workspace = Workspace::default();
let root = fixture();
let site = workspace
.site_for(&root.join("pages/index.liquid"))
.unwrap();
let objects = root.join("objects");
let listed = site.definition_for(&objects.join("section/one.toml"));
assert_eq!(listed.map(|d| &d.name[..]), Some("section"));
let single = site.definition_for(&objects.join("section.toml"));
assert_eq!(single.map(|d| &d.name[..]), Some("section"));
assert!(site
.definition_for(&root.join("pages/index.liquid"))
.is_none());
assert!(site
.definition_for(&objects.join("nonexistent/a.toml"))
.is_none());
}
}