use std::collections::HashMap;
const OPERATOR_SUFFIX: [(&str, &str); 5] =
[("eq", ""), ("gt", ">"), ("gte", ">="), ("lt", "<"), ("lte", "<=")];
pub type QueryParams = HashMap<String, QueryValue>;
#[derive(Debug, Clone)]
pub enum QueryValue {
Scalar(String),
Bool(bool),
Number(i64),
Float(f64),
Filter(HashMap<String, QueryValue>),
}
pub fn build_query_params(params: QueryParams) -> HashMap<String, String> {
let mut result = HashMap::new();
for (field, raw_value) in params {
if field == "csv" {
if matches!(raw_value, QueryValue::Bool(true)) {
result.insert("csv".to_string(), "true".to_string());
}
continue;
}
if let QueryValue::Filter(filter) = raw_value {
for (op, suffix) in OPERATOR_SUFFIX {
if let Some(value) = filter.get(op) {
result.insert(format!("{field}{suffix}"), serialize(value));
}
}
continue;
}
result.insert(field, serialize(&raw_value));
}
result
}
fn serialize(value: &QueryValue) -> String {
match value {
QueryValue::Scalar(text) => text.clone(),
QueryValue::Bool(true) => "true".to_string(),
QueryValue::Bool(false) => "false".to_string(),
QueryValue::Number(number) => number.to_string(),
QueryValue::Float(number) => number.to_string(),
QueryValue::Filter(_) => String::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[derive(serde::Deserialize)]
struct Case {
description: String,
input: HashMap<String, serde_json::Value>,
output: HashMap<String, String>,
}
fn json_to_query_value(value: &serde_json::Value) -> QueryValue {
match value {
serde_json::Value::String(text) => QueryValue::Scalar(text.clone()),
serde_json::Value::Number(number) => {
if let Some(as_i64) = number.as_i64() {
QueryValue::Number(as_i64)
} else if let Some(as_f64) = number.as_f64() {
QueryValue::Float(as_f64)
} else {
QueryValue::Scalar(number.to_string())
}
}
serde_json::Value::Bool(value) => QueryValue::Bool(*value),
serde_json::Value::Object(map) => {
let mut filter = HashMap::new();
for (key, nested) in map {
filter.insert(key.clone(), json_to_query_value(nested));
}
QueryValue::Filter(filter)
}
_ => QueryValue::Scalar(value.to_string()),
}
}
#[test]
fn conformance_vectors_match_typescript() {
let raw = fs::read_to_string(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../../specs/conformance/query-builder.json"
))
.expect("read conformance vectors");
let cases: Vec<Case> = serde_json::from_str(&raw).expect("parse vectors");
for case in cases {
let mut params = QueryParams::new();
for (key, value) in case.input {
params.insert(key, json_to_query_value(&value));
}
let got = build_query_params(params);
assert_eq!(got, case.output, "{}", case.description);
}
}
}