1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
6#[serde(rename_all = "snake_case")]
7pub enum ArtifactType {
8 Table,
9 List,
10 PresentationCard,
11 Text,
12 CopyPasteText,
13 Chart,
14 Form,
15 Dashboard,
16}
17
18#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
19#[serde(rename_all = "snake_case")]
20pub enum ChartType {
21 Bar,
22 Line,
23 Pie,
24 Area,
25}
26
27#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
28pub struct RenderingHints {
29 #[serde(skip_serializing_if = "Option::is_none")]
30 pub columns: Option<Vec<String>>,
31 #[serde(skip_serializing_if = "Option::is_none")]
32 pub chart_type: Option<ChartType>,
33 #[serde(skip_serializing_if = "Option::is_none")]
34 pub theme: Option<String>,
35 #[serde(flatten)]
36 pub extra: HashMap<String, serde_json::Value>,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
40pub struct CommandResult<T> {
41 pub data: T,
42 pub artifact_type: ArtifactType,
43 #[serde(skip_serializing_if = "Option::is_none")]
44 pub title: Option<String>,
45 #[serde(skip_serializing_if = "Option::is_none")]
46 pub hints: Option<RenderingHints>,
47 #[serde(skip)]
48 skip_render: bool,
49}
50
51impl<T> CommandResult<T> {
52 const fn new(data: T, artifact_type: ArtifactType) -> Self {
53 Self {
54 data,
55 artifact_type,
56 title: None,
57 hints: None,
58 skip_render: false,
59 }
60 }
61
62 pub const fn should_skip_render(&self) -> bool {
63 self.skip_render
64 }
65
66 pub const fn with_skip_render(mut self) -> Self {
67 self.skip_render = true;
68 self
69 }
70
71 pub const fn table(data: T) -> Self {
72 Self::new(data, ArtifactType::Table)
73 }
74
75 pub const fn list(data: T) -> Self {
76 Self::new(data, ArtifactType::List)
77 }
78
79 pub const fn card(data: T) -> Self {
80 Self::new(data, ArtifactType::PresentationCard)
81 }
82
83 pub const fn text(data: T) -> Self {
84 Self::new(data, ArtifactType::Text)
85 }
86
87 pub const fn copy_paste(data: T) -> Self {
88 Self::new(data, ArtifactType::CopyPasteText)
89 }
90
91 pub fn chart(data: T, chart_type: ChartType) -> Self {
92 let mut result = Self::new(data, ArtifactType::Chart);
93 result.hints = Some(RenderingHints {
94 chart_type: Some(chart_type),
95 ..Default::default()
96 });
97 result
98 }
99
100 pub const fn form(data: T) -> Self {
101 Self::new(data, ArtifactType::Form)
102 }
103
104 pub const fn dashboard(data: T) -> Self {
105 Self::new(data, ArtifactType::Dashboard)
106 }
107
108 pub fn with_title(mut self, title: impl Into<String>) -> Self {
109 self.title = Some(title.into());
110 self
111 }
112
113 pub fn with_hints(mut self, hints: RenderingHints) -> Self {
114 self.hints = Some(hints);
115 self
116 }
117
118 pub fn with_columns(mut self, columns: Vec<String>) -> Self {
119 let mut hints = self.hints.unwrap_or_else(RenderingHints::default);
120 hints.columns = Some(columns);
121 self.hints = Some(hints);
122 self
123 }
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
127pub struct TextOutput {
128 pub message: String,
129}
130
131impl TextOutput {
132 pub fn new(message: impl Into<String>) -> Self {
133 Self {
134 message: message.into(),
135 }
136 }
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
140pub struct SuccessOutput {
141 pub message: String,
142 #[serde(skip_serializing_if = "Option::is_none")]
143 pub details: Option<Vec<String>>,
144}
145
146impl SuccessOutput {
147 pub fn new(message: impl Into<String>) -> Self {
148 Self {
149 message: message.into(),
150 details: None,
151 }
152 }
153
154 pub fn with_details(mut self, details: Vec<String>) -> Self {
155 self.details = Some(details);
156 self
157 }
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
161pub struct KeyValueOutput {
162 pub items: Vec<KeyValueItem>,
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
166pub struct KeyValueItem {
167 pub key: String,
168 pub value: String,
169}
170
171impl KeyValueOutput {
172 pub const fn new() -> Self {
173 Self { items: Vec::new() }
174 }
175
176 pub fn add(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
177 self.items.push(KeyValueItem {
178 key: key.into(),
179 value: value.into(),
180 });
181 self
182 }
183}
184
185impl Default for KeyValueOutput {
186 fn default() -> Self {
187 Self::new()
188 }
189}
190
191#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
192pub struct TableOutput<T> {
193 pub rows: Vec<T>,
194}
195
196impl<T> TableOutput<T> {
197 pub const fn new(rows: Vec<T>) -> Self {
198 Self { rows }
199 }
200}
201
202impl<T> Default for TableOutput<T> {
203 fn default() -> Self {
204 Self { rows: Vec::new() }
205 }
206}
207
208use crate::cli_settings::{get_global_config, OutputFormat};
209use systemprompt_logging::CliService;
210
211pub fn render_result<T: Serialize>(result: &CommandResult<T>) {
212 if result.should_skip_render() {
213 return;
214 }
215
216 let config = get_global_config();
217
218 match config.output_format() {
219 OutputFormat::Json => {
220 CliService::json(result);
221 },
222 OutputFormat::Yaml => {
223 CliService::yaml(result);
224 },
225 OutputFormat::Table => {
226 if let Some(title) = &result.title {
227 CliService::section(title);
228 }
229 CliService::json(&result.data);
230 },
231 }
232}