lattice_sdk/core/
query_parameter_builder.rs1use chrono::{DateTime, Utc};
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 int(mut self, key: &str, value: impl Into<Option<i64>>) -> Self {
27 if let Some(v) = value.into() {
28 self.params.push((key.to_string(), v.to_string()));
29 }
30 self
31 }
32
33 pub fn float(mut self, key: &str, value: impl Into<Option<f64>>) -> Self {
35 if let Some(v) = value.into() {
36 self.params.push((key.to_string(), v.to_string()));
37 }
38 self
39 }
40
41 pub fn bool(mut self, key: &str, value: impl Into<Option<bool>>) -> Self {
43 if let Some(v) = value.into() {
44 self.params.push((key.to_string(), v.to_string()));
45 }
46 self
47 }
48
49 pub fn datetime(mut self, key: &str, value: impl Into<Option<DateTime<Utc>>>) -> Self {
51 if let Some(v) = value.into() {
52 self.params.push((key.to_string(), v.to_rfc3339()));
53 }
54 self
55 }
56
57 pub fn uuid(mut self, key: &str, value: impl Into<Option<uuid::Uuid>>) -> Self {
59 if let Some(v) = value.into() {
60 self.params.push((key.to_string(), v.to_string()));
61 }
62 self
63 }
64
65 pub fn date(mut self, key: &str, value: impl Into<Option<chrono::NaiveDate>>) -> Self {
67 if let Some(v) = value.into() {
68 let datetime = v.and_hms_opt(0, 0, 0).unwrap().and_utc();
70 self.params.push((key.to_string(), datetime.to_rfc3339()));
71 }
72 self
73 }
74
75 pub fn serialize<T: Serialize>(mut self, key: &str, value: Option<T>) -> Self {
77 if let Some(v) = value {
78 if let Ok(serialized) = serde_json::to_string(&v) {
81 let cleaned = if serialized.starts_with('"') && serialized.ends_with('"') {
83 serialized.trim_matches('"').to_string()
84 } else {
85 serialized
86 };
87 self.params.push((key.to_string(), cleaned));
88 }
89 }
90 self
91 }
92
93 pub fn structured_query(mut self, key: &str, value: impl Into<Option<String>>) -> Self {
100 if let Some(query_str) = value.into() {
101 if let Ok(parsed_params) = parse_structured_query(&query_str) {
102 self.params.extend(parsed_params);
103 } else {
104 self.params.push((key.to_string(), query_str));
106 }
107 }
108 self
109 }
110
111 pub fn build(self) -> Option<Vec<(String, String)>> {
113 if self.params.is_empty() {
114 None
115 } else {
116 Some(self.params)
117 }
118 }
119}
120
121#[derive(Debug)]
123pub enum QueryBuilderError {
124 InvalidQuerySyntax(String),
125}
126
127impl std::fmt::Display for QueryBuilderError {
128 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129 match self {
130 QueryBuilderError::InvalidQuerySyntax(msg) => {
131 write!(f, "Invalid query syntax: {}", msg)
132 }
133 }
134 }
135}
136
137impl std::error::Error for QueryBuilderError {}
138
139pub fn parse_structured_query(query: &str) -> Result<Vec<(String, String)>, QueryBuilderError> {
148 let mut params = Vec::new();
149 let terms = tokenize_query(query);
150
151 for term in terms {
152 if let Some((key, values)) = term.split_once(':') {
153 for value in values.split(',') {
155 let clean_value = value.trim_matches('"'); params.push((key.to_string(), clean_value.to_string()));
157 }
158 } else {
159 return Err(QueryBuilderError::InvalidQuerySyntax(format!(
161 "Cannot parse term '{}' - expected 'key:value' format for structured queries",
162 term
163 )));
164 }
165 }
166
167 Ok(params)
168}
169
170fn tokenize_query(input: &str) -> Vec<String> {
172 let mut tokens = Vec::new();
173 let mut current_token = String::new();
174 let mut in_quotes = false;
175 let mut chars = input.chars().peekable();
176
177 while let Some(c) = chars.next() {
178 match c {
179 '"' => {
180 in_quotes = !in_quotes;
182 current_token.push(c);
183 }
184 ' ' if !in_quotes => {
185 if !current_token.is_empty() {
187 tokens.push(current_token.trim().to_string());
188 current_token.clear();
189 }
190 }
191 _ => {
192 current_token.push(c);
194 }
195 }
196 }
197
198 if !current_token.is_empty() {
200 tokens.push(current_token.trim().to_string());
201 }
202
203 tokens
204}