pub struct CqlBuilder {
conditions: Vec<String>,
}
impl CqlBuilder {
pub fn new() -> Self {
Self {
conditions: Vec::new(),
}
}
pub fn eq(mut self, field: &str, value: &str) -> Self {
let normalized = Self::normalize_value(field, value);
self.conditions.push(format!("{} = {}", field, normalized));
self
}
pub fn in_list(mut self, field: &str, values: &[String]) -> Self {
if values.is_empty() {
return self;
}
let escaped_values: Vec<String> =
values.iter().map(|v| Self::escape_and_quote(v)).collect();
self.conditions
.push(format!("{} IN ({})", field, escaped_values.join(", ")));
self
}
pub fn contains(mut self, field: &str, value: &str) -> Self {
let escaped = Self::escape_and_quote(value);
self.conditions.push(format!("{} ~ {}", field, escaped));
self
}
pub fn finish(self) -> String {
if self.conditions.is_empty() {
return String::new();
}
self.conditions.join(" AND ")
}
fn escape_and_quote(value: &str) -> String {
let escaped = value
.replace('\\', "\\\\") .replace('"', "\\\""); format!("\"{}\"", escaped)
}
fn normalize_value(field: &str, value: &str) -> String {
match (field, value) {
("creator" | "contributor" | "mention", "@me") => "currentUser()".to_string(),
_ => Self::escape_and_quote(value),
}
}
}
impl Default for CqlBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_builder() {
let query = CqlBuilder::new().finish();
assert_eq!(query, "");
}
#[test]
fn test_single_eq() {
let query = CqlBuilder::new().eq("space", "ENG").finish();
assert_eq!(query, "space = \"ENG\"");
}
#[test]
fn test_creator_me_shorthand() {
let query = CqlBuilder::new().eq("creator", "@me").finish();
assert_eq!(query, "creator = currentUser()");
}
#[test]
fn test_multiple_conditions() {
let query = CqlBuilder::new()
.eq("space", "ENG")
.eq("type", "page")
.finish();
assert_eq!(query, "space = \"ENG\" AND type = \"page\"");
}
#[test]
fn test_in_list_single() {
let query = CqlBuilder::new()
.in_list("type", &[String::from("page")])
.finish();
assert_eq!(query, "type IN (\"page\")");
}
#[test]
fn test_in_list_multiple() {
let query = CqlBuilder::new()
.in_list("type", &[String::from("page"), String::from("blogpost")])
.finish();
assert_eq!(query, "type IN (\"page\", \"blogpost\")");
}
#[test]
fn test_in_list_empty() {
let query = CqlBuilder::new().in_list("type", &[]).finish();
assert_eq!(query, "");
}
#[test]
fn test_contains_title() {
let query = CqlBuilder::new().contains("title", "API").finish();
assert_eq!(query, "title ~ \"API\"");
}
#[test]
fn test_contains_text() {
let query = CqlBuilder::new().contains("text", "documentation").finish();
assert_eq!(query, "text ~ \"documentation\"");
}
#[test]
fn test_quote_escape() {
let query = CqlBuilder::new()
.eq("title", "Page \"with quotes\"")
.finish();
assert_eq!(query, "title = \"Page \\\"with quotes\\\"\"");
}
#[test]
fn test_backslash_escape() {
let query = CqlBuilder::new().eq("title", "Path\\to\\page").finish();
assert_eq!(query, "title = \"Path\\\\to\\\\page\"");
}
#[test]
fn test_complex_query() {
let query = CqlBuilder::new()
.eq("space", "ENG")
.eq("type", "page")
.eq("creator", "@me")
.in_list("label", &[String::from("api"), String::from("backend")])
.contains("title", "REST")
.finish();
assert_eq!(
query,
"space = \"ENG\" AND type = \"page\" AND creator = currentUser() AND label IN (\"api\", \"backend\") AND title ~ \"REST\""
);
}
}