Skip to main content

amql_engine/
pipeline.rs

1//! QueryPipeline — composable builder for running AQL queries with pluggable middleware.
2//!
3//! External consumers (e.g. datum) construct a pipeline once, register resolvers,
4//! extractors, and middleware, then call `query` or `select` repeatedly.
5
6use crate::code_cache::CodeCache;
7use crate::error::AqlError;
8use crate::extractor::{BuiltinExtractor, ExtractorRegistry};
9use crate::query::{unified_query, QueryResult};
10use crate::resolver::{CodeResolver, ResolverRegistry};
11use crate::store::{Annotation, AnnotationStore};
12use crate::types::{ProjectRoot, Scope, SelectorStr};
13
14/// Context passed to each middleware.
15///
16/// Contains the selector and scope that produced the results.
17/// Read-only — middleware transforms results, not the query.
18pub struct QueryContext<'a> {
19    pub selector: &'a SelectorStr,
20    pub scope: &'a Scope,
21}
22
23/// A middleware that can transform query results after AQL runs them.
24///
25/// Implement this trait to filter, sort, augment, or replace results.
26/// Middleware are applied in registration order.
27///
28/// # Example
29///
30/// ```rust
31/// use amql_engine::{QueryContext, QueryMiddleware, QueryResult};
32///
33/// struct OwnerFilter { owner: String }
34///
35/// impl QueryMiddleware for OwnerFilter {
36///     fn name(&self) -> &str { "owner-filter" }
37///     fn transform(&self, results: Vec<QueryResult>, _ctx: &QueryContext<'_>) -> Vec<QueryResult> {
38///         results.into_iter()
39///             .filter(|r| r.annotations.iter()
40///                 .any(|a| a.attrs.get("owner")
41///                     .and_then(|v| v.as_str())
42///                     .map(|v| v == self.owner)
43///                     .unwrap_or(false)))
44///             .collect()
45///     }
46/// }
47/// ```
48pub trait QueryMiddleware: Send + Sync {
49    /// Unique name for this middleware.
50    fn name(&self) -> &str;
51    /// Transform query results. Called after query execution, in registration order.
52    fn transform(&self, results: Vec<QueryResult>, ctx: &QueryContext<'_>) -> Vec<QueryResult>;
53}
54
55/// Builder and runner for AQL queries.
56///
57/// Wraps all registries and state needed to run queries. Create once,
58/// call `query` or `select` many times.
59///
60/// # Example
61///
62/// ```rust,no_run
63/// use amql_engine::{QueryPipeline, ProjectRoot};
64///
65/// let root = ProjectRoot::from(std::path::Path::new("."));
66/// let mut pipeline = QueryPipeline::builder(&root).build();
67/// let results = pipeline.query("route[method=\"GET\"]", "src/").unwrap();
68/// ```
69pub struct QueryPipeline {
70    root: ProjectRoot,
71    store: AnnotationStore,
72    cache: CodeCache,
73    resolvers: ResolverRegistry,
74    extractors: ExtractorRegistry,
75    middleware: Vec<Box<dyn QueryMiddleware>>,
76}
77
78/// Builder for `QueryPipeline`.
79pub struct QueryPipelineBuilder {
80    root: ProjectRoot,
81    resolvers: ResolverRegistry,
82    extractors: ExtractorRegistry,
83    middleware: Vec<Box<dyn QueryMiddleware>>,
84}
85
86impl QueryPipelineBuilder {
87    /// Register an additional resolver. Called after `QueryPipeline::builder`.
88    pub fn with_resolver(mut self, resolver: impl CodeResolver + 'static) -> Self {
89        self.resolvers.register(Box::new(resolver));
90        self
91    }
92
93    /// Register an additional extractor.
94    pub fn with_extractor(mut self, extractor: impl BuiltinExtractor + 'static) -> Self {
95        self.extractors.register(Box::new(extractor));
96        self
97    }
98
99    /// Register a middleware. Applied in order after query execution.
100    pub fn with_middleware(mut self, middleware: impl QueryMiddleware + 'static) -> Self {
101        self.middleware.push(Box::new(middleware));
102        self
103    }
104
105    /// Build the pipeline, loading all annotations from the project.
106    pub fn build(self) -> QueryPipeline {
107        let mut store = AnnotationStore::new(self.root.as_ref());
108        store.load_all_from_locator();
109        let cache = CodeCache::new(self.root.as_ref());
110        QueryPipeline {
111            root: self.root,
112            store,
113            cache,
114            resolvers: self.resolvers,
115            extractors: self.extractors,
116            middleware: self.middleware,
117        }
118    }
119}
120
121impl QueryPipeline {
122    /// Create a builder pre-loaded with all default resolvers and extractors.
123    pub fn builder(root: &ProjectRoot) -> QueryPipelineBuilder {
124        QueryPipelineBuilder {
125            root: root.clone(),
126            resolvers: ResolverRegistry::with_defaults(),
127            extractors: ExtractorRegistry::with_defaults(),
128            middleware: Vec::new(),
129        }
130    }
131
132    /// Run a unified query (code + annotations) and apply middleware.
133    pub fn query(&mut self, selector: &str, scope: &str) -> Result<Vec<QueryResult>, AqlError> {
134        let selector_str = SelectorStr::from(selector);
135        let scope_val = Scope::from(scope);
136        let ctx = QueryContext {
137            selector: &selector_str,
138            scope: &scope_val,
139        };
140
141        let mut results = unified_query(
142            selector,
143            &scope_val,
144            &mut self.cache,
145            &mut self.store,
146            &self.resolvers,
147            None,
148        )?;
149
150        for mw in &self.middleware {
151            results = mw.transform(results, &ctx);
152        }
153
154        Ok(results)
155    }
156
157    /// Select annotations by selector (no code parsing).
158    pub fn select(&self, selector: &str, scope: Option<&str>) -> Result<Vec<Annotation>, AqlError> {
159        self.store.select(selector, None, scope, None)
160    }
161
162    /// Reload all annotations from disk (call after external writes).
163    pub fn reload(&mut self) {
164        self.store.load_all_from_locator();
165    }
166
167    /// Return the extractor registry for this pipeline.
168    pub fn extractors(&self) -> &ExtractorRegistry {
169        &self.extractors
170    }
171
172    /// Return the project root for this pipeline.
173    pub fn root(&self) -> &ProjectRoot {
174        &self.root
175    }
176}