Skip to main content

mant_loader/loading/
error.rs

1//! Loading failures own acquisition context, never query-view errors.
2use mant_protocol::ScopeTextError;
3use std::{error::Error, fmt, path::PathBuf};
4
5/// Invalid loading input or failure to acquire readable local content.
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub enum LoadError {
8    /// Native input was selected but this build does not enable the roff backend.
9    NativeBackendUnavailable {
10        /// Optional cached quick reference available through explicit tldr policy.
11        tldr_topic: Option<String>,
12    },
13    /// A document selector was empty after trimming.
14    EmptyName,
15    /// A native manual category was empty or malformed.
16    InvalidManualSection,
17    /// A tldr command query was qualified by a non-command manual section.
18    TldrManualSection {
19        /// Incompatible native manual section.
20        section: String,
21    },
22    /// An explicit Markdown source name was empty.
23    InvalidSource,
24    /// Markdown-source and native-manual selectors were combined.
25    ConflictingSourceSelectors,
26    /// A direct Markdown input path was empty.
27    EmptyMarkdownPath,
28    /// Automatic format inference did not recognize a direct input.
29    UnsupportedInputFormat {
30        /// Caller-facing input path.
31        path: String,
32    },
33    /// A document selector or source violated its bounded loading contract.
34    InvalidSelector {
35        /// Loading input field name.
36        field: &'static str,
37        /// Precise bound or character violation.
38        error: ScopeTextError,
39    },
40    /// Markdown input could not be read or parsed.
41    Markdown {
42        /// Caller-facing source path.
43        path: String,
44        /// Stable failure detail.
45        detail: String,
46    },
47    /// Markdown parsing produced neither document nor tldr content.
48    EmptyMarkdown {
49        /// Selected-document label.
50        label: String,
51    },
52    /// Registered-document discovery failed.
53    Registry {
54        /// Stable source-configuration or discovery detail.
55        detail: String,
56    },
57    /// Native manual loading failed.
58    Manual(ManualLoadError),
59    /// No full document was found, but an optional tldr entry is available.
60    ManualWithTldr {
61        /// Native-manual failure retained as the authoritative lookup error.
62        error: ManualLoadError,
63        /// Topic that can be queried explicitly with `--tldr`.
64        topic: String,
65    },
66    /// An explicit tldr query found no quick-reference candidate.
67    TldrNotFound {
68        /// Requested tldr topic.
69        topic: String,
70    },
71    /// An explicit tldr candidate could not be read or parsed.
72    Tldr {
73        /// Requested tldr topic.
74        topic: String,
75        /// Stable cache or Markdown failure detail.
76        detail: String,
77    },
78    /// No Markdown, manual, or quick-reference content could be resolved.
79    NoReadableContent {
80        /// Requested document name.
81        name: String,
82    },
83}
84
85/// Native-manual resolution or lowering failed after candidate selection.
86#[derive(Debug, Clone, PartialEq, Eq)]
87pub enum ManualLoadError {
88    /// No indexed native manual matched the request.
89    NotFound {
90        /// Requested manual name.
91        name: String,
92        /// Search-path and candidate detail.
93        detail: String,
94    },
95    /// A selected manual could not be parsed or lowered.
96    Parse {
97        /// Requested manual name.
98        name: String,
99        /// Stable parser or source-policy detail.
100        detail: String,
101    },
102    /// Parsing succeeded but produced no readable semantic content.
103    Empty {
104        /// Requested manual name.
105        name: String,
106        /// Physical selected manual path.
107        path: PathBuf,
108        /// Non-fatal parser findings explaining the empty result.
109        diagnostics: Vec<String>,
110    },
111}
112
113impl fmt::Display for LoadError {
114    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
115        match self {
116            Self::NativeBackendUnavailable { tldr_topic } => {
117                formatter.write_str("native manual loading requires the 'roff' feature")?;
118                if let Some(topic) = tldr_topic {
119                    write!(
120                        formatter,
121                        "\nhint: a tldr entry is available; run `mant {topic} --tldr`"
122                    )?;
123                }
124                Ok(())
125            }
126            Self::EmptyName => formatter.write_str("name must not be empty"),
127            Self::InvalidManualSection => formatter.write_str(
128                "manual section must be a conventional number or the single letter 'l' or 'n'",
129            ),
130            Self::TldrManualSection { section } => write!(
131                formatter,
132                "manual section '{section}' does not identify a command quick reference; tldr supports section families 1 and 8"
133            ),
134            Self::InvalidSource => formatter.write_str("document source must not be empty"),
135            Self::ConflictingSourceSelectors => formatter.write_str(
136                "document source cannot be combined with a manual section or manual-only policy",
137            ),
138            Self::EmptyMarkdownPath => formatter.write_str("Markdown path must not be empty"),
139            Self::UnsupportedInputFormat { path } => write!(
140                formatter,
141                "could not infer the input format for '{path}'; use --input-format markdown or roff"
142            ),
143            Self::InvalidSelector { field, error } => {
144                write!(formatter, "{field} {}", selector_error_message(*error))
145            }
146            Self::Markdown { path, detail } => {
147                write!(
148                    formatter,
149                    "could not load Markdown document '{path}': {detail}"
150                )
151            }
152            Self::EmptyMarkdown { label } => {
153                write!(
154                    formatter,
155                    "Markdown document '{label}' has no readable content"
156                )
157            }
158            Self::Registry { detail } => formatter.write_str(detail),
159            Self::Manual(error) => error.fmt(formatter),
160            Self::ManualWithTldr { error, topic } => {
161                error.fmt(formatter)?;
162                write!(
163                    formatter,
164                    "\nhint: a tldr entry is available; run `mant {topic} --tldr`"
165                )
166            }
167            Self::TldrNotFound { topic } => {
168                write!(formatter, "no tldr quick reference was found for '{topic}'")
169            }
170            Self::Tldr { topic, detail } => {
171                write!(formatter, "could not load tldr entry '{topic}': {detail}")
172            }
173            Self::NoReadableContent { name } => {
174                write!(
175                    formatter,
176                    "no readable document content was found for '{name}'"
177                )
178            }
179        }
180    }
181}
182
183impl Error for LoadError {
184    fn source(&self) -> Option<&(dyn Error + 'static)> {
185        match self {
186            Self::Manual(error) | Self::ManualWithTldr { error, .. } => Some(error),
187            _ => None,
188        }
189    }
190}
191
192fn selector_error_message(error: ScopeTextError) -> String {
193    match error {
194        ScopeTextError::Empty => "must not be empty".to_owned(),
195        ScopeTextError::ControlCharacter => "must not contain control characters".to_owned(),
196        ScopeTextError::TooLong { maximum } => {
197            format!("must not exceed {maximum} Unicode scalar values")
198        }
199    }
200}
201
202impl fmt::Display for ManualLoadError {
203    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
204        match self {
205            Self::NotFound { name, detail } => {
206                write!(formatter, "could not load manual '{name}': {detail}")
207            }
208            Self::Parse { name, detail } => write!(
209                formatter,
210                "could not load manual '{name}': manual source: {detail}"
211            ),
212            Self::Empty {
213                name,
214                path,
215                diagnostics,
216            } => {
217                write!(
218                    formatter,
219                    "could not load manual '{name}': libmandoc parsed {} but produced no readable sections",
220                    path.display()
221                )?;
222                if !diagnostics.is_empty() {
223                    write!(formatter, "; diagnostics: {}", diagnostics.join("; "))?;
224                }
225                Ok(())
226            }
227        }
228    }
229}
230
231impl Error for ManualLoadError {}