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}