#![allow(dead_code)]
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::process;
pub mod exit_codes {
pub const SUCCESS: i32 = 0;
pub const GENERAL_ERROR: i32 = 1;
pub const INVALID_ARGUMENTS: i32 = 2;
pub const NETWORK_ERROR: i32 = 3;
pub const AUTH_ERROR: i32 = 4;
pub const VALIDATION_ERROR: i32 = 5;
pub const NOT_FOUND: i32 = 6;
pub const TIMEOUT: i32 = 7;
pub const PARTIAL_SUCCESS: i32 = 10; }
pub mod env_vars {
pub const DB_PATH: &str = "MRAPIDS_DB_PATH";
pub const CONFIG_PATH: &str = "MRAPIDS_CONFIG_PATH";
pub const SPEC_PATH: &str = "MRAPIDS_SPEC_PATH";
pub const OUTPUT_FORMAT: &str = "MRAPIDS_OUTPUT";
pub const AUTH_TOKEN: &str = "MRAPIDS_AUTH_TOKEN";
pub const BASE_URL: &str = "MRAPIDS_BASE_URL";
pub const LOG_LEVEL: &str = "MRAPIDS_LOG_LEVEL";
pub const NO_COLOR: &str = "MRAPIDS_NO_COLOR";
pub const MACHINE_MODE: &str = "MRAPIDS_MACHINE";
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResponseEnvelope<T: Serialize> {
pub success: bool,
pub command: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<T>,
pub metadata: ResponseMetadata,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub errors: Vec<ErrorDetail>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub warnings: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResponseMetadata {
#[serde(skip_serializing_if = "Option::is_none")]
pub run_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub request_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub duration_ms: Option<f64>,
pub version: String,
pub timestamp: DateTime<Utc>,
pub exit_code: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorDetail {
pub code: String,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub context: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub suggestion: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunResponse {
pub operation: String,
pub method: String,
pub url: String,
pub status_code: u16,
#[serde(skip_serializing_if = "Option::is_none")]
pub status_text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub headers: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub body: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub body_raw: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub body_size_bytes: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub request: Option<RequestDetails>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RequestDetails {
#[serde(skip_serializing_if = "Option::is_none")]
pub headers: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub query_params: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub path_params: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub body: Option<serde_json::Value>,
}
impl<T: Serialize> ResponseEnvelope<T> {
pub fn success(command: &str, data: T) -> Self {
Self {
success: true,
command: command.to_string(),
data: Some(data),
metadata: ResponseMetadata::new(exit_codes::SUCCESS),
errors: vec![],
warnings: vec![],
}
}
pub fn success_with_run(
command: &str,
data: T,
run_id: String,
request_id: Option<String>,
duration_ms: f64,
) -> Self {
let mut envelope = Self::success(command, data);
envelope.metadata.run_id = Some(run_id);
envelope.metadata.request_id = request_id;
envelope.metadata.duration_ms = Some(duration_ms);
envelope
}
pub fn with_warning(mut self, warning: &str) -> Self {
self.warnings.push(warning.to_string());
self
}
pub fn output_and_exit(self) -> ! {
let exit_code = self.metadata.exit_code;
println!(
"{}",
serde_json::to_string_pretty(&self).unwrap_or_else(|_| "{}".to_string())
);
process::exit(exit_code);
}
pub fn to_json(&self) -> String {
serde_json::to_string_pretty(self).unwrap_or_else(|_| "{}".to_string())
}
pub fn to_json_compact(&self) -> String {
serde_json::to_string(self).unwrap_or_else(|_| "{}".to_string())
}
}
pub fn error_response(
command: &str,
code: &str,
message: &str,
exit_code: i32,
) -> ResponseEnvelope<serde_json::Value> {
ResponseEnvelope {
success: false,
command: command.to_string(),
data: None,
metadata: ResponseMetadata::new(exit_code),
errors: vec![ErrorDetail {
code: code.to_string(),
message: message.to_string(),
context: None,
suggestion: None,
}],
warnings: vec![],
}
}
pub fn error_with_suggestion(
command: &str,
code: &str,
message: &str,
suggestion: &str,
exit_code: i32,
) -> ResponseEnvelope<serde_json::Value> {
ResponseEnvelope {
success: false,
command: command.to_string(),
data: None,
metadata: ResponseMetadata::new(exit_code),
errors: vec![ErrorDetail {
code: code.to_string(),
message: message.to_string(),
context: None,
suggestion: Some(suggestion.to_string()),
}],
warnings: vec![],
}
}
impl ResponseMetadata {
pub fn new(exit_code: i32) -> Self {
Self {
run_id: None,
request_id: None,
duration_ms: None,
version: env!("CARGO_PKG_VERSION").to_string(),
timestamp: Utc::now(),
exit_code,
}
}
}
impl ErrorDetail {
pub fn new(code: &str, message: &str) -> Self {
Self {
code: code.to_string(),
message: message.to_string(),
context: None,
suggestion: None,
}
}
pub fn with_context(mut self, context: &str) -> Self {
self.context = Some(context.to_string());
self
}
pub fn with_suggestion(mut self, suggestion: &str) -> Self {
self.suggestion = Some(suggestion.to_string());
self
}
}
#[derive(Debug, Clone, Default)]
pub struct OutputConfig {
pub json: bool,
pub machine: bool,
pub quiet: bool,
pub verbose: bool,
pub no_color: bool,
}
impl OutputConfig {
pub fn from_env() -> Self {
Self {
json: std::env::var("MRAPIDS_JSON")
.map(|v| v == "1" || v.to_lowercase() == "true")
.unwrap_or(false)
|| std::env::var(env_vars::OUTPUT_FORMAT)
.map(|v| v.to_lowercase() == "json")
.unwrap_or(false),
machine: std::env::var(env_vars::MACHINE_MODE)
.map(|v| v == "1" || v.to_lowercase() == "true")
.unwrap_or(false),
quiet: std::env::var("MRAPIDS_QUIET")
.map(|v| v == "1" || v.to_lowercase() == "true")
.unwrap_or(false),
verbose: std::env::var("MRAPIDS_VERBOSE")
.map(|v| v == "1" || v.to_lowercase() == "true")
.unwrap_or(false),
no_color: std::env::var(env_vars::NO_COLOR)
.map(|v| v == "1" || v.to_lowercase() == "true")
.unwrap_or(false),
}
}
pub fn show_decorations(&self) -> bool {
!self.machine && !self.json && !self.quiet
}
pub fn use_colors(&self) -> bool {
!self.no_color && !self.machine && !self.json
}
}
pub fn is_json_mode() -> bool {
std::env::var("MRAPIDS_JSON")
.map(|v| v == "1" || v.to_lowercase() == "true")
.unwrap_or(false)
|| std::env::var("MRAPIDS_OUTPUT")
.map(|v| v.to_lowercase() == "json")
.unwrap_or(false)
}
pub fn is_machine_mode() -> bool {
std::env::var("MRAPIDS_MACHINE")
.map(|v| v == "1" || v.to_lowercase() == "true")
.unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_success_response() {
let response = ResponseEnvelope::success("run", serde_json::json!({"status": "ok"}));
assert!(response.success);
assert_eq!(response.command, "run");
assert_eq!(response.metadata.exit_code, exit_codes::SUCCESS);
}
#[test]
fn test_error_response() {
let response = error_response(
"run",
"NETWORK_ERROR",
"Connection failed",
exit_codes::NETWORK_ERROR,
);
assert!(!response.success);
assert_eq!(response.errors.len(), 1);
assert_eq!(response.errors[0].code, "NETWORK_ERROR");
assert_eq!(response.metadata.exit_code, exit_codes::NETWORK_ERROR);
}
#[test]
fn test_success_with_run() {
let response = ResponseEnvelope::success_with_run(
"run",
serde_json::json!({"data": "test"}),
"abc123".to_string(),
Some("req_xyz".to_string()),
150.5,
);
assert!(response.success);
assert_eq!(response.metadata.run_id, Some("abc123".to_string()));
assert_eq!(response.metadata.request_id, Some("req_xyz".to_string()));
assert_eq!(response.metadata.duration_ms, Some(150.5));
}
#[test]
fn test_json_serialization() {
let response = ResponseEnvelope::success("test", serde_json::json!({"key": "value"}));
let json = response.to_json();
assert!(json.contains("\"success\": true"));
assert!(json.contains("\"command\": \"test\""));
}
#[test]
fn test_output_config_from_env() {
let config = OutputConfig::default();
assert!(!config.json);
assert!(!config.machine);
assert!(config.show_decorations());
}
#[test]
fn test_exit_codes() {
assert_eq!(exit_codes::SUCCESS, 0);
assert_eq!(exit_codes::GENERAL_ERROR, 1);
assert_eq!(exit_codes::NETWORK_ERROR, 3);
assert_eq!(exit_codes::PARTIAL_SUCCESS, 10);
}
}