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