use crate::core::output::OutputFormat;
use crate::core::row::Row;
use serde_json::Value;
use std::collections::HashSet;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ColumnAlignment {
#[default]
Default,
Left,
Center,
Right,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Group {
pub groups: Row,
pub aggregates: Row,
pub rows: Vec<Row>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct OutputMeta {
pub key_index: Vec<String>,
pub column_align: Vec<ColumnAlignment>,
pub wants_copy: bool,
pub grouped: bool,
pub render_recommendation: Option<RenderRecommendation>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RenderRecommendation {
Format(OutputFormat),
Guide,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum OutputDocumentKind {
Guide,
}
#[derive(Clone, Debug, PartialEq)]
pub struct OutputDocument {
pub kind: OutputDocumentKind,
pub value: Value,
}
impl OutputDocument {
pub fn new(kind: OutputDocumentKind, value: Value) -> Self {
Self { kind, value }
}
pub fn project_over_items(&self, items: &OutputItems) -> Self {
Self {
kind: self.kind,
value: output_items_to_value(items),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum OutputItems {
Rows(Vec<Row>),
Groups(Vec<Group>),
}
#[derive(Clone, Debug, PartialEq)]
pub struct OutputResult {
pub items: OutputItems,
pub document: Option<OutputDocument>,
pub meta: OutputMeta,
}
impl OutputResult {
pub fn from_rows(rows: Vec<Row>) -> Self {
let key_index = compute_key_index(&rows);
Self {
items: OutputItems::Rows(rows),
document: None,
meta: OutputMeta {
key_index,
column_align: Vec::new(),
wants_copy: false,
grouped: false,
render_recommendation: None,
},
}
}
#[must_use]
pub fn with_document(mut self, document: OutputDocument) -> Self {
self.document = Some(document);
self
}
pub fn as_rows(&self) -> Option<&[Row]> {
match &self.items {
OutputItems::Rows(rows) => Some(rows),
OutputItems::Groups(_) => None,
}
}
pub fn into_rows(self) -> Option<Vec<Row>> {
match self.items {
OutputItems::Rows(rows) => Some(rows),
OutputItems::Groups(_) => None,
}
}
}
pub(crate) fn output_items_to_rows(items: &OutputItems) -> Vec<Row> {
match items {
OutputItems::Rows(rows) => rows.clone(),
OutputItems::Groups(groups) => groups.iter().flat_map(group_rows).collect(),
}
}
pub(crate) fn group_rows(group: &Group) -> Vec<Row> {
if group.rows.is_empty() {
return vec![group_header_row(group)];
}
group
.rows
.iter()
.map(|row| group_member_row(group, row))
.collect()
}
pub(crate) fn group_header_row(group: &Group) -> Row {
let mut row = group.groups.clone();
for (key, value) in &group.aggregates {
row.insert(key.clone(), value.clone());
}
row
}
pub(crate) fn group_member_row(group: &Group, row: &Row) -> Row {
let mut merged = group.groups.clone();
for (key, value) in &group.aggregates {
merged.insert(key.clone(), value.clone());
}
for (key, value) in row {
merged.insert(key.clone(), value.clone());
}
merged
}
pub fn compute_key_index(rows: &[Row]) -> Vec<String> {
let mut key_index = Vec::new();
let mut seen = HashSet::new();
for row in rows {
for key in row.keys() {
if seen.insert(key.clone()) {
key_index.push(key.clone());
}
}
}
key_index
}
pub fn output_items_to_value(items: &OutputItems) -> Value {
match items {
OutputItems::Rows(rows) if rows.len() == 1 => rows
.first()
.cloned()
.map(Value::Object)
.unwrap_or_else(|| Value::Array(Vec::new())),
OutputItems::Rows(rows) => {
Value::Array(rows.iter().cloned().map(Value::Object).collect::<Vec<_>>())
}
OutputItems::Groups(groups) => Value::Array(
groups
.iter()
.map(|group| {
let mut item = Row::new();
item.insert("groups".to_string(), Value::Object(group.groups.clone()));
item.insert(
"aggregates".to_string(),
Value::Object(group.aggregates.clone()),
);
item.insert(
"rows".to_string(),
Value::Array(
group
.rows
.iter()
.cloned()
.map(Value::Object)
.collect::<Vec<_>>(),
),
);
Value::Object(item)
})
.collect::<Vec<_>>(),
),
}
}
pub fn output_items_from_value(value: Value) -> OutputItems {
match value {
Value::Array(items) => {
if let Some(groups) = groups_from_values(&items) {
OutputItems::Groups(groups)
} else if items.iter().all(|item| matches!(item, Value::Object(_))) {
OutputItems::Rows(
items
.into_iter()
.filter_map(|item| item.as_object().cloned())
.collect::<Vec<_>>(),
)
} else {
OutputItems::Rows(vec![row_with_value(Value::Array(items))])
}
}
Value::Object(map) => OutputItems::Rows(vec![map]),
scalar => OutputItems::Rows(vec![row_with_value(scalar)]),
}
}
pub fn rows_from_value(value: Value) -> Vec<Row> {
match value {
Value::Array(values) => values.into_iter().flat_map(row_items_from_value).collect(),
other => row_items_from_value(other),
}
}
fn row_items_from_value(value: Value) -> Vec<Row> {
match value {
Value::Object(map) => vec![map],
other => vec![row_with_value(other)],
}
}
fn row_with_value(value: Value) -> Row {
let mut row = Row::new();
row.insert("value".to_string(), value);
row
}
fn groups_from_values(values: &[Value]) -> Option<Vec<Group>> {
values.iter().map(group_from_value).collect()
}
fn group_from_value(value: &Value) -> Option<Group> {
let Value::Object(map) = value else {
return None;
};
let groups = map.get("groups")?.as_object()?.clone();
let aggregates = map.get("aggregates")?.as_object()?.clone();
let Value::Array(rows) = map.get("rows")? else {
return None;
};
let rows = rows
.iter()
.map(|row| row.as_object().cloned())
.collect::<Option<Vec<_>>>()?;
Some(Group {
groups,
aggregates,
rows,
})
}
#[cfg(test)]
mod tests {
use super::{
Group, OutputDocument, OutputDocumentKind, OutputItems, OutputMeta, OutputResult,
output_items_from_value, output_items_to_rows, output_items_to_value,
};
use serde_json::Value;
use serde_json::json;
#[test]
fn row_results_keep_first_seen_key_order_and_expose_row_views_unit() {
let rows = vec![
json!({"uid": "oistes", "cn": "Oistein"})
.as_object()
.cloned()
.expect("object"),
json!({"mail": "o@uio.no", "uid": "oistes", "title": "Engineer"})
.as_object()
.cloned()
.expect("object"),
];
let output = OutputResult::from_rows(rows.clone());
assert_eq!(output.meta.key_index, vec!["uid", "cn", "mail", "title"]);
assert_eq!(output.as_rows(), Some(rows.as_slice()));
assert_eq!(output.into_rows(), Some(rows));
}
#[test]
fn grouped_results_and_semantic_documents_cover_non_row_views_unit() {
let grouped_output = OutputResult {
items: OutputItems::Groups(vec![Group {
groups: json!({"team": "ops"}).as_object().cloned().expect("object"),
aggregates: json!({"count": 1}).as_object().cloned().expect("object"),
rows: vec![
json!({"user": "alice"})
.as_object()
.cloned()
.expect("object"),
],
}]),
document: None,
meta: OutputMeta::default(),
};
assert_eq!(grouped_output.as_rows(), None);
assert_eq!(grouped_output.into_rows(), None);
let document_output = OutputResult::from_rows(Vec::new()).with_document(
OutputDocument::new(OutputDocumentKind::Guide, json!({"usage": ["osp"]})),
);
assert!(matches!(
document_output.document,
Some(OutputDocument {
kind: OutputDocumentKind::Guide,
value: Value::Object(_),
})
));
}
#[test]
fn output_items_projection_round_trips_rows_and_groups_unit() {
let rows = OutputItems::Rows(vec![
json!({"uid": "alice"})
.as_object()
.cloned()
.expect("object"),
]);
let rows_value = output_items_to_value(&rows);
assert!(matches!(rows_value, Value::Object(_)));
assert_eq!(output_items_from_value(rows_value), rows);
let groups = OutputItems::Groups(vec![Group {
groups: json!({"team": "ops"}).as_object().cloned().expect("object"),
aggregates: json!({"count": 1}).as_object().cloned().expect("object"),
rows: vec![
json!({"uid": "alice"})
.as_object()
.cloned()
.expect("object"),
],
}]);
let groups_value = output_items_to_value(&groups);
assert!(matches!(groups_value, Value::Array(_)));
assert_eq!(output_items_from_value(groups_value), groups);
}
#[test]
fn output_items_to_rows_merges_group_context_once_unit() {
let items = OutputItems::Groups(vec![
Group {
groups: json!({"team": "ops"}).as_object().cloned().expect("object"),
aggregates: json!({"count": 2}).as_object().cloned().expect("object"),
rows: vec![
json!({"user": "alice"})
.as_object()
.cloned()
.expect("object"),
json!({"user": "bob"}).as_object().cloned().expect("object"),
],
},
Group {
groups: json!({"team": "dev"}).as_object().cloned().expect("object"),
aggregates: json!({"count": 0}).as_object().cloned().expect("object"),
rows: Vec::new(),
},
]);
let rows = output_items_to_rows(&items);
assert_eq!(rows.len(), 3);
assert_eq!(rows[0]["team"], "ops");
assert_eq!(rows[0]["count"], 2);
assert_eq!(rows[0]["user"], "alice");
assert_eq!(rows[1]["user"], "bob");
assert_eq!(rows[2]["team"], "dev");
assert_eq!(rows[2]["count"], 0);
assert!(rows[2].get("user").is_none());
}
}