1use crate::DocumentResolver;
3use mant_loader::{LoadedDocumentScope, ScopeLoadError, validate_document_scope};
4use mant_protocol::{
5 DocumentScope, MAX_SEMANTIC_ENTRY_CHARS, ScopeQueryRequest, ScopeQueryResult, ScopeQueryView,
6 ScopeTextError, SearchQuery, validate_scope_text,
7};
8use mant_query::validate_search_query;
9use std::{error::Error, fmt};
10
11mod execute;
12pub use execute::{PreparedScopeQuery, execute_scope_query};
13
14#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum ScopeQueryError {
17 Load(ScopeLoadError),
19 Execution(mant_query::ScopeExecutionError),
21 InvalidLoadedScope(mant_query::ScopeInputError),
23 EntrySelector(ScopeTextError),
25 Explanation(mant_query::ExplanationError),
27 Search(mant_query::SearchError),
29}
30impl fmt::Display for ScopeQueryError {
31 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
32 match self {
33 Self::Load(error) => error.fmt(formatter),
34 Self::Execution(error) => error.fmt(formatter),
35 Self::InvalidLoadedScope(error) => error.fmt(formatter),
36 Self::EntrySelector(error) => write!(
37 formatter,
38 "semantic entry {}",
39 scope_text_error_message(*error)
40 ),
41 Self::Explanation(error) => error.fmt(formatter),
42 Self::Search(error) => error.fmt(formatter),
43 }
44 }
45}
46impl Error for ScopeQueryError {
47 fn source(&self) -> Option<&(dyn Error + 'static)> {
48 match self {
49 Self::Load(error) => Some(error),
50 Self::Execution(error) => Some(error),
51 Self::InvalidLoadedScope(error) => Some(error),
52 Self::Explanation(error) => Some(error),
53 Self::Search(error) => Some(error),
54 Self::EntrySelector(_) => None,
55 }
56 }
57}
58impl DocumentResolver {
59 pub fn resolve_scope(
65 &self,
66 scope: &DocumentScope,
67 ) -> Result<LoadedDocumentScope, ScopeQueryError> {
68 self.loader()
69 .resolve_scope(scope)
70 .map_err(ScopeQueryError::Load)
71 }
72}
73
74pub fn validate_scope_query_request(request: &ScopeQueryRequest) -> Result<(), ScopeQueryError> {
80 validate_document_scope(&request.scope).map_err(ScopeQueryError::Load)?;
81 match &request.view {
82 ScopeQueryView::Explain { entry, options } => {
83 validate_scope_text(entry, MAX_SEMANTIC_ENTRY_CHARS)
84 .map_err(ScopeQueryError::EntrySelector)?;
85 mant_query::validate_explanation_query(&mant_protocol::ExplanationQuery {
86 entry: entry.clone(),
87 options: *options,
88 })
89 .map_err(ScopeQueryError::Explanation)
90 }
91 ScopeQueryView::Search {
92 pattern,
93 syntax,
94 case,
95 scope,
96 word,
97 context_lines,
98 limit,
99 offset,
100 } => validate_search_query(&SearchQuery {
101 pattern: pattern.clone(),
102 syntax: *syntax,
103 case: *case,
104 scope: *scope,
105 word: *word,
106 context_lines: *context_lines,
107 limit: *limit,
108 offset: *offset,
109 })
110 .map_err(ScopeQueryError::Search),
111 }
112}
113
114fn scope_text_error_message(error: ScopeTextError) -> String {
115 match error {
116 ScopeTextError::Empty => "must not be empty".to_owned(),
117 ScopeTextError::ControlCharacter => "must not contain control characters".to_owned(),
118 ScopeTextError::TooLong { maximum } => {
119 format!("must not exceed {maximum} Unicode scalar values")
120 }
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127 use mant_ir::{DocumentAddress, MarkdownOrigin, ResolvedContent};
128 use mant_protocol::{DocumentSelector, ResolvedDocumentScope, ScopedDocument};
129
130 struct QueryFixture {
132 scope: ResolvedDocumentScope,
133 documents: Vec<ResolvedContent>,
134 }
135
136 #[test]
137 fn native_scope_request_enforces_entry_selector_contract() {
138 let mut request = ScopeQueryRequest {
139 schema: mant_protocol::ScopeRequestSchema::V0Dot11,
140 scope: DocumentScope {
141 documents: vec![DocumentSelector {
142 selector: "root".to_owned(),
143 source: None,
144 manual_section: None,
145 }],
146 traversal: mant_protocol::DocumentTraversal::default(),
147 },
148 view: ScopeQueryView::Explain {
149 entry: "x".repeat(MAX_SEMANTIC_ENTRY_CHARS + 1),
150 options: mant_protocol::ExplanationOptions::default(),
151 },
152 };
153 assert_eq!(
154 validate_scope_query_request(&request),
155 Err(ScopeQueryError::EntrySelector(ScopeTextError::TooLong {
156 maximum: MAX_SEMANTIC_ENTRY_CHARS,
157 }))
158 );
159
160 request.view = ScopeQueryView::Explain {
161 entry: "界".repeat(MAX_SEMANTIC_ENTRY_CHARS),
162 options: mant_protocol::ExplanationOptions::default(),
163 };
164 assert_eq!(validate_scope_query_request(&request), Ok(()));
165 }
166
167 #[test]
168 fn scope_explain_retains_a_visible_text_probe_as_a_qualified_failure() {
169 let address = DocumentAddress::Markdown {
170 path: "shell".to_owned(),
171 origin: MarkdownOrigin::Documents,
172 };
173 let mut loaded = QueryFixture {
174 scope: ResolvedDocumentScope {
175 query: DocumentScope {
176 documents: vec![DocumentSelector {
177 selector: "documents/shell".to_owned(),
178 source: None,
179 manual_section: None,
180 }],
181 traversal: mant_protocol::DocumentTraversal::default(),
182 },
183 documents: vec![ScopedDocument {
184 address: address.clone(),
185 depth: 0,
186 root_indices: vec![0],
187 reached_from: Vec::new(),
188 }],
189 edges: Vec::new(),
190 frontier: Vec::new(),
191 unresolved: Vec::new(),
192 reference_limits: Vec::new(),
193 },
194 documents: vec![
195 mant_loader::load_markdown_text(
196 "# Shell\n\n## Startup\n\nThe `VISUAL` name selects an editor.\n",
197 Some("shell.md".to_owned()),
198 )
199 .expect("probe fixture"),
200 ],
201 };
202
203 for (source, content) in loaded.scope.documents.iter().zip(&mut loaded.documents) {
204 content.address = Some(source.address.clone());
205 }
206 let explanation = mant_query::explain_scope(
207 mant_query::QueryScopeView::new(&loaded.scope, &loaded.documents).unwrap(),
208 &mant_protocol::ExplanationQuery {
209 entry: "VISUAL".to_owned(),
210 options: mant_protocol::ExplanationOptions::default(),
211 },
212 )
213 .unwrap();
214 assert!(explanation.failures.is_empty());
215 assert_eq!(explanation.total, 1);
216 assert_eq!(explanation.documents[0].address, address);
217 assert_eq!(explanation.evidence[0].document_index, 0);
218 let evidence = &explanation.evidence[0].evidence;
219 assert_eq!(evidence.outline.path(), "1");
220 assert_eq!(evidence.outline.title(), "Startup");
221 assert!(evidence.entry.is_none());
222 assert!(evidence.source.is_some());
223 }
224
225 #[test]
226 fn scope_search_uses_one_global_cursor_and_global_ordinals() {
227 let address = |path: &str| DocumentAddress::Markdown {
228 path: path.to_owned(),
229 origin: MarkdownOrigin::Documents,
230 };
231 let markdown = |title: &str, count: usize| {
232 let body = (1..=count)
233 .map(|index| format!("needle {index}"))
234 .collect::<Vec<_>>()
235 .join("\n\n");
236 mant_loader::load_markdown_text(&format!("# {title}\n\n{body}\n"), None)
237 .expect("search fixture")
238 };
239 let documents = ["alpha", "beta"]
240 .into_iter()
241 .map(|path| ScopedDocument {
242 address: address(path),
243 depth: 0,
244 root_indices: Vec::new(),
245 reached_from: Vec::new(),
246 })
247 .collect::<Vec<_>>();
248 let mut loaded = QueryFixture {
249 scope: ResolvedDocumentScope {
250 query: DocumentScope {
251 documents: Vec::new(),
252 traversal: mant_protocol::DocumentTraversal::default(),
253 },
254 documents,
255 edges: Vec::new(),
256 frontier: Vec::new(),
257 unresolved: Vec::new(),
258 reference_limits: Vec::new(),
259 },
260 documents: vec![markdown("Alpha", 3), markdown("Beta", 10)],
261 };
262 for (source, content) in loaded.scope.documents.iter().zip(&mut loaded.documents) {
263 content.address = Some(source.address.clone());
264 }
265 let query = SearchQuery {
266 pattern: "needle".to_owned(),
267 syntax: mant_protocol::SearchSyntax::Literal,
268 case: mant_protocol::SearchCase::Insensitive,
269 scope: mant_protocol::SearchScope::Visible,
270 word: false,
271 context_lines: 0,
272 limit: 5,
273 offset: 0,
274 };
275
276 let search = mant_query::search_scope(
277 mant_query::QueryScopeView::new(&loaded.scope, &loaded.documents).unwrap(),
278 &query,
279 )
280 .expect("scope search");
281
282 assert_eq!(search.total, 13);
283 assert_eq!(search.returned, 5);
284 assert_eq!(search.next_offset, Some(5));
285 assert_eq!(search.documents.len(), 2);
286 assert_eq!(
287 search
288 .documents
289 .iter()
290 .flat_map(|document| document.matches.iter().map(|hit| hit.ordinal))
291 .collect::<Vec<_>>(),
292 [1, 2, 3, 4, 5]
293 );
294
295 let cross_boundary_query = SearchQuery { offset: 2, ..query };
296 let search = mant_query::search_scope(
297 mant_query::QueryScopeView::new(&loaded.scope, &loaded.documents).unwrap(),
298 &cross_boundary_query,
299 )
300 .expect("scope search");
301
302 assert_eq!(search.total, 13);
303 assert_eq!(search.returned, 5);
304 assert_eq!(search.next_offset, Some(7));
305 assert_eq!(search.documents.len(), 2);
306 assert_eq!(
307 search
308 .documents
309 .iter()
310 .flat_map(|document| document.matches.iter().map(|hit| hit.ordinal))
311 .collect::<Vec<_>>(),
312 [3, 4, 5, 6, 7]
313 );
314 }
315}