lattice_sdk/core/
query_parameter_builder.rs

1use chrono::{DateTime, Utc};
2use serde::Serialize;
3
4/// Modern query builder with type-safe method chaining
5/// Provides a clean, Swift-like API for building HTTP query parameters
6#[derive(Debug, Default)]
7pub struct QueryBuilder {
8    params: Vec<(String, String)>,
9}
10
11impl QueryBuilder {
12    /// Create a new query parameter builder
13    pub fn new() -> Self {
14        Self::default()
15    }
16
17    /// Add a string parameter (accept both required/optional)
18    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    /// Add an integer parameter (accept both required/optional)
26    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    /// Add a float parameter
34    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    /// Add a boolean parameter
42    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    /// Add a datetime parameter
50    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    /// Add a UUID parameter (converts to string)
58    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    /// Add a date parameter (converts NaiveDate to `DateTime<Utc>`)
66    pub fn date(mut self, key: &str, value: impl Into<Option<chrono::NaiveDate>>) -> Self {
67        if let Some(v) = value.into() {
68            // Convert NaiveDate to DateTime<Utc> at start of day
69            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    /// Add any serializable parameter (for enums and complex types)
76    pub fn serialize<T: Serialize>(mut self, key: &str, value: Option<T>) -> Self {
77        if let Some(v) = value {
78            // For enums that implement Display, use the Display implementation
79            // to avoid JSON quotes in query parameters
80            if let Ok(serialized) = serde_json::to_string(&v) {
81                // Remove JSON quotes if the value is a simple string
82                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    /// Parse and add a structured query string
94    /// Handles complex query patterns like:
95    /// - "key:value" patterns
96    /// - "key:value1,value2" (comma-separated values)
97    /// - Quoted values: "key:\"value with spaces\""
98    /// - Space-separated terms (treated as AND logic)
99    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                // Fall back to simple query parameter if parsing fails
105                self.params.push((key.to_string(), query_str));
106            }
107        }
108        self
109    }
110
111    /// Build the final query parameters
112    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/// Errors that can occur during structured query parsing
122#[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
139/// Parse structured query strings like "key:value key2:value1,value2"
140/// Used for complex filtering patterns in APIs like Foxglove
141///
142/// Supported patterns:
143/// - Simple: "status:active"
144/// - Multiple values: "type:sensor,camera"
145/// - Quoted values: "location:\"New York\""
146/// - Complex: "status:active type:sensor location:\"San Francisco\""
147pub 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            // Handle comma-separated values
154            for value in values.split(',') {
155                let clean_value = value.trim_matches('"'); // Remove quotes
156                params.push((key.to_string(), clean_value.to_string()));
157            }
158        } else {
159            // For terms without colons, return error to be explicit about expected format
160            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
170/// Tokenize a query string, properly handling quoted strings
171fn 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                // Toggle quote state and include the quote in the token
181                in_quotes = !in_quotes;
182                current_token.push(c);
183            }
184            ' ' if !in_quotes => {
185                // Space outside quotes - end current token
186                if !current_token.is_empty() {
187                    tokens.push(current_token.trim().to_string());
188                    current_token.clear();
189                }
190            }
191            _ => {
192                // Any other character (including spaces inside quotes)
193                current_token.push(c);
194            }
195        }
196    }
197
198    // Add the last token if there is one
199    if !current_token.is_empty() {
200        tokens.push(current_token.trim().to_string());
201    }
202
203    tokens
204}