mod browser;
mod model;
mod publication;
mod repository;
mod storage;
pub use model::File;
use std::fmt;
use std::path::{Path, PathBuf};
pub(crate) const COORDINATOR_DIRECTORY: &str = ".coordinator";
pub struct Lib {
source_root: PathBuf,
publications_root: PathBuf,
name: String,
expected_generation: String,
pub files: Vec<File>,
}
pub fn create(
web_libs_root: impl AsRef<Path>,
publications_root: impl AsRef<Path>,
name: &str,
) -> Result<Lib> {
repository::create(web_libs_root.as_ref(), publications_root.as_ref(), name)
}
pub fn open(
web_libs_root: impl AsRef<Path>,
publications_root: impl AsRef<Path>,
name: &str,
) -> Result<Lib> {
repository::open(web_libs_root.as_ref(), publications_root.as_ref(), name)
}
pub fn docs(web_libs_root: impl AsRef<Path>, name: &str) -> Result<(String, String)> {
repository::docs(web_libs_root.as_ref(), name)
}
impl Lib {
pub(crate) fn from_parts(
source_root: PathBuf,
publications_root: PathBuf,
name: String,
expected_generation: String,
mut files: Vec<File>,
) -> Self {
files.sort_by(|left, right| left.path.cmp(&right.path));
Self {
source_root,
publications_root,
name,
expected_generation,
files,
}
}
pub fn write(&mut self) -> Result<()> {
let generation = repository::write(
&self.source_root,
&self.name,
&self.expected_generation,
&self.files,
)?;
self.expected_generation = generation;
self.files.sort_by(|left, right| left.path.cmp(&right.path));
Ok(())
}
pub fn check(&self) -> Result<()> {
let source = model::validate_tree(&self.files, &self.name)?;
browser::check(
&self.publications_root,
&self.name,
&self.files,
&source.manifest,
)
}
pub fn publish(&self) -> Result<()> {
let source = model::validate_tree(&self.files, &self.name)?;
browser::check(
&self.publications_root,
&self.name,
&self.files,
&source.manifest,
)?;
publication::publish(&self.publications_root, &self.name, &self.files, &source)
}
}
impl fmt::Debug for Lib {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("Lib")
.field("files", &self.files)
.field("source_root", &self.source_root)
.field("publications_root", &self.publications_root)
.field("name", &self.name)
.field("expected_generation", &"[PRIVATE]")
.finish()
}
}
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Error(String);
impl Error {
pub(crate) fn new(message: impl Into<String>) -> Self {
let mut message = message.into();
const LIMIT: usize = 12 * 1024;
if message.len() > LIMIT {
let mut end = LIMIT;
while !message.is_char_boundary(end) {
end -= 1;
}
message.truncate(end);
message.push_str("\n[diagnostic truncated]");
}
Self(message)
}
pub(crate) fn invalid_name(message: impl Into<String>) -> Self {
Self::new(format!("invalid_name: {}", message.into()))
}
pub(crate) fn invalid_source(message: impl Into<String>) -> Self {
Self::new(format!("invalid_source: {}", message.into()))
}
pub(crate) fn io(action: &str, error: impl fmt::Display) -> Self {
Self::new(format!("io: {action}: {error}"))
}
}
impl fmt::Display for Error {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.0)
}
}
impl std::error::Error for Error {}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn complete_replacement_commits_and_removes_omitted_files() {
let root = TempDir::new().unwrap();
let source = root.path().join("source");
let publications = root.path().join("publications");
let mut library = create(&source, &publications, "example").unwrap();
library
.files
.push(File::new("nested/value.js", "export const value = 7;"));
library.write().unwrap();
library.files.retain(|file| file.path != "nested/value.js");
library.write().unwrap();
let reopened = open(&source, &publications, "example").unwrap();
assert!(
!reopened
.files
.iter()
.any(|file| file.path == "nested/value.js")
);
}
#[test]
fn stale_handles_must_reopen() {
let root = TempDir::new().unwrap();
let source = root.path().join("source");
let publications = root.path().join("publications");
let mut first = create(&source, &publications, "example").unwrap();
let mut second = open(&source, &publications, "example").unwrap();
first.files.push(File::new("first.js", "export {};"));
first.write().unwrap();
second.files.push(File::new("second.js", "export {};"));
let error = second.write().unwrap_err();
assert!(error.to_string().starts_with("stale_snapshot:"));
}
}