Skip to main content

mant_core/
catalog.rs

1//! Unifies registered Markdown and indexed manual pages for discovery clients.
2
3use std::path::PathBuf;
4
5use mant_sources::{
6    RegisteredDocument, RegisteredDocumentOrigin, SourceConfigError, list_registered_documents,
7};
8
9use crate::{ManualIndex, discover_manual_roots};
10
11/// Source family used to resolve one available document.
12#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
13pub enum AvailableDocumentKind {
14    Markdown,
15    Manual,
16}
17
18/// Precedence class and storage family for one available document.
19#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
20pub enum AvailableDocumentOrigin {
21    Documents,
22    Source(String),
23    ManualPath,
24}
25
26/// One document discoverable by name through the ordinary query boundary.
27#[derive(Clone, Debug, Eq, PartialEq)]
28pub struct AvailableDocument {
29    pub name: String,
30    pub kind: AvailableDocumentKind,
31    pub section: Option<String>,
32    pub path: PathBuf,
33    pub origin: AvailableDocumentOrigin,
34}
35
36/// List every registered document candidate and locally indexed manual page.
37///
38/// # Errors
39///
40/// Returns an error when the platform data root or source configuration cannot
41/// be read or validated.
42pub fn list_available_documents() -> Result<Vec<AvailableDocument>, SourceConfigError> {
43    let manuals = ManualIndex::from_roots(discover_manual_roots());
44    Ok(list_available_documents_from(
45        list_registered_documents()?,
46        manuals.pages(),
47    ))
48}
49
50fn list_available_documents_from(
51    registered: Vec<RegisteredDocument>,
52    manuals: &[crate::ManualPage],
53) -> Vec<AvailableDocument> {
54    let mut documents = registered
55        .into_iter()
56        .map(|document| AvailableDocument {
57            name: document.name,
58            kind: AvailableDocumentKind::Markdown,
59            section: None,
60            path: document.path,
61            origin: match document.origin {
62                RegisteredDocumentOrigin::Documents => AvailableDocumentOrigin::Documents,
63                RegisteredDocumentOrigin::Source(source) => AvailableDocumentOrigin::Source(source),
64            },
65        })
66        .chain(manuals.iter().map(|page| AvailableDocument {
67            name: page.name.clone(),
68            kind: AvailableDocumentKind::Manual,
69            section: Some(page.section.clone()),
70            path: page.path.clone(),
71            origin: AvailableDocumentOrigin::ManualPath,
72        }))
73        .collect::<Vec<_>>();
74    documents.sort_by(|left, right| {
75        (&left.name, left.kind, &left.section).cmp(&(&right.name, right.kind, &right.section))
76    });
77    documents
78}
79
80#[cfg(test)]
81mod tests {
82    use std::path::PathBuf;
83
84    use mant_sources::{RegisteredDocument, RegisteredDocumentOrigin};
85
86    use crate::ManualPage;
87
88    use super::{AvailableDocumentKind, AvailableDocumentOrigin, list_available_documents_from};
89
90    #[test]
91    fn merges_both_namespaces_without_hiding_manual_sections() {
92        let documents = list_available_documents_from(
93            vec![RegisteredDocument {
94                name: "printf".to_owned(),
95                path: PathBuf::from("/home/demo/.local/share/mant/documents/printf.md"),
96                origin: RegisteredDocumentOrigin::Documents,
97            }],
98            &[
99                ManualPage {
100                    name: "printf".to_owned(),
101                    section: "1".to_owned(),
102                    path: PathBuf::from("/usr/share/man/man1/printf.1.gz"),
103                    manual_root: PathBuf::from("/usr/share/man"),
104                },
105                ManualPage {
106                    name: "printf".to_owned(),
107                    section: "3".to_owned(),
108                    path: PathBuf::from("/usr/share/man/man3/printf.3.gz"),
109                    manual_root: PathBuf::from("/usr/share/man"),
110                },
111            ],
112        );
113
114        assert_eq!(documents.len(), 3);
115        assert_eq!(documents[0].kind, AvailableDocumentKind::Markdown);
116        assert_eq!(documents[0].origin, AvailableDocumentOrigin::Documents);
117        assert_eq!(documents[1].section.as_deref(), Some("1"));
118        assert_eq!(documents[2].section.as_deref(), Some("3"));
119    }
120
121    #[test]
122    fn keeps_shadowed_markdown_candidates_in_fallback_order() {
123        let documents = list_available_documents_from(
124            vec![
125                RegisteredDocument {
126                    name: "tool".to_owned(),
127                    path: PathBuf::from("/data/mant/documents/tool.md"),
128                    origin: RegisteredDocumentOrigin::Documents,
129                },
130                RegisteredDocument {
131                    name: "tool".to_owned(),
132                    path: PathBuf::from("/data/mant/documents/sources/alpha/tool.md"),
133                    origin: RegisteredDocumentOrigin::Source("alpha".to_owned()),
134                },
135            ],
136            &[],
137        );
138        assert_eq!(documents.len(), 2);
139        assert_eq!(documents[0].origin, AvailableDocumentOrigin::Documents);
140        assert_eq!(
141            documents[1].origin,
142            AvailableDocumentOrigin::Source("alpha".to_owned())
143        );
144    }
145}