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