mirror of
https://git.proxmox.com/git/proxmox
synced 2025-05-29 21:14:18 +00:00
797 lines
23 KiB
Rust
797 lines
23 KiB
Rust
use std::io::Write;
|
|
|
|
use anyhow::{bail, Error};
|
|
use serde_json::Value;
|
|
use unicode_width::UnicodeWidthStr;
|
|
|
|
use proxmox_lang::c_str;
|
|
use proxmox_schema::{ObjectSchemaType, Schema, SchemaPropertyEntry};
|
|
|
|
/// allows to configure the default output fromat using environment vars
|
|
pub const ENV_VAR_PROXMOX_OUTPUT_FORMAT: &str = "PROXMOX_OUTPUT_FORMAT";
|
|
/// if set, supress borders (and headers) when printing tables
|
|
pub const ENV_VAR_PROXMOX_OUTPUT_NO_BORDER: &str = "PROXMOX_OUTPUT_NO_BORDER";
|
|
/// if set, supress headers when printing tables
|
|
pub const ENV_VAR_PROXMOX_OUTPUT_NO_HEADER: &str = "PROXMOX_OUTPUT_NO_HEADER";
|
|
|
|
/// Helper to get output format from parameters or environment
|
|
pub fn get_output_format(param: &Value) -> String {
|
|
let mut output_format = None;
|
|
|
|
if let Some(format) = param["output-format"].as_str() {
|
|
output_format = Some(format.to_owned());
|
|
} else if let Ok(format) = std::env::var(ENV_VAR_PROXMOX_OUTPUT_FORMAT) {
|
|
output_format = Some(format);
|
|
}
|
|
|
|
output_format.unwrap_or_else(|| String::from("text"))
|
|
}
|
|
|
|
/// Helper to get output format from parameters or environment
|
|
/// and removing from parameters
|
|
pub fn extract_output_format(param: &mut Value) -> String {
|
|
let output_format = get_output_format(param);
|
|
|
|
if let Some(param) = param.as_object_mut() {
|
|
param.remove("output-format");
|
|
}
|
|
|
|
output_format
|
|
}
|
|
|
|
/// Helper to get TableFormatOptions with default from environment
|
|
pub fn default_table_format_options() -> TableFormatOptions {
|
|
let no_border = std::env::var(ENV_VAR_PROXMOX_OUTPUT_NO_BORDER)
|
|
.ok()
|
|
.is_some();
|
|
let no_header = std::env::var(ENV_VAR_PROXMOX_OUTPUT_NO_HEADER)
|
|
.ok()
|
|
.is_some();
|
|
|
|
TableFormatOptions::new()
|
|
.noborder(no_border)
|
|
.noheader(no_header)
|
|
}
|
|
|
|
/// Render function
|
|
///
|
|
/// Should convert the json `value` into a text string. `record` points to
|
|
/// the surrounding data object.
|
|
pub type RenderFunction =
|
|
fn(/* value: */ &Value, /* record: */ &Value) -> Result<String, Error>;
|
|
|
|
fn data_to_text(data: &Value, schema: &Schema) -> Result<String, Error> {
|
|
if data.is_null() {
|
|
return Ok(String::new());
|
|
}
|
|
|
|
match schema {
|
|
Schema::Null => {
|
|
// makes no sense to display Null columns
|
|
bail!("internal error");
|
|
}
|
|
Schema::Boolean(_) => match data.as_bool() {
|
|
Some(value) => Ok(String::from(if value { "1" } else { "0" })),
|
|
None => bail!("got unexpected data (expected bool)."),
|
|
},
|
|
Schema::Integer(_) => match data.as_i64() {
|
|
Some(value) => Ok(format!("{}", value)),
|
|
None => bail!("got unexpected data (expected integer)."),
|
|
},
|
|
Schema::Number(_) => match data.as_f64() {
|
|
Some(value) => Ok(format!("{}", value)),
|
|
None => bail!("got unexpected data (expected number)."),
|
|
},
|
|
Schema::String(_) => match data.as_str() {
|
|
Some(value) => Ok(value.to_string()),
|
|
None => bail!("got unexpected data (expected string)."),
|
|
},
|
|
Schema::Object(_) => Ok(data.to_string()),
|
|
Schema::Array(_) => Ok(data.to_string()),
|
|
Schema::AllOf(_) => Ok(data.to_string()),
|
|
}
|
|
}
|
|
|
|
struct TableBorders {
|
|
column_separator: char,
|
|
top: String,
|
|
head: String,
|
|
middle: String,
|
|
bottom: String,
|
|
}
|
|
|
|
impl TableBorders {
|
|
fn new<I>(column_widths: I, ascii_delimiters: bool) -> Self
|
|
where
|
|
I: Iterator<Item = usize>,
|
|
{
|
|
let mut top = String::new();
|
|
let mut head = String::new();
|
|
let mut middle = String::new();
|
|
let mut bottom = String::new();
|
|
|
|
let column_separator = if ascii_delimiters { '|' } else { '│' };
|
|
|
|
for (i, column_width) in column_widths.enumerate() {
|
|
if ascii_delimiters {
|
|
top.push('+');
|
|
head.push('+');
|
|
middle.push('+');
|
|
bottom.push('+');
|
|
} else if i == 0 {
|
|
top.push('┌');
|
|
head.push('╞');
|
|
middle.push('├');
|
|
bottom.push('└');
|
|
} else {
|
|
top.push('┬');
|
|
head.push('╪');
|
|
middle.push('┼');
|
|
bottom.push('┴');
|
|
}
|
|
|
|
for _j in 0..(column_width + 2) {
|
|
if ascii_delimiters {
|
|
top.push('=');
|
|
head.push('=');
|
|
middle.push('-');
|
|
bottom.push('=');
|
|
} else {
|
|
top.push('─');
|
|
head.push('═');
|
|
middle.push('─');
|
|
bottom.push('─');
|
|
}
|
|
}
|
|
}
|
|
if ascii_delimiters {
|
|
top.push('+');
|
|
head.push('+');
|
|
middle.push('+');
|
|
bottom.push('+');
|
|
} else {
|
|
top.push('┐');
|
|
head.push('╡');
|
|
middle.push('┤');
|
|
bottom.push('┘');
|
|
}
|
|
|
|
Self {
|
|
column_separator,
|
|
top,
|
|
head,
|
|
middle,
|
|
bottom,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Table Column configuration
|
|
///
|
|
/// This structure can be used to set additional rendering information for a table column.
|
|
pub struct ColumnConfig {
|
|
pub name: String,
|
|
pub header: Option<String>,
|
|
pub right_align: Option<bool>,
|
|
pub renderer: Option<RenderFunction>,
|
|
}
|
|
|
|
impl ColumnConfig {
|
|
pub fn new(name: &str) -> Self {
|
|
Self {
|
|
name: name.to_string(),
|
|
header: None,
|
|
right_align: None,
|
|
renderer: None,
|
|
}
|
|
}
|
|
|
|
pub fn right_align(mut self, right_align: bool) -> Self {
|
|
self.right_align = Some(right_align);
|
|
self
|
|
}
|
|
|
|
pub fn renderer(mut self, renderer: RenderFunction) -> Self {
|
|
self.renderer = Some(renderer);
|
|
self
|
|
}
|
|
|
|
pub fn header<S: Into<String>>(mut self, header: S) -> Self {
|
|
self.header = Some(header.into());
|
|
self
|
|
}
|
|
}
|
|
|
|
/// Get the current size of the terminal (for stdout).
|
|
/// # Safety
|
|
///
|
|
/// uses unsafe call to tty_ioctl, see man tty_ioctl(2).
|
|
fn stdout_terminal_size() -> (usize, usize) {
|
|
let mut winsize = libc::winsize {
|
|
ws_row: 0,
|
|
ws_col: 0,
|
|
ws_xpixel: 0,
|
|
ws_ypixel: 0,
|
|
};
|
|
unsafe { libc::ioctl(libc::STDOUT_FILENO, libc::TIOCGWINSZ, &mut winsize) };
|
|
(winsize.ws_row as usize, winsize.ws_col as usize)
|
|
}
|
|
|
|
/// Table formatter configuration
|
|
#[derive(Default)]
|
|
pub struct TableFormatOptions {
|
|
/// Can be used to sort after a specific columns, if it isn't set
|
|
/// we sort after the leftmost column (with no undef value in
|
|
/// $data) this can be turned off by passing and empty array. The
|
|
/// boolean argument specifies the sort order (false => ASC, true => DESC)
|
|
pub sortkeys: Option<Vec<(String, bool)>>,
|
|
/// Print without asciiart border.
|
|
pub noborder: bool,
|
|
/// Print without table header.
|
|
pub noheader: bool,
|
|
/// Limit output width.
|
|
pub columns: Option<usize>,
|
|
/// Use ascii characters for table delimiters (instead of utf8).
|
|
pub ascii_delimiters: bool,
|
|
/// Comumn configurations
|
|
pub column_config: Vec<ColumnConfig>,
|
|
}
|
|
|
|
impl TableFormatOptions {
|
|
/// Create a new Instance with reasonable defaults for terminal output
|
|
///
|
|
/// This tests if stdout is a TTY and sets the columns to the terminal width,
|
|
/// and sets ascii_delimiters to true If the locale CODESET is not UTF-8.
|
|
pub fn new() -> Self {
|
|
let mut me = Self::default();
|
|
|
|
let is_tty = unsafe { libc::isatty(libc::STDOUT_FILENO) == 1 };
|
|
|
|
if is_tty {
|
|
let (_rows, columns) = stdout_terminal_size();
|
|
if columns > 0 {
|
|
me.columns = Some(columns);
|
|
}
|
|
}
|
|
|
|
let empty_cstr = c_str!("");
|
|
|
|
use std::ffi::CStr;
|
|
let encoding = unsafe {
|
|
libc::setlocale(libc::LC_CTYPE, empty_cstr.as_ptr());
|
|
CStr::from_ptr(libc::nl_langinfo(libc::CODESET))
|
|
};
|
|
|
|
if encoding != c_str!("UTF-8") {
|
|
me.ascii_delimiters = true;
|
|
}
|
|
|
|
me
|
|
}
|
|
|
|
pub fn disable_sort(mut self) -> Self {
|
|
self.sortkeys = Some(Vec::new());
|
|
self
|
|
}
|
|
|
|
pub fn sortby<S: Into<String>>(mut self, key: S, sort_desc: bool) -> Self {
|
|
let key = key.into();
|
|
match self.sortkeys {
|
|
None => {
|
|
self.sortkeys = Some(vec![(key, sort_desc)]);
|
|
}
|
|
Some(ref mut list) => {
|
|
list.push((key, sort_desc));
|
|
}
|
|
}
|
|
self
|
|
}
|
|
|
|
pub fn noborder(mut self, noborder: bool) -> Self {
|
|
self.noborder = noborder;
|
|
self
|
|
}
|
|
|
|
pub fn noheader(mut self, noheader: bool) -> Self {
|
|
self.noheader = noheader;
|
|
self
|
|
}
|
|
|
|
pub fn ascii_delimiters(mut self, ascii_delimiters: bool) -> Self {
|
|
self.ascii_delimiters = ascii_delimiters;
|
|
self
|
|
}
|
|
|
|
pub fn columns(mut self, columns: Option<usize>) -> Self {
|
|
self.columns = columns;
|
|
self
|
|
}
|
|
|
|
pub fn column_config(mut self, column_config: Vec<ColumnConfig>) -> Self {
|
|
self.column_config = column_config;
|
|
self
|
|
}
|
|
|
|
/// Add a single column configuration
|
|
pub fn column(mut self, column_config: ColumnConfig) -> Self {
|
|
self.column_config.push(column_config);
|
|
self
|
|
}
|
|
|
|
fn lookup_column_info(
|
|
&self,
|
|
column_name: &str,
|
|
) -> (String, Option<bool>, Option<RenderFunction>) {
|
|
let mut renderer = None;
|
|
|
|
let header;
|
|
let mut right_align = None;
|
|
|
|
if let Some(column_config) = self.column_config.iter().find(|v| v.name == *column_name) {
|
|
renderer = column_config.renderer;
|
|
right_align = column_config.right_align;
|
|
if let Some(ref h) = column_config.header {
|
|
header = h.to_owned();
|
|
} else {
|
|
header = column_name.to_string();
|
|
}
|
|
} else {
|
|
header = column_name.to_string();
|
|
}
|
|
|
|
(header, right_align, renderer)
|
|
}
|
|
}
|
|
|
|
struct TableCell {
|
|
lines: Vec<String>,
|
|
}
|
|
|
|
struct TableColumn {
|
|
cells: Vec<TableCell>,
|
|
width: usize,
|
|
right_align: bool,
|
|
}
|
|
|
|
fn format_table<W: Write>(
|
|
output: W,
|
|
list: &mut Vec<Value>,
|
|
schema: &dyn ObjectSchemaType,
|
|
options: &TableFormatOptions,
|
|
) -> Result<(), Error> {
|
|
let properties_to_print = if options.column_config.is_empty() {
|
|
extract_properties_to_print(schema.properties())
|
|
} else {
|
|
options
|
|
.column_config
|
|
.iter()
|
|
.map(|v| v.name.clone())
|
|
.collect()
|
|
};
|
|
|
|
let column_count = properties_to_print.len();
|
|
if column_count == 0 {
|
|
return Ok(());
|
|
};
|
|
|
|
let sortkeys = if let Some(ref sortkeys) = options.sortkeys {
|
|
sortkeys.clone()
|
|
} else {
|
|
vec![(properties_to_print[0].clone(), false)] // leftmost, ASC
|
|
};
|
|
|
|
let mut sortinfo = Vec::new();
|
|
|
|
for (sortkey, sort_order) in sortkeys {
|
|
let (_optional, sort_prop_schema) = match schema.lookup(&sortkey) {
|
|
Some(tup) => tup,
|
|
None => bail!("property {} does not exist in schema.", sortkey),
|
|
};
|
|
let numeric_sort = matches!(sort_prop_schema, Schema::Integer(_) | Schema::Number(_));
|
|
sortinfo.push((sortkey, sort_order, numeric_sort));
|
|
}
|
|
|
|
use std::cmp::Ordering;
|
|
list.sort_unstable_by(move |a, b| {
|
|
for &(ref sortkey, sort_desc, numeric) in &sortinfo {
|
|
let res = if numeric {
|
|
let (v1, v2) = if sort_desc {
|
|
(b[&sortkey].as_f64(), a[&sortkey].as_f64())
|
|
} else {
|
|
(a[&sortkey].as_f64(), b[&sortkey].as_f64())
|
|
};
|
|
match (v1, v2) {
|
|
(None, None) => Ordering::Greater,
|
|
(Some(_), None) => Ordering::Greater,
|
|
(None, Some(_)) => Ordering::Less,
|
|
(Some(a), Some(b)) =>
|
|
{
|
|
#[allow(clippy::if_same_then_else)]
|
|
if a.is_nan() {
|
|
Ordering::Greater
|
|
} else if b.is_nan() {
|
|
Ordering::Less
|
|
} else if a < b {
|
|
Ordering::Less
|
|
} else if a > b {
|
|
Ordering::Greater
|
|
} else {
|
|
Ordering::Equal
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
let (v1, v2) = if sort_desc {
|
|
(b[sortkey].as_str(), a[sortkey].as_str())
|
|
} else {
|
|
(a[sortkey].as_str(), b[sortkey].as_str())
|
|
};
|
|
v1.cmp(&v2)
|
|
};
|
|
|
|
if res != Ordering::Equal {
|
|
return res;
|
|
}
|
|
}
|
|
Ordering::Equal
|
|
});
|
|
|
|
let mut tabledata: Vec<TableColumn> = Vec::new();
|
|
|
|
let mut column_names = Vec::new();
|
|
|
|
for name in properties_to_print.iter() {
|
|
let (_optional, prop_schema) = match schema.lookup(name) {
|
|
Some(tup) => tup,
|
|
None => bail!("property {} does not exist in schema.", name),
|
|
};
|
|
|
|
let is_numeric = matches!(
|
|
prop_schema,
|
|
Schema::Integer(_) | Schema::Number(_) | Schema::Boolean(_)
|
|
);
|
|
|
|
let (header, right_align, renderer) = options.lookup_column_info(name);
|
|
|
|
let right_align = right_align.unwrap_or(is_numeric);
|
|
|
|
let mut max_width = if options.noheader || options.noborder {
|
|
0
|
|
} else {
|
|
header.chars().count()
|
|
};
|
|
|
|
column_names.push(header);
|
|
|
|
let mut cells = Vec::new();
|
|
for entry in list.iter() {
|
|
let result = if let Some(renderer) = renderer {
|
|
(renderer)(&entry[name], entry)
|
|
} else {
|
|
data_to_text(&entry[name], prop_schema)
|
|
};
|
|
|
|
let text = match result {
|
|
Ok(text) => text,
|
|
Err(err) => bail!("unable to format property {} - {}", name, err),
|
|
};
|
|
|
|
let lines: Vec<String> = text
|
|
.lines()
|
|
.map(|line| {
|
|
let width = UnicodeWidthStr::width(line);
|
|
if width > max_width {
|
|
max_width = width;
|
|
}
|
|
line.to_string()
|
|
})
|
|
.collect();
|
|
|
|
cells.push(TableCell { lines });
|
|
}
|
|
|
|
tabledata.push(TableColumn {
|
|
cells,
|
|
width: max_width,
|
|
right_align,
|
|
});
|
|
}
|
|
|
|
render_table(output, &tabledata, &column_names, options)
|
|
}
|
|
|
|
fn render_table<W: Write>(
|
|
mut output: W,
|
|
tabledata: &[TableColumn],
|
|
column_names: &[String],
|
|
options: &TableFormatOptions,
|
|
) -> Result<(), Error> {
|
|
let mut write_line = |line: &str| -> Result<(), Error> {
|
|
if let Some(columns) = options.columns {
|
|
let line: String = line.chars().take(columns).collect();
|
|
output.write_all(line.as_bytes())?;
|
|
} else {
|
|
output.write_all(line.as_bytes())?;
|
|
}
|
|
output.write_all(b"\n")?;
|
|
Ok(())
|
|
};
|
|
|
|
let column_widths = tabledata.iter().map(|d| d.width);
|
|
let borders = TableBorders::new(column_widths, options.ascii_delimiters);
|
|
|
|
if !options.noborder {
|
|
write_line(&borders.top)?;
|
|
}
|
|
|
|
let mut header = String::new();
|
|
for (i, name) in column_names.iter().enumerate() {
|
|
let column = &tabledata[i];
|
|
header.push(borders.column_separator);
|
|
header.push(' ');
|
|
if column.right_align {
|
|
header.push_str(&format!("{:>width$}", name, width = column.width));
|
|
} else {
|
|
header.push_str(&format!("{:<width$}", name, width = column.width));
|
|
}
|
|
header.push(' ');
|
|
}
|
|
|
|
if !(options.noheader || options.noborder) {
|
|
header.push(borders.column_separator);
|
|
|
|
write_line(&header)?;
|
|
write_line(&borders.head)?;
|
|
}
|
|
|
|
let rows = tabledata[0].cells.len();
|
|
for pos in 0..rows {
|
|
let mut max_lines = 0;
|
|
for (i, _name) in column_names.iter().enumerate() {
|
|
let cells = &tabledata[i].cells;
|
|
let lines = &cells[pos].lines;
|
|
if lines.len() > max_lines {
|
|
max_lines = lines.len();
|
|
}
|
|
}
|
|
for line_nr in 0..max_lines {
|
|
let mut text = String::new();
|
|
let empty_string = String::new();
|
|
for (i, _name) in column_names.iter().enumerate() {
|
|
let column = &tabledata[i];
|
|
let lines = &column.cells[pos].lines;
|
|
let line = lines.get(line_nr).unwrap_or(&empty_string);
|
|
|
|
if options.noborder {
|
|
if i > 0 {
|
|
text.push(' ');
|
|
}
|
|
} else {
|
|
text.push(borders.column_separator);
|
|
text.push(' ');
|
|
}
|
|
|
|
let padding = column.width - UnicodeWidthStr::width(line.as_str());
|
|
if column.right_align {
|
|
text.push_str(&format!("{:>width$}{}", "", line, width = padding));
|
|
} else {
|
|
text.push_str(&format!("{}{:<width$}", line, "", width = padding));
|
|
}
|
|
|
|
if !options.noborder {
|
|
text.push(' ');
|
|
}
|
|
}
|
|
if !options.noborder {
|
|
text.push(borders.column_separator);
|
|
}
|
|
write_line(&text)?;
|
|
}
|
|
|
|
if !options.noborder {
|
|
if (pos + 1) == rows {
|
|
write_line(&borders.bottom)?;
|
|
} else {
|
|
write_line(&borders.middle)?;
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn format_object<W: Write>(
|
|
output: W,
|
|
data: &Value,
|
|
schema: &dyn ObjectSchemaType,
|
|
options: &TableFormatOptions,
|
|
) -> Result<(), Error> {
|
|
let properties_to_print = if options.column_config.is_empty() {
|
|
extract_properties_to_print(schema.properties())
|
|
} else {
|
|
options
|
|
.column_config
|
|
.iter()
|
|
.map(|v| v.name.clone())
|
|
.collect()
|
|
};
|
|
|
|
let row_count = properties_to_print.len();
|
|
if row_count == 0 {
|
|
return Ok(());
|
|
};
|
|
|
|
const NAME_TITLE: &str = "Name";
|
|
const VALUE_TITLE: &str = "Value";
|
|
|
|
let mut max_name_width = if options.noheader || options.noborder {
|
|
0
|
|
} else {
|
|
NAME_TITLE.len()
|
|
};
|
|
let mut max_value_width = if options.noheader || options.noborder {
|
|
0
|
|
} else {
|
|
VALUE_TITLE.len()
|
|
};
|
|
|
|
let column_names = vec![NAME_TITLE.to_string(), VALUE_TITLE.to_string()];
|
|
|
|
let mut name_cells = Vec::new();
|
|
let mut value_cells = Vec::new();
|
|
|
|
let mut all_right_aligned = true;
|
|
|
|
for name in properties_to_print.iter() {
|
|
let (optional, prop_schema) = match schema.lookup(name) {
|
|
Some(tup) => tup,
|
|
None => bail!("property {} does not exist in schema.", name),
|
|
};
|
|
|
|
let is_numeric = matches!(
|
|
prop_schema,
|
|
Schema::Integer(_) | Schema::Number(_) | Schema::Boolean(_)
|
|
);
|
|
|
|
let (header, right_align, renderer) = options.lookup_column_info(name);
|
|
|
|
let right_align = right_align.unwrap_or(is_numeric);
|
|
|
|
if !right_align {
|
|
all_right_aligned = false;
|
|
}
|
|
|
|
if optional {
|
|
if let Some(object) = data.as_object() {
|
|
if object.get(name).is_none() {
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
let header_width = header.chars().count();
|
|
if header_width > max_name_width {
|
|
max_name_width = header_width;
|
|
}
|
|
|
|
name_cells.push(TableCell {
|
|
lines: vec![header],
|
|
});
|
|
|
|
let result = if let Some(renderer) = renderer {
|
|
(renderer)(&data[name], data)
|
|
} else {
|
|
data_to_text(&data[name], prop_schema)
|
|
};
|
|
|
|
let text = match result {
|
|
Ok(text) => text,
|
|
Err(err) => bail!("unable to format property {} - {}", name, err),
|
|
};
|
|
|
|
let lines: Vec<String> = text
|
|
.lines()
|
|
.map(|line| {
|
|
let width = line.chars().count();
|
|
if width > max_value_width {
|
|
max_value_width = width;
|
|
}
|
|
line.to_string()
|
|
})
|
|
.collect();
|
|
|
|
value_cells.push(TableCell { lines });
|
|
}
|
|
|
|
let name_column = TableColumn {
|
|
cells: name_cells,
|
|
width: max_name_width,
|
|
right_align: false,
|
|
};
|
|
let value_column = TableColumn {
|
|
cells: value_cells,
|
|
width: max_value_width,
|
|
right_align: all_right_aligned,
|
|
};
|
|
|
|
let tabledata = vec![name_column, value_column];
|
|
|
|
render_table(output, &tabledata, &column_names, options)
|
|
}
|
|
|
|
fn extract_properties_to_print<I>(properties: I) -> Vec<String>
|
|
where
|
|
I: Iterator<Item = &'static SchemaPropertyEntry>,
|
|
{
|
|
let mut result = Vec::new();
|
|
let mut opt_properties = Vec::new();
|
|
|
|
for (name, optional, _prop_schema) in properties {
|
|
if *optional {
|
|
opt_properties.push(name.to_string());
|
|
} else {
|
|
result.push(name.to_string());
|
|
}
|
|
}
|
|
|
|
result.extend(opt_properties);
|
|
|
|
result
|
|
}
|
|
|
|
/// Format data using TableFormatOptions
|
|
pub fn value_to_text<W: Write>(
|
|
output: W,
|
|
data: &mut Value,
|
|
schema: &Schema,
|
|
options: &TableFormatOptions,
|
|
) -> Result<(), Error> {
|
|
match schema {
|
|
Schema::Null => {
|
|
if *data != Value::Null {
|
|
bail!("got unexpected data (expected null).");
|
|
}
|
|
}
|
|
Schema::Boolean(_boolean_schema) => {
|
|
unimplemented!();
|
|
}
|
|
Schema::Integer(_integer_schema) => {
|
|
unimplemented!();
|
|
}
|
|
Schema::Number(_number_schema) => {
|
|
unimplemented!();
|
|
}
|
|
Schema::String(_string_schema) => {
|
|
unimplemented!();
|
|
}
|
|
Schema::Object(object_schema) => {
|
|
format_object(output, data, object_schema, options)?;
|
|
}
|
|
Schema::Array(array_schema) => {
|
|
let list = match data.as_array_mut() {
|
|
Some(list) => list,
|
|
None => bail!("got unexpected data (expected array)."),
|
|
};
|
|
if list.is_empty() {
|
|
return Ok(());
|
|
}
|
|
|
|
match array_schema.items {
|
|
Schema::Object(object_schema) => {
|
|
format_table(output, list, object_schema, options)?;
|
|
}
|
|
Schema::AllOf(all_of_schema) => {
|
|
format_table(output, list, all_of_schema, options)?;
|
|
}
|
|
_ => {
|
|
unimplemented!();
|
|
}
|
|
}
|
|
}
|
|
Schema::AllOf(all_of_schema) => {
|
|
format_object(output, data, all_of_schema, options)?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|