hayson_binding_shared/
aggregate.rs1use std::collections::HashMap;
4
5use hayson_engine::query::{AggregateResult, AggregateSpec, GroupedAggregateResult};
6use serde_json::{json, Value};
7
8pub fn parse_aggregate(value: &Value) -> Result<AggregateSpec, String> {
11 if value.is_null() {
12 return Ok(AggregateSpec::default());
13 }
14 let object = value
15 .as_object()
16 .ok_or_else(|| "the aggregate must be a JSON object".to_string())?;
17 const KNOWN: [&str; 6] = ["count", "groupBy", "sum", "avg", "min", "max"];
18 if let Some(key) = object.keys().find(|key| !KNOWN.contains(&key.as_str())) {
19 return Err(format!("unknown aggregate field \"{key}\""));
20 }
21 let count = match object.get("count") {
22 None | Some(Value::Null) => false,
23 Some(Value::Bool(flag)) => *flag,
24 Some(_) => return Err("\"count\" must be a boolean".to_string()),
25 };
26 let group_by = match object.get("groupBy") {
27 None | Some(Value::Null) => None,
28 Some(Value::String(field)) => Some(field.clone()),
29 Some(_) => return Err("\"groupBy\" must be a field name".to_string()),
30 };
31 Ok(AggregateSpec {
32 count,
33 sum: field_list(object.get("sum"), "sum")?,
34 avg: field_list(object.get("avg"), "avg")?,
35 min: field_list(object.get("min"), "min")?,
36 max: field_list(object.get("max"), "max")?,
37 group_by,
38 })
39}
40
41fn field_list(value: Option<&Value>, name: &str) -> Result<Vec<String>, String> {
42 match value {
43 None | Some(Value::Null) => Ok(Vec::new()),
44 Some(Value::Array(items)) => items
45 .iter()
46 .map(|item| {
47 item.as_str()
48 .map(str::to_owned)
49 .ok_or_else(|| format!("\"{name}\" must list field names as strings"))
50 })
51 .collect(),
52 Some(_) => Err(format!("\"{name}\" must be a JSON array of field names")),
53 }
54}
55
56fn field_map_to_json<T: Copy>(values: &HashMap<String, T>, to_json: impl Fn(T) -> Value) -> Value {
57 let mut fields: Vec<(&String, &T)> = values.iter().collect();
58 fields.sort_by_key(|(field, _)| *field);
59 Value::Object(
60 fields
61 .into_iter()
62 .map(|(field, value)| (field.clone(), to_json(*value)))
63 .collect(),
64 )
65}
66
67fn optional_number(value: Option<f64>) -> Value {
68 value.map(Value::from).unwrap_or(Value::Null)
69}
70
71pub fn aggregate_result_to_json(result: &AggregateResult) -> Value {
73 json!({
74 "count": result.count,
75 "sum": field_map_to_json(&result.sum, Value::from),
76 "avg": field_map_to_json(&result.avg, optional_number),
77 "min": field_map_to_json(&result.min, optional_number),
78 "max": field_map_to_json(&result.max, optional_number),
79 })
80}
81
82pub fn grouped_aggregate_result_to_json(result: &GroupedAggregateResult) -> Value {
84 Value::Array(
85 result
86 .0
87 .iter()
88 .map(|(key, group)| json!({ "key": key, "result": aggregate_result_to_json(group) }))
89 .collect(),
90 )
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96
97 #[test]
98 fn the_document_names_fields_and_a_group() {
99 let spec = parse_aggregate(&json!({ "count": true, "sum": ["bytes"], "groupBy": "ns" })).unwrap();
100
101 assert!(spec.count);
102 assert_eq!(spec.sum, vec!["bytes".to_string()]);
103 assert_eq!(spec.group_by.as_deref(), Some("ns"));
104 assert_eq!(parse_aggregate(&Value::Null).unwrap(), AggregateSpec::default());
105 }
106
107 #[test]
108 fn a_malformed_document_is_refused_by_name() {
109 for bad in [
110 json!(3),
111 json!({ "sums": [] }),
112 json!({ "sum": "bytes" }),
113 json!({ "sum": [1] }),
114 json!({ "groupBy": 1 }),
115 json!({ "count": "yes" }),
116 ] {
117 assert!(parse_aggregate(&bad).is_err(), "{bad}");
118 }
119 }
120}