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