1use std::collections::HashMap;
18use std::fs;
19use std::ops::Deref;
20use std::path::{Path, PathBuf};
21
22use url::Url;
23
24use crate::error::{AnnotatedError, SourceError};
25use crate::util;
26
27use super::Meta;
28use super::yaml::{self, Yaml};
29use super::{Entry, StaticEntry};
30
31#[derive(Debug, Clone)]
35pub struct Source<SourceMeta = (), EntryMeta = ()>
36where
37 SourceMeta: Meta,
38 EntryMeta: Meta,
39{
40 pub title: String,
44 pub root: PathBuf,
48 pub origin: String,
52 pub prefix: String,
56 pub entries: Vec<Entry<EntryMeta>>,
65 pub static_entries: Vec<StaticEntry>,
74 pub stylesheets: Vec<PathBuf>,
80 pub javascript: Vec<PathBuf>,
86 pub icon: Option<PathBuf>,
90 pub well_known: Option<PathBuf>,
94 pub meta: SourceMeta,
96}
97
98pub struct IndexedSource<'a, SourceMeta, EntryMeta>
99where
100 SourceMeta: Meta,
101 EntryMeta: Meta,
102{
103 source: &'a Source<SourceMeta, EntryMeta>,
104 by_name: HashMap<&'a str, &'a Entry<EntryMeta>>,
105 children: HashMap<&'a str, Vec<&'a Entry<EntryMeta>>>,
106 references: HashMap<&'a str, Vec<&'a Entry<EntryMeta>>>,
107}
108
109impl<'a, SourceMeta, EntryMeta> Deref for IndexedSource<'a, SourceMeta, EntryMeta>
110where
111 SourceMeta: Meta,
112 EntryMeta: Meta,
113{
114 type Target = Source<SourceMeta, EntryMeta>;
115
116 fn deref(&self) -> &Self::Target {
117 self.source
118 }
119}
120
121impl<'a, SourceMeta, EntryMeta> IndexedSource<'a, SourceMeta, EntryMeta>
122where
123 SourceMeta: Meta,
124 EntryMeta: Meta,
125{
126 pub fn entry(&self, name: &str) -> Option<&Entry<EntryMeta>> {
127 self.by_name.get(name).copied()
128 }
129 pub fn children(&self, name: &str) -> &[&Entry<EntryMeta>] {
130 self.children.get(name).map(|v| &**v).unwrap_or_default()
131 }
132 pub fn references(&self, name: &str) -> &[&Entry<EntryMeta>] {
133 self.references.get(name).map(|v| &**v).unwrap_or_default()
134 }
135}
136
137impl<SourceMeta, EntryMeta> Source<SourceMeta, EntryMeta>
138where
139 SourceMeta: Meta,
140 EntryMeta: Meta,
141{
142 pub fn index<'a>(&'a self) -> Result<IndexedSource<'a, SourceMeta, EntryMeta>, SourceError> {
144 use crate::model::index::SortDirection::*;
145
146 let by_name: HashMap<_, _> = self.entries.iter().map(|e| (&*e.name, e)).collect();
147 let references = self
148 .entries
149 .iter()
150 .map(|e| {
151 (
152 &*e.name,
153 e.cc.iter()
154 .filter_map(|n| by_name.get(&**n))
155 .copied()
156 .collect(),
157 )
158 })
159 .collect();
160
161 let mut children: HashMap<_, Vec<_>> = {
162 let mut by_directory: HashMap<_, _> = self
163 .entries
164 .iter()
165 .filter_map(|e| e.index.as_ref())
166 .flat_map(|i| &i.directories)
167 .map(|d| &**d)
168 .map(|d| (d, Vec::new()))
169 .collect();
170
171 for entry in &self.entries {
172 if let Some(parent) = Path::new(&entry.name).parent()
173 && let Some(idx) = by_directory.get_mut(parent)
174 {
175 idx.push(entry);
176 }
177 }
178 self.entries
179 .iter()
180 .filter_map(|e| {
181 Some((
182 &*e.name,
183 e.index
184 .as_ref()?
185 .directories
186 .iter()
187 .filter_map(|d| by_directory.get(&**d))
188 .flatten()
189 .copied()
190 .collect(),
191 ))
192 })
193 .collect()
194 };
195
196 for entry in &self.entries {
197 for cc in &entry.cc {
198 let Some(child_entries) = children.get_mut(&**cc) else {
199 return Err(SourceError::Config(
200 format!(
201 "entry {} CCs entry {} which either doesn't exist or has no index",
202 entry.name, cc
203 )
204 .into(),
205 ));
206 };
207 child_entries.push(entry);
208 }
209 }
210
211 for (name, child_entries) in &mut children {
212 let entry = by_name.get(name).expect("missing entry");
213 let index = entry.index.as_ref().expect("missing index");
214
215 match index.sort.direction {
216 Ascending => child_entries.sort_by(|e1, e2| index.sort.field.compare(e1, e2)),
217 Descending => child_entries.sort_by(|e1, e2| index.sort.field.compare(e2, e1)),
218 }
219
220 if let Some(max) = index.max {
221 child_entries.truncate(max as usize);
222 }
223 }
224
225 Ok(IndexedSource {
226 source: self,
227 by_name,
228 references,
229 children,
230 })
231 }
232
233 pub fn new<P: AsRef<Path>>(root: P) -> Result<Self, AnnotatedError<SourceError>> {
235 Self::_new(root.as_ref())
236 }
237
238 fn _new(root: &Path) -> Result<Self, AnnotatedError<SourceError>> {
240 let config_path = root.join("gazetta.yaml");
241 let mut source = Source::from_config(root, &config_path)
242 .map_err(|e| AnnotatedError::new(config_path, e))?;
243 source.reload()?;
244 Ok(source)
245 }
246
247 pub fn reload(&mut self) -> Result<(), AnnotatedError<SourceError>> {
251 self.static_entries.clear();
252 self.entries.clear();
253 self.stylesheets.clear();
254 self.javascript.clear();
255 self.icon = None;
256 self.well_known = None;
257 self.load_entries("")?;
258 self.load_assets()?;
259 self.load_well_known()?;
260 Ok(())
261 }
262
263 #[inline(always)]
264 fn from_config(root: &Path, config_path: &Path) -> Result<Self, SourceError> {
265 let mut config = yaml::load(config_path)?;
266 let (origin, prefix) = match config.remove(&yaml::KEYS.base) {
267 Some(Yaml::String(base)) => {
268 let mut url = Url::parse(&base)?;
269 if url.cannot_be_a_base() {
270 return Err("url cannot be a base".into());
271 }
272 if url.fragment().is_some() {
273 return Err("base url must not specify a fragment".into());
274 }
275 if url.query().is_some() {
276 return Err("base url must not specify a query".into());
277 }
278
279 let prefix = url.path().to_string();
280
281 url.set_path("");
282 let mut origin = url.to_string();
283 if origin.ends_with("/") {
285 origin.pop();
286 }
287
288 (origin, prefix)
289 }
290 Some(..) => return Err("the base url must be a string".into()),
291 None => return Err("you must specify a base url".into()),
292 };
293
294 Ok(Source {
295 title: match config.remove(&yaml::KEYS.title) {
296 Some(Yaml::String(title)) => title,
297 Some(..) => return Err("title must be a string".into()),
298 None => return Err("must specify title".into()),
299 },
300 origin,
301 prefix,
302 root: root.to_owned(),
303 well_known: None,
304 entries: Vec::new(),
305 static_entries: Vec::new(),
306 stylesheets: Vec::new(),
307 javascript: Vec::new(),
308 icon: None,
309 meta: SourceMeta::from_yaml(config)?,
310 })
311 }
312
313 fn load_well_known(&mut self) -> Result<(), AnnotatedError<SourceError>> {
314 let path = self.root.join(".well-known");
315 if try_annotate!(util::exists(&path), path) {
316 self.well_known = Some(path.clone());
317 }
318 Ok(())
319 }
320
321 fn load_assets(&mut self) -> Result<(), AnnotatedError<SourceError>> {
322 let mut path = self.root.join("assets");
323
324 path.push("icon.png");
325 if try_annotate!(util::exists(&path), path) {
326 self.icon = Some(path.clone());
327 }
328
329 path.set_file_name("javascript");
330 if try_annotate!(util::exists(&path), path) {
331 self.javascript = try_annotate!(util::walk_sorted(&path), path);
332 }
333
334 path.set_file_name("stylesheets");
335 if try_annotate!(util::exists(&path), path) {
336 self.stylesheets = try_annotate!(util::walk_sorted(&path), path);
337 }
338 Ok(())
339 }
340
341 fn load_entries(&mut self, dir: &str) -> Result<(), AnnotatedError<SourceError>> {
342 let base_dir = self.root.join(dir);
343
344 for dir_entry in try_annotate!(fs::read_dir(&base_dir), base_dir) {
345 let dir_entry = try_annotate!(dir_entry, base_dir);
346 let file_name = match dir_entry.file_name().into_string() {
347 Ok(s) => {
348 if s.starts_with('.') {
349 continue;
350 } else {
351 s
352 }
353 }
354 Err(s) => {
355 if s.to_string_lossy().starts_with('.') {
359 continue;
360 } else {
361 return Err(AnnotatedError::new(
362 dir_entry.path(),
363 "file names must be valid utf8".into(),
364 ));
365 }
366 }
367 };
368
369 if dir.is_empty() && file_name == "assets" {
371 continue;
372 }
373
374 let file_type = try_annotate!(dir_entry.file_type(), dir_entry.path());
375
376 if file_type.is_file() {
377 if Path::new(&file_name).file_stem().unwrap() == "index" {
378 let entry =
379 try_annotate!(Entry::from_file(dir_entry.path(), dir), dir_entry.path());
380 self.entries.push(entry);
381 }
382 } else if file_type.is_dir() {
383 let name = if dir.is_empty() {
384 file_name.to_owned()
385 } else {
386 format!("{}/{}", dir, &file_name)
387 };
388 match &*file_name {
389 "static" => self.static_entries.push(StaticEntry {
390 name,
391 source: dir_entry.path(),
392 }),
393 "index" => {
394 return Err(AnnotatedError::new(
395 dir_entry.path(),
396 "paths ending in index are reserved for \
397 indices"
398 .into(),
399 ));
400 }
401 _ => self.load_entries(&name)?,
402 }
403 } else if file_type.is_symlink() {
404 unimplemented!();
406 }
407 }
408 Ok(())
409 }
410}