osp_cli/core/
output_model.rs1use crate::core::row::Row;
2use std::collections::HashSet;
3
4#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
5pub enum ColumnAlignment {
6 #[default]
7 Default,
8 Left,
9 Center,
10 Right,
11}
12
13#[derive(Clone, Debug, PartialEq)]
14pub struct Group {
15 pub groups: Row,
16 pub aggregates: Row,
17 pub rows: Vec<Row>,
18}
19
20#[derive(Clone, Debug, Default, PartialEq, Eq)]
21pub struct OutputMeta {
22 pub key_index: Vec<String>,
23 pub column_align: Vec<ColumnAlignment>,
24 pub wants_copy: bool,
25 pub grouped: bool,
26}
27
28#[derive(Clone, Debug, PartialEq)]
29pub enum OutputItems {
30 Rows(Vec<Row>),
31 Groups(Vec<Group>),
32}
33
34#[derive(Clone, Debug, PartialEq)]
35pub struct OutputResult {
36 pub items: OutputItems,
37 pub meta: OutputMeta,
38}
39
40impl OutputResult {
41 pub fn from_rows(rows: Vec<Row>) -> Self {
42 let key_index = compute_key_index(&rows);
43 Self {
44 items: OutputItems::Rows(rows),
45 meta: OutputMeta {
46 key_index,
47 column_align: Vec::new(),
48 wants_copy: false,
49 grouped: false,
50 },
51 }
52 }
53
54 pub fn as_rows(&self) -> Option<&[Row]> {
55 match &self.items {
56 OutputItems::Rows(rows) => Some(rows),
57 OutputItems::Groups(_) => None,
58 }
59 }
60
61 pub fn into_rows(self) -> Option<Vec<Row>> {
62 match self.items {
63 OutputItems::Rows(rows) => Some(rows),
64 OutputItems::Groups(_) => None,
65 }
66 }
67}
68
69pub fn compute_key_index(rows: &[Row]) -> Vec<String> {
70 let mut key_index = Vec::new();
71 let mut seen = HashSet::new();
72
73 for row in rows {
74 for key in row.keys() {
75 if seen.insert(key.clone()) {
76 key_index.push(key.clone());
77 }
78 }
79 }
80
81 key_index
82}
83
84#[cfg(test)]
85mod tests {
86 use super::{Group, OutputItems, OutputMeta, OutputResult};
87 use serde_json::json;
88
89 #[test]
90 fn from_rows_keeps_first_seen_key_order() {
91 let rows = vec![
92 json!({"uid": "oistes", "cn": "Oistein"})
93 .as_object()
94 .cloned()
95 .expect("object"),
96 json!({"mail": "o@uio.no", "uid": "oistes", "title": "Engineer"})
97 .as_object()
98 .cloned()
99 .expect("object"),
100 ];
101
102 let output = OutputResult::from_rows(rows);
103 assert_eq!(output.meta.key_index, vec!["uid", "cn", "mail", "title"]);
104 }
105
106 #[test]
107 fn grouped_output_does_not_expose_rows_views() {
108 let output = OutputResult {
109 items: OutputItems::Groups(vec![Group {
110 groups: json!({"team": "ops"}).as_object().cloned().expect("object"),
111 aggregates: json!({"count": 1}).as_object().cloned().expect("object"),
112 rows: vec![
113 json!({"user": "alice"})
114 .as_object()
115 .cloned()
116 .expect("object"),
117 ],
118 }]),
119 meta: OutputMeta::default(),
120 };
121
122 assert_eq!(output.as_rows(), None);
123 assert_eq!(output.into_rows(), None);
124 }
125
126 #[test]
127 fn row_output_exposes_rows_views() {
128 let rows = vec![
129 json!({"uid": "alice"})
130 .as_object()
131 .cloned()
132 .expect("object"),
133 ];
134 let output = OutputResult::from_rows(rows.clone());
135
136 assert_eq!(output.as_rows(), Some(rows.as_slice()));
137 assert_eq!(output.into_rows(), Some(rows));
138 }
139}