1use crate::core::error::{ErrorCategory, ExitCode, HivemindError};
6use comfy_table::{Cell, Table};
7use serde::Serialize;
8
9#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, clap::ValueEnum)]
11pub enum OutputFormat {
12 #[default]
14 Table,
15 Json,
17 Yaml,
19}
20
21#[derive(Debug, Serialize)]
23pub struct CliResponse<T> {
24 pub success: bool,
25 #[serde(skip_serializing_if = "Option::is_none")]
26 pub data: Option<T>,
27 #[serde(skip_serializing_if = "Option::is_none")]
28 pub error: Option<ErrorOutput>,
29}
30
31#[derive(Debug, Serialize)]
33pub struct ErrorOutput {
34 pub category: String,
35 pub code: String,
36 pub message: String,
37 pub origin: String,
38 #[serde(skip_serializing_if = "Option::is_none")]
39 pub hint: Option<String>,
40}
41
42impl From<&HivemindError> for ErrorOutput {
43 fn from(err: &HivemindError) -> Self {
44 Self {
45 category: err.category.to_string(),
46 code: err.code.clone(),
47 message: err.message.clone(),
48 origin: err.origin.clone(),
49 hint: err.recovery_hint.clone(),
50 }
51 }
52}
53
54impl<T: Serialize> CliResponse<T> {
55 pub fn success(data: T) -> Self {
57 Self {
58 success: true,
59 data: Some(data),
60 error: None,
61 }
62 }
63
64 pub fn error(err: &HivemindError) -> CliResponse<()> {
66 CliResponse {
67 success: false,
68 data: None,
69 error: Some(ErrorOutput::from(err)),
70 }
71 }
72}
73
74pub fn output<T: Serialize>(data: T, format: OutputFormat) -> std::io::Result<()> {
76 match format {
77 OutputFormat::Json => {
78 let response = CliResponse::success(data);
79 println!("{}", serde_json::to_string_pretty(&response)?);
80 }
81 OutputFormat::Table => {
82 println!("{}", serde_json::to_string_pretty(&data)?);
83 }
84 OutputFormat::Yaml => {
85 let response = CliResponse::success(data);
86 if let Ok(yaml) = serde_yaml::to_string(&response) {
87 print!("{yaml}");
88 }
89 }
90 }
91 Ok(())
92}
93
94pub fn output_error(err: &HivemindError, format: OutputFormat) -> ExitCode {
96 match format {
97 OutputFormat::Json => {
98 let response = CliResponse::<()>::error(err);
99 if let Ok(json) = serde_json::to_string(&response) {
100 eprintln!("{json}");
101 }
102 }
103 OutputFormat::Yaml => {
104 let response = CliResponse::<()>::error(err);
105 if let Ok(yaml) = serde_yaml::to_string(&response) {
106 eprint!("{yaml}");
107 }
108 }
109 OutputFormat::Table => {
110 eprintln!("Error: {err}");
111 if let Some(hint) = &err.recovery_hint {
112 eprintln!("Hint: {hint}");
113 }
114
115 let response = CliResponse::<()>::error(err);
116 if let Ok(json) = serde_json::to_string(&response) {
117 eprintln!("{json}");
118 }
119 }
120 }
121 error_to_exit_code(err)
122}
123
124fn error_to_exit_code(err: &HivemindError) -> ExitCode {
126 if matches!(err.category, ErrorCategory::Scope) {
127 return ExitCode::PermissionDenied;
128 }
129
130 match err.code.as_str() {
131 c if c.contains("not_found") => ExitCode::NotFound,
132 c if c.contains("conflict")
133 || c.contains("immutable")
134 || c.contains("in_use")
135 || c.contains("in_active_flow")
136 || c.contains("already_attached")
137 || c.contains("already_terminal")
138 || c.contains("already_running")
139 || c.contains("not_running")
140 || c.contains("requires_force")
141 || c.contains("not_paused") =>
142 {
143 ExitCode::Conflict
144 }
145 "override_not_permitted" => ExitCode::PermissionDenied,
146 _ => ExitCode::Error,
147 }
148}
149
150#[must_use]
152pub fn create_table(headers: &[&str]) -> Table {
153 let mut table = Table::new();
154 table.set_header(headers.iter().map(|h| Cell::new(*h)));
155 table
156}
157
158pub trait TableRow {
160 fn to_row(&self) -> Vec<String>;
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 #[derive(Serialize)]
168 struct TestData {
169 name: String,
170 value: i32,
171 }
172
173 #[test]
174 fn cli_response_success_serialization() {
175 let data = TestData {
176 name: "test".to_string(),
177 value: 42,
178 };
179 let response = CliResponse::success(data);
180 let json = serde_json::to_string(&response).unwrap();
181
182 assert!(json.contains("\"success\":true"));
183 assert!(json.contains("\"name\":\"test\""));
184 }
185
186 #[test]
187 fn cli_response_error_serialization() {
188 let err =
189 HivemindError::user("invalid", "Invalid input", "cli:test").with_hint("Try again");
190 let response = CliResponse::<()>::error(&err);
191 let json = serde_json::to_string(&response).unwrap();
192
193 assert!(json.contains("\"success\":false"));
194 assert!(json.contains("\"code\":\"invalid\""));
195 }
196}