Skip to main content

systemprompt_cli/shared/
command_result.rs

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    // JSON: open-ended renderer hint bag; keys are renderer-defined and not known at compile time
36    #[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}