flams_math_archives/
lib.rs

1#![allow(unexpected_cfgs)]
2#![cfg_attr(all(doc, CHANNEL_NIGHTLY), feature(doc_cfg))]
3#![doc = include_str!("../README.md")]
4/*!
5 * ## Feature flags
6 */
7#![cfg_attr(doc,doc = document_features::document_features!())]
8
9pub mod archive_iter;
10pub mod artifacts;
11pub mod backend;
12pub mod document_file;
13pub mod formats;
14pub mod manager;
15pub mod manifest;
16pub mod mathhub;
17pub mod source_files;
18#[cfg(feature = "rdf")]
19pub mod triple_store;
20pub mod utils;
21
22#[cfg(feature = "rdf")]
23use crate::triple_store::RDFStore;
24use crate::{
25    artifacts::{Artifact, ContentResult, FileOrString},
26    formats::{BuildTargetId, SourceFormatId},
27    manifest::RepositoryData,
28    source_files::{FileStates, SourceDir},
29    utils::{
30        AsyncEngine,
31        errors::{ArtifactSaveError, BackendError, FileError},
32        ignore_source::IgnoreSource,
33        path_ext::PathExt,
34    },
35};
36use flams_backend_types::{
37    archive_json::{ArchiveIndex, Institution},
38    archives::FileStateSummary,
39};
40use ftml_ontology::{domain::modules::Module, narrative::documents::Document};
41use ftml_uris::{
42    ArchiveId, ArchiveUri, IsDomainUri, Language, SimpleUriName, UriPath, UriWithArchive,
43    UriWithPath,
44};
45use std::{
46    hint::unreachable_unchecked,
47    path::{Path, PathBuf},
48    str,
49};
50
51type Result<T> = std::result::Result<T, BackendError>;
52/*
53pub trait DocumentSource: std::fmt::Debug {
54    fn get_document(&self) -> impl Future<Output = Result<Document>>
55    where
56        Self: Sized;
57    fn get_css(&self) -> impl Future<Output = Result<Box<[Css]>>>
58    where
59        Self: Sized;
60    fn get_html(&self) -> impl Future<Output = Result<Box<str>>>
61    where
62        Self: Sized;
63    fn get_document_sync(&self) -> Result<Document>;
64    fn get_css_sync(&self) -> Result<Box<[Css]>>;
65    fn get_html_sync(&self) -> Result<Box<str>>;
66}
67 */
68
69pub trait MathArchive {
70    fn uri(&self) -> &ArchiveUri;
71    fn path(&self) -> &Path;
72    fn is_meta(&self) -> bool;
73
74    #[inline]
75    fn id(&self) -> &ArchiveId {
76        self.uri().archive_id()
77    }
78
79    /// # Errors
80    fn load_module(&self, path: Option<&UriPath>, name: &str) -> Result<Module>;
81
82    /// # Errors
83    fn load_module_async<A: AsyncEngine>(
84        &self,
85        path: Option<&UriPath>,
86        name: &str,
87    ) -> impl Future<Output = Result<Module>> + 'static + use<Self, A>
88    where
89        Self: Sized;
90
91    /*
92    fn load_html(&self, path: Option<&UriPath>, name: &str, language: Language) -> Option<String>;
93    fn load_html_body(
94        &self,
95        path: Option<&UriPath>,
96        name: &str,
97        language: Language,
98        full: bool,
99    ) -> Option<(Vec<Css>, String)>;
100    fn load_html_fragment(
101        &self,
102        path: Option<&UriPath>,
103        name: &str,
104        language: Language,
105        range: DocumentRange,
106    ) -> Option<(Vec<Css>, String)>;
107    fn load_reference<T: flams_ontology::Resourcable>(
108        &self,
109        path: Option<&UriPath>,
110        name: &str,
111        language: Language,
112        range: DocumentRange,
113    ) -> eyre::Result<T>;
114    */
115}
116
117pub trait ExternalArchive: Send + Sync + MathArchive + std::any::Any + std::fmt::Debug {
118    #[inline]
119    fn local_out(&self) -> Option<&dyn LocallyBuilt> {
120        None
121    }
122    #[inline]
123    fn buildable(&self) -> Option<&dyn BuildableArchive> {
124        None
125    }
126
127    fn load_document(
128        &self,
129        path: Option<&UriPath>,
130        name: &str,
131        language: Language,
132    ) -> Option<Document>;
133}
134
135#[derive(Debug)]
136pub enum Archive {
137    Local(Box<LocalArchive>),
138    Ext(&'static ArchiveKind, Box<dyn ExternalArchive>),
139}
140impl Archive {
141    fn buildable(&self) -> Option<&dyn BuildableArchive> {
142        match self {
143            Self::Local(l) => Some(&**l as _),
144            Self::Ext(_, a) => a.buildable(),
145        }
146    }
147}
148
149impl std::hash::Hash for Archive {
150    #[inline]
151    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
152        self.uri().hash(state);
153    }
154}
155impl std::borrow::Borrow<ArchiveUri> for Archive {
156    #[inline]
157    fn borrow(&self) -> &ArchiveUri {
158        self.uri()
159    }
160}
161impl PartialEq for Archive {
162    #[inline]
163    fn eq(&self, other: &Self) -> bool {
164        *self.uri() == *other.uri()
165    }
166}
167impl Eq for Archive {}
168
169impl MathArchive for Archive {
170    fn uri(&self) -> &ArchiveUri {
171        match self {
172            Self::Local(a) => a.uri(),
173            Self::Ext(_, a) => a.uri(),
174        }
175    }
176    fn path(&self) -> &Path {
177        match self {
178            Self::Local(a) => a.path(),
179            Self::Ext(_, a) => a.path(),
180        }
181    }
182    fn is_meta(&self) -> bool {
183        match self {
184            Self::Local(a) => a.is_meta(),
185            Self::Ext(_, a) => a.is_meta(),
186        }
187    }
188    fn load_module(&self, path: Option<&UriPath>, name: &str) -> Result<Module> {
189        match self {
190            Self::Local(a) => a.load_module(path, name),
191            Self::Ext(_, a) => a.load_module(path, name),
192        }
193    }
194    fn load_module_async<A: AsyncEngine>(
195        &self,
196        path: Option<&UriPath>,
197        name: &str,
198    ) -> impl Future<Output = Result<Module>> + 'static + use<A> {
199        match self {
200            Self::Local(a) => a.load_module_async::<A>(path, name),
201            Self::Ext(_, a) => todo!(),
202        }
203    }
204}
205
206#[derive(Copy, Clone, Debug)]
207pub struct ArchiveKind {
208    pub name: &'static str,
209    #[allow(clippy::type_complexity)]
210    make_new: fn(RepositoryData, &Path) -> std::result::Result<Box<dyn ExternalArchive>, String>,
211}
212
213impl ArchiveKind {
214    #[inline]
215    pub fn all() -> impl Iterator<Item = &'static Self> {
216        inventory::iter.into_iter()
217    }
218    #[must_use]
219    pub fn get(name: &str) -> Option<&'static Self> {
220        Self::all().find(|e| e.name == name)
221    }
222}
223inventory::collect!(ArchiveKind);
224#[macro_export]
225macro_rules! archive_kind {
226    ($i:ident { $($t:tt)* }) => {
227        pub static $i : $crate::ArchiveKind = $crate::ArchiveKind { $($t)* };
228        $crate::formats::__reexport::submit!{ $i }
229    };
230}
231
232pub trait BuildableArchive: MathArchive {
233    fn file_state(&self) -> FileStates;
234    fn formats(&self) -> &[SourceFormatId];
235    fn get_log(&self, relative_path: &str, target: BuildTargetId) -> PathBuf;
236
237    #[allow(clippy::too_many_arguments)]
238    /// # Errors
239    fn save(
240        &self,
241        in_doc: &ftml_uris::DocumentUri,
242        rel_path: Option<&UriPath>,
243        log: FileOrString,
244        from: BuildTargetId,
245        result: Option<Box<dyn Artifact>>,
246        #[cfg(feature = "rdf")] relational: &RDFStore,
247        #[cfg(feature = "rdf")] load: bool,
248    ) -> std::result::Result<(), ArtifactSaveError>;
249
250    #[cfg(feature = "rdf")]
251    fn save_triples(
252        &self,
253        in_doc: &ftml_uris::DocumentUri,
254        rel_path: Option<&UriPath>,
255        relational: &RDFStore,
256        load: bool,
257        iter: Vec<ulo::rdf_types::Triple>,
258    );
259}
260
261pub trait LocallyBuilt: BuildableArchive {
262    fn out_dir(&self) -> &Path;
263
264    fn out_path_of(
265        &self,
266        path: Option<&UriPath>,
267        doc_name: &SimpleUriName,
268        rel_path: Option<&UriPath>,
269        language: Language,
270    ) -> PathBuf;
271
272    fn document_file(
273        &self,
274        path: Option<&UriPath>,
275        rel_path: Option<&UriPath>,
276        doc_name: &SimpleUriName,
277        language: Language,
278    ) -> PathBuf {
279        self.out_path_of(path, doc_name, rel_path, language)
280            .join("content")
281    }
282}
283
284#[derive(Debug)]
285pub struct LocalArchive {
286    pub(crate) uri: ArchiveUri,
287    pub(crate) out_path: PathBuf,
288    pub(crate) source: Option<Box<str>>,
289    //pub(crate) attributes: Vec<(Box<str>, Box<str>)>,
290    pub(crate) formats: smallvec::SmallVec<SourceFormatId, 1>,
291    //pub dependencies: Box<[ArchiveId]>,
292    pub(crate) file_state: parking_lot::RwLock<SourceDir>,
293    //pub(crate) institutions: Box<[Institution]>,
294    //pub(crate) index: Box<[ArchiveIndex]>,
295    pub ignore: IgnoreSource,
296    #[cfg(feature = "git")]
297    pub(crate) is_managed: std::sync::OnceLock<Option<flams_git::GitUrl>>,
298}
299impl MathArchive for LocalArchive {
300    #[inline]
301    fn uri(&self) -> &ArchiveUri {
302        &self.uri
303    }
304    fn path(&self) -> &Path {
305        self.out_path
306            .parent()
307            .expect("out path of an archive *must* have a parent")
308    }
309
310    fn is_meta(&self) -> bool {
311        self.uri.archive_id().is_meta()
312    }
313
314    fn load_module(&self, path: Option<&UriPath>, name: &str) -> Result<Module> {
315        let out = path.map_or_else(
316            || self.out_dir().join(".modules"),
317            |n| self.out_dir().join_uri_path(n).join(".modules"),
318        );
319        let out = Self::escape_module_name(&out, name);
320        if !out.exists() {
321            return Err(BackendError::NotFound(ftml_uris::UriKind::Module));
322        }
323        let file = std::io::BufReader::new(std::fs::File::open(out)?);
324        let ret = bincode::decode_from_reader(file, bincode::config::standard())?;
325        Ok(ret)
326    }
327
328    fn load_module_async<A: AsyncEngine>(
329        &self,
330        path: Option<&UriPath>,
331        name: &str,
332    ) -> impl Future<Output = Result<Module>> + 'static + use<A>
333    where
334        Self: Sized,
335    {
336        let out = path.map_or_else(
337            || self.out_dir().join(".modules"),
338            |n| self.out_dir().join_uri_path(n).join(".modules"),
339        );
340        let out = Self::escape_module_name(&out, name);
341        A::block_on(move || {
342            if !out.exists() {
343                return Err(BackendError::NotFound(ftml_uris::UriKind::Module));
344            }
345            let file = std::io::BufReader::new(std::fs::File::open(out)?);
346            let ret = bincode::decode_from_reader(file, bincode::config::standard())?;
347            Ok(ret)
348        })
349    }
350}
351impl BuildableArchive for LocalArchive {
352    #[inline]
353    fn file_state(&self) -> FileStates {
354        self.file_state.read().state().clone()
355    }
356
357    #[inline]
358    fn formats(&self) -> &[SourceFormatId] {
359        &self.formats
360    }
361    fn get_log(&self, relative_path: &str, target: BuildTargetId) -> PathBuf {
362        use std::str::FromStr;
363        let rel_path = if let Some((first, lang)) = relative_path.rsplit_once('.')
364            && Language::from_str(lang).is_err()
365        {
366            first
367        } else {
368            relative_path
369        };
370        self.out_dir()
371            .join(rel_path)
372            .join(target.name)
373            .with_extension("log")
374    }
375
376    fn save(
377        &self,
378        in_doc: &ftml_uris::DocumentUri,
379        rel_path: Option<&UriPath>,
380        log: FileOrString,
381        from: BuildTargetId,
382        result: Option<Box<dyn Artifact>>,
383        #[cfg(feature = "rdf")] relational: &RDFStore,
384        #[cfg(feature = "rdf")] load: bool,
385    ) -> std::result::Result<(), ArtifactSaveError> {
386        let out = self.out_path_of(in_doc.path(), &in_doc.name, rel_path, in_doc.language);
387        if let Err(e) = std::fs::create_dir_all(&out) {
388            return Err(ArtifactSaveError::Fs(FileError::Creation(out, e)));
389        }
390        let logfile = out.join(from.name).with_extension("log");
391        match log {
392            FileOrString::File(f) => f.rename_safe(&logfile)?,
393            FileOrString::Str(s) => {
394                if let Err(e) = std::fs::write(&logfile, s.as_bytes()) {
395                    return Err(ArtifactSaveError::Fs(FileError::Write(logfile, e)));
396                }
397            }
398        }
399        let Some(mut res) = result else { return Ok(()) };
400        let outfile = out.join(res.kind());
401        res.write(&outfile)?;
402        if let Some(e) = res.as_any_mut().downcast_mut::<ContentResult>() {
403            #[cfg(feature = "rdf")]
404            self.save_triples(
405                in_doc,
406                rel_path,
407                relational,
408                load,
409                std::mem::take(&mut e.triples),
410            );
411            for m in &e.modules {
412                let path = m.uri.path();
413                let name = m.uri.module_name();
414                let out = path.map_or_else(
415                    || self.out_dir().join(".modules"),
416                    |n| self.out_dir().join_uri_path(n).join(".modules"),
417                );
418                std::fs::create_dir_all(&out)
419                    .map_err(|e| ArtifactSaveError::Fs(FileError::Creation(out.clone(), e)))?;
420                let out = Self::escape_module_name(&out, name.as_ref());
421                let file = std::fs::File::create(&out)
422                    .map_err(|e| ArtifactSaveError::Fs(FileError::Creation(out, e)))?;
423                let mut buf = std::io::BufWriter::new(file);
424                bincode::encode_into_std_write(m, &mut buf, bincode::config::standard())?;
425                //postcard::to_io(m, &mut buf)?;
426            }
427        }
428        Ok(())
429    }
430
431    #[cfg(feature = "rdf")]
432    fn save_triples(
433        &self,
434        in_doc: &ftml_uris::DocumentUri,
435        rel_path: Option<&UriPath>,
436        relational: &RDFStore,
437        load: bool,
438        iter: Vec<ulo::rdf_types::Triple>,
439    ) {
440        use ftml_uris::FtmlUri;
441        let out = self.out_path_of(in_doc.path(), &in_doc.name, rel_path, in_doc.language);
442        let _ = std::fs::create_dir_all(&out);
443        let out = out.join("index.ttl");
444        relational.export(iter.into_iter(), &out, in_doc);
445        if load {
446            relational.load(&out, in_doc.to_iri());
447        }
448    }
449}
450
451impl LocallyBuilt for LocalArchive {
452    #[inline]
453    fn out_dir(&self) -> &Path {
454        &self.out_path
455    }
456
457    fn out_path_of(
458        &self,
459        path: Option<&UriPath>,
460        doc_name: &SimpleUriName,
461        rel_path: Option<&UriPath>,
462        language: Language,
463    ) -> PathBuf {
464        if let Some(rp) = rel_path {
465            use std::str::FromStr;
466            let mut steps = rp.steps();
467            let Some(mut last) = steps.next_back() else {
468                //SAFETY steps is never empty
469                unsafe { unreachable_unchecked() }
470            };
471            let out = steps.fold(self.out_dir().to_path_buf(), |p, s| p.join(s));
472            if let Some((first, lang)) = last.rsplit_once('.')
473                && Language::from_str(lang).is_err()
474            {
475                last = first;
476            }
477            return out.join(last);
478        }
479        self.rel_path_of(path, doc_name, language).map_or_else(
480            || {
481                let lang: &'static str = language.into();
482                let p = path.map_or_else(
483                    || self.out_path.join(doc_name.as_ref()),
484                    |n| self.out_path.join_uri_path(n).join(doc_name.as_ref()),
485                );
486                let mp = p.with_extension(lang);
487                if mp.exists() { mp } else { p }
488            },
489            |source| {
490                // SAFETY source is ancestor of source_dir
491                let rel_path = unsafe { source.relative_to(&self.source_dir()).unwrap_unchecked() };
492                self.out_path.join(rel_path)
493            },
494        )
495    }
496}
497impl LocalArchive {
498    #[cfg(feature = "git")]
499    pub fn git_url(&self, on_host: &url::Url) -> Option<&flams_git::GitUrl> {
500        self.is_managed
501            .get_or_init(|| {
502                let Ok(repo) = flams_git::repos::GitRepo::open(self.path()) else {
503                    return None;
504                };
505                on_host.host_str().and_then(|s| repo.is_managed(s))
506            })
507            .as_ref()
508    }
509
510    pub fn state_summary(&self) -> FileStateSummary {
511        self.file_state.read().state().summarize()
512    }
513
514    fn escape_module_name(in_path: &Path, name: &str) -> PathBuf {
515        in_path.join(name.replace('*', "__AST__"))
516    }
517
518    #[must_use]
519    pub fn source_dir(&self) -> PathBuf {
520        self.path().join(self.source.as_deref().unwrap_or("source"))
521    }
522
523    /// blocks
524    #[must_use]
525    pub fn manifest_of(p: &Path) -> Option<PathBuf> {
526        for e in std::fs::read_dir(p).ok()? {
527            let Ok(e) = e else { continue };
528            let Ok(md) = e.metadata() else { continue };
529            if md.is_dir() && e.file_name().eq_ignore_ascii_case("meta-inf") {
530                return crate::archive_iter::find_manifest(&e.path());
531            }
532        }
533        None
534    }
535
536    #[inline]
537    #[must_use]
538    fn out_dir_of(p: &Path) -> PathBuf
539    where
540        Self: Sized,
541    {
542        p.join(".flams")
543    }
544
545    #[inline]
546    pub fn with_sources<R>(&self, f: impl FnOnce(&SourceDir) -> R) -> R {
547        f(&self.file_state.read())
548    }
549
550    pub(crate) fn update_sources(&self) {
551        let dir = SourceDir::new(&self.source_dir(), &self.ignore, self.formats());
552        let mut state = self.file_state.write();
553        state.update(dir);
554    }
555
556    /// blocks!
557    pub fn rel_path_of(
558        &self,
559        path: Option<&UriPath>,
560        doc_name: &SimpleUriName,
561        language: Language,
562    ) -> Option<PathBuf> {
563        let dir = path.map_or_else(|| self.source_dir(), |n| self.source_dir().join_uri_path(n));
564
565        for f in std::fs::read_dir(&dir)
566            .ok()?
567            .filter_map(std::result::Result::ok)
568        {
569            let Ok(m) = dir.metadata() else { continue };
570            if !m.is_file() {
571                continue;
572            }
573            let fname = f.file_name();
574            let Some(name) = fname.to_str() else { continue };
575
576            if !name.starts_with(doc_name.as_ref()) {
577                continue;
578            }
579            let rest = &name[doc_name.as_ref().len()..];
580            if !rest.is_empty() && !rest.starts_with('.') {
581                continue;
582            }
583            let rest = rest.strip_prefix('.').unwrap_or(rest);
584            if rest.contains('.') {
585                let lang: &'static str = language.into();
586                if !rest.starts_with(lang) {
587                    continue;
588                }
589            }
590            return Some(f.path());
591        }
592        None
593    }
594}