use std::borrow::Borrow;
use gmsol_sdk::{programs::anchor_lang::prelude::Pubkey, serde::StringPubkey};
use indexmap::IndexMap;
use prettytable::{
format::{FormatBuilder, LinePosition, LineSeparator, TableFormat},
row, Cell, Table,
};
use serde::Serialize;
use serde_json::{Map, Value};
#[derive(clap::ValueEnum, Debug, Default, Clone, Copy, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum OutputFormat {
#[default]
Table,
Json,
Toml,
}
impl OutputFormat {
pub fn display_keyed_account(
&self,
pubkey: &Pubkey,
account: impl Serialize,
options: DisplayOptions,
) -> eyre::Result<String> {
let keyed_account = KeyedAccount {
pubkey: (*pubkey).into(),
account,
};
let Value::Object(map) = serde_json::to_value(keyed_account)? else {
eyre::bail!("internal: only map-like structures are supported");
};
let map = self.project(map, &options);
match self {
Self::Table => Self::display_table_one(&map),
Self::Json => Self::display_json_one(&map),
Self::Toml => Self::display_toml_one(&map),
}
}
pub fn display_keyed_accounts(
&self,
accounts: impl IntoIterator<Item = (impl Borrow<Pubkey>, impl Serialize)>,
options: DisplayOptions,
) -> eyre::Result<String> {
let accounts = accounts.into_iter().map(|(pubkey, account)| KeyedAccount {
pubkey: (*pubkey.borrow()).into(),
account,
});
self.display_many(accounts, options)
}
pub fn display_many(
&self,
items: impl IntoIterator<Item = impl Serialize>,
options: DisplayOptions,
) -> eyre::Result<String> {
let items = items
.into_iter()
.map(|item| {
let Value::Object(map) = serde_json::to_value(item)? else {
eyre::bail!("internal: only map-like structures are supported");
};
Ok(self.project(map, &options))
})
.collect::<eyre::Result<Vec<_>>>()?;
match self {
Self::Table => {
Self::display_table_many(&items, || options.empty_message.unwrap_or_default())
}
Self::Json => Self::display_json_many(&items),
Self::Toml => Self::display_toml_many(&items),
}
}
fn projection<'a>(&self, options: &'a DisplayOptions) -> Option<&'a IndexMap<String, String>> {
let proj = options.projection.as_ref()?;
if options.projection_table_only && !matches!(self, Self::Table) {
None
} else {
Some(proj)
}
}
fn project(&self, mut map: Map<String, Value>, options: &DisplayOptions) -> Map<String, Value> {
map.append(&mut options.extra.clone());
if let Some(proj) = self.projection(options) {
let mut flat = Map::new();
flatten_json(&map, None, &mut flat);
proj.iter()
.map(|(key, name)| (name.clone(), flat.get(key).cloned().unwrap_or(Value::Null)))
.collect()
} else {
map
}
}
fn display_json_many(items: &[Map<String, Value>]) -> eyre::Result<String> {
Ok(serde_json::to_string_pretty(items)?)
}
fn display_toml_many(items: &[Map<String, Value>]) -> eyre::Result<String> {
Ok(toml::to_string_pretty(items)?)
}
fn display_table_many(
items: &[Map<String, Value>],
empty_msg: impl FnOnce() -> String,
) -> eyre::Result<String> {
let mut items = items.iter().peekable();
let Some(first) = items.peek() else {
return Ok(empty_msg());
};
let mut table = Table::new();
table.set_format(table_format());
table.set_titles(first.keys().into());
for item in items {
table.add_row(item.values().map(json_value_to_cell).collect());
}
Ok(table.to_string())
}
fn display_json_one(item: &Map<String, Value>) -> eyre::Result<String> {
Ok(serde_json::to_string_pretty(item)?)
}
fn display_toml_one(item: &Map<String, Value>) -> eyre::Result<String> {
Ok(toml::to_string_pretty(item)?)
}
fn display_table_one(item: &Map<String, Value>) -> eyre::Result<String> {
let mut table = Table::new();
table.set_format(table_format());
table.set_titles(row!["Key", "Value"]);
for (k, v) in item {
table.add_row(row![k, json_value_to_cell(v)]);
}
Ok(table.to_string())
}
}
#[derive(Debug, Clone, Default)]
pub struct DisplayOptions {
pub projection: Option<IndexMap<String, String>>,
pub projection_table_only: bool,
pub extra: Map<String, Value>,
pub empty_message: Option<String>,
}
impl DisplayOptions {
pub fn table_projection(
keys: impl IntoIterator<Item = (impl ToString, impl ToString)>,
) -> Self {
Self::projection(keys, true)
}
pub fn projection(
keys: impl IntoIterator<Item = (impl ToString, impl ToString)>,
projection_table_only: bool,
) -> Self {
Self {
projection: Some(
keys.into_iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
),
projection_table_only,
extra: Default::default(),
empty_message: None,
}
}
pub fn add_extra(mut self, value: impl Serialize) -> eyre::Result<Self> {
let Value::Object(mut map) = serde_json::to_value(value)? else {
eyre::bail!("internal: only map-like structures are supported");
};
self.extra.append(&mut map);
Ok(self)
}
pub fn set_empty_message(mut self, message: impl ToString) -> Self {
self.empty_message = Some(message.to_string());
self
}
}
#[derive(serde::Serialize, serde::Deserialize)]
struct KeyedAccount<T> {
pubkey: StringPubkey,
#[serde(flatten)]
account: T,
}
fn table_format() -> TableFormat {
FormatBuilder::new()
.padding(0, 2)
.separator(LinePosition::Title, LineSeparator::new('-', '+', '+', '+'))
.build()
}
fn json_value_to_cell(value: &Value) -> Cell {
let content = match value {
Value::String(s) => s.clone(),
Value::Number(n) => n.to_string(),
Value::Bool(b) => b.to_string(),
Value::Null => "".to_string(),
other => other.to_string(),
};
Cell::new(&content)
}
fn flatten_json(map: &Map<String, Value>, prefix: Option<String>, out: &mut Map<String, Value>) {
for (key, value) in map {
let full_key = match &prefix {
Some(p) => format!("{p}.{key}"),
None => key.to_string(),
};
match value {
Value::Object(obj) => {
flatten_json(obj, Some(full_key), out);
}
_ => {
out.insert(full_key, value.clone());
}
}
}
}