zhiburt / tabled

An easy to use library for pretty print tables of Rust structs and enums.
MIT License
2.01k stars 84 forks source link

Json Table to a normal Table? #342

Closed stefan-mysten closed 1 year ago

stefan-mysten commented 1 year ago

Hi @zhiburt

Love the library, this is really a fantastic work that you and others have done.

I have a few questions on how to taylor the style of JSON tables using the json-table crate.

Thanks in advance!

zhiburt commented 1 year ago

Hi @stefan-mysten

You have touched a hard topic :)

In summary:

  1. because currently it's not clear how to index recursive json, you can't do changes to the content. (though maybe we shall).
  2. because currently it's not clear how to generally do overall width control it's not implemented (though maybe we shall; see bellow).

My main struggle is that I have a table where my first column is the header for each row, and 2nd column has the values for each row. One row has 360 characters, and I'd like to wrap it (maybe 80 chars per line?), because the table gets really weird.

Could you provide your table? Cause sounds like it's has relatively simple structure and you might better just really convert it into tables::Table.

I think we could provide such a function for a not recursive json (unfortunately yet it's not implemented, but could be a good first issue).

  • is there a way to transform a JsonTable type into a tabled::Table type?

No. Though it would be possible to convert it into PoolTable (it is not yet in implemented; but it also does not support Wrap).

  • how can one taylor the style of a JsonTable using the standard methods from Table such as Width, Row, Wrap, etc. Doing something like: table.with(Width::wrap(120)); where table is a JsonTable yields this error
the trait `TableOption<EmptyRecords, CompleteDimension<'static>, ColoredConfig>` is not implemented for `tabled::settings::width::Wrap`

You can't. The main reason why it was not implemented is because it's not clear how to do that. I mean some people would like to do the width control differently so it was not implemented.


I am not sure if you're aware of https://github.com/nushell/nushell/. But bellow I've provide an example to do something similar.

The example is a bit long and quite complex but.... it does it's job I do think. You can just copy&paste and try with your json. Let me know what do you think.


use serde_json::Value;
use tabled::{
    grid::dimension::{DimensionPriority, PoolTableDimension},
    grid::util::string::{string_width, string_width_multiline},
    settings::style::Style,
    settings::width::Wrap,
    tables::{PoolTable, TableValue},
    Table,
};

fn main() {
    let json = serde_json::json!({
        "key1": "value1",
        "key2": {
            "key1": 123,
            "key2": [1, 2, 3, 4, 5],
        },
        "key3": [
            {"key": 123.3},
            2,
            "asd"
        ],
    });

    let table = build_table(json, 20);

    println!("{table}");
}

fn build_table(json: Value, width: usize) -> PoolTable {
    let mut value = json_to_table(json);

    truncate_table_value(&mut value, true, width);

    let mut table = PoolTable::from(value);
    table.with(PoolTableDimension::new(
        DimensionPriority::Last,
        DimensionPriority::Last,
    ));
    table.with(Style::modern());

    table
}

fn json_to_table(json: Value) -> TableValue {
    match json {
        Value::Array(list) => {
            let values = list.into_iter().map(|value| json_to_table(value)).collect();
            TableValue::Row(values)
        }
        Value::Object(map) => {
            let values = map
                .into_iter()
                .map(|(key, value)| {
                    let key = TableValue::Cell(key);
                    let value = json_to_table(value);
                    TableValue::Row(vec![key, value])
                })
                .collect();
            TableValue::Column(values)
        }
        value => TableValue::Cell(value.to_string()),
    }
}

fn truncate_table_value(
    value: &mut TableValue,
    has_vertical: bool,
    available: usize,
) -> Option<usize> {
    const MIN_CONTENT_WIDTH: usize = 10;
    const TRUNCATE_CELL_WIDTH: usize = 3;
    const PAD: usize = 2;

    match value {
        TableValue::Row(row) => {
            if row.is_empty() {
                return Some(PAD);
            }

            if row.len() == 1 {
                return truncate_table_value(&mut row[0], has_vertical, available);
            }

            let count_cells = row.len();
            let mut row_width = 0;
            let mut i = 0;
            let mut last_used_width = 0;
            for cell in row.iter_mut() {
                let vertical = (has_vertical && i + 1 != count_cells) as usize;
                if available < row_width + vertical {
                    break;
                }

                let available = available - row_width - vertical;
                let width = match truncate_table_value(cell, has_vertical, available) {
                    Some(width) => width,
                    None => break,
                };

                row_width += width + vertical;
                last_used_width = row_width;
                i += 1;
            }

            if i == row.len() {
                return Some(row_width);
            }

            if i == 0 {
                if available >= PAD + TRUNCATE_CELL_WIDTH {
                    *value = TableValue::Cell(String::from("..."));
                    return Some(PAD + TRUNCATE_CELL_WIDTH);
                } else {
                    return None;
                }
            }

            let available = available - row_width;
            let has_space_empty_cell = available >= PAD + TRUNCATE_CELL_WIDTH;
            if has_space_empty_cell {
                row[i] = TableValue::Cell(String::from("..."));
                row.truncate(i + 1);
                row_width += PAD + TRUNCATE_CELL_WIDTH;
            } else if i == 0 {
                return None;
            } else {
                row[i - 1] = TableValue::Cell(String::from("..."));
                row.truncate(i);
                row_width -= last_used_width;
                row_width += PAD + TRUNCATE_CELL_WIDTH;
            }

            Some(row_width)
        }
        TableValue::Column(column) => {
            let mut max_width = PAD;
            for cell in column.iter_mut() {
                let width = truncate_table_value(cell, has_vertical, available)?;
                max_width = std::cmp::max(max_width, width);
            }

            Some(max_width)
        }
        TableValue::Cell(text) => {
            if available <= PAD {
                return None;
            }

            let available = available - PAD;
            let width = string_width(text);

            if width > available {
                if available > MIN_CONTENT_WIDTH {
                    *text = string_wrap(text, available);
                    Some(available + PAD)
                } else if available >= 3 {
                    *text = String::from("...");
                    Some(3 + PAD)
                } else {
                    // situation where we have too little space
                    None
                }
            } else {
                Some(width + PAD)
            }
        }
    }
}

fn increase_width(text: &str, width: usize) -> String {
    text.lines()
        .map(|line| {
            let line_width = string_width(line);
            let rest_width = width - line_width;
            let mut line = line.to_string();
            line.extend((0..rest_width).map(|_| ' '));
            line
        })
        .collect::<Vec<_>>()
        .join("\n")
}

fn string_wrap(text: &str, width: usize) -> String {
    Wrap::wrap_text(text, width, false)
}
┌──────┬────────────┐
│ key1 │ "value1"   │
├──────┼──────┬─────┤
│ key2 │ key1 │ 123 │
│      ├──────┼─────┤
│      │ key2 │ ... │
├──────┼──────┴─────┤
│ key3 │ ...        │
└──────┴────────────┘

┌──────┬────────────────────────┐
│ key1 │ "value1"               │
├──────┼──────┬─────────────────┤
│ key2 │ key1 │ 123             │
│      ├──────┼───┬───┬───┬─────┤
│      │ key2 │ 1 │ 2 │ 3 │ ... │
├──────┼─────┬┴───┴──┬┴──┬┴─────┤
│ key3 │ key │ 123.3 │ 2 │ ...  │
└──────┴─────┴───────┴───┴──────┘
stefan-mysten commented 1 year ago

Thanks @zhiburt for the detailed explanation. I know nushell, used to work on it in the past. I know they have a nice nu-table crate, but it feels a bit too complex now and thought I could go with something simpler.

There are structs that have nested structs, so I understand the issue with the recursive json.

Here's my table

Screenshot 2023-06-21 at 4 49 14 PM

Here's an equivalent of that in JSON format.

{
  "suiAddress": "0x90f3e6d73b5730f16974f4df1d3441394ebae62186baf83608599f226455afa7",
  "rawTxData": "AAACACDIdthIYsGLxKX2T+y/C2wRDyHou3ADyZRHB3GrjKrcyQEAYTnJWTQ4/2sVfz1ZiEQvuZRaXYLm1G2hTxoGQvWpmEUMAAAAAAAAACB1WzYQcNy1l8/25289FS3t42iXEz+84jGDHHdjNxeLbAEBAQEBAAEAAJDz5tc7VzDxaXT03x00QTlOuuYhhrr4NghZnyJkVa+nAWE5yVk0OP9rFX89WYhEL7mUWl2C5tRtoU8aBkL1qZhFDAAAAAAAAAAgdVs2EHDctZfP9udvPRUt7eNolxM/vOIxgxx3YzcXi2yQ8+bXO1cw8Wl09N8dNEE5TrrmIYa6+DYIWZ8iZFWvp+gDAAAAAAAAAMqaOwAAAAAA",
  "intent": {
    "scope": 0,
    "version": 0,
    "app_id": 0
  },
  "rawIntentMsg": "AAAAAAACACDIdthIYsGLxKX2T+y/C2wRDyHou3ADyZRHB3GrjKrcyQEAYTnJWTQ4/2sVfz1ZiEQvuZRaXYLm1G2hTxoGQvWpmEUMAAAAAAAAACB1WzYQcNy1l8/25289FS3t42iXEz+84jGDHHdjNxeLbAEBAQEBAAEAAJDz5tc7VzDxaXT03x00QTlOuuYhhrr4NghZnyJkVa+nAWE5yVk0OP9rFX89WYhEL7mUWl2C5tRtoU8aBkL1qZhFDAAAAAAAAAAgdVs2EHDctZfP9udvPRUt7eNolxM/vOIxgxx3YzcXi2yQ8+bXO1cw8Wl09N8dNEE5TrrmIYa6+DYIWZ8iZFWvp+gDAAAAAAAAAMqaOwAAAAAA",
  "digest": "TENCFuVcNXDih1VYx5WsBFiRvCRZpfUvzV9tdoexJwc=",
  "suiSignature": "ADIEI66nM+vOJ17ucV+K57PoJSw0D2X2ykptSSnxmjdvshPzQce9Re/3p+DTKeeyGgdkqFL2DTQ9Qpe8Hk3VVQyVTIgDvKm1lnHdsZEu82DvfnF2grmahEY40NJEWv9tpQ=="
}
zhiburt commented 1 year ago

I've done some steps forward;

You can try either approach you like; must work.

It's not yet published so you need to include tabled from github.

tabled = { git = "https://github.com/zhiburt/tabled/", rev = "e449317a1c02eb6b29e409ad6617e5d9eb7b3bd4" }
use tabled::settings::{Modify, Style, Width};

fn main() {
    let json = serde_json::json!({
        "key1": "Some loooooooooooong string",
        "key2": {
            "key1": 123,
            "key2": [1, 2, 3, 4, 5],
        },
        "key3": [
            {"key": 123.3},
            2,
            "asd"
        ],
    });

    let mut table = json_to_table::parse(&json);
    table.with(Modify::new((0, 1)).with(Width::wrap(6)));
    table.with(Style::ascii_rounded());

    println!("{table}");
}
.----------------------------------------.
| key1 | Some l                          |
|      | oooooo                          |
|      | oooooo                          |
|      | ng str                          |
|      | ing                             |
| key2 | {"key1":123,"key2":[1,2,3,4,5]} |
| key3 | [{"key":123.3},2,"asd"]         |
'----------------------------------------'
use json_to_table::Orientation;
use tabled::settings::{Modify, Style, Width};

fn main() {
    let json = serde_json::json!({
        "key1": "Some loooooooooooong string",
        "key2": {
            "key1": 123,
            "key2": [1, 2, 3, 4, 5],
        },
        "key3": [
            {"key": 123.3},
            2,
            "asd"
        ],
    });

    let mut table = json_to_table::json_to_table(&json)
        .with(Style::dots())
        .array_orientation(Orientation::Row)
        .into_table();
    table.with(Style::ascii_rounded());
    table.with(Modify::new((0, 1)).with(Width::wrap(6)));

    println!("{table}");
}
.---------------------------------------------------.
| key1 | Some l                                     |
|      | oooooo                                     |
|      | oooooo                                     |
|      | ng str                                     |
|      | ing                                        |
| key2 | .......................................... |
|      | : key1 :  123                            : |
|      | :......:.................................: |
|      | : key2 : ............................... : |
|      | :      : :  1  :  2  :  3  :  4  :  5  : : |
|      | :      : :.....:.....:.....:.....:.....: : |
|      | :......:.................................: |
| key3 | ...................................        |
|      | : ................. :  2  :  asd  :        |
|      | : : key :  123.3  : :     :       :        |
|      | : :.....:.........: :     :       :        |
|      | :...................:.....:.......:        |
'---------------------------------------------------'
stefan-mysten commented 1 year ago

You're amazing @zhiburt, thanks a lot! I will give it a spin asap.

stefan-mysten commented 1 year ago

I've been trying to play with this but somehow I don't manage to make it work. Maybe I am doing something wrong, but basically I copy pasted the example and I get an error. @zhiburt Any hints what am I doing wrong? Toml file

[dependencies]
serde_json = "*"
json_to_table = { git = "https://github.com/zhiburt/tabled/", rev = "e449317a1c02eb6b29e409ad6617e5d9eb7b3bd4"}
tabled = { git = "https://github.com/zhiburt/tabled/", rev = "e449317a1c02eb6b29e409ad6617e5d9eb7b3bd4", features = ["color"] }

Main file

use tabled::settings::{Modify, Style, Width};

fn main() {
    let json = serde_json::json!({
        "key1": "Some loooooooooooong string",
        "key2": {
            "key1": 123,
            "key2": [1, 2, 3, 4, 5],
        },
        "key3": [
            {"key": 123.3},
            2,
            "asd"
        ],
    });

    let mut table = json_to_table::parse(&json);
    table.with(Modify::new((0, 1)).with(Width::wrap(6)));
    table.with(Style::ascii_rounded());

    println!("{table}");
}

Error:

   Compiling tabled-test v0.1.0 
   error[E0277]: the trait bound `ModifyList<({integer}, {integer}), Wrap>: tabled::settings::table_option::TableOption<VecRecords<CellInfo<std::string::String>>, tabled::grid::dimension::complete_dimension_vec_records::CompleteDimensionVecRecords<'static>, tabled::grid::colored_config::ColoredConfig>` is not satisfied
   --> src/main.rs:18:16
    |
18  |     table.with(Modify::new((0, 1)).with(Width::wrap(6)));
    |           ---- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `tabled::settings::table_option::TableOption<VecRecords<CellInfo<std::string::String>>, tabled::grid::dimension::complete_dimension_vec_records::CompleteDimensionVecRecords<'static>, tabled::grid::colored_config::ColoredConfig>` is not implemented for `ModifyList<({integer}, {integer}), Wrap>`
    |           |
    |           required by a bound introduced by this call
    |
    = help: the following other types implement trait `tabled::settings::table_option::TableOption<R, D, C>`:
              <&SpannedConfig as tabled::settings::table_option::TableOption<R, D, tabled::grid::colored_config::ColoredConfig>>
              <&[T] as tabled::settings::table_option::TableOption<R, D, C>>
              <&tabled::settings::style::raw_style::RawStyle as tabled::settings::table_option::TableOption<R, D, tabled::grid::colored_config::ColoredConfig>>
              <CompactConfig as tabled::settings::table_option::TableOption<R, D, CompactConfig>>
              <CompactConfig as tabled::settings::table_option::TableOption<R, D, tabled::grid::colored_config::ColoredConfig>>
              <SpannedConfig as tabled::settings::table_option::TableOption<R, D, tabled::grid::colored_config::ColoredConfig>>
              <Vec<T> as tabled::settings::table_option::TableOption<R, D, C>>
              <tabled::grid::colored_config::ColoredConfig as tabled::settings::table_option::TableOption<R, D, tabled::grid::colored_config::ColoredConfig>>
            and 61 others
note: required by a bound in `tabled::tables::table::Table::with`
   --> /.cargo/registry/src/index.crates.io-6f17d22bba15001f/tabled-0.12.2/src/tables/table.rs:180:12
    |
180 |           O: TableOption<
    |  ____________^
181 | |             VecRecords<CellInfo<String>>,
182 | |             CompleteDimensionVecRecords<'static>,
183 | |             ColoredConfig,
184 | |         >,
    | |_________^ required by this bound in `Table::with`

error[E0277]: the trait bound `Style<On, On, On, On, (), On>: tabled::settings::table_option::TableOption<VecRecords<CellInfo<std::string::String>>, tabled::grid::dimension::complete_dimension_vec_records::CompleteDimensionVecRecords<'static>, tabled::grid::colored_config::ColoredConfig>` is not satisfied
   --> src/main.rs:19:16
    |
19  |     table.with(Style::ascii_rounded());
    |           ---- ^^^^^^^^^^^^^^^^^^^^^^ the trait `tabled::settings::table_option::TableOption<VecRecords<CellInfo<std::string::String>>, tabled::grid::dimension::complete_dimension_vec_records::CompleteDimensionVecRecords<'static>, tabled::grid::colored_config::ColoredConfig>` is not implemented for `Style<On, On, On, On, (), On>`
    |           |
    |           required by a bound introduced by this call
    |
    = help: the following other types implement trait `tabled::settings::table_option::TableOption<R, D, C>`:
              <&SpannedConfig as tabled::settings::table_option::TableOption<R, D, tabled::grid::colored_config::ColoredConfig>>
              <&[T] as tabled::settings::table_option::TableOption<R, D, C>>
              <&tabled::settings::style::raw_style::RawStyle as tabled::settings::table_option::TableOption<R, D, tabled::grid::colored_config::ColoredConfig>>
              <CompactConfig as tabled::settings::table_option::TableOption<R, D, CompactConfig>>
              <CompactConfig as tabled::settings::table_option::TableOption<R, D, tabled::grid::colored_config::ColoredConfig>>
              <SpannedConfig as tabled::settings::table_option::TableOption<R, D, tabled::grid::colored_config::ColoredConfig>>
              <Vec<T> as tabled::settings::table_option::TableOption<R, D, C>>
              <tabled::grid::colored_config::ColoredConfig as tabled::settings::table_option::TableOption<R, D, tabled::grid::colored_config::ColoredConfig>>
            and 61 others
note: required by a bound in `tabled::tables::table::Table::with`
   --> /.cargo/registry/src/index.crates.io-6f17d22bba15001f/tabled-0.12.2/src/tables/table.rs:180:12
    |
180 |           O: TableOption<
    |  ____________^
181 | |             VecRecords<CellInfo<String>>,
182 | |             CompleteDimensionVecRecords<'static>,
183 | |             ColoredConfig,
184 | |         >,
    | |_________^ required by this bound in `Table::with`

For more information about this error, try `rustc --explain E0277`.
error: could not compile `tabled-test` (bin "tabled-test") due to 2 previous errors
zhiburt commented 1 year ago

I am not an expert but seems like cargo treats git dependencies as a different kind compared from the registry (even if they would be on the same commit?). You just need to include general tabled.

[dependencies]
serde_json = "*"
json_to_table = { git = "https://github.com/zhiburt/tabled/", rev = "e449317a1c02eb6b29e409ad6617e5d9eb7b3bd4"}
tabled = "0.12"

Works for me

stefan-mysten commented 1 year ago

That worked, thanks a lot!

zhiburt commented 1 year ago

Hi @gngpp

Hmmm, to be honest I don't remember why it was not published. I'll to do it soon.

Take care. Thanks for notion.

zhiburt commented 1 year ago

Hi @gngpp

Yeeee sorry got distracted quite a bit....

I believe it was released yesterday (version 0.6.0).

zhiburt commented 1 year ago

Notice that you shall use tabled 0.12 but not higher.

[dependencies]
json_to_table = "0.6.0"
serde_json = "1"
tabled = "0.12"

PS: I guess we probably shall bump it to the latest 0.14 PS2: I guess it would be WAY better to resolve it completely by relaying on a minumum minor version but... I am not sure whether it's safe.