1use chrono::{DateTime, TimeZone};
2use serde::Serialize;
3
4#[derive(Debug, Default)]
7pub struct QueryBuilder {
8 params: Vec<(String, String)>,
9}
10
11impl QueryBuilder {
12 pub fn new() -> Self {
14 Self::default()
15 }
16
17 pub fn string(mut self, key: &str, value: impl Into<Option<String>>) -> Self {
19 if let Some(v) = value.into() {
20 self.params.push((key.to_string(), v));
21 }
22 self
23 }
24
25 pub fn string_array<I, T>(mut self, key: &str, values: I) -> Self
28 where
29 I: IntoIterator<Item = T>,
30 T: Into<Option<String>>,
31 {
32 for value in values {
33 if let Some(v) = value.into() {
34 self.params.push((key.to_string(), v));
35 }
36 }
37 self
38 }
39
40 pub fn int(mut self, key: &str, value: impl Into<Option<i64>>) -> Self {
42 if let Some(v) = value.into() {
43 self.params.push((key.to_string(), v.to_string()));
44 }
45 self
46 }
47
48 pub fn int_array<I, T>(mut self, key: &str, values: I) -> Self
51 where
52 I: IntoIterator<Item = T>,
53 T: Into<Option<i64>>,
54 {
55 for value in values {
56 if let Some(v) = value.into() {
57 self.params.push((key.to_string(), v.to_string()));
58 }
59 }
60 self
61 }
62
63 pub fn float(mut self, key: &str, value: impl Into<Option<f64>>) -> Self {
65 if let Some(v) = value.into() {
66 self.params.push((key.to_string(), v.to_string()));
67 }
68 self
69 }
70
71 pub fn float_array<I, T>(mut self, key: &str, values: I) -> Self
74 where
75 I: IntoIterator<Item = T>,
76 T: Into<Option<f64>>,
77 {
78 for value in values {
79 if let Some(v) = value.into() {
80 self.params.push((key.to_string(), v.to_string()));
81 }
82 }
83 self
84 }
85
86 pub fn bool(mut self, key: &str, value: impl Into<Option<bool>>) -> Self {
88 if let Some(v) = value.into() {
89 self.params.push((key.to_string(), v.to_string()));
90 }
91 self
92 }
93
94 pub fn bool_array<I, T>(mut self, key: &str, values: I) -> Self
97 where
98 I: IntoIterator<Item = T>,
99 T: Into<Option<bool>>,
100 {
101 for value in values {
102 if let Some(v) = value.into() {
103 self.params.push((key.to_string(), v.to_string()));
104 }
105 }
106 self
107 }
108
109 pub fn datetime<Tz: TimeZone>(
111 mut self,
112 key: &str,
113 value: impl Into<Option<DateTime<Tz>>>,
114 ) -> Self
115 where
116 Tz::Offset: std::fmt::Display,
117 {
118 if let Some(v) = value.into() {
119 self.params.push((
120 key.to_string(),
121 v.to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
122 ));
123 }
124 self
125 }
126
127 pub fn date(mut self, key: &str, value: impl Into<Option<chrono::NaiveDate>>) -> Self {
129 if let Some(v) = value.into() {
130 let datetime = v.and_hms_opt(0, 0, 0).unwrap().and_utc();
132 self.params.push((
133 key.to_string(),
134 datetime.to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
135 ));
136 }
137 self
138 }
139
140 pub fn serialize<T: Serialize>(mut self, key: &str, value: Option<T>) -> Self {
142 if let Some(v) = value {
143 if let Ok(serialized) = serde_json::to_string(&v) {
146 let cleaned = if serialized.starts_with('"') && serialized.ends_with('"') {
148 serialized.trim_matches('"').to_string()
149 } else {
150 serialized
151 };
152 self.params.push((key.to_string(), cleaned));
153 }
154 }
155 self
156 }
157
158 pub fn serialize_array<T: Serialize>(
161 mut self,
162 key: &str,
163 values: impl IntoIterator<Item = T>,
164 ) -> Self {
165 for value in values {
166 if let Ok(serialized) = serde_json::to_string(&value) {
167 if serialized == "null" {
169 continue;
170 }
171 let cleaned = if serialized.starts_with('"') && serialized.ends_with('"') {
173 serialized.trim_matches('"').to_string()
174 } else {
175 serialized
176 };
177 self.params.push((key.to_string(), cleaned));
178 }
179 }
180 self
181 }
182
183 pub fn structured_query(mut self, key: &str, value: impl Into<Option<String>>) -> Self {
190 if let Some(query_str) = value.into() {
191 if let Ok(parsed_params) = parse_structured_query(&query_str) {
192 self.params.extend(parsed_params);
193 } else {
194 self.params.push((key.to_string(), query_str));
196 }
197 }
198 self
199 }
200
201 pub fn build(self) -> Option<Vec<(String, String)>> {
203 if self.params.is_empty() {
204 None
205 } else {
206 Some(self.params)
207 }
208 }
209}
210
211#[derive(Debug)]
213pub enum QueryBuilderError {
214 InvalidQuerySyntax(String),
215}
216
217impl std::fmt::Display for QueryBuilderError {
218 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
219 match self {
220 QueryBuilderError::InvalidQuerySyntax(msg) => {
221 write!(f, "Invalid query syntax: {}", msg)
222 }
223 }
224 }
225}
226
227impl std::error::Error for QueryBuilderError {}
228
229pub fn parse_structured_query(query: &str) -> Result<Vec<(String, String)>, QueryBuilderError> {
238 let mut params = Vec::new();
239 let terms = tokenize_query(query);
240
241 for term in terms {
242 if let Some((key, values)) = term.split_once(':') {
243 for value in values.split(',') {
245 let clean_value = value.trim_matches('"'); params.push((key.to_string(), clean_value.to_string()));
247 }
248 } else {
249 return Err(QueryBuilderError::InvalidQuerySyntax(format!(
251 "Cannot parse term '{}' - expected 'key:value' format for structured queries",
252 term
253 )));
254 }
255 }
256
257 Ok(params)
258}
259
260fn tokenize_query(input: &str) -> Vec<String> {
262 let mut tokens = Vec::new();
263 let mut current_token = String::new();
264 let mut in_quotes = false;
265 let mut chars = input.chars().peekable();
266
267 while let Some(c) = chars.next() {
268 match c {
269 '"' => {
270 in_quotes = !in_quotes;
272 current_token.push(c);
273 }
274 ' ' if !in_quotes => {
275 if !current_token.is_empty() {
277 tokens.push(current_token.trim().to_string());
278 current_token.clear();
279 }
280 }
281 _ => {
282 current_token.push(c);
284 }
285 }
286 }
287
288 if !current_token.is_empty() {
290 tokens.push(current_token.trim().to_string());
291 }
292
293 tokens
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299 use chrono::{NaiveDate, TimeZone, Utc};
300
301 #[test]
306 fn test_empty_builder_returns_none() {
307 let result = QueryBuilder::new().build();
308 assert!(result.is_none());
309 }
310
311 #[test]
312 fn test_string_param_some() {
313 let result = QueryBuilder::new()
314 .string("name", Some("alice".to_string()))
315 .build();
316 assert_eq!(
317 result,
318 Some(vec![("name".to_string(), "alice".to_string())])
319 );
320 }
321
322 #[test]
323 fn test_string_param_none_skipped() {
324 let result = QueryBuilder::new().string("name", None::<String>).build();
325 assert!(result.is_none());
326 }
327
328 #[test]
329 fn test_int_param() {
330 let result = QueryBuilder::new().int("page", Some(42i64)).build();
331 assert_eq!(result, Some(vec![("page".to_string(), "42".to_string())]));
332 }
333
334 #[test]
335 fn test_int_param_none_skipped() {
336 let result = QueryBuilder::new().int("page", None::<i64>).build();
337 assert!(result.is_none());
338 }
339
340 #[test]
341 fn test_float_param() {
342 let result = QueryBuilder::new().float("score", Some(3.14f64)).build();
343 assert_eq!(
344 result,
345 Some(vec![("score".to_string(), "3.14".to_string())])
346 );
347 }
348
349 #[test]
350 fn test_bool_param() {
351 let result = QueryBuilder::new().bool("active", Some(true)).build();
352 assert_eq!(
353 result,
354 Some(vec![("active".to_string(), "true".to_string())])
355 );
356 }
357
358 #[test]
359 fn test_datetime_param_formats_rfc3339() {
360 let dt = Utc.with_ymd_and_hms(2024, 1, 15, 9, 30, 0).unwrap();
361 let result = QueryBuilder::new().datetime("since", Some(dt)).build();
362 assert_eq!(
363 result,
364 Some(vec![(
365 "since".to_string(),
366 "2024-01-15T09:30:00Z".to_string()
367 )])
368 );
369 }
370
371 #[test]
372 fn test_date_param_converts_to_midnight_utc() {
373 let date = NaiveDate::from_ymd_opt(2024, 1, 15).unwrap();
374 let result = QueryBuilder::new().date("on", Some(date)).build();
375 assert_eq!(
376 result,
377 Some(vec![("on".to_string(), "2024-01-15T00:00:00Z".to_string())])
378 );
379 }
380
381 #[test]
382 fn test_string_array_multiple_entries() {
383 let result = QueryBuilder::new()
384 .string_array(
385 "tag",
386 vec!["a".to_string(), "b".to_string(), "c".to_string()],
387 )
388 .build();
389 assert_eq!(
390 result,
391 Some(vec![
392 ("tag".to_string(), "a".to_string()),
393 ("tag".to_string(), "b".to_string()),
394 ("tag".to_string(), "c".to_string()),
395 ])
396 );
397 }
398
399 #[test]
400 fn test_int_array() {
401 let result = QueryBuilder::new()
402 .int_array("ids", vec![1i64, 2, 3])
403 .build();
404 assert_eq!(
405 result,
406 Some(vec![
407 ("ids".to_string(), "1".to_string()),
408 ("ids".to_string(), "2".to_string()),
409 ("ids".to_string(), "3".to_string()),
410 ])
411 );
412 }
413
414 #[test]
415 fn test_float_array() {
416 let result = QueryBuilder::new()
417 .float_array("scores", vec![1.1f64, 2.2])
418 .build();
419 assert_eq!(
420 result,
421 Some(vec![
422 ("scores".to_string(), "1.1".to_string()),
423 ("scores".to_string(), "2.2".to_string()),
424 ])
425 );
426 }
427
428 #[test]
429 fn test_bool_array() {
430 let result = QueryBuilder::new()
431 .bool_array("flags", vec![true, false])
432 .build();
433 assert_eq!(
434 result,
435 Some(vec![
436 ("flags".to_string(), "true".to_string()),
437 ("flags".to_string(), "false".to_string()),
438 ])
439 );
440 }
441
442 #[test]
443 fn test_serialize_strips_json_quotes() {
444 let result = QueryBuilder::new()
445 .serialize("status", Some("active"))
446 .build();
447 assert_eq!(
448 result,
449 Some(vec![("status".to_string(), "active".to_string())])
450 );
451 }
452
453 #[test]
454 fn test_serialize_none_skipped() {
455 let result = QueryBuilder::new()
456 .serialize::<String>("status", None)
457 .build();
458 assert!(result.is_none());
459 }
460
461 #[test]
462 fn test_serialize_numeric_no_quotes() {
463 let result = QueryBuilder::new().serialize("count", Some(42)).build();
464 assert_eq!(result, Some(vec![("count".to_string(), "42".to_string())]));
465 }
466
467 #[test]
468 fn test_serialize_array_skips_null() {
469 let values: Vec<Option<&str>> = vec![Some("a"), None, Some("b")];
470 let result = QueryBuilder::new().serialize_array("items", values).build();
471 assert_eq!(
472 result,
473 Some(vec![
474 ("items".to_string(), "a".to_string()),
475 ("items".to_string(), "b".to_string()),
476 ])
477 );
478 }
479
480 #[test]
481 fn test_method_chaining() {
482 let result = QueryBuilder::new()
483 .string("name", Some("alice".to_string()))
484 .int("page", Some(1i64))
485 .bool("active", Some(true))
486 .build();
487 assert_eq!(
488 result,
489 Some(vec![
490 ("name".to_string(), "alice".to_string()),
491 ("page".to_string(), "1".to_string()),
492 ("active".to_string(), "true".to_string()),
493 ])
494 );
495 }
496
497 #[test]
502 fn test_parse_simple_key_value() {
503 let result = parse_structured_query("status:active").unwrap();
504 assert_eq!(result, vec![("status".to_string(), "active".to_string())]);
505 }
506
507 #[test]
508 fn test_parse_comma_separated_values() {
509 let result = parse_structured_query("type:sensor,camera").unwrap();
510 assert_eq!(
511 result,
512 vec![
513 ("type".to_string(), "sensor".to_string()),
514 ("type".to_string(), "camera".to_string()),
515 ]
516 );
517 }
518
519 #[test]
520 fn test_parse_multiple_terms() {
521 let result = parse_structured_query("status:active type:sensor").unwrap();
522 assert_eq!(
523 result,
524 vec![
525 ("status".to_string(), "active".to_string()),
526 ("type".to_string(), "sensor".to_string()),
527 ]
528 );
529 }
530
531 #[test]
532 fn test_parse_quoted_value() {
533 let result = parse_structured_query("location:\"New York\"").unwrap();
534 assert_eq!(
535 result,
536 vec![("location".to_string(), "New York".to_string())]
537 );
538 }
539
540 #[test]
541 fn test_parse_bare_word_returns_error() {
542 let result = parse_structured_query("bareword");
543 assert!(result.is_err());
544 }
545
546 #[test]
547 fn test_structured_query_builder_fallback() {
548 let result = QueryBuilder::new()
550 .structured_query("q", Some("bareword".to_string()))
551 .build();
552 assert_eq!(
553 result,
554 Some(vec![("q".to_string(), "bareword".to_string())])
555 );
556 }
557
558 #[test]
559 fn test_structured_query_builder_parses() {
560 let result = QueryBuilder::new()
561 .structured_query("q", Some("status:active".to_string()))
562 .build();
563 assert_eq!(
564 result,
565 Some(vec![("status".to_string(), "active".to_string())])
566 );
567 }
568
569 #[test]
570 fn test_structured_query_none_skipped() {
571 let result = QueryBuilder::new()
572 .structured_query("q", None::<String>)
573 .build();
574 assert!(result.is_none());
575 }
576}