use serde_json::Value;
pub const EXIT_CALLER_ERROR: i32 = 2;
pub const EXIT_RUNTIME_ERROR: i32 = 1;
pub fn format_error_json(message: &str) -> String {
classify_error(message).0
}
pub fn classify_error(message: &str) -> (String, i32) {
let message = message.trim_end();
if let Some(rest) = strip_json_rpc_code(message, -32602) {
return (error_envelope("CALLER_ERROR", rest), EXIT_CALLER_ERROR);
}
if let Some(rest) = strip_json_rpc_code(message, -32603) {
return (error_envelope("RUNTIME_ERROR", rest), EXIT_RUNTIME_ERROR);
}
let (code, message) = split_error_code(message).unwrap_or(("RUNTIME_ERROR", message));
(error_envelope(code, message), EXIT_RUNTIME_ERROR)
}
fn error_envelope(code: &str, message: &str) -> String {
serde_json::json!({"error": {"code": code, "message": message}}).to_string()
}
fn strip_json_rpc_code(message: &str, code: i64) -> Option<&str> {
let prefix = format!("[{code}]");
message.strip_prefix(&prefix).map(|rest| rest.trim_start())
}
pub(crate) fn split_error_code(message: &str) -> Option<(&str, &str)> {
let (code, rest) = message.split_once(':')?;
if !code.is_empty()
&& code
.chars()
.all(|ch| ch.is_ascii_uppercase() || ch.is_ascii_digit() || ch == '_')
{
Some((code, rest.trim_start()))
} else {
None
}
}
pub(crate) fn normalize_list_envelope(
value: Value,
list_key: &str,
limit: Option<u64>,
offset: u64,
) -> Value {
if let Value::Array(arr) = value {
let total = arr.len() as u64;
let mut obj = serde_json::Map::new();
obj.insert(list_key.to_string(), Value::Array(arr));
obj.insert("total".to_string(), Value::from(total));
if let Some(limit) = limit {
obj.insert("limit".to_string(), Value::from(limit));
}
obj.insert("offset".to_string(), Value::from(offset));
obj.insert("hasMore".to_string(), Value::Bool(false));
return Value::Object(obj);
}
let mut obj = match value {
Value::Object(obj) if obj.contains_key(list_key) => obj,
other => return other,
};
let items_len = obj
.get(list_key)
.and_then(Value::as_array)
.map_or(0, |a| a.len()) as u64;
let total = obj
.get("total")
.and_then(Value::as_u64)
.unwrap_or(items_len);
obj.insert("total".to_string(), Value::from(total));
if let Some(limit) = limit {
obj.insert("limit".to_string(), Value::from(limit));
}
obj.insert("offset".to_string(), Value::from(offset));
obj.insert(
"hasMore".to_string(),
Value::Bool(offset + items_len < total),
);
Value::Object(obj)
}
pub(crate) fn raw_value_output(value: &Value) -> Result<String, String> {
let mut out = match value {
Value::Null => String::new(),
Value::String(content) => content.clone(),
other => serde_json::to_string(other).map_err(|e| e.to_string())?,
};
out.push('\n');
Ok(out)
}
pub(crate) fn format_json(value: &Value) -> Result<String, String> {
let mut out = serde_json::to_string(value).map_err(|e| e.to_string())?;
out.push('\n');
Ok(out)
}