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("not_paused") =>
141 {
142 ExitCode::Conflict
143 }
144 "override_not_permitted" => ExitCode::PermissionDenied,
145 _ => ExitCode::Error,
146 }
147}
148
149#[must_use]
151pub fn create_table(headers: &[&str]) -> Table {
152 let mut table = Table::new();
153 table.set_header(headers.iter().map(|h| Cell::new(*h)));
154 table
155}
156
157pub trait TableRow {
159 fn to_row(&self) -> Vec<String>;
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 #[derive(Serialize)]
167 struct TestData {
168 name: String,
169 value: i32,
170 }
171
172 #[test]
173 fn cli_response_success_serialization() {
174 let data = TestData {
175 name: "test".to_string(),
176 value: 42,
177 };
178 let response = CliResponse::success(data);
179 let json = serde_json::to_string(&response).unwrap();
180
181 assert!(json.contains("\"success\":true"));
182 assert!(json.contains("\"name\":\"test\""));
183 }
184
185 #[test]
186 fn cli_response_error_serialization() {
187 let err =
188 HivemindError::user("invalid", "Invalid input", "cli:test").with_hint("Try again");
189 let response = CliResponse::<()>::error(&err);
190 let json = serde_json::to_string(&response).unwrap();
191
192 assert!(json.contains("\"success\":false"));
193 assert!(json.contains("\"code\":\"invalid\""));
194 }
195}