Skip to main content

osp_cli/core/
output_model.rs

1use 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}