flams_math_archives/
manifest.rs

1use crate::{
2    Archive, ArchiveKind, LocalArchive,
3    formats::{SourceFormat, SourceFormatId},
4    source_files::SourceDir,
5    utils::{errors::ManifestParseError, ignore_source::IgnoreSource, path_ext::RelPath},
6};
7use flams_backend_types::archive_json::{ArchiveDatum, ArchiveIndex, Institution};
8use ftml_uris::{ArchiveId, ArchiveUri, BaseUri, UriWithArchive};
9use std::path::Path;
10
11#[derive(Debug)]
12pub struct RepositoryData {
13    pub uri: ArchiveUri,
14    pub attributes: Vec<(Box<str>, Box<str>)>,
15    pub formats: smallvec::SmallVec<SourceFormatId, 1>,
16    //pub dependencies: Box<[ArchiveId]>,
17    //pub institutions: Box<[Institution]>,
18    //pub index: Box<[ArchiveIndex]>,
19}
20
21#[allow(clippy::too_many_lines)]
22/// # Errors
23pub fn parse_manifest(path: &Path, id: RelPath) -> Result<Archive, ManifestParseError> {
24    use std::io::BufRead;
25    let Some(top_dir) = path.parent().and_then(Path::parent) else {
26        return Err(ManifestParseError::NoParent);
27    };
28    let out_path = LocalArchive::out_dir_of(top_dir);
29    let reader = std::fs::File::open(path)?;
30    let reader = std::io::BufReader::new(reader);
31    let mut lines = reader.lines();
32
33    let mut source = None;
34    let mut formats = smallvec::SmallVec::<_, 1>::new();
35    let mut url_base: Option<BaseUri> = None;
36    let mut ignore = IgnoreSource::default();
37    let mut attributes: Vec<(Box<str>, Box<str>)> = Vec::new();
38    let mut real_id: Option<ArchiveId> = None;
39    let mut kind = None;
40    loop {
41        let line = match lines.next() {
42            Some(Err(_)) => continue,
43            Some(Ok(l)) => l,
44            _ => break,
45        };
46        let (k, v) = match line.split_once(':') {
47            Some((k, v)) => (k.trim(), v.trim()),
48            _ => continue,
49        };
50        match k {
51            "id" => {
52                if id != *v {
53                    return Err(ManifestParseError::IdMismatch(v.to_string()));
54                } else if v.is_empty() {
55                    return Err(ManifestParseError::EmptyId);
56                }
57                real_id = Some(
58                    v.parse()
59                        .map_err(|_| ManifestParseError::InvalidId(v.to_string()))?,
60                );
61            }
62            "format" => {
63                for f in v.split(',') {
64                    formats.push(
65                        SourceFormat::get(f)
66                            .ok_or_else(|| ManifestParseError::UnknownFormat(f.to_string()))?,
67                    );
68                }
69            }
70            "url-base" => {
71                url_base = Some(
72                    v.parse()
73                        .map_err(|e| ManifestParseError::InvalidUrlBase(v.to_string(), e))?,
74                );
75            }
76            "ignore" => {
77                ignore = IgnoreSource::new(v, &top_dir.join("source")); //Some(v.into());
78            }
79            "source" => source = Some(v.to_string().into_boxed_str()),
80            "kind" => {
81                if let Some(k) = ArchiveKind::get(v) {
82                    kind = Some(k);
83                } else {
84                    return Err(ManifestParseError::UnknownKind(v.to_string()));
85                }
86            }
87            _ => {
88                attributes.push((k.into(), v.into()));
89            }
90        }
91    }
92    let Some(id) = real_id else {
93        return Err(ManifestParseError::EmptyId);
94    };
95    if formats.is_empty() && !id.is_meta() && kind.is_none() {
96        return Err(ManifestParseError::NoFormatOrKind);
97    }
98    let Some(dom_uri) = url_base else {
99        return Err(ManifestParseError::NoUrlBase);
100    };
101    let uri = dom_uri & id;
102    /*let (institutions, index) =
103    read_archive_json(&uri, &path.with_file_name("archive.json"), external_url);
104     */
105    if let Some(kind) = kind {
106        let data = RepositoryData {
107            uri,
108            attributes,
109            formats,
110            //institutions,
111            //index, //dependencies: dependencies.into(),
112        };
113        (kind.make_new)(data, top_dir).map_or_else(
114            |e| Err(ManifestParseError::InvalidKind(kind.name, e)),
115            |r| Ok(Archive::Ext(kind, r)),
116        )
117    } else {
118        Ok(Archive::Local(Box::new(LocalArchive {
119            uri,
120            //attributes,
121            formats,
122            //institutions,
123            //index,
124            ignore,
125            out_path,
126            source,
127            //ignore,
128            file_state: parking_lot::RwLock::new(SourceDir::default()),
129            #[cfg(feature = "git")]
130            is_managed: std::sync::OnceLock::new(),
131        })))
132    }
133}
134
135pub fn read_archive_json(
136    archive: &ArchiveUri,
137    path: &Path,
138    external_url: &str,
139) -> (Box<[Institution]>, Box<[ArchiveIndex]>) {
140    if !path.exists() {
141        return (Vec::new().into(), Vec::new().into());
142    }
143    let reader = match std::fs::File::open(path) {
144        Ok(reader) => reader,
145        Err(e) => {
146            tracing::error!("Could not read index file {}: {e}", path.display());
147            return (Vec::new().into(), Vec::new().into());
148        }
149    };
150    let reader = std::io::BufReader::new(reader);
151    let v = match serde_json::from_reader::<_, Vec<ArchiveDatum>>(reader) {
152        Ok(v) => v,
153        Err(e) => {
154            tracing::error!("Invalid JSON file {}: {e}", path.display());
155            return (Vec::new().into(), Vec::new().into());
156        }
157    };
158    let mut insts = Vec::new();
159    let mut idxs = Vec::new();
160    for d in v {
161        match d {
162            ArchiveDatum::Document(mut d) => {
163                if d.teaser().is_none() {
164                    let desc = path.with_file_name("desc.html");
165                    if desc.exists()
166                        && let Ok(s) = std::fs::read_to_string(desc)
167                    {
168                        d.set_teaser(s.into_boxed_str());
169                    }
170                }
171                match ArchiveIndex::from_kind(d, archive, |i| {
172                    format!(
173                        "{external_url}/img?a={}&rp=source/{i}",
174                        archive.archive_id()
175                    )
176                    .into_boxed_str()
177                }) {
178                    Ok(e) => idxs.push(e),
179                    Err(e) => tracing::error!("Error in index file {}: {e:#}", path.display()),
180                }
181            }
182            ArchiveDatum::Institution(i) => insts.push(match i {
183                Institution::University {
184                    title,
185                    place,
186                    country,
187                    url,
188                    acronym,
189                    logo,
190                } => Institution::University {
191                    title,
192                    place,
193                    country,
194                    url,
195                    acronym,
196                    logo: format!(
197                        "{external_url}/img?a={}&rp=source/{logo}",
198                        archive.archive_id()
199                    )
200                    .into_boxed_str(),
201                },
202                Institution::School {
203                    title,
204                    place,
205                    country,
206                    url,
207                    acronym,
208                    logo,
209                } => Institution::School {
210                    title,
211                    place,
212                    country,
213                    url,
214                    acronym,
215                    logo: format!(
216                        "{external_url}/img?a={}&rp=source/{logo}",
217                        archive.archive_id()
218                    )
219                    .into_boxed_str(),
220                },
221            }),
222        }
223    }
224    (insts.into(), idxs.into())
225}