1use crate::cache::models::CachedCommand;
8use crate::cli::OutputFormat;
9use crate::constants;
10use crate::engine::executor::apply_jq_filter;
11use crate::error::Error;
12use crate::invocation::ExecutionResult;
13use crate::utils::to_kebab_case;
14use serde_json::Value;
15use std::collections::BTreeMap;
16use std::fmt::Write;
17use tabled::Table;
18
19const MAX_TABLE_ROWS: usize = 1000;
21
22#[derive(tabled::Tabled)]
24struct TableRow {
25 #[tabled(rename = "Key")]
26 key: String,
27 #[tabled(rename = "Value")]
28 value: String,
29}
30
31#[derive(tabled::Tabled)]
32struct KeyValue {
33 #[tabled(rename = "Key")]
34 key: String,
35 #[tabled(rename = "Value")]
36 value: String,
37}
38
39pub fn render_result(
45 result: &ExecutionResult,
46 format: &OutputFormat,
47 jq_filter: Option<&str>,
48) -> Result<(), Error> {
49 match result {
50 ExecutionResult::Success { body, .. } | ExecutionResult::Cached { body } => {
51 if body.is_empty() {
52 return Ok(());
53 }
54 format_and_print(body, format, jq_filter, false)?;
55 }
56 ExecutionResult::DryRun { request_info } => {
57 let output = serde_json::to_string_pretty(request_info).map_err(|e| {
58 Error::serialization_error(format!("Failed to serialize dry run info: {e}"))
59 })?;
60 println!("{output}");
62 }
63 ExecutionResult::Empty => {}
64 }
65 Ok(())
66}
67
68pub fn render_result_to_string(
76 result: &ExecutionResult,
77 format: &OutputFormat,
78 jq_filter: Option<&str>,
79) -> Result<Option<String>, Error> {
80 match result {
81 ExecutionResult::Success { body, .. } | ExecutionResult::Cached { body } => {
82 if body.is_empty() {
83 return Ok(None);
84 }
85 format_and_print(body, format, jq_filter, true)
86 }
87 ExecutionResult::DryRun { request_info } => {
88 let output = serde_json::to_string_pretty(request_info).map_err(|e| {
89 Error::serialization_error(format!("Failed to serialize dry run info: {e}"))
90 })?;
91 Ok(Some(output))
92 }
93 ExecutionResult::Empty => Ok(None),
94 }
95}
96
97pub fn render_examples(operation: &CachedCommand) {
99 println!("Command: {}\n", to_kebab_case(&operation.operation_id));
101
102 if let Some(ref summary) = operation.summary {
103 println!("Description: {summary}\n");
105 }
106
107 println!("Method: {} {}\n", operation.method, operation.path);
109
110 if operation.examples.is_empty() {
111 println!("No examples available for this command.");
113 return;
114 }
115
116 println!("Examples:\n");
118 for (i, example) in operation.examples.iter().enumerate() {
119 println!("{}. {}", i + 1, example.description);
121 println!(" {}", example.command_line);
123 if let Some(ref explanation) = example.explanation {
124 println!(" {explanation}");
126 }
127 println!();
129 }
130
131 if operation.parameters.is_empty() {
133 return;
134 }
135
136 println!("Parameters:");
138 for param in &operation.parameters {
139 let required = if param.required { " (required)" } else { "" };
140 let param_type = param.schema_type.as_deref().unwrap_or("string");
141 println!(" --{}{} [{}]", param.name, required, param_type);
143
144 let Some(ref desc) = param.description else {
145 continue;
146 };
147 println!(" {desc}");
149 }
150 println!();
152
153 if operation.request_body.is_some() {
154 println!("Request Body:");
156 println!(" --body JSON (required)");
158 println!(" JSON data to send in the request body");
160 }
161}
162
163fn format_and_print(
167 response_text: &str,
168 output_format: &OutputFormat,
169 jq_filter: Option<&str>,
170 capture_output: bool,
171) -> Result<Option<String>, Error> {
172 let processed_text = if let Some(filter) = jq_filter {
174 apply_jq_filter(response_text, filter)?
175 } else {
176 response_text.to_string()
177 };
178
179 match output_format {
180 OutputFormat::Json => {
181 let output = serde_json::from_str::<Value>(&processed_text)
182 .ok()
183 .and_then(|json_value| serde_json::to_string_pretty(&json_value).ok())
184 .unwrap_or_else(|| processed_text.clone());
185
186 if capture_output {
187 return Ok(Some(output));
188 }
189 println!("{output}");
191 }
192 OutputFormat::Yaml => {
193 let output = serde_json::from_str::<Value>(&processed_text)
194 .ok()
195 .and_then(|json_value| serde_yaml::to_string(&json_value).ok())
196 .unwrap_or_else(|| processed_text.clone());
197
198 if capture_output {
199 return Ok(Some(output));
200 }
201 println!("{output}");
203 }
204 OutputFormat::Table => {
205 let Ok(json_value) = serde_json::from_str::<Value>(&processed_text) else {
206 if capture_output {
207 return Ok(Some(processed_text));
208 }
209 println!("{processed_text}");
211 return Ok(None);
212 };
213
214 let table_output = print_as_table(&json_value, capture_output)?;
215 if capture_output {
216 return Ok(table_output);
217 }
218 }
219 }
220
221 Ok(None)
222}
223
224fn print_numbered_list(items: &[Value], capture_output: bool) -> Option<String> {
226 if capture_output {
227 let mut output = String::new();
228 for (i, item) in items.iter().enumerate() {
229 writeln!(&mut output, "{}: {}", i, format_value_for_table(item))
230 .expect("writing to String cannot fail");
231 }
232 return Some(output.trim_end().to_string());
233 }
234
235 for (i, item) in items.iter().enumerate() {
236 println!("{}: {}", i, format_value_for_table(item));
238 }
239 None
240}
241
242fn output_or_capture(message: &str, capture_output: bool) -> Option<String> {
244 if capture_output {
245 return Some(message.to_string());
246 }
247 println!("{message}");
249 None
250}
251
252#[allow(clippy::unnecessary_wraps, clippy::too_many_lines)]
254fn print_as_table(json_value: &Value, capture_output: bool) -> Result<Option<String>, Error> {
255 match json_value {
256 Value::Array(items) => {
257 if items.is_empty() {
258 return Ok(output_or_capture(constants::EMPTY_ARRAY, capture_output));
259 }
260
261 if items.len() > MAX_TABLE_ROWS {
262 let msg = format!(
263 "Array too large: {} items (max {} for table display)\nUse --format json or --jq to process the full data",
264 items.len(),
265 MAX_TABLE_ROWS
266 );
267 return Ok(output_or_capture(&msg, capture_output));
268 }
269
270 let Some(Value::Object(_)) = items.first() else {
271 return Ok(print_numbered_list(items, capture_output));
272 };
273
274 let mut table_data: Vec<BTreeMap<String, String>> = Vec::new();
275
276 for item in items {
277 let Value::Object(obj) = item else {
278 continue;
279 };
280 let mut row = BTreeMap::new();
281 for (key, value) in obj {
282 row.insert(key.clone(), format_value_for_table(value));
283 }
284 table_data.push(row);
285 }
286
287 if table_data.is_empty() {
288 return Ok(print_numbered_list(items, capture_output));
289 }
290
291 let mut rows = Vec::new();
292 for (i, row) in table_data.iter().enumerate() {
293 if i > 0 {
294 rows.push(TableRow {
295 key: "---".to_string(),
296 value: "---".to_string(),
297 });
298 }
299 for (key, value) in row {
300 rows.push(TableRow {
301 key: key.clone(),
302 value: value.clone(),
303 });
304 }
305 }
306
307 let table = Table::new(&rows);
308 Ok(output_or_capture(&table.to_string(), capture_output))
309 }
310 Value::Object(obj) => {
311 if obj.len() > MAX_TABLE_ROWS {
312 let msg = format!(
313 "Object too large: {} fields (max {} for table display)\nUse --format json or --jq to process the full data",
314 obj.len(),
315 MAX_TABLE_ROWS
316 );
317 return Ok(output_or_capture(&msg, capture_output));
318 }
319
320 let rows: Vec<KeyValue> = obj
321 .iter()
322 .map(|(key, value)| KeyValue {
323 key: key.clone(),
324 value: format_value_for_table(value),
325 })
326 .collect();
327
328 let table = Table::new(&rows);
329 Ok(output_or_capture(&table.to_string(), capture_output))
330 }
331 _ => {
332 let formatted = format_value_for_table(json_value);
333 Ok(output_or_capture(&formatted, capture_output))
334 }
335 }
336}
337
338fn format_value_for_table(value: &Value) -> String {
340 match value {
341 Value::Null => constants::NULL_VALUE.to_string(),
342 Value::Bool(b) => b.to_string(),
343 Value::Number(n) => n.to_string(),
344 Value::String(s) => s.clone(),
345 Value::Array(arr) => {
346 if arr.len() <= 3 {
347 format!(
348 "[{}]",
349 arr.iter()
350 .map(format_value_for_table)
351 .collect::<Vec<_>>()
352 .join(", ")
353 )
354 } else {
355 format!("[{} items]", arr.len())
356 }
357 }
358 Value::Object(obj) => {
359 if obj.len() <= 2 {
360 format!(
361 "{{{}}}",
362 obj.iter()
363 .map(|(k, v)| format!("{}: {}", k, format_value_for_table(v)))
364 .collect::<Vec<_>>()
365 .join(", ")
366 )
367 } else {
368 format!("{{object with {} fields}}", obj.len())
369 }
370 }
371 }
372}