fraiseql_core/runtime/
matcher.rs1use std::collections::HashMap;
4
5use crate::{
6 error::{FraiseQLError, Result},
7 graphql::{DirectiveEvaluator, FieldSelection, FragmentResolver, ParsedQuery, parse_query},
8 schema::{CompiledSchema, QueryDefinition},
9};
10
11#[derive(Debug, Clone)]
13pub struct QueryMatch {
14 pub query_def: QueryDefinition,
16
17 pub fields: Vec<String>,
19
20 pub selections: Vec<FieldSelection>,
22
23 pub arguments: HashMap<String, serde_json::Value>,
25
26 pub operation_name: Option<String>,
28
29 pub parsed_query: ParsedQuery,
31}
32
33pub struct QueryMatcher {
38 schema: CompiledSchema,
39}
40
41impl QueryMatcher {
42 #[must_use]
44 pub fn new(schema: CompiledSchema) -> Self {
45 Self { schema }
46 }
47
48 pub fn match_query(
77 &self,
78 query: &str,
79 variables: Option<&serde_json::Value>,
80 ) -> Result<QueryMatch> {
81 let parsed = parse_query(query).map_err(|e| FraiseQLError::Parse {
83 message: e.to_string(),
84 location: "query".to_string(),
85 })?;
86
87 let variables_map = self.build_variables_map(variables);
89
90 let resolver = FragmentResolver::new(&parsed.fragments);
92 let resolved_selections = resolver.resolve_spreads(&parsed.selections).map_err(|e| {
93 FraiseQLError::Validation {
94 message: e.to_string(),
95 path: Some("fragments".to_string()),
96 }
97 })?;
98
99 let final_selections =
101 DirectiveEvaluator::filter_selections(&resolved_selections, &variables_map).map_err(
102 |e| FraiseQLError::Validation {
103 message: e.to_string(),
104 path: Some("directives".to_string()),
105 },
106 )?;
107
108 let query_def = self
110 .schema
111 .find_query(&parsed.root_field)
112 .ok_or_else(|| FraiseQLError::Validation {
113 message: format!("Query '{}' not found in schema", parsed.root_field),
114 path: None,
115 })?
116 .clone();
117
118 let fields = self.extract_field_names(&final_selections);
120
121 let arguments = self.extract_arguments(variables);
123
124 Ok(QueryMatch {
125 query_def,
126 fields,
127 selections: final_selections,
128 arguments,
129 operation_name: parsed.operation_name.clone(),
130 parsed_query: parsed,
131 })
132 }
133
134 fn build_variables_map(
136 &self,
137 variables: Option<&serde_json::Value>,
138 ) -> HashMap<String, serde_json::Value> {
139 if let Some(serde_json::Value::Object(map)) = variables {
140 map.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
141 } else {
142 HashMap::new()
143 }
144 }
145
146 fn extract_field_names(&self, selections: &[FieldSelection]) -> Vec<String> {
148 selections.iter().map(|s| s.name.clone()).collect()
149 }
150
151 fn extract_arguments(
153 &self,
154 variables: Option<&serde_json::Value>,
155 ) -> HashMap<String, serde_json::Value> {
156 if let Some(serde_json::Value::Object(map)) = variables {
157 map.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
158 } else {
159 HashMap::new()
160 }
161 }
162
163 #[must_use]
165 pub const fn schema(&self) -> &CompiledSchema {
166 &self.schema
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173 use crate::schema::{CompiledSchema, QueryDefinition};
174
175 fn test_schema() -> CompiledSchema {
176 let mut schema = CompiledSchema::new();
177 schema.queries.push(QueryDefinition {
178 name: "users".to_string(),
179 return_type: "User".to_string(),
180 returns_list: true,
181 nullable: false,
182 arguments: Vec::new(),
183 sql_source: Some("v_user".to_string()),
184 description: None,
185 auto_params: crate::schema::AutoParams::default(),
186 deprecation: None,
187 jsonb_column: "data".to_string(),
188 });
189 schema
190 }
191
192 #[test]
193 fn test_matcher_new() {
194 let schema = test_schema();
195 let matcher = QueryMatcher::new(schema.clone());
196 assert_eq!(matcher.schema().queries.len(), 1);
197 }
198
199 #[test]
200 fn test_match_simple_query() {
201 let schema = test_schema();
202 let matcher = QueryMatcher::new(schema);
203
204 let query = "{ users { id name } }";
205 let result = matcher.match_query(query, None).unwrap();
206
207 assert_eq!(result.query_def.name, "users");
208 assert_eq!(result.fields.len(), 1); assert!(result.selections[0].nested_fields.len() >= 2); }
211
212 #[test]
213 fn test_match_query_with_operation_name() {
214 let schema = test_schema();
215 let matcher = QueryMatcher::new(schema);
216
217 let query = "query GetUsers { users { id name } }";
218 let result = matcher.match_query(query, None).unwrap();
219
220 assert_eq!(result.query_def.name, "users");
221 assert_eq!(result.operation_name, Some("GetUsers".to_string()));
222 }
223
224 #[test]
225 fn test_match_query_with_fragment() {
226 let schema = test_schema();
227 let matcher = QueryMatcher::new(schema);
228
229 let query = r"
230 fragment UserFields on User {
231 id
232 name
233 }
234 query { users { ...UserFields } }
235 ";
236 let result = matcher.match_query(query, None).unwrap();
237
238 assert_eq!(result.query_def.name, "users");
239 let root_selection = &result.selections[0];
241 assert!(root_selection.nested_fields.iter().any(|f| f.name == "id"));
242 assert!(root_selection.nested_fields.iter().any(|f| f.name == "name"));
243 }
244
245 #[test]
246 fn test_match_query_with_skip_directive() {
247 let schema = test_schema();
248 let matcher = QueryMatcher::new(schema);
249
250 let query = r"{ users { id name @skip(if: true) } }";
251 let result = matcher.match_query(query, None).unwrap();
252
253 assert_eq!(result.query_def.name, "users");
254 let root_selection = &result.selections[0];
256 assert!(root_selection.nested_fields.iter().any(|f| f.name == "id"));
257 assert!(!root_selection.nested_fields.iter().any(|f| f.name == "name"));
258 }
259
260 #[test]
261 fn test_match_query_with_include_directive_variable() {
262 let schema = test_schema();
263 let matcher = QueryMatcher::new(schema);
264
265 let query =
266 r"query($includeEmail: Boolean!) { users { id email @include(if: $includeEmail) } }";
267 let variables = serde_json::json!({ "includeEmail": false });
268 let result = matcher.match_query(query, Some(&variables)).unwrap();
269
270 assert_eq!(result.query_def.name, "users");
271 let root_selection = &result.selections[0];
273 assert!(root_selection.nested_fields.iter().any(|f| f.name == "id"));
274 assert!(!root_selection.nested_fields.iter().any(|f| f.name == "email"));
275 }
276
277 #[test]
278 fn test_match_query_unknown_query() {
279 let schema = test_schema();
280 let matcher = QueryMatcher::new(schema);
281
282 let query = "{ unknown { id } }";
283 let result = matcher.match_query(query, None);
284
285 assert!(result.is_err());
286 }
287
288 #[test]
289 fn test_extract_arguments_none() {
290 let schema = test_schema();
291 let matcher = QueryMatcher::new(schema);
292
293 let args = matcher.extract_arguments(None);
294 assert!(args.is_empty());
295 }
296
297 #[test]
298 fn test_extract_arguments_some() {
299 let schema = test_schema();
300 let matcher = QueryMatcher::new(schema);
301
302 let variables = serde_json::json!({
303 "id": "123",
304 "limit": 10
305 });
306
307 let args = matcher.extract_arguments(Some(&variables));
308 assert_eq!(args.len(), 2);
309 assert_eq!(args.get("id"), Some(&serde_json::json!("123")));
310 assert_eq!(args.get("limit"), Some(&serde_json::json!(10)));
311 }
312}