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