1mod browser;
4mod model;
5mod publication;
6mod repository;
7mod storage;
8
9pub use model::File;
10
11use std::fmt;
12use std::path::{Path, PathBuf};
13
14pub(crate) const PUBLISHED_DIRECTORY: &str = ".published";
15pub(crate) const COORDINATOR_DIRECTORY: &str = ".coordinator";
16
17pub struct Lib {
22 root: PathBuf,
23 name: String,
24 expected_generation: String,
25 pub files: Vec<File>,
26}
27
28pub fn create(web_libs_root: impl AsRef<Path>, name: &str) -> Result<Lib> {
30 repository::create(web_libs_root.as_ref(), name)
31}
32
33pub fn open(web_libs_root: impl AsRef<Path>, name: &str) -> Result<Lib> {
35 repository::open(web_libs_root.as_ref(), name)
36}
37
38pub fn docs(web_libs_root: impl AsRef<Path>, name: &str) -> Result<(String, String)> {
40 repository::docs(web_libs_root.as_ref(), name)
41}
42
43impl Lib {
44 pub(crate) fn from_parts(
45 root: PathBuf,
46 name: String,
47 expected_generation: String,
48 mut files: Vec<File>,
49 ) -> Self {
50 files.sort_by(|left, right| left.path.cmp(&right.path));
51 Self {
52 root,
53 name,
54 expected_generation,
55 files,
56 }
57 }
58
59 pub fn write(&mut self) -> Result<()> {
64 let generation = repository::write(
65 &self.root,
66 &self.name,
67 &self.expected_generation,
68 &self.files,
69 )?;
70 self.expected_generation = generation;
71 self.files.sort_by(|left, right| left.path.cmp(&right.path));
72 Ok(())
73 }
74
75 pub fn check(&self) -> Result<()> {
77 let source = model::validate_tree(&self.files, &self.name)?;
78 browser::check(&self.root, &self.name, &self.files, &source.manifest)
79 }
80
81 pub fn publish(&self) -> Result<()> {
83 let source = model::validate_tree(&self.files, &self.name)?;
84 browser::check(&self.root, &self.name, &self.files, &source.manifest)?;
85 publication::publish(&self.root, &self.name, &self.files, &source)
86 }
87}
88
89impl fmt::Debug for Lib {
90 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
91 formatter
92 .debug_struct("Lib")
93 .field("files", &self.files)
94 .field("root", &self.root)
95 .field("name", &self.name)
96 .field("expected_generation", &"[PRIVATE]")
97 .finish()
98 }
99}
100
101pub type Result<T> = std::result::Result<T, Error>;
102
103#[derive(Clone, Debug, Eq, PartialEq)]
105pub struct Error(String);
106
107impl Error {
108 pub(crate) fn new(message: impl Into<String>) -> Self {
109 let mut message = message.into();
110 const LIMIT: usize = 12 * 1024;
111 if message.len() > LIMIT {
112 let mut end = LIMIT;
113 while !message.is_char_boundary(end) {
114 end -= 1;
115 }
116 message.truncate(end);
117 message.push_str("\n[diagnostic truncated]");
118 }
119 Self(message)
120 }
121
122 pub(crate) fn invalid_name(message: impl Into<String>) -> Self {
123 Self::new(format!("invalid_name: {}", message.into()))
124 }
125
126 pub(crate) fn invalid_source(message: impl Into<String>) -> Self {
127 Self::new(format!("invalid_source: {}", message.into()))
128 }
129
130 pub(crate) fn io(action: &str, error: impl fmt::Display) -> Self {
131 Self::new(format!("io: {action}: {error}"))
132 }
133}
134
135impl fmt::Display for Error {
136 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
137 formatter.write_str(&self.0)
138 }
139}
140
141impl std::error::Error for Error {}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use tempfile::TempDir;
147
148 #[test]
149 fn complete_replacement_commits_and_removes_omitted_files() {
150 let root = TempDir::new().unwrap();
151 let mut library = create(root.path(), "example").unwrap();
152
153 library
154 .files
155 .push(File::new("nested/value.js", "export const value = 7;"));
156 library.write().unwrap();
157 library.files.retain(|file| file.path != "nested/value.js");
158 library.write().unwrap();
159 let reopened = open(root.path(), "example").unwrap();
160 assert!(
161 !reopened
162 .files
163 .iter()
164 .any(|file| file.path == "nested/value.js")
165 );
166 }
167
168 #[test]
169 fn stale_handles_must_reopen() {
170 let root = TempDir::new().unwrap();
171 let mut first = create(root.path(), "example").unwrap();
172 let mut second = open(root.path(), "example").unwrap();
173
174 first.files.push(File::new("first.js", "export {};"));
175 first.write().unwrap();
176
177 second.files.push(File::new("second.js", "export {};"));
178 let error = second.write().unwrap_err();
179 assert!(error.to_string().starts_with("stale_snapshot:"));
180 }
181}