use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;
pub use structable_derive::StructTable;
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct OutputConfig {
#[serde(default)]
pub fields: BTreeSet<String>,
#[serde(default)]
pub wide: bool,
#[serde(default)]
pub pretty: bool,
}
pub trait StructTableOptions {
fn wide_mode(&self) -> bool;
fn pretty_mode(&self) -> bool;
fn should_return_field<S: AsRef<str>>(&self, field: S, is_wide_field: bool) -> bool;
fn field_data_json_pointer<S: AsRef<str>>(&self, _field: S) -> Option<String> {
None
}
}
impl StructTableOptions for OutputConfig {
fn wide_mode(&self) -> bool {
self.wide
}
fn pretty_mode(&self) -> bool {
self.pretty
}
fn should_return_field<S: AsRef<str>>(&self, field: S, is_wide_field: bool) -> bool {
if !is_wide_field {
self.fields.is_empty()
|| self
.fields
.iter()
.any(|x| x.to_lowercase() == field.as_ref().to_lowercase())
} else {
(self.fields.is_empty() && self.wide_mode())
|| self
.fields
.iter()
.any(|x| x.to_lowercase() == field.as_ref().to_lowercase())
}
}
}
pub trait StructTable {
fn class_headers<O: StructTableOptions>(_config: &O) -> Option<Vec<String>> {
None
}
fn instance_headers<O: StructTableOptions>(&self, _config: &O) -> Option<Vec<String>> {
None
}
fn data<O: StructTableOptions>(&self, config: &O) -> Vec<Option<String>>;
fn status(&self) -> Option<String> {
None
}
}
pub fn build_table<T, O>(data: &T, options: &O) -> (Vec<String>, Vec<Vec<String>>)
where
T: StructTable,
O: StructTableOptions,
{
let headers = Vec::from(["Attribute".into(), "Value".into()]);
let mut rows: Vec<Vec<String>> = Vec::new();
let col_headers = T::class_headers(options).or_else(|| data.instance_headers(options));
if let Some(hdr) = col_headers {
for (a, v) in hdr.iter().zip(data.data(options).iter()) {
if let Some(data) = v {
rows.push(Vec::from([a.to_string(), data.to_string()]));
}
}
}
(headers, rows)
}
pub fn build_list_table<I, T, O>(data: I, options: &O) -> (Vec<String>, Vec<Vec<String>>)
where
I: Iterator<Item = T>,
T: StructTable,
O: StructTableOptions,
{
if let Some(headers) = T::class_headers(options) {
let rows: Vec<Vec<String>> = Vec::from_iter(data.map(|item| {
item.data(options)
.into_iter()
.map(|el| el.unwrap_or_else(|| String::from(" ")))
.collect::<Vec<String>>()
}));
(headers, rows)
} else {
(Vec::new(), Vec::new())
}
}
#[cfg(test)]
mod tests {
use serde_json::{json, Value};
use std::collections::BTreeMap;
use super::*;
#[derive(Default, Deserialize, Serialize, StructTable)]
struct User {
#[structable(title = "ID")]
id: u64,
first_name: String,
last_name: String,
#[structable(title = "Long", wide)]
extra: String,
#[structable(optional, serialize, wide)]
complex_data: Option<Value>,
#[structable(optional)]
dummy: Option<String>,
}
#[derive(Deserialize, Serialize, StructTable)]
struct StatusStruct {
#[structable(status)]
status: String,
}
#[derive(Clone, Deserialize, Serialize)]
enum Status {
Dummy,
}
#[derive(Deserialize, Serialize, StructTable)]
struct SerializeStatusStruct {
#[structable(serialize, status)]
status: Status,
}
#[derive(Deserialize, Serialize, StructTable)]
struct SerializeOptionStatusStruct {
#[structable(optional, serialize, status)]
status: Option<Status>,
}
#[derive(Deserialize, Serialize, StructTable)]
struct OptionStatusStruct {
#[structable(status, optional)]
status: Option<String>,
}
#[test]
fn test_single() {
let config = OutputConfig::default();
let user = User {
id: 1,
first_name: "Scooby".into(),
last_name: "Doo".into(),
extra: "XYZ".into(),
complex_data: Some(json!({"a": "b", "c": "d"})),
dummy: None,
};
assert_eq!(
build_table(&user, &config),
(
vec!["Attribute".into(), "Value".into()],
vec![
vec!["ID".into(), "1".into()],
vec!["first_name".into(), "Scooby".into()],
vec!["last_name".into(), "Doo".into()],
]
)
);
}
#[test]
fn test_single_wide() {
let config = OutputConfig {
wide: true,
..Default::default()
};
let user = User {
id: 1,
first_name: "Scooby".into(),
last_name: "Doo".into(),
extra: "XYZ".into(),
complex_data: Some(json!({"a": "b", "c": "d"})),
dummy: None,
};
assert_eq!(
build_table(&user, &config),
(
vec!["Attribute".into(), "Value".into()],
vec![
vec!["ID".into(), "1".into()],
vec!["first_name".into(), "Scooby".into()],
vec!["last_name".into(), "Doo".into()],
vec!["Long".into(), "XYZ".into()],
vec![
"complex_data".into(),
"{\"a\":\"b\",\"c\":\"d\"}".to_string()
],
]
)
);
}
#[test]
fn test_single_wide_column() {
let config = OutputConfig {
fields: BTreeSet::from(["Long".into()]),
..Default::default()
};
let user = User {
id: 1,
first_name: "Scooby".into(),
last_name: "Doo".into(),
extra: "XYZ".into(),
complex_data: Some(json!({"a": "b", "c": "d"})),
dummy: None,
};
assert_eq!(
build_table(&user, &config),
(
vec!["Attribute".into(), "Value".into()],
vec![vec!["Long".into(), "XYZ".into()],]
)
);
}
#[test]
fn test_single_wide_column_wide_mode() {
let config = OutputConfig {
fields: BTreeSet::from(["Long".into()]),
wide: true,
..Default::default()
};
let user = User {
id: 1,
first_name: "Scooby".into(),
last_name: "Doo".into(),
extra: "XYZ".into(),
complex_data: Some(json!({"a": "b", "c": "d"})),
dummy: None,
};
assert_eq!(
build_table(&user, &config),
(
vec!["Attribute".into(), "Value".into()],
vec![vec!["Long".into(), "XYZ".into()],]
)
);
}
#[test]
fn test_single_wide_pretty() {
let config = OutputConfig {
wide: true,
pretty: true,
..Default::default()
};
let user = User {
id: 1,
first_name: "Scooby".into(),
last_name: "Doo".into(),
extra: "XYZ".into(),
complex_data: Some(json!({"a": "b", "c": "d"})),
dummy: None,
};
assert_eq!(
build_table(&user, &config),
(
vec!["Attribute".into(), "Value".into()],
vec![
vec!["ID".into(), "1".into()],
vec!["first_name".into(), "Scooby".into()],
vec!["last_name".into(), "Doo".into()],
vec!["Long".into(), "XYZ".into()],
vec![
"complex_data".into(),
"{\n \"a\": \"b\",\n \"c\": \"d\"\n}".to_string()
],
]
)
);
}
#[test]
fn test_single_status() {
assert_eq!(
StatusStruct {
status: "foo".into(),
}
.status(),
Some("foo".into())
);
}
#[test]
fn test_single_no_status() {
assert_eq!(User::default().status(), None);
}
#[test]
fn test_single_option_status() {
assert_eq!(
OptionStatusStruct {
status: Some("foo".into()),
}
.status(),
Some("foo".into())
);
}
#[test]
fn test_complex_status() {
assert_eq!(
SerializeStatusStruct {
status: Status::Dummy,
}
.status(),
Some("Dummy".into())
);
assert_eq!(
SerializeOptionStatusStruct {
status: Some(Status::Dummy),
}
.status(),
Some("Dummy".into())
);
let (_, rows) = build_table(
&SerializeOptionStatusStruct {
status: Some(Status::Dummy),
},
&OutputConfig::default(),
);
assert_eq!(vec![vec!["status".to_string(), "Dummy".to_string()]], rows);
let (_, rows) = build_list_table(
[SerializeOptionStatusStruct {
status: Some(Status::Dummy),
}]
.iter(),
&OutputConfig::default(),
);
assert_eq!(vec![vec!["Dummy".to_string()]], rows);
}
#[test]
fn test_status() {
#[derive(Deserialize, Serialize, StructTable)]
struct StatusStruct {
#[structable(title = "ID")]
id: u64,
#[structable(status)]
status: String,
}
}
#[test]
fn test_list() {
let config = OutputConfig::default();
let users = vec![
User {
id: 1,
first_name: "Scooby".into(),
last_name: "Doo".into(),
extra: "Foo".into(),
complex_data: Some(json!({"a": "b", "c": "d"})),
dummy: None,
},
User {
id: 2,
first_name: "John".into(),
last_name: "Cena".into(),
extra: "Bar".into(),
complex_data: None,
dummy: None,
},
];
assert_eq!(
build_list_table(users.iter(), &config),
(
vec![
"ID".into(),
"first_name".into(),
"last_name".into(),
"dummy".into()
],
vec![
vec!["1".into(), "Scooby".into(), "Doo".into(), " ".into()],
vec!["2".into(), "John".into(), "Cena".into(), " ".into()],
]
)
);
}
#[test]
fn test_list_wide_column() {
let config = OutputConfig {
fields: BTreeSet::from(["Long".into()]),
..Default::default()
};
let users = vec![
User {
id: 1,
first_name: "Scooby".into(),
last_name: "Doo".into(),
extra: "Foo".into(),
complex_data: Some(json!({"a": "b", "c": "d"})),
dummy: None,
},
User {
id: 2,
first_name: "John".into(),
last_name: "Cena".into(),
extra: "Bar".into(),
complex_data: None,
dummy: Some("foo".into()),
},
];
assert_eq!(
build_list_table(users.iter(), &config),
(
vec!["Long".into(),],
vec![vec!["Foo".into(),], vec!["Bar".into(),],]
)
);
}
#[test]
fn test_list_wide_column_wide_mode() {
let config = OutputConfig {
fields: BTreeSet::from(["Long".into()]),
wide: true,
pretty: false,
};
let users = vec![
User {
id: 1,
first_name: "Scooby".into(),
last_name: "Doo".into(),
extra: "Foo".into(),
complex_data: Some(json!({"a": "b", "c": "d"})),
dummy: None,
},
User {
id: 2,
first_name: "John".into(),
last_name: "Cena".into(),
extra: "Bar".into(),
complex_data: None,
dummy: Some("foo".into()),
},
];
assert_eq!(
build_list_table(users.iter(), &config),
(
vec!["Long".into(),],
vec![vec!["Foo".into(),], vec!["Bar".into(),],]
)
);
}
#[test]
fn test_list_wide() {
let config = OutputConfig {
fields: BTreeSet::new(),
wide: true,
pretty: false,
};
let users = vec![
User {
id: 1,
first_name: "Scooby".into(),
last_name: "Doo".into(),
extra: "Foo".into(),
complex_data: Some(json!({"a": "b", "c": "d"})),
dummy: None,
},
User {
id: 2,
first_name: "John".into(),
last_name: "Cena".into(),
extra: "Bar".into(),
complex_data: None,
dummy: Some("foo".into()),
},
];
assert_eq!(
build_list_table(users.iter(), &config),
(
vec![
"ID".into(),
"first_name".into(),
"last_name".into(),
"Long".into(),
"complex_data".into(),
"dummy".into()
],
vec![
vec![
"1".into(),
"Scooby".into(),
"Doo".into(),
"Foo".into(),
"{\"a\":\"b\",\"c\":\"d\"}".to_string(),
" ".to_string()
],
vec![
"2".into(),
"John".into(),
"Cena".into(),
"Bar".into(),
" ".to_string(),
"foo".into()
],
]
)
);
}
#[test]
fn test_deser() {
#[derive(Deserialize, Serialize, StructTable)]
struct Foo {
#[structable(title = "ID")]
id: u64,
#[structable(optional)]
foo: Option<String>,
#[structable(optional)]
bar: Option<bool>,
}
let foo: Foo = serde_json::from_value(json!({"id": 1})).expect("Foo object");
assert_eq!(
build_table(&foo, &OutputConfig::default()),
(
vec!["Attribute".into(), "Value".into()],
vec![vec!["ID".into(), "1".into()],]
)
);
}
#[test]
fn test_output_config() {
let config = OutputConfig {
fields: BTreeSet::from(["Foo".into(), "bAr".into(), "BAZ".into(), "a:b-c".into()]),
..Default::default()
};
assert!(config.should_return_field("Foo", false));
assert!(config.should_return_field("FOO", false));
assert!(config.should_return_field("bar", false));
assert!(config.should_return_field("baz", false));
assert!(config.should_return_field("a:b-c", false));
}
#[test]
fn test_instance_headers() {
struct Sot(BTreeMap<String, String>);
impl StructTable for Sot {
fn instance_headers<O: StructTableOptions>(&self, _config: &O) -> Option<Vec<String>> {
Some(self.0.keys().map(Into::into).collect())
}
fn data<O: StructTableOptions>(&self, _config: &O) -> Vec<Option<String>> {
Vec::from_iter(self.0.values().map(|x| Some(x.to_string())))
}
}
let sot = Sot(BTreeMap::from([
("a".into(), "1".into()),
("b".into(), "2".into()),
("c".into(), "3".into()),
]));
assert_eq!(
build_table(&sot, &OutputConfig::default()),
(
vec!["Attribute".into(), "Value".into()],
vec![
vec!["a".into(), "1".into()],
vec!["b".into(), "2".into()],
vec!["c".into(), "3".into()]
]
)
);
}
#[test]
fn test_json_pointer() {
struct CustomConfig {
jp: Option<String>,
}
impl StructTableOptions for CustomConfig {
fn wide_mode(&self) -> bool {
true
}
fn pretty_mode(&self) -> bool {
true
}
fn should_return_field<S: AsRef<str>>(&self, _field: S, _is_wide_field: bool) -> bool {
true
}
fn field_data_json_pointer<S: AsRef<str>>(&self, _field: S) -> Option<String> {
self.jp.clone()
}
}
#[derive(StructTable)]
struct Data {
#[structable(serialize)]
a: Value,
#[structable(optional, serialize)]
b: Option<Value>,
#[structable(serialize)]
c: NestedData,
}
#[derive(Clone, Deserialize, Serialize)]
struct NestedData {
b: NestedData2,
}
#[derive(Clone, Deserialize, Serialize)]
struct NestedData2 {
c: String,
}
let config = CustomConfig {
jp: Some("/b/c".to_string()),
};
let sot = Data {
a: json!({"b": {"c": "d", "e": "f"}}),
b: Some(json!({"b": {"c": "x", "e": "f"}})),
c: NestedData {
b: NestedData2 { c: "x".to_string() },
},
};
assert_eq!(
build_table(&sot, &config),
(
vec!["Attribute".to_string(), "Value".to_string()],
vec![
vec!["a".to_string(), "d".to_string()],
vec!["b".to_string(), "x".to_string()],
vec!["c".to_string(), "x".to_string()],
]
),
);
}
}