use serde::Serialize;
use serde_json::Value;
use std::collections::BTreeMap;
#[derive(Debug, Clone, Default, Serialize)]
pub struct Output {
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub subtitle: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub blocks: Vec<Block>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub data: BTreeMap<String, Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub plain: Option<Value>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub jsonl_records: Vec<Value>,
}
impl Output {
pub fn new() -> Self {
Self::default()
}
pub fn title(mut self, value: impl Into<String>) -> Self {
self.title = Some(value.into());
self
}
pub fn subtitle(mut self, value: impl Into<String>) -> Self {
self.subtitle = Some(value.into());
self
}
pub fn data(mut self, key: impl Into<String>, value: impl Serialize) -> Self {
let value = serde_json::to_value(value).unwrap_or(Value::Null);
self.data.insert(key.into(), value);
self
}
pub fn plain(mut self, value: impl Serialize) -> Self {
self.plain = Some(serde_json::to_value(value).unwrap_or(Value::Null));
self
}
pub fn jsonl_record(mut self, value: impl Serialize) -> Self {
self.jsonl_records
.push(serde_json::to_value(value).unwrap_or(Value::Null));
self
}
pub fn heading(mut self, level: u8, text: impl Into<String>) -> Self {
self.blocks.push(Block::Heading {
level,
text: text.into(),
});
self
}
pub fn paragraph(mut self, text: impl Into<String>) -> Self {
self.blocks.push(Block::Paragraph { text: text.into() });
self
}
pub fn line(mut self, text: impl Into<String>) -> Self {
self.blocks.push(Block::Line { text: text.into() });
self
}
pub fn separator(mut self) -> Self {
self.blocks.push(Block::Separator);
self
}
pub fn list(mut self, ordered: bool, items: Vec<String>) -> Self {
self.blocks.push(Block::List { ordered, items });
self
}
pub fn code(mut self, language: Option<String>, code: impl Into<String>) -> Self {
self.blocks.push(Block::Code {
language,
code: code.into(),
});
self
}
pub fn table(mut self, title: Option<String>, table: Table) -> Self {
self.blocks.push(Block::Table { title, table });
self
}
pub fn json(mut self, value: impl Serialize) -> Self {
self.blocks.push(Block::Json {
value: serde_json::to_value(value).unwrap_or(Value::Null),
});
self
}
pub fn key_value(mut self, key: impl Into<String>, value: impl ToString) -> Self {
let entry = KeyValueEntry {
key: key.into(),
value: value.to_string(),
};
match self.blocks.last_mut() {
Some(Block::KeyValue { entries }) => entries.push(entry),
_ => self.blocks.push(Block::KeyValue {
entries: vec![entry],
}),
}
self
}
pub fn definition(mut self, term: impl Into<String>, description: impl Into<String>) -> Self {
let entry = DefinitionEntry {
term: term.into(),
description: description.into(),
};
match self.blocks.last_mut() {
Some(Block::DefinitionList { entries }) => entries.push(entry),
_ => self.blocks.push(Block::DefinitionList {
entries: vec![entry],
}),
}
self
}
pub fn status(mut self, kind: StatusKind, text: impl Into<String>) -> Self {
self.blocks.push(Block::Status {
kind,
text: text.into(),
});
self
}
pub fn section(
mut self,
title: impl Into<String>,
content: impl Into<String>,
language: impl Into<Option<String>>,
) -> Self {
self.blocks.push(Block::Heading {
level: 2,
text: title.into(),
});
self.blocks.push(Block::Code {
language: language.into(),
code: content.into(),
});
self
}
pub fn styled_paragraph(mut self, styled: crate::output::style::Styled) -> Self {
self.blocks.push(Block::StyledText {
text: styled.text,
style: styled.style,
});
self
}
pub fn styled_heading(mut self, level: u8, styled: crate::output::style::Styled) -> Self {
self.blocks.push(Block::Heading {
level,
text: styled.text,
});
self
}
pub fn from_serializable(value: impl Serialize) -> Self {
let json = serde_json::to_value(value).unwrap_or(Value::Null);
match json {
Value::Object(map) => Self {
title: None,
subtitle: None,
blocks: Vec::new(),
data: map.into_iter().collect(),
plain: None,
jsonl_records: Vec::new(),
},
other => Self::new().data("value", other),
}
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Block {
Heading { level: u8, text: String },
Paragraph { text: String },
Line { text: String },
Separator,
List { ordered: bool, items: Vec<String> },
Code {
language: Option<String>,
code: String,
},
Table { title: Option<String>, table: Table },
Json { value: Value },
KeyValue { entries: Vec<KeyValueEntry> },
DefinitionList { entries: Vec<DefinitionEntry> },
Status { kind: StatusKind, text: String },
StyledText {
text: String,
style: crate::output::style::TextStyle,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize)]
pub enum TableLayout {
#[default]
Full,
Compact,
Stacked,
}
impl TableLayout {
pub fn is_full(self) -> bool {
matches!(self, Self::Full)
}
pub fn is_compact(self) -> bool {
matches!(self, Self::Compact)
}
pub fn is_stacked(self) -> bool {
matches!(self, Self::Stacked)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Table {
pub headers: Vec<String>,
pub rows: Vec<Vec<String>>,
#[serde(default)]
pub show_index: bool,
#[serde(default = "default_index_header")]
pub index_header: String,
#[serde(default = "default_table_layout")]
pub layout: TableLayout,
}
fn default_table_layout() -> TableLayout {
TableLayout::Full
}
fn default_index_header() -> String {
"#".to_string()
}
impl Table {
pub fn new(headers: Vec<String>, rows: Vec<Vec<String>>) -> Self {
Self {
headers,
rows,
show_index: false,
index_header: default_index_header(),
layout: default_table_layout(),
}
}
pub fn with_index(mut self) -> Self {
self.show_index = true;
self
}
pub fn with_index_header(mut self, value: impl Into<String>) -> Self {
self.show_index = true;
self.index_header = value.into();
self
}
pub fn with_layout(mut self, layout: TableLayout) -> Self {
self.layout = layout;
self
}
pub fn with_layout_full(mut self) -> Self {
self.layout = TableLayout::Full;
self
}
pub fn with_layout_compact(mut self) -> Self {
self.layout = TableLayout::Compact;
self
}
pub fn with_layout_stacked(mut self) -> Self {
self.layout = TableLayout::Stacked;
self
}
pub fn from_slices(headers: &[&str], rows: &[Vec<String>]) -> Self {
Self {
headers: headers.iter().map(|s| (*s).to_string()).collect(),
rows: rows.to_vec(),
show_index: false,
index_header: default_index_header(),
layout: default_table_layout(),
}
}
pub fn materialized(&self) -> Self {
if !self.show_index {
return self.clone();
}
let mut headers = Vec::with_capacity(self.headers.len() + 1);
headers.push(self.index_header.clone());
headers.extend(self.headers.clone());
let rows = self
.rows
.iter()
.enumerate()
.map(|(idx, row)| {
let mut new_row = Vec::with_capacity(row.len() + 1);
new_row.push((idx + 1).to_string());
new_row.extend(row.clone());
new_row
})
.collect();
Self {
headers,
rows,
show_index: false,
index_header: self.index_header.clone(),
layout: self.layout,
}
}
pub fn to_json_value(&self) -> Value {
serde_json::to_value(self.materialized()).unwrap_or(Value::Null)
}
}
#[derive(Debug, Clone, Serialize)]
pub struct KeyValueEntry {
pub key: String,
pub value: String,
}
impl KeyValueEntry {
pub fn new(key: impl Into<String>, value: impl Into<String>) -> Self {
Self {
key: key.into(),
value: value.into(),
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct DefinitionEntry {
pub term: String,
pub description: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum StatusKind {
Info,
Ok,
Warning,
Error,
#[deprecated(since = "0.2.0", note = "use `StatusKind::Ok` instead")]
Success,
}