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}
22
23impl OutputFormat {
24 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 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 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#[derive(Debug, Clone, Default)]
145pub struct DisplayOptions {
146 pub projection: Option<IndexMap<String, String>>,
148 pub projection_table_only: bool,
150 pub extra: Map<String, Value>,
152 pub empty_message: Option<String>,
154}
155
156impl DisplayOptions {
157 pub fn table_projection(
159 keys: impl IntoIterator<Item = (impl ToString, impl ToString)>,
160 ) -> Self {
161 Self::projection(keys, true)
162 }
163
164 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 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 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
223fn 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}