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#[derive(clap::ValueEnum, Debug, Default, Clone, Copy, serde::Serialize, serde::Deserialize)]
14#[serde(rename_all = "kebab-case")]
15pub enum OutputFormat {
16 #[default]
18 Table,
19 Json,
21 Toml,
23}
24
25impl OutputFormat {
26 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 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 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#[derive(Debug, Clone, Default)]
157pub struct DisplayOptions {
158 pub projection: Option<IndexMap<String, String>>,
160 pub projection_table_only: bool,
162 pub extra: Map<String, Value>,
164 pub empty_message: Option<String>,
166}
167
168impl DisplayOptions {
169 pub fn table_projection(
171 keys: impl IntoIterator<Item = (impl ToString, impl ToString)>,
172 ) -> Self {
173 Self::projection(keys, true)
174 }
175
176 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 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 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
235fn 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}