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