1use super::{LoadedDocumentScope, ScopeQueryError, ScopeQueryResult, SearchQuery};
3use mant_protocol::{ScopeQueryRequest, ScopeQueryResponse, ScopeQuerySchema, ScopeQueryView};
4use mant_query::QueryScopeView;
5
6pub struct PreparedScopeQuery<'request> {
11 request: &'request ScopeQueryRequest,
12}
13
14impl<'request> PreparedScopeQuery<'request> {
15 pub fn new(request: &'request ScopeQueryRequest) -> Result<Self, ScopeQueryError> {
20 super::validate_scope_query_request(request)?;
21 Ok(Self { request })
22 }
23
24 #[must_use]
26 pub const fn request(&self) -> &'request ScopeQueryRequest {
27 self.request
28 }
29
30 pub fn execute(
35 &self,
36 resolver: &crate::DocumentResolver,
37 ) -> Result<ScopeQueryResponse, ScopeQueryError> {
38 resolver.execute_validated_scope_query(self.request)
39 }
40}
41
42pub fn execute_scope_query(
48 request: &ScopeQueryRequest,
49) -> Result<ScopeQueryResponse, ScopeQueryError> {
50 let (prepared, resolver) =
51 validated_scope_resolver(request, crate::DocumentResolver::from_system)?;
52 prepared.execute(&resolver)
53}
54
55fn validated_scope_resolver<T>(
56 request: &ScopeQueryRequest,
57 factory: impl FnOnce() -> T,
58) -> Result<(PreparedScopeQuery<'_>, T), ScopeQueryError> {
59 let prepared = PreparedScopeQuery::new(request)?;
60 Ok((prepared, factory()))
61}
62
63impl crate::DocumentResolver {
64 pub fn execute_scope_query(
71 &self,
72 request: &ScopeQueryRequest,
73 ) -> Result<ScopeQueryResponse, ScopeQueryError> {
74 PreparedScopeQuery::new(request)?.execute(self)
75 }
76
77 fn execute_validated_scope_query(
80 &self,
81 request: &ScopeQueryRequest,
82 ) -> Result<ScopeQueryResponse, ScopeQueryError> {
83 let loaded = self.resolve_scope(&request.scope)?;
84 let result = match &request.view {
85 ScopeQueryView::Explain { entry, options } => execute_scope_explain(
86 &loaded,
87 &mant_protocol::ExplanationQuery {
88 entry: entry.clone(),
89 options: *options,
90 },
91 )?,
92 ScopeQueryView::Search {
93 pattern,
94 syntax,
95 case,
96 scope,
97 word,
98 context_lines,
99 limit,
100 offset,
101 } => execute_scope_search(
102 &loaded,
103 &SearchQuery {
104 pattern: pattern.clone(),
105 syntax: *syntax,
106 case: *case,
107 scope: *scope,
108 word: *word,
109 context_lines: *context_lines,
110 limit: *limit,
111 offset: *offset,
112 },
113 )?,
114 };
115 Ok(ScopeQueryResponse {
116 schema: ScopeQuerySchema::V0Dot11,
117 scope: loaded.into_parts().0,
118 result,
119 })
120 }
121}
122
123pub(super) fn execute_scope_explain(
124 loaded: &LoadedDocumentScope,
125 query: &mant_protocol::ExplanationQuery,
126) -> Result<ScopeQueryResult, ScopeQueryError> {
127 let input = QueryScopeView::new(loaded.scope(), loaded.documents())
128 .map_err(ScopeQueryError::InvalidLoadedScope)?;
129 mant_query::explain_scope(input, query)
130 .map(|explanation| ScopeQueryResult::Explain { explanation })
131 .map_err(ScopeQueryError::Execution)
132}
133
134pub(super) fn execute_scope_search(
135 loaded: &LoadedDocumentScope,
136 query: &SearchQuery,
137) -> Result<ScopeQueryResult, ScopeQueryError> {
138 let input = QueryScopeView::new(loaded.scope(), loaded.documents())
139 .map_err(ScopeQueryError::InvalidLoadedScope)?;
140 mant_query::search_scope(input, query)
141 .map(|search| ScopeQueryResult::Search { search })
142 .map_err(ScopeQueryError::Execution)
143}
144
145#[cfg(test)]
146mod tests {
147 use std::cell::Cell;
148
149 use super::*;
150 use mant_protocol::{
151 DocumentScope, DocumentSelector, DocumentTraversal, ExplanationOptions,
152 MAX_SEMANTIC_ENTRY_CHARS, ScopeRequestSchema, SearchCase, SearchScope, SearchSyntax,
153 };
154
155 fn request() -> ScopeQueryRequest {
156 ScopeQueryRequest {
157 schema: ScopeRequestSchema::V0Dot11,
158 scope: DocumentScope {
159 documents: vec![DocumentSelector {
160 selector: "root".to_owned(),
161 source: None,
162 manual_section: None,
163 }],
164 traversal: DocumentTraversal::default(),
165 },
166 view: ScopeQueryView::Explain {
167 entry: "--help".to_owned(),
168 options: ExplanationOptions::default(),
169 },
170 }
171 }
172
173 #[test]
174 fn invalid_scope_views_and_roots_never_construct_the_environment() {
175 let mut empty_roots = request();
176 empty_roots.scope.documents.clear();
177 let mut invalid_root = request();
178 invalid_root.scope.documents[0].selector = "root\nother".to_owned();
179 let mut invalid_entry = request();
180 invalid_entry.view = ScopeQueryView::Explain {
181 entry: "x".repeat(MAX_SEMANTIC_ENTRY_CHARS + 1),
182 options: ExplanationOptions::default(),
183 };
184 let mut invalid_explanation_budget = request();
185 invalid_explanation_budget.view = ScopeQueryView::Explain {
186 entry: "--help".to_owned(),
187 options: ExplanationOptions {
188 content_bytes: 0,
189 ..ExplanationOptions::default()
190 },
191 };
192 let mut invalid_search = request();
193 invalid_search.view = ScopeQueryView::Search {
194 pattern: "needle".to_owned(),
195 syntax: SearchSyntax::Literal,
196 case: SearchCase::Sensitive,
197 scope: SearchScope::Visible,
198 word: false,
199 context_lines: 101,
200 limit: 10,
201 offset: 0,
202 };
203 for invalid in [
204 empty_roots,
205 invalid_root,
206 invalid_entry,
207 invalid_explanation_budget,
208 invalid_search,
209 ] {
210 let calls = Cell::new(0);
211 let result = validated_scope_resolver(&invalid, || calls.set(calls.get() + 1));
212 assert!(result.is_err());
213 assert_eq!(
214 calls.get(),
215 0,
216 "validation must precede environment discovery"
217 );
218 }
219 }
220
221 #[test]
222 fn valid_scope_constructs_exactly_one_environment_after_validation() {
223 let calls = Cell::new(0);
224 let request = request();
225 let (prepared, value) = validated_scope_resolver(&request, || {
226 calls.set(calls.get() + 1);
227 "snapshot"
228 })
229 .unwrap();
230 assert_eq!(value, "snapshot");
231 assert_eq!(calls.get(), 1);
232 assert!(std::ptr::eq(prepared.request(), &raw const request));
233 }
234}