gmsol_cli/config/
output.rs

1use std::borrow::Borrow;
2
3use gmsol_sdk::{programs::anchor_lang::prelude::Pubkey, serde::StringPubkey};
4use indexmap::IndexMap;
5use prettytable::{
6    format::{FormatBuilder, LinePosition, LineSeparator, TableFormat},
7    row, Cell, Table,
8};
9use serde::Serialize;
10use serde_json::{Map, Value};
11
12/// Output format.
13#[derive(clap::ValueEnum, Debug, Default, Clone, Copy, serde::Serialize, serde::Deserialize)]
14#[serde(rename_all = "kebab-case")]
15pub enum OutputFormat {
16    /// Table.
17    #[default]
18    Table,
19    /// JSON.
20    Json,
21}
22
23impl OutputFormat {
24    /// Display keyed account.
25    pub fn display_keyed_account(
26        &self,
27        pubkey: &Pubkey,
28        account: impl Serialize,
29        options: DisplayOptions,
30    ) -> eyre::Result<String> {
31        let keyed_account = KeyedAccount {
32            pubkey: (*pubkey).into(),
33            account,
34        };
35        let Value::Object(map) = serde_json::to_value(keyed_account)? else {
36            eyre::bail!("internal: only map-like structures are supported");
37        };
38        let map = self.project(map, &options);
39        match self {
40            Self::Json => Self::display_json_one(&map),
41            Self::Table => Self::display_table_one(&map),
42        }
43    }
44
45    /// Display keyed accounts.
46    pub fn display_keyed_accounts(
47        &self,
48        accounts: impl IntoIterator<Item = (impl Borrow<Pubkey>, impl Serialize)>,
49        options: DisplayOptions,
50    ) -> eyre::Result<String> {
51        let accounts = accounts.into_iter().map(|(pubkey, account)| KeyedAccount {
52            pubkey: (*pubkey.borrow()).into(),
53            account,
54        });
55        self.display_many(accounts, options)
56    }
57
58    /// Display a list of serializable items.
59    pub fn display_many(
60        &self,
61        items: impl IntoIterator<Item = impl Serialize>,
62        options: DisplayOptions,
63    ) -> eyre::Result<String> {
64        let items = items
65            .into_iter()
66            .map(|item| {
67                let Value::Object(map) = serde_json::to_value(item)? else {
68                    eyre::bail!("internal: only map-like structures are supported");
69                };
70                Ok(self.project(map, &options))
71            })
72            .collect::<eyre::Result<Vec<_>>>()?;
73        match self {
74            Self::Json => Self::display_json_many(&items),
75            Self::Table => {
76                Self::display_table_many(&items, || options.empty_message.unwrap_or_default())
77            }
78        }
79    }
80
81    fn projection<'a>(&self, options: &'a DisplayOptions) -> Option<&'a IndexMap<String, String>> {
82        let proj = options.projection.as_ref()?;
83        if options.projection_table_only && matches!(self, Self::Json) {
84            None
85        } else {
86            Some(proj)
87        }
88    }
89
90    fn project(&self, mut map: Map<String, Value>, options: &DisplayOptions) -> Map<String, Value> {
91        map.append(&mut options.extra.clone());
92        if let Some(proj) = self.projection(options) {
93            let mut flat = Map::new();
94            flatten_json(&map, None, &mut flat);
95            proj.iter()
96                .map(|(key, name)| (name.clone(), flat.get(key).cloned().unwrap_or(Value::Null)))
97                .collect()
98        } else {
99            map
100        }
101    }
102
103    fn display_json_many(items: &[Map<String, Value>]) -> eyre::Result<String> {
104        Ok(serde_json::to_string_pretty(items)?)
105    }
106
107    fn display_table_many(
108        items: &[Map<String, Value>],
109        empty_msg: impl FnOnce() -> String,
110    ) -> eyre::Result<String> {
111        let mut items = items.iter().peekable();
112        let Some(first) = items.peek() else {
113            return Ok(empty_msg());
114        };
115        let mut table = Table::new();
116        table.set_format(table_format());
117        table.set_titles(first.keys().into());
118
119        for item in items {
120            table.add_row(item.values().map(json_value_to_cell).collect());
121        }
122
123        Ok(table.to_string())
124    }
125
126    fn display_json_one(item: &Map<String, Value>) -> eyre::Result<String> {
127        Ok(serde_json::to_string_pretty(item)?)
128    }
129
130    fn display_table_one(item: &Map<String, Value>) -> eyre::Result<String> {
131        let mut table = Table::new();
132        table.set_format(table_format());
133        table.set_titles(row!["Key", "Value"]);
134
135        for (k, v) in item {
136            table.add_row(row![k, json_value_to_cell(v)]);
137        }
138
139        Ok(table.to_string())
140    }
141}
142
143/// Display options.
144#[derive(Debug, Clone, Default)]
145pub struct DisplayOptions {
146    /// An ordered list of keys indicating which parts of the map should be used (i.e., a projection).
147    pub projection: Option<IndexMap<String, String>>,
148    /// Whether projection should be applied only when the format is `table`.
149    pub projection_table_only: bool,
150    /// Extra fields.
151    pub extra: Map<String, Value>,
152    /// Empty message.
153    pub empty_message: Option<String>,
154}
155
156impl DisplayOptions {
157    /// Create a projection for table format only.
158    pub fn table_projection(
159        keys: impl IntoIterator<Item = (impl ToString, impl ToString)>,
160    ) -> Self {
161        Self::projection(keys, true)
162    }
163
164    /// Create a projection.
165    pub fn projection(
166        keys: impl IntoIterator<Item = (impl ToString, impl ToString)>,
167        projection_table_only: bool,
168    ) -> Self {
169        Self {
170            projection: Some(
171                keys.into_iter()
172                    .map(|(k, v)| (k.to_string(), v.to_string()))
173                    .collect(),
174            ),
175            projection_table_only,
176            extra: Default::default(),
177            empty_message: None,
178        }
179    }
180
181    /// Add extra fields.
182    pub fn add_extra(mut self, value: impl Serialize) -> eyre::Result<Self> {
183        let Value::Object(mut map) = serde_json::to_value(value)? else {
184            eyre::bail!("internal: only map-like structures are supported");
185        };
186        self.extra.append(&mut map);
187        Ok(self)
188    }
189
190    /// Set message to display when the table is empty.
191    pub fn set_empty_message(mut self, message: impl ToString) -> Self {
192        self.empty_message = Some(message.to_string());
193        self
194    }
195}
196
197#[derive(serde::Serialize, serde::Deserialize)]
198struct KeyedAccount<T> {
199    pubkey: StringPubkey,
200    #[serde(flatten)]
201    account: T,
202}
203
204fn table_format() -> TableFormat {
205    FormatBuilder::new()
206        .padding(0, 2)
207        .separator(LinePosition::Title, LineSeparator::new('-', '+', '+', '+'))
208        .build()
209}
210
211fn json_value_to_cell(value: &Value) -> Cell {
212    let content = match value {
213        Value::String(s) => s.clone(),
214        Value::Number(n) => n.to_string(),
215        Value::Bool(b) => b.to_string(),
216        Value::Null => "".to_string(),
217        other => other.to_string(),
218    };
219
220    Cell::new(&content)
221}
222
223/// Flatten a nested JSON object into a flat map with `_`-joined keys.
224fn flatten_json(map: &Map<String, Value>, prefix: Option<String>, out: &mut Map<String, Value>) {
225    for (key, value) in map {
226        let full_key = match &prefix {
227            Some(p) => format!("{}.{}", p, key),
228            None => key.to_string(),
229        };
230
231        match value {
232            Value::Object(obj) => {
233                flatten_json(obj, Some(full_key), out);
234            }
235            _ => {
236                out.insert(full_key, value.clone());
237            }
238        }
239    }
240}