1use serde::Serialize;
2use serde_json::Value;
3use std::collections::BTreeMap;
4
5#[derive(Debug, Clone, Default, Serialize)]
22pub struct Output {
23 #[serde(skip_serializing_if = "Option::is_none")]
24 pub title: Option<String>,
25
26 #[serde(skip_serializing_if = "Option::is_none")]
27 pub subtitle: Option<String>,
28
29 #[serde(default, skip_serializing_if = "Vec::is_empty")]
30 pub blocks: Vec<Block>,
31
32 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
33 pub data: BTreeMap<String, Value>,
34
35 #[serde(skip_serializing_if = "Option::is_none")]
36 pub plain: Option<Value>,
37
38 #[serde(default, skip_serializing_if = "Vec::is_empty")]
39 pub jsonl_records: Vec<Value>,
40}
41
42impl Output {
43 pub fn new() -> Self {
45 Self::default()
46 }
47
48 pub fn title(mut self, value: impl Into<String>) -> Self {
50 self.title = Some(value.into());
51 self
52 }
53
54 pub fn subtitle(mut self, value: impl Into<String>) -> Self {
56 self.subtitle = Some(value.into());
57 self
58 }
59
60 pub fn data(mut self, key: impl Into<String>, value: impl Serialize) -> Self {
64 let value = serde_json::to_value(value).unwrap_or(Value::Null);
65 self.data.insert(key.into(), value);
66 self
67 }
68
69 pub fn plain(mut self, value: impl Serialize) -> Self {
73 self.plain = Some(serde_json::to_value(value).unwrap_or(Value::Null));
74 self
75 }
76
77 pub fn jsonl_record(mut self, value: impl Serialize) -> Self {
81 self.jsonl_records
82 .push(serde_json::to_value(value).unwrap_or(Value::Null));
83 self
84 }
85
86 pub fn heading(mut self, level: u8, text: impl Into<String>) -> Self {
91 self.blocks.push(Block::Heading {
92 level,
93 text: text.into(),
94 });
95 self
96 }
97
98 pub fn paragraph(mut self, text: impl Into<String>) -> Self {
100 self.blocks.push(Block::Paragraph { text: text.into() });
101 self
102 }
103
104 pub fn line(mut self, text: impl Into<String>) -> Self {
106 self.blocks.push(Block::Line { text: text.into() });
107 self
108 }
109
110 pub fn separator(mut self) -> Self {
112 self.blocks.push(Block::Separator);
113 self
114 }
115
116 pub fn list(mut self, ordered: bool, items: Vec<String>) -> Self {
120 self.blocks.push(Block::List { ordered, items });
121 self
122 }
123
124 pub fn code(mut self, language: Option<String>, code: impl Into<String>) -> Self {
129 self.blocks.push(Block::Code {
130 language,
131 code: code.into(),
132 });
133 self
134 }
135
136 pub fn table(mut self, title: Option<String>, table: Table) -> Self {
138 self.blocks.push(Block::Table { title, table });
139 self
140 }
141
142 pub fn json(mut self, value: impl Serialize) -> Self {
144 self.blocks.push(Block::Json {
145 value: serde_json::to_value(value).unwrap_or(Value::Null),
146 });
147 self
148 }
149
150 pub fn key_value(mut self, key: impl Into<String>, value: impl ToString) -> Self {
154 let entry = KeyValueEntry {
155 key: key.into(),
156 value: value.to_string(),
157 };
158
159 match self.blocks.last_mut() {
160 Some(Block::KeyValue { entries }) => entries.push(entry),
161 _ => self.blocks.push(Block::KeyValue {
162 entries: vec![entry],
163 }),
164 }
165
166 self
167 }
168
169 pub fn definition(mut self, term: impl Into<String>, description: impl Into<String>) -> Self {
173 let entry = DefinitionEntry {
174 term: term.into(),
175 description: description.into(),
176 };
177
178 match self.blocks.last_mut() {
179 Some(Block::DefinitionList { entries }) => entries.push(entry),
180 _ => self.blocks.push(Block::DefinitionList {
181 entries: vec![entry],
182 }),
183 }
184
185 self
186 }
187
188 pub fn status(mut self, kind: StatusKind, text: impl Into<String>) -> Self {
190 self.blocks.push(Block::Status {
191 kind,
192 text: text.into(),
193 });
194 self
195 }
196
197 pub fn section(
201 mut self,
202 title: impl Into<String>,
203 content: impl Into<String>,
204 language: impl Into<Option<String>>,
205 ) -> Self {
206 self.blocks.push(Block::Heading {
207 level: 2,
208 text: title.into(),
209 });
210
211 self.blocks.push(Block::Code {
212 language: language.into(),
213 code: content.into(),
214 });
215
216 self
217 }
218
219 pub fn styled_paragraph(mut self, styled: crate::output::style::Styled) -> Self {
223 self.blocks.push(Block::StyledText {
224 text: styled.text,
225 style: styled.style,
226 });
227 self
228 }
229
230 pub fn styled_heading(mut self, level: u8, styled: crate::output::style::Styled) -> Self {
234 self.blocks.push(Block::Heading {
235 level,
236 text: styled.text,
237 });
238 self
239 }
240
241 pub fn from_serializable(value: impl Serialize) -> Self {
245 let json = serde_json::to_value(value).unwrap_or(Value::Null);
246
247 match json {
248 Value::Object(map) => Self {
249 title: None,
250 subtitle: None,
251 blocks: Vec::new(),
252 data: map.into_iter().collect(),
253 plain: None,
254 jsonl_records: Vec::new(),
255 },
256 other => Self::new().data("value", other),
257 }
258 }
259}
260
261#[derive(Debug, Clone, Serialize)]
265#[serde(tag = "type", rename_all = "snake_case")]
266pub enum Block {
267 Heading { level: u8, text: String },
269 Paragraph { text: String },
271 Line { text: String },
273 Separator,
275 List { ordered: bool, items: Vec<String> },
277 Code {
279 language: Option<String>,
280 code: String,
281 },
282 Table { title: Option<String>, table: Table },
284 Json { value: Value },
286 KeyValue { entries: Vec<KeyValueEntry> },
288 DefinitionList { entries: Vec<DefinitionEntry> },
290 Status { kind: StatusKind, text: String },
292 StyledText {
294 text: String,
295 style: crate::output::style::TextStyle,
296 },
297}
298
299#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize)]
313pub enum TableLayout {
314 #[default]
318 Full,
319 Compact,
323 Stacked,
333}
334
335impl TableLayout {
336 pub fn is_full(self) -> bool {
338 matches!(self, Self::Full)
339 }
340
341 pub fn is_compact(self) -> bool {
343 matches!(self, Self::Compact)
344 }
345
346 pub fn is_stacked(self) -> bool {
348 matches!(self, Self::Stacked)
349 }
350}
351
352#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
368pub struct Table {
369 pub headers: Vec<String>,
371 pub rows: Vec<Vec<String>>,
373 #[serde(default)]
374 pub show_index: bool,
376 #[serde(default = "default_index_header")]
377 pub index_header: String,
379 #[serde(default = "default_table_layout")]
380 pub layout: TableLayout,
382}
383
384fn default_table_layout() -> TableLayout {
385 TableLayout::Full
386}
387
388fn default_index_header() -> String {
389 "#".to_string()
390}
391
392impl Table {
393 pub fn new(headers: Vec<String>, rows: Vec<Vec<String>>) -> Self {
395 Self {
396 headers,
397 rows,
398 show_index: false,
399 index_header: default_index_header(),
400 layout: default_table_layout(),
401 }
402 }
403
404 pub fn with_index(mut self) -> Self {
406 self.show_index = true;
407 self
408 }
409
410 pub fn with_index_header(mut self, value: impl Into<String>) -> Self {
412 self.show_index = true;
413 self.index_header = value.into();
414 self
415 }
416
417 pub fn with_layout(mut self, layout: TableLayout) -> Self {
419 self.layout = layout;
420 self
421 }
422
423 pub fn with_layout_full(mut self) -> Self {
425 self.layout = TableLayout::Full;
426 self
427 }
428
429 pub fn with_layout_compact(mut self) -> Self {
431 self.layout = TableLayout::Compact;
432 self
433 }
434
435 pub fn with_layout_stacked(mut self) -> Self {
437 self.layout = TableLayout::Stacked;
438 self
439 }
440
441 pub fn from_slices(headers: &[&str], rows: &[Vec<String>]) -> Self {
443 Self {
444 headers: headers.iter().map(|s| (*s).to_string()).collect(),
445 rows: rows.to_vec(),
446 show_index: false,
447 index_header: default_index_header(),
448 layout: default_table_layout(),
449 }
450 }
451
452 pub fn materialized(&self) -> Self {
454 if !self.show_index {
455 return self.clone();
456 }
457
458 let mut headers = Vec::with_capacity(self.headers.len() + 1);
459 headers.push(self.index_header.clone());
460 headers.extend(self.headers.clone());
461
462 let rows = self
463 .rows
464 .iter()
465 .enumerate()
466 .map(|(idx, row)| {
467 let mut new_row = Vec::with_capacity(row.len() + 1);
468 new_row.push((idx + 1).to_string());
469 new_row.extend(row.clone());
470 new_row
471 })
472 .collect();
473
474 Self {
475 headers,
476 rows,
477 show_index: false,
478 index_header: self.index_header.clone(),
479 layout: self.layout,
480 }
481 }
482
483 pub fn to_json_value(&self) -> Value {
485 serde_json::to_value(self.materialized()).unwrap_or(Value::Null)
486 }
487}
488
489#[derive(Debug, Clone, Serialize)]
491pub struct KeyValueEntry {
492 pub key: String,
494 pub value: String,
496}
497
498impl KeyValueEntry {
499 pub fn new(key: impl Into<String>, value: impl Into<String>) -> Self {
501 Self {
502 key: key.into(),
503 value: value.into(),
504 }
505 }
506}
507
508#[derive(Debug, Clone, Serialize)]
519pub struct DefinitionEntry {
520 pub term: String,
522 pub description: String,
524}
525
526#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
540#[serde(rename_all = "snake_case")]
541pub enum StatusKind {
542 Info,
544 Ok,
546 Warning,
548 Error,
550 #[deprecated(since = "0.2.0", note = "use `StatusKind::Ok` instead")]
552 Success,
553}