Skip to main content

mant_loader/
catalog.rs

1//! Unifies registered Markdown and indexed manual pages for discovery clients.
2
3use crate::{ManualIndex, discover_manual_roots};
4use mant_protocol::{CatalogQuery, DocumentCatalog, MAX_CATALOG_PATTERN_CHARS};
5use mant_sources::{SourceConfigError, list_registered_documents};
6use std::{error::Error, fmt, path::PathBuf};
7
8mod inventory;
9mod selection;
10#[cfg(test)]
11mod tests;
12
13pub(crate) use inventory::list_available_documents_from;
14pub use selection::{PreparedCatalogQuery, query_available_documents};
15
16/// Source family used to resolve one available document.
17#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
18pub enum AvailableDocumentKind {
19    /// Registered Markdown document.
20    Markdown,
21    /// Indexed native manual page.
22    Manual,
23}
24
25/// Precedence class and storage family for one available document.
26#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
27pub enum AvailableDocumentOrigin {
28    /// User-authored primary documents tree.
29    Documents,
30    /// One configured source cache, named by its configuration key.
31    Source(String),
32    /// A directory discovered through the native manual search path.
33    ManualPath,
34}
35
36/// One document discoverable by name through the ordinary query boundary.
37#[derive(Clone, Debug, Eq, PartialEq)]
38pub struct AvailableDocument {
39    /// Short lookup name.
40    pub name: String,
41    /// Extension-free path relative to this document's origin.
42    pub logical_path: String,
43    /// Broad source format family.
44    pub kind: AvailableDocumentKind,
45    /// Native manual category, present only for manual pages.
46    pub manual_section: Option<String>,
47    /// Physical local source path.
48    pub path: PathBuf,
49    /// Storage namespace and precedence class.
50    pub origin: AvailableDocumentOrigin,
51    /// Configured priority relative to native manuals, or `None` otherwise.
52    pub source_priority: Option<i32>,
53}
54
55/// Invalid document-catalog filter or regular expression.
56#[derive(Clone, Debug, Eq, PartialEq)]
57pub enum CatalogError {
58    /// An explicit pattern contained no text.
59    EmptyPattern,
60    /// A pattern exceeded the bounded request size.
61    PatternTooLong,
62    /// Pagination limit was zero or exceeded the protocol maximum.
63    InvalidLimit,
64    /// Source-family filters cannot describe any valid document.
65    ConflictingSelectors,
66    /// A regular expression could not be compiled.
67    InvalidPattern(String),
68}
69
70impl fmt::Display for CatalogError {
71    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
72        match self {
73            Self::EmptyPattern => formatter.write_str("catalog pattern must not be empty"),
74            Self::PatternTooLong => {
75                write!(
76                    formatter,
77                    "catalog pattern exceeds the {MAX_CATALOG_PATTERN_CHARS}-character limit"
78                )
79            }
80            Self::InvalidLimit => formatter.write_str("catalog limit must be between 1 and 10000"),
81            Self::ConflictingSelectors => {
82                formatter.write_str("catalog source and manual-section filters cannot be combined")
83            }
84            Self::InvalidPattern(message) => {
85                write!(formatter, "invalid catalog pattern: {message}")
86            }
87        }
88    }
89}
90
91impl Error for CatalogError {}
92
93/// List every registered document candidate and locally indexed manual page.
94///
95/// # Errors
96///
97/// Returns an error when the platform data root or source configuration cannot
98/// be read or validated.
99pub fn list_available_documents() -> Result<Vec<AvailableDocument>, SourceConfigError> {
100    let manuals = ManualIndex::from_roots(discover_manual_roots());
101    Ok(list_available_documents_from(
102        list_registered_documents()?,
103        manuals.pages(),
104    ))
105}
106
107/// Load and query the current local document catalog.
108///
109/// # Errors
110///
111/// Returns source configuration or catalog validation failures as text because
112/// both are operational boundaries for every frontend.
113pub fn discover_documents(query: &CatalogQuery) -> Result<DocumentCatalog, String> {
114    discover_with(query, list_available_documents)
115}
116
117fn discover_with(
118    query: &CatalogQuery,
119    inventory: impl FnOnce() -> Result<Vec<AvailableDocument>, SourceConfigError>,
120) -> Result<DocumentCatalog, String> {
121    let plan = PreparedCatalogQuery::new(query).map_err(|error| error.to_string())?;
122    let documents = inventory().map_err(|error| error.to_string())?;
123    Ok(plan.apply(&documents))
124}