Skip to content

Commit

Permalink
[CLI] Use tabled to convert JSON to table format (#2887)
Browse files Browse the repository at this point in the history
* use tabled

* update use table

* update use table

* add json_to_table

* update

* Refactor: Remove unnecessary to_string and redundant if-else block

* update testsuite

---------

Co-authored-by: stom <[email protected]>
  • Loading branch information
baicaiyihao and stom authored Nov 14, 2024
1 parent 8d9f1d1 commit 30ef683
Show file tree
Hide file tree
Showing 10 changed files with 228 additions and 9 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/rooch-rpc-api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ hex = { workspace = true }
jsonrpsee = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
tabled = { workspace = true }
thiserror = { workspace = true }
schemars = { workspace = true }
bitcoin = { workspace = true }
Expand Down
200 changes: 200 additions & 0 deletions crates/rooch-rpc-api/src/jsonrpc_types/json_to_table_display.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
// Copyright (c) RoochNetwork
// SPDX-License-Identifier: Apache-2.0

use serde_json::Value;
use tabled::{
settings::{object::Rows, Disable, Panel, Style},
Table, Tabled,
};

// Define the `json_to_table` function to be used by external callers
pub fn json_to_table(json_value: Value) {
json_to_table_display(&json_value);
}

// Struct for a single-row table to display each line of content
#[derive(Tabled)]
struct TableRow {
line: String,
}

// Wrap long strings to specified width
fn wrap_long_text(text: &str, width: usize) -> String {
text.chars()
.collect::<Vec<_>>()
.chunks(width)
.map(|chunk| chunk.iter().collect::<String>())
.collect::<Vec<_>>()
.join("\n")
}

// Display JSON object as a table
fn display_json(value: &Value, title: &str) {
let mut rows = Vec::new();

if let Some(obj) = value.as_object() {
for (k, v) in obj {
if k == "tx_order_signature" && v.is_string() {
rows.push(TableRow {
line: format!("{}: \n{}", k, wrap_long_text(v.as_str().unwrap(), 96)),
});
} else if k == "status" && v.is_object() {
rows.push(TableRow {
line: format!("[{}]", k),
});
rows.extend(format_nested_json(v));
} else if v.is_object() || v.is_array() {
rows.push(TableRow {
line: format!("{}: [complex data]", k),
});
} else {
rows.push(TableRow {
line: format!("{}: {}", k, v),
});
}
}
}

let main_table = Table::new(&rows)
.with(Style::rounded())
.with(Panel::header(title))
.with(Disable::row(Rows::single(1)))
.to_string();

println!("{}", main_table);
}

// Display changeset table, with dynamic field extraction and nested field handling
fn display_changes(changeset: &Value) {
let mut change_rows = Vec::new();

if let Some(changes) = changeset.get("changes").and_then(|v| v.as_array()) {
for change in changes {
if let Some(obj) = change.as_object() {
for (key, value) in obj {
if key == "metadata" || key == "value" {
change_rows.push(TableRow {
line: format!("[{}]", key),
});
change_rows.extend(format_nested_json(value));
}
}
}
change_rows.push(TableRow {
line: "─────────────────────".to_string(),
});
}
}

for (key, value) in changeset.as_object().unwrap() {
if key != "changes" && !value.is_object() && !value.is_array() {
let line = format!("{}: {}", key, value);
change_rows.push(TableRow { line });
}
}

let changes_table = Table::new(&change_rows)
.with(Style::rounded())
.with(Panel::header("Changeset"))
.with(Disable::row(Rows::single(1)))
.to_string();

println!("{}", changes_table);
}

// Recursively parse nested JSON items, formatting for multi-line display
fn format_nested_json(value: &Value) -> Vec<TableRow> {
let mut rows = Vec::new();
if let Some(obj) = value.as_object() {
for (k, v) in obj {
if v.is_object() || v.is_array() {
rows.push(TableRow {
line: format!("[{}]", k),
});
rows.extend(format_nested_json(v));
} else {
let line = format!("{}: {}", k, v);
rows.push(TableRow { line });
}
}
}
rows
}

// Display events table, with dynamic field extraction and nested field handling
fn display_events(events: &Value) {
let mut event_rows = Vec::new();

for event in events.as_array().unwrap() {
if let Some(obj) = event.as_object() {
for (key, value) in obj {
if key == "event_data" {
continue;
}

if value.is_object() {
event_rows.push(TableRow {
line: format!("[{}]", key),
});
event_rows.extend(format_nested_json(value));
} else {
event_rows.push(TableRow {
line: format!("{}: {}", key, value),
});
}
}
}
event_rows.push(TableRow {
line: "─────────────────────".to_string(),
});
}

let events_table = Table::new(&event_rows)
.with(Style::rounded())
.with(Panel::header("Events"))
.with(Disable::row(Rows::single(1)))
.to_string();

println!("{}", events_table);
}

// Main parsing function for handling JSON data
pub fn json_to_table_display(value: &Value) {
let mut sequence_info_value = None;
let mut unknown_data = Vec::new();

if let Some(obj) = value.as_object() {
for (key, val) in obj {
match key.as_str() {
"sequence_info" => sequence_info_value = Some(val.clone()),
"execution_info" => display_json(val, "Execution Info"),
"output" => {
if let Some(changeset) = val.get("changeset") {
display_changes(changeset);
}
if let Some(events) = val.get("events") {
display_events(events);
}
display_json(val, "Output Status");
}
"error_info" => {
if !val.is_null() {
display_json(val, "Error Info");
}
}
_ => unknown_data.push(TableRow {
line: format!("{}: {}", key, val),
}),
}
}
}

if let Some(sequence_info) = sequence_info_value {
display_json(&sequence_info, "Sequence Info");
}

if !unknown_data.is_empty() {
let unknown_table = Table::new(&unknown_data).with(Style::rounded()).to_string();
println!("Unknown Data:\n{}", unknown_table);
}
}
1 change: 1 addition & 0 deletions crates/rooch-rpc-api/src/jsonrpc_types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ mod transaction_argument_view;
pub mod account_view;
pub mod event_view;
pub mod export_view;
pub mod json_to_table_display;
pub mod transaction_view;

pub mod address;
Expand Down
3 changes: 3 additions & 0 deletions crates/rooch/src/commands/account/commands/transfer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ pub struct TransferCommand {

#[clap(flatten)]
context: WalletContextOptions,

#[clap(long)]
pub json: bool,
}

#[async_trait]
Expand Down
15 changes: 14 additions & 1 deletion crates/rooch/src/commands/account/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ use commands::{
export::ExportCommand, import::ImportCommand, list::ListCommand, nullify::NullifyCommand,
sign::SignCommand, switch::SwitchCommand, transfer::TransferCommand, verify::VerifyCommand,
};
use rooch_rpc_api::jsonrpc_types::json_to_table_display::json_to_table;
use rooch_types::error::RoochResult;
use serde_json::Value;
use std::path::PathBuf;

pub mod commands;
Expand All @@ -34,7 +36,18 @@ impl CommandAction<String> for Account {
AccountCommand::Switch(switch) => switch.execute_serialized().await,
AccountCommand::Nullify(nullify) => nullify.execute_serialized().await,
AccountCommand::Balance(balance) => balance.execute_serialized().await,
AccountCommand::Transfer(transfer) => transfer.execute_serialized().await,
AccountCommand::Transfer(transfer) => {
let output_as_json = transfer.json;
let output = transfer.execute_serialized().await?;
if output_as_json {
Ok(output)
} else if let Ok(json_value) = serde_json::from_str::<Value>(&output) {
json_to_table(json_value);
Ok(String::new())
} else {
Ok(output)
}
}
AccountCommand::Export(export) => export.execute_serialized().await,
AccountCommand::Import(import) => import.execute_serialized().await,
AccountCommand::Sign(sign) => sign.execute_serialized().await,
Expand Down
2 changes: 1 addition & 1 deletion crates/rooch/src/commands/session_key/commands/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ pub struct ListCommand {

/// Display output as a table instead of JSON
#[clap(long)]
pub table: bool,
pub json: bool,
}

#[async_trait]
Expand Down
8 changes: 4 additions & 4 deletions crates/rooch/src/commands/session_key/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,16 @@ impl CommandAction<String> for SessionKey {
serde_json::to_string_pretty(&resp).expect("Failed to serialize response")
}),
SessionKeyCommand::List(list) => {
let display_as_table = list.table;
let display_as_json = list.json;
let json_output = list.execute_serialized().await?;
let json_value: Value =
serde_json::from_str(&json_output).expect("Failed to parse JSON");

if display_as_table {
if display_as_json {
Ok(json_output)
} else {
display_json_as_table(&json_value);
Ok(String::new())
} else {
Ok(json_output)
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions crates/testsuite/features/cmd.feature
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ Feature: Rooch CLI integration tests

# session key
Then cmd: "session-key create --app-name test --app-url https:://test.rooch.network --scope 0x3::empty::empty"
Then cmd: "session-key list"
Then cmd: "session-key list --json"
Then assert: "'{{$.session-key[-1]}}' not_contains error"
Then cmd: "move run --function 0x3::empty::empty --session-key {{$.session-key[-1][0].name}} --json"
Then assert: "{{$.move[-1].execution_info.status.type}} == executed"
Expand Down Expand Up @@ -323,7 +323,7 @@ Feature: Rooch CLI integration tests
Then assert: "'{{$.rpc[-1].coin_type}}' == '{{$.address_mapping.default}}::fixed_supply_coin::FSC'"
Then assert: "'{{$.rpc[-1].balance}}' != '0'"

Then cmd: "account transfer --coin-type default::fixed_supply_coin::FSC --to {{$.account[-1].account0.bitcoin_address}} --amount 1"
Then cmd: "account transfer --coin-type default::fixed_supply_coin::FSC --to {{$.account[-1].account0.bitcoin_address}} --amount 1 --json"

Then assert: "{{$.account[-1].execution_info.status.type}} == executed"
Then stop the server
Expand Down
2 changes: 1 addition & 1 deletion crates/testsuite/features/multisign.feature
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Feature: Rooch CLI multisign integration tests
Then assert: "{{$.account[-1].BTC.balance}} == 100000000"

#transfer some gas to multisign account
Then cmd: "account transfer --to {{$.account[-2].multisign_address}} --amount 10000000000 --coin-type rooch_framework::gas_coin::RGas"
Then cmd: "account transfer --to {{$.account[-2].multisign_address}} --amount 10000000000 --coin-type rooch_framework::gas_coin::RGas --json"
Then assert: "{{$.account[-1].execution_info.status.type}} == executed"

# l2 transaction
Expand Down

0 comments on commit 30ef683

Please sign in to comment.