jpx_core/
query_library.rs1use std::fmt;
67
68#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct ParseError {
71 pub message: String,
73 pub line: Option<usize>,
75}
76
77impl fmt::Display for ParseError {
78 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79 match self.line {
80 Some(line) => write!(f, "{} at line {}", self.message, line),
81 None => write!(f, "{}", self.message),
82 }
83 }
84}
85
86impl std::error::Error for ParseError {}
87
88impl ParseError {
89 pub fn new(message: impl Into<String>) -> Self {
91 Self {
92 message: message.into(),
93 line: None,
94 }
95 }
96
97 pub fn with_line(message: impl Into<String>, line: usize) -> Self {
99 Self {
100 message: message.into(),
101 line: Some(line),
102 }
103 }
104}
105
106#[derive(Debug, Clone, PartialEq, Eq)]
108pub struct NamedQuery {
109 pub name: String,
111 pub description: Option<String>,
113 pub expression: String,
115 pub line_number: usize,
117}
118
119#[derive(Debug, Clone, Default)]
121pub struct QueryLibrary {
122 queries: Vec<NamedQuery>,
123}
124
125impl QueryLibrary {
126 pub fn new() -> Self {
128 Self::default()
129 }
130
131 pub fn parse(content: &str) -> Result<Self, ParseError> {
162 let mut queries = Vec::new();
163 let mut current_name: Option<String> = None;
164 let mut current_desc: Option<String> = None;
165 let mut current_expr = String::new();
166 let mut current_line_number = 0usize;
167
168 for (line_num, line) in content.lines().enumerate() {
169 let line_number = line_num + 1; let trimmed = line.trim();
171
172 if let Some(rest) = trimmed.strip_prefix("-- :name ").or_else(|| {
173 if trimmed == "-- :name" {
175 Some("")
176 } else {
177 None
178 }
179 }) {
180 if let Some(name) = current_name.take() {
182 let expr = current_expr.trim().to_string();
183 if expr.is_empty() {
184 return Err(ParseError::with_line(
185 format!("Query '{}' has no expression", name),
186 current_line_number,
187 ));
188 }
189 queries.push(NamedQuery {
190 name,
191 description: current_desc.take(),
192 expression: expr,
193 line_number: current_line_number,
194 });
195 current_expr.clear();
196 }
197
198 let name = rest.trim().to_string();
200 if name.is_empty() {
201 return Err(ParseError::with_line("Empty query name", line_number));
202 }
203
204 if queries.iter().any(|q| q.name == name) {
206 return Err(ParseError::with_line(
207 format!("Duplicate query name '{}'", name),
208 line_number,
209 ));
210 }
211
212 current_name = Some(name);
213 current_line_number = line_number;
214 } else if let Some(rest) = trimmed.strip_prefix("-- :desc ") {
215 if current_name.is_some() {
217 current_desc = Some(rest.trim().to_string());
218 }
219 } else if trimmed.starts_with("-- ") || trimmed == "--" {
220 } else if !trimmed.is_empty() {
222 if current_name.is_some() {
224 if !current_expr.is_empty() {
225 current_expr.push('\n');
226 }
227 current_expr.push_str(line);
228 }
229 }
230 }
231
232 if let Some(name) = current_name {
234 let expr = current_expr.trim().to_string();
235 if expr.is_empty() {
236 return Err(ParseError::with_line(
237 format!("Query '{}' has no expression", name),
238 current_line_number,
239 ));
240 }
241 queries.push(NamedQuery {
242 name,
243 description: current_desc,
244 expression: expr,
245 line_number: current_line_number,
246 });
247 }
248
249 if queries.is_empty() {
250 return Err(ParseError::new(
251 "No queries found. Use '-- :name <query-name>' to define queries.",
252 ));
253 }
254
255 Ok(QueryLibrary { queries })
256 }
257
258 pub fn get(&self, name: &str) -> Option<&NamedQuery> {
270 self.queries.iter().find(|q| q.name == name)
271 }
272
273 pub fn list(&self) -> &[NamedQuery] {
284 &self.queries
285 }
286
287 pub fn names(&self) -> Vec<&str> {
298 self.queries.iter().map(|q| q.name.as_str()).collect()
299 }
300
301 pub fn len(&self) -> usize {
303 self.queries.len()
304 }
305
306 pub fn is_empty(&self) -> bool {
308 self.queries.is_empty()
309 }
310
311 pub fn iter(&self) -> impl Iterator<Item = &NamedQuery> {
313 self.queries.iter()
314 }
315}
316
317impl<'a> IntoIterator for &'a QueryLibrary {
318 type Item = &'a NamedQuery;
319 type IntoIter = std::slice::Iter<'a, NamedQuery>;
320
321 fn into_iter(self) -> Self::IntoIter {
322 self.queries.iter()
323 }
324}
325
326pub fn is_query_library(content: &str) -> bool {
343 content
344 .lines()
345 .find(|line| !line.trim().is_empty())
346 .map(|line| line.trim().starts_with("-- :name "))
347 .unwrap_or(false)
348}
349
350#[cfg(test)]
351mod tests {
352 use super::*;
353
354 #[test]
355 fn test_parse_simple_library() {
356 let content = r#"
357-- :name greet
358-- :desc Simple greeting
359`"hello"`
360
361-- :name count
362length(@)
363"#;
364 let lib = QueryLibrary::parse(content).unwrap();
365 assert_eq!(lib.len(), 2);
366
367 let greet = lib.get("greet").unwrap();
368 assert_eq!(greet.name, "greet");
369 assert_eq!(greet.description, Some("Simple greeting".to_string()));
370 assert_eq!(greet.expression, "`\"hello\"`");
371
372 let count = lib.get("count").unwrap();
373 assert_eq!(count.name, "count");
374 assert_eq!(count.description, None);
375 assert_eq!(count.expression, "length(@)");
376 }
377
378 #[test]
379 fn test_parse_multiline_expression() {
380 let content = r#"
381-- :name complex
382-- :desc Multi-line query
383{
384 total: length(@),
385 first: @[0]
386}
387"#;
388 let lib = QueryLibrary::parse(content).unwrap();
389 let query = lib.get("complex").unwrap();
390 assert!(query.expression.contains("total: length(@)"));
391 assert!(query.expression.contains("first: @[0]"));
392 }
393
394 #[test]
395 fn test_parse_empty_name_error() {
396 let content = "-- :name \nlength(@)";
397 let result = QueryLibrary::parse(content);
398 assert!(result.is_err());
399 let err = result.unwrap_err();
400 assert!(err.message.contains("Empty query name"));
401 assert_eq!(err.line, Some(1));
402 }
403
404 #[test]
405 fn test_parse_duplicate_name_error() {
406 let content = r#"
407-- :name foo
408length(@)
409
410-- :name foo
411keys(@)
412"#;
413 let result = QueryLibrary::parse(content);
414 assert!(result.is_err());
415 assert!(result.unwrap_err().message.contains("Duplicate query name"));
416 }
417
418 #[test]
419 fn test_parse_no_expression_error() {
420 let content = "-- :name empty\n-- :name another\nlength(@)";
421 let result = QueryLibrary::parse(content);
422 assert!(result.is_err());
423 assert!(result.unwrap_err().message.contains("has no expression"));
424 }
425
426 #[test]
427 fn test_parse_no_queries_error() {
428 let content = "-- just a comment\nlength(@)";
429 let result = QueryLibrary::parse(content);
430 assert!(result.is_err());
431 assert!(result.unwrap_err().message.contains("No queries found"));
432 }
433
434 #[test]
435 fn test_is_query_library() {
436 assert!(is_query_library("-- :name foo\nlength(@)"));
437 assert!(is_query_library(" -- :name foo\nlength(@)"));
438 assert!(is_query_library("\n-- :name foo\nlength(@)"));
439 assert!(!is_query_library("length(@)"));
440 assert!(!is_query_library("-- comment\nlength(@)"));
441 }
442
443 #[test]
444 fn test_comments_ignored() {
445 let content = r#"
446-- :name test
447-- :desc Description
448-- This is a regular comment
449-- Another comment
450length(@)
451-- Trailing comment
452"#;
453 let lib = QueryLibrary::parse(content).unwrap();
454 let query = lib.get("test").unwrap();
455 assert_eq!(query.expression, "length(@)");
456 }
457
458 #[test]
459 fn test_iter() {
460 let content = "-- :name a\n`1`\n-- :name b\n`2`";
461 let lib = QueryLibrary::parse(content).unwrap();
462 let names: Vec<_> = lib.iter().map(|q| &q.name).collect();
463 assert_eq!(names, vec!["a", "b"]);
464 }
465
466 #[test]
467 fn test_into_iter() {
468 let content = "-- :name x\n`1`";
469 let lib = QueryLibrary::parse(content).unwrap();
470 for query in &lib {
471 assert_eq!(query.name, "x");
472 }
473 }
474}