use std::collections::HashMap;
use hayson_engine::query::{AggregateResult, AggregateSpec, GroupedAggregateResult};
use serde_json::{json, Value};
pub fn parse_aggregate(value: &Value) -> Result<AggregateSpec, String> {
if value.is_null() {
return Ok(AggregateSpec::default());
}
let object = value
.as_object()
.ok_or_else(|| "the aggregate must be a JSON object".to_string())?;
const KNOWN: [&str; 6] = ["count", "groupBy", "sum", "avg", "min", "max"];
if let Some(key) = object.keys().find(|key| !KNOWN.contains(&key.as_str())) {
return Err(format!("unknown aggregate field \"{key}\""));
}
let count = match object.get("count") {
None | Some(Value::Null) => false,
Some(Value::Bool(flag)) => *flag,
Some(_) => return Err("\"count\" must be a boolean".to_string()),
};
let group_by = match object.get("groupBy") {
None | Some(Value::Null) => None,
Some(Value::String(field)) => Some(field.clone()),
Some(_) => return Err("\"groupBy\" must be a field name".to_string()),
};
Ok(AggregateSpec {
count,
sum: field_list(object.get("sum"), "sum")?,
avg: field_list(object.get("avg"), "avg")?,
min: field_list(object.get("min"), "min")?,
max: field_list(object.get("max"), "max")?,
group_by,
})
}
fn field_list(value: Option<&Value>, name: &str) -> Result<Vec<String>, String> {
match value {
None | Some(Value::Null) => Ok(Vec::new()),
Some(Value::Array(items)) => items
.iter()
.map(|item| {
item.as_str()
.map(str::to_owned)
.ok_or_else(|| format!("\"{name}\" must list field names as strings"))
})
.collect(),
Some(_) => Err(format!("\"{name}\" must be a JSON array of field names")),
}
}
fn field_map_to_json<T: Copy>(values: &HashMap<String, T>, to_json: impl Fn(T) -> Value) -> Value {
let mut fields: Vec<(&String, &T)> = values.iter().collect();
fields.sort_by_key(|(field, _)| *field);
Value::Object(
fields
.into_iter()
.map(|(field, value)| (field.clone(), to_json(*value)))
.collect(),
)
}
fn optional_number(value: Option<f64>) -> Value {
value.map(Value::from).unwrap_or(Value::Null)
}
pub fn aggregate_result_to_json(result: &AggregateResult) -> Value {
json!({
"count": result.count,
"sum": field_map_to_json(&result.sum, Value::from),
"avg": field_map_to_json(&result.avg, optional_number),
"min": field_map_to_json(&result.min, optional_number),
"max": field_map_to_json(&result.max, optional_number),
})
}
pub fn grouped_aggregate_result_to_json(result: &GroupedAggregateResult) -> Value {
Value::Array(
result
.0
.iter()
.map(|(key, group)| json!({ "key": key, "result": aggregate_result_to_json(group) }))
.collect(),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_document_names_fields_and_a_group() {
let spec = parse_aggregate(&json!({ "count": true, "sum": ["bytes"], "groupBy": "ns" })).unwrap();
assert!(spec.count);
assert_eq!(spec.sum, vec!["bytes".to_string()]);
assert_eq!(spec.group_by.as_deref(), Some("ns"));
assert_eq!(parse_aggregate(&Value::Null).unwrap(), AggregateSpec::default());
}
#[test]
fn a_malformed_document_is_refused_by_name() {
for bad in [
json!(3),
json!({ "sums": [] }),
json!({ "sum": "bytes" }),
json!({ "sum": [1] }),
json!({ "groupBy": 1 }),
json!({ "count": "yes" }),
] {
assert!(parse_aggregate(&bad).is_err(), "{bad}");
}
}
}