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