1#![allow(dead_code)]
7
8use chrono::{DateTime, Utc};
9use serde::{Deserialize, Serialize};
10use std::process;
11
12pub mod exit_codes {
14 pub const SUCCESS: i32 = 0;
15 pub const GENERAL_ERROR: i32 = 1;
16 pub const INVALID_ARGUMENTS: i32 = 2;
17 pub const NETWORK_ERROR: i32 = 3;
18 pub const AUTH_ERROR: i32 = 4;
19 pub const VALIDATION_ERROR: i32 = 5;
20 pub const NOT_FOUND: i32 = 6;
21 pub const TIMEOUT: i32 = 7;
22 pub const PARTIAL_SUCCESS: i32 = 10; }
24
25pub mod env_vars {
27 pub const DB_PATH: &str = "MRAPIDS_DB_PATH";
28 pub const CONFIG_PATH: &str = "MRAPIDS_CONFIG_PATH";
29 pub const SPEC_PATH: &str = "MRAPIDS_SPEC_PATH";
30 pub const OUTPUT_FORMAT: &str = "MRAPIDS_OUTPUT";
31 pub const AUTH_TOKEN: &str = "MRAPIDS_AUTH_TOKEN";
32 pub const BASE_URL: &str = "MRAPIDS_BASE_URL";
33 pub const LOG_LEVEL: &str = "MRAPIDS_LOG_LEVEL";
34 pub const NO_COLOR: &str = "MRAPIDS_NO_COLOR";
35 pub const MACHINE_MODE: &str = "MRAPIDS_MACHINE";
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct ResponseEnvelope<T: Serialize> {
41 pub success: bool,
43
44 pub command: String,
46
47 #[serde(skip_serializing_if = "Option::is_none")]
49 pub data: Option<T>,
50
51 pub metadata: ResponseMetadata,
53
54 #[serde(skip_serializing_if = "Vec::is_empty")]
56 pub errors: Vec<ErrorDetail>,
57
58 #[serde(skip_serializing_if = "Vec::is_empty")]
60 pub warnings: Vec<String>,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct ResponseMetadata {
66 #[serde(skip_serializing_if = "Option::is_none")]
68 pub run_id: Option<String>,
69
70 #[serde(skip_serializing_if = "Option::is_none")]
72 pub request_id: Option<String>,
73
74 #[serde(skip_serializing_if = "Option::is_none")]
76 pub duration_ms: Option<f64>,
77
78 pub version: String,
80
81 pub timestamp: DateTime<Utc>,
83
84 pub exit_code: i32,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct ErrorDetail {
91 pub code: String,
93
94 pub message: String,
96
97 #[serde(skip_serializing_if = "Option::is_none")]
99 pub context: Option<String>,
100
101 #[serde(skip_serializing_if = "Option::is_none")]
103 pub suggestion: Option<String>,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct RunResponse {
109 pub operation: String,
111
112 pub method: String,
114
115 pub url: String,
117
118 pub status_code: u16,
120
121 #[serde(skip_serializing_if = "Option::is_none")]
123 pub status_text: Option<String>,
124
125 #[serde(skip_serializing_if = "Option::is_none")]
127 pub headers: Option<serde_json::Value>,
128
129 #[serde(skip_serializing_if = "Option::is_none")]
131 pub body: Option<serde_json::Value>,
132
133 #[serde(skip_serializing_if = "Option::is_none")]
135 pub body_raw: Option<String>,
136
137 #[serde(skip_serializing_if = "Option::is_none")]
139 pub body_size_bytes: Option<usize>,
140
141 #[serde(skip_serializing_if = "Option::is_none")]
143 pub request: Option<RequestDetails>,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct RequestDetails {
149 #[serde(skip_serializing_if = "Option::is_none")]
151 pub headers: Option<serde_json::Value>,
152
153 #[serde(skip_serializing_if = "Option::is_none")]
155 pub query_params: Option<serde_json::Value>,
156
157 #[serde(skip_serializing_if = "Option::is_none")]
159 pub path_params: Option<serde_json::Value>,
160
161 #[serde(skip_serializing_if = "Option::is_none")]
163 pub body: Option<serde_json::Value>,
164}
165
166impl<T: Serialize> ResponseEnvelope<T> {
167 pub fn success(command: &str, data: T) -> Self {
169 Self {
170 success: true,
171 command: command.to_string(),
172 data: Some(data),
173 metadata: ResponseMetadata::new(exit_codes::SUCCESS),
174 errors: vec![],
175 warnings: vec![],
176 }
177 }
178
179 pub fn success_with_run(
181 command: &str,
182 data: T,
183 run_id: String,
184 request_id: Option<String>,
185 duration_ms: f64,
186 ) -> Self {
187 let mut envelope = Self::success(command, data);
188 envelope.metadata.run_id = Some(run_id);
189 envelope.metadata.request_id = request_id;
190 envelope.metadata.duration_ms = Some(duration_ms);
191 envelope
192 }
193
194 pub fn with_warning(mut self, warning: &str) -> Self {
196 self.warnings.push(warning.to_string());
197 self
198 }
199
200 pub fn output_and_exit(self) -> ! {
202 let exit_code = self.metadata.exit_code;
203 println!(
204 "{}",
205 serde_json::to_string_pretty(&self).unwrap_or_else(|_| "{}".to_string())
206 );
207 process::exit(exit_code);
208 }
209
210 pub fn to_json(&self) -> String {
212 serde_json::to_string_pretty(self).unwrap_or_else(|_| "{}".to_string())
213 }
214
215 pub fn to_json_compact(&self) -> String {
217 serde_json::to_string(self).unwrap_or_else(|_| "{}".to_string())
218 }
219}
220
221pub fn error_response(
223 command: &str,
224 code: &str,
225 message: &str,
226 exit_code: i32,
227) -> ResponseEnvelope<serde_json::Value> {
228 ResponseEnvelope {
229 success: false,
230 command: command.to_string(),
231 data: None,
232 metadata: ResponseMetadata::new(exit_code),
233 errors: vec![ErrorDetail {
234 code: code.to_string(),
235 message: message.to_string(),
236 context: None,
237 suggestion: None,
238 }],
239 warnings: vec![],
240 }
241}
242
243pub fn error_with_suggestion(
245 command: &str,
246 code: &str,
247 message: &str,
248 suggestion: &str,
249 exit_code: i32,
250) -> ResponseEnvelope<serde_json::Value> {
251 ResponseEnvelope {
252 success: false,
253 command: command.to_string(),
254 data: None,
255 metadata: ResponseMetadata::new(exit_code),
256 errors: vec![ErrorDetail {
257 code: code.to_string(),
258 message: message.to_string(),
259 context: None,
260 suggestion: Some(suggestion.to_string()),
261 }],
262 warnings: vec![],
263 }
264}
265
266impl ResponseMetadata {
267 pub fn new(exit_code: i32) -> Self {
268 Self {
269 run_id: None,
270 request_id: None,
271 duration_ms: None,
272 version: env!("CARGO_PKG_VERSION").to_string(),
273 timestamp: Utc::now(),
274 exit_code,
275 }
276 }
277}
278
279impl ErrorDetail {
280 pub fn new(code: &str, message: &str) -> Self {
281 Self {
282 code: code.to_string(),
283 message: message.to_string(),
284 context: None,
285 suggestion: None,
286 }
287 }
288
289 pub fn with_context(mut self, context: &str) -> Self {
290 self.context = Some(context.to_string());
291 self
292 }
293
294 pub fn with_suggestion(mut self, suggestion: &str) -> Self {
295 self.suggestion = Some(suggestion.to_string());
296 self
297 }
298}
299
300#[derive(Debug, Clone, Default)]
302pub struct OutputConfig {
303 pub json: bool,
305 pub machine: bool,
307 pub quiet: bool,
309 pub verbose: bool,
311 pub no_color: bool,
313}
314
315impl OutputConfig {
316 pub fn from_env() -> Self {
318 Self {
319 json: std::env::var("MRAPIDS_JSON")
320 .map(|v| v == "1" || v.to_lowercase() == "true")
321 .unwrap_or(false)
322 || std::env::var(env_vars::OUTPUT_FORMAT)
323 .map(|v| v.to_lowercase() == "json")
324 .unwrap_or(false),
325 machine: std::env::var(env_vars::MACHINE_MODE)
326 .map(|v| v == "1" || v.to_lowercase() == "true")
327 .unwrap_or(false),
328 quiet: std::env::var("MRAPIDS_QUIET")
329 .map(|v| v == "1" || v.to_lowercase() == "true")
330 .unwrap_or(false),
331 verbose: std::env::var("MRAPIDS_VERBOSE")
332 .map(|v| v == "1" || v.to_lowercase() == "true")
333 .unwrap_or(false),
334 no_color: std::env::var(env_vars::NO_COLOR)
335 .map(|v| v == "1" || v.to_lowercase() == "true")
336 .unwrap_or(false),
337 }
338 }
339
340 pub fn show_decorations(&self) -> bool {
342 !self.machine && !self.json && !self.quiet
343 }
344
345 pub fn use_colors(&self) -> bool {
347 !self.no_color && !self.machine && !self.json
348 }
349}
350
351pub fn is_json_mode() -> bool {
354 std::env::var("MRAPIDS_JSON")
355 .map(|v| v == "1" || v.to_lowercase() == "true")
356 .unwrap_or(false)
357 || std::env::var("MRAPIDS_OUTPUT")
358 .map(|v| v.to_lowercase() == "json")
359 .unwrap_or(false)
360}
361
362pub fn is_machine_mode() -> bool {
364 std::env::var("MRAPIDS_MACHINE")
365 .map(|v| v == "1" || v.to_lowercase() == "true")
366 .unwrap_or(false)
367}
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372
373 #[test]
374 fn test_success_response() {
375 let response = ResponseEnvelope::success("run", serde_json::json!({"status": "ok"}));
376 assert!(response.success);
377 assert_eq!(response.command, "run");
378 assert_eq!(response.metadata.exit_code, exit_codes::SUCCESS);
379 }
380
381 #[test]
382 fn test_error_response() {
383 let response = error_response(
384 "run",
385 "NETWORK_ERROR",
386 "Connection failed",
387 exit_codes::NETWORK_ERROR,
388 );
389 assert!(!response.success);
390 assert_eq!(response.errors.len(), 1);
391 assert_eq!(response.errors[0].code, "NETWORK_ERROR");
392 assert_eq!(response.metadata.exit_code, exit_codes::NETWORK_ERROR);
393 }
394
395 #[test]
396 fn test_success_with_run() {
397 let response = ResponseEnvelope::success_with_run(
398 "run",
399 serde_json::json!({"data": "test"}),
400 "abc123".to_string(),
401 Some("req_xyz".to_string()),
402 150.5,
403 );
404 assert!(response.success);
405 assert_eq!(response.metadata.run_id, Some("abc123".to_string()));
406 assert_eq!(response.metadata.request_id, Some("req_xyz".to_string()));
407 assert_eq!(response.metadata.duration_ms, Some(150.5));
408 }
409
410 #[test]
411 fn test_json_serialization() {
412 let response = ResponseEnvelope::success("test", serde_json::json!({"key": "value"}));
413 let json = response.to_json();
414 assert!(json.contains("\"success\": true"));
415 assert!(json.contains("\"command\": \"test\""));
416 }
417
418 #[test]
419 fn test_output_config_from_env() {
420 let config = OutputConfig::default();
422 assert!(!config.json);
423 assert!(!config.machine);
424 assert!(config.show_decorations());
425 }
426
427 #[test]
428 fn test_exit_codes() {
429 assert_eq!(exit_codes::SUCCESS, 0);
430 assert_eq!(exit_codes::GENERAL_ERROR, 1);
431 assert_eq!(exit_codes::NETWORK_ERROR, 3);
432 assert_eq!(exit_codes::PARTIAL_SUCCESS, 10);
433 }
434}