Skip to main content

mant_engine/scope/
execute.rs

1//! Application adapters join a loading report to a pure collection query.
2use super::{LoadedDocumentScope, ScopeQueryError, ScopeQueryResult, SearchQuery};
3use mant_protocol::{ScopeQueryRequest, ScopeQueryResponse, ScopeQuerySchema, ScopeQueryView};
4use mant_query::QueryScopeView;
5
6/// A complete scope query validated before local environment capture.
7///
8/// Preparation binds the immutable request, not loaded documents or a cached
9/// search matcher. The same prepared request may use an explicit host snapshot.
10pub struct PreparedScopeQuery<'request> {
11    request: &'request ScopeQueryRequest,
12}
13
14impl<'request> PreparedScopeQuery<'request> {
15    /// Validate roots, traversal and the projection without local IO.
16    ///
17    /// # Errors
18    /// Returns scope, selector or query configuration failures.
19    pub fn new(request: &'request ScopeQueryRequest) -> Result<Self, ScopeQueryError> {
20        super::validate_scope_query_request(request)?;
21        Ok(Self { request })
22    }
23
24    /// The immutable scope request whose roots and view were validated together.
25    #[must_use]
26    pub const fn request(&self) -> &'request ScopeQueryRequest {
27        self.request
28    }
29
30    /// Load and query against the supplied snapshot using the shared pipeline.
31    ///
32    /// # Errors
33    /// Returns scope loading, mapping or pure query execution failures.
34    pub fn execute(
35        &self,
36        resolver: &crate::DocumentResolver,
37    ) -> Result<ScopeQueryResponse, ScopeQueryError> {
38        resolver.execute_validated_scope_query(self.request)
39    }
40}
41
42/// Validate a complete scope query before capturing the local source environment.
43///
44/// # Errors
45///
46/// Returns invalid query input, scope-loading failure, or query-execution failure.
47pub 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    /// Resolve a scope and apply its closed multi-document projection.
65    ///
66    /// # Errors
67    ///
68    /// Returns request validation, resolution, or search errors. Ordinary
69    /// per-document explanation misses do not fail the aggregate query.
70    pub fn execute_scope_query(
71        &self,
72        request: &ScopeQueryRequest,
73    ) -> Result<ScopeQueryResponse, ScopeQueryError> {
74        PreparedScopeQuery::new(request)?.execute(self)
75    }
76
77    // Both public entry points validate the complete request exactly once before
78    // this shared pipeline. Pure query APIs retain their own input validation.
79    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}