Skip to main content

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