use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Deserialize)]
pub struct RequestEnvelope {
#[serde(default)]
pub id: Option<String>,
pub method: String,
#[serde(default)]
pub params: Value,
}
#[derive(Debug, Clone, Serialize)]
pub struct ResponseEnvelope {
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub op_id: Option<String>,
#[serde(rename = "type")]
pub response_type: ResponseType,
pub data: Value,
}
impl ResponseEnvelope {
pub fn result(id: String, data: impl Serialize) -> Self {
Self {
id,
op_id: None,
response_type: ResponseType::Result,
data: serde_json::to_value(data).unwrap_or(Value::Null),
}
}
pub fn result_with_op(id: String, op_id: String, data: impl Serialize) -> Self {
Self {
id,
op_id: Some(op_id),
response_type: ResponseType::Result,
data: serde_json::to_value(data).unwrap_or(Value::Null),
}
}
pub fn progress(id: String, op_id: String, data: impl Serialize) -> Self {
Self {
id,
op_id: Some(op_id),
response_type: ResponseType::Progress,
data: serde_json::to_value(data).unwrap_or(Value::Null),
}
}
pub fn stream(id: String, op_id: String, data: impl Serialize) -> Self {
Self {
id,
op_id: Some(op_id),
response_type: ResponseType::Stream,
data: serde_json::to_value(data).unwrap_or(Value::Null),
}
}
pub fn error(id: String, op_id: Option<String>, error: ErrorData) -> Self {
Self {
id,
op_id,
response_type: ResponseType::Error,
data: serde_json::to_value(error).unwrap_or(Value::Null),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ResponseType {
Result,
Progress,
Stream,
Error,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorData {
pub code: ErrorCode,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub details: Option<Value>,
}
impl ErrorData {
pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
details: None,
}
}
pub fn with_details(code: ErrorCode, message: impl Into<String>, details: Value) -> Self {
Self {
code,
message: message.into(),
details: Some(details),
}
}
pub fn invalid_request(message: impl Into<String>) -> Self {
Self::new(ErrorCode::InvalidRequest, message)
}
pub fn unknown_method(method: &str) -> Self {
Self::new(
ErrorCode::UnknownMethod,
format!("Unknown method: {}", method),
)
}
pub fn invalid_params(message: impl Into<String>) -> Self {
Self::new(ErrorCode::InvalidParams, message)
}
pub fn operation_failed(message: impl Into<String>) -> Self {
Self::new(ErrorCode::OperationFailed, message)
}
pub fn operation_cancelled() -> Self {
Self::new(ErrorCode::OperationCancelled, "Operation was cancelled")
}
pub fn not_initialized(resource: &str) -> Self {
Self::new(
ErrorCode::NotInitialized,
format!(
"{} data not initialized. Run bootstrap/refresh first.",
resource
),
)
}
pub fn rate_limited() -> Self {
Self::new(ErrorCode::RateLimited, "Too many concurrent operations")
}
pub fn internal(message: impl Into<String>) -> Self {
Self::new(ErrorCode::InternalError, message)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum ErrorCode {
InvalidRequest,
UnknownMethod,
InvalidParams,
OperationFailed,
OperationCancelled,
NotInitialized,
RateLimited,
InternalError,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ProgressStage {
Queued,
Running,
Downloading,
Processing,
Finalizing,
Done,
}
#[derive(Debug, Clone, Serialize)]
pub struct SystemInfo {
pub protocol_version: u32,
pub server_version: String,
pub build: BuildInfo,
pub features: FeatureFlags,
}
#[derive(Debug, Clone, Serialize)]
pub struct BuildInfo {
pub git_sha: String,
pub timestamp: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct FeatureFlags {
pub streaming: bool,
pub auth_required: bool,
}
impl Default for SystemInfo {
fn default() -> Self {
Self {
protocol_version: 1,
server_version: env!("CARGO_PKG_VERSION").to_string(),
build: BuildInfo {
git_sha: option_env!("GIT_SHA").unwrap_or("unknown").to_string(),
timestamp: option_env!("BUILD_TIMESTAMP")
.unwrap_or("unknown")
.to_string(),
},
features: FeatureFlags {
streaming: true,
auth_required: false,
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_request_envelope_deserialization() {
let json =
r#"{"id": "test-1", "method": "time.parse", "params": {"times": ["1234567890"]}}"#;
let req: RequestEnvelope = serde_json::from_str(json).unwrap();
assert_eq!(req.id, Some("test-1".to_string()));
assert_eq!(req.method, "time.parse");
let json = r#"{"method": "time.parse", "params": {}}"#;
let req: RequestEnvelope = serde_json::from_str(json).unwrap();
assert!(req.id.is_none());
assert_eq!(req.method, "time.parse");
let json = r#"{"method": "time.parse"}"#;
let req: RequestEnvelope = serde_json::from_str(json).unwrap();
assert!(req.params.is_null());
}
#[test]
fn test_response_envelope_serialization() {
let resp =
ResponseEnvelope::result("test-1".to_string(), serde_json::json!({"foo": "bar"}));
let json = serde_json::to_string(&resp).unwrap();
assert!(json.contains("\"id\":\"test-1\""));
assert!(json.contains("\"type\":\"result\""));
assert!(!json.contains("op_id"));
let resp = ResponseEnvelope::progress(
"test-1".to_string(),
"op-1".to_string(),
serde_json::json!({"stage": "running"}),
);
let json = serde_json::to_string(&resp).unwrap();
assert!(json.contains("\"op_id\":\"op-1\""));
assert!(json.contains("\"type\":\"progress\""));
}
#[test]
fn test_error_codes_serialization() {
let error = ErrorData::new(ErrorCode::InvalidRequest, "test error");
let json = serde_json::to_string(&error).unwrap();
assert!(json.contains("\"code\":\"INVALID_REQUEST\""));
}
#[test]
fn test_progress_stage_serialization() {
let stage = ProgressStage::Running;
let json = serde_json::to_string(&stage).unwrap();
assert_eq!(json, "\"running\"");
}
}