zinit 0.3.6

Process supervisor with dependency management
Documentation
//! JSON-RPC 2.0 protocol types for zinit IPC.

use serde::{Deserialize, Serialize};
use serde_json::Value;

/// JSON-RPC 2.0 request.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RpcRequest {
    pub jsonrpc: String,
    pub id: u64,
    pub method: String,
    #[serde(default, skip_serializing_if = "Value::is_null")]
    pub params: Value,
}

impl RpcRequest {
    /// Create a new RPC request.
    pub fn new(id: u64, method: impl Into<String>, params: Value) -> Self {
        Self {
            jsonrpc: "2.0".to_string(),
            id,
            method: method.into(),
            params,
        }
    }

    /// Create a new RPC request with no parameters.
    pub fn new_no_params(id: u64, method: impl Into<String>) -> Self {
        Self::new(id, method, Value::Null)
    }
}

/// JSON-RPC 2.0 response.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RpcResponse {
    pub jsonrpc: String,
    pub id: u64,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub result: Option<Value>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error: Option<RpcError>,
}

impl RpcResponse {
    /// Create a successful response.
    pub fn success(id: u64, result: Value) -> Self {
        Self {
            jsonrpc: "2.0".to_string(),
            id,
            result: Some(result),
            error: None,
        }
    }

    /// Create an error response.
    pub fn error(id: u64, code: i32, message: impl Into<String>) -> Self {
        Self {
            jsonrpc: "2.0".to_string(),
            id,
            result: None,
            error: Some(RpcError {
                code,
                message: message.into(),
                data: None,
            }),
        }
    }

    /// Create an error response with additional data.
    pub fn error_with_data(id: u64, code: i32, message: impl Into<String>, data: Value) -> Self {
        Self {
            jsonrpc: "2.0".to_string(),
            id,
            result: None,
            error: Some(RpcError {
                code,
                message: message.into(),
                data: Some(data),
            }),
        }
    }

    /// Check if this response is successful.
    pub fn is_ok(&self) -> bool {
        self.error.is_none()
    }

    /// Convert to a Result, parsing the result into the specified type.
    pub fn into_result<T: for<'de> Deserialize<'de>>(self) -> Result<T, RpcError> {
        if let Some(error) = self.error {
            Err(error)
        } else if let Some(result) = self.result {
            serde_json::from_value(result).map_err(|e| RpcError {
                code: error_codes::INTERNAL_ERROR,
                message: format!("Failed to parse result: {}", e),
                data: None,
            })
        } else {
            Err(RpcError {
                code: error_codes::INTERNAL_ERROR,
                message: "Response has neither result nor error".to_string(),
                data: None,
            })
        }
    }
}

/// JSON-RPC 2.0 error.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RpcError {
    pub code: i32,
    pub message: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub data: Option<Value>,
}

impl std::fmt::Display for RpcError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "[{}] {}", self.code, self.message)
    }
}

impl std::error::Error for RpcError {}

/// Standard JSON-RPC 2.0 error codes and zinit-specific codes.
pub mod error_codes {
    // Standard JSON-RPC 2.0 error codes
    pub const PARSE_ERROR: i32 = -32700;
    pub const INVALID_REQUEST: i32 = -32600;
    pub const METHOD_NOT_FOUND: i32 = -32601;
    pub const INVALID_PARAMS: i32 = -32602;
    pub const INTERNAL_ERROR: i32 = -32603;

    // Zinit-specific error codes
    pub const SERVICE_NOT_FOUND: i32 = -32000;
    pub const SERVICE_ALREADY_RUNNING: i32 = -32001;
    pub const SERVICE_NOT_RUNNING: i32 = -32002;
    pub const INVALID_CONFIG: i32 = -32003;
    pub const CYCLE_DETECTED: i32 = -32004;
    pub const UNSAFE_REMOVAL: i32 = -32005;

    // Add-service specific error codes
    pub const SERVICE_EXISTS: i32 = -32010;
    pub const VALIDATION_FAILED: i32 = -32011;
    pub const DEPENDENCY_MISSING: i32 = -32012;
    pub const EXEC_NOT_FOUND: i32 = -32013;
    pub const PERSIST_FAILED: i32 = -32014;
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_request_creation() {
        let req = RpcRequest::new(1, "service.status", serde_json::json!({"name": "test"}));
        assert_eq!(req.jsonrpc, "2.0");
        assert_eq!(req.id, 1);
        assert_eq!(req.method, "service.status");
    }

    #[test]
    fn test_request_serialization() {
        let req = RpcRequest::new(1, "system.ping", Value::Null);
        let json = serde_json::to_string(&req).unwrap();
        assert!(json.contains("\"jsonrpc\":\"2.0\""));
        assert!(json.contains("\"id\":1"));
        assert!(json.contains("\"method\":\"system.ping\""));
    }

    #[test]
    fn test_success_response() {
        let resp = RpcResponse::success(1, serde_json::json!({"version": "0.1.0"}));
        assert!(resp.is_ok());
        assert!(resp.result.is_some());
        assert!(resp.error.is_none());
    }

    #[test]
    fn test_error_response() {
        let resp = RpcResponse::error(1, error_codes::SERVICE_NOT_FOUND, "Service not found");
        assert!(!resp.is_ok());
        assert!(resp.result.is_none());
        assert!(resp.error.is_some());

        let err = resp.error.unwrap();
        assert_eq!(err.code, error_codes::SERVICE_NOT_FOUND);
        assert_eq!(err.message, "Service not found");
    }

    #[test]
    fn test_into_result_success() {
        #[derive(Debug, Deserialize, PartialEq)]
        struct Version {
            version: String,
        }

        let resp = RpcResponse::success(1, serde_json::json!({"version": "0.1.0"}));
        let result: Version = resp.into_result().unwrap();
        assert_eq!(result.version, "0.1.0");
    }

    #[test]
    fn test_into_result_error() {
        let resp = RpcResponse::error(1, error_codes::SERVICE_NOT_FOUND, "not found");
        let result: Result<String, _> = resp.into_result();
        assert!(result.is_err());

        let err = result.unwrap_err();
        assert_eq!(err.code, error_codes::SERVICE_NOT_FOUND);
    }

    #[test]
    fn test_rpc_error_display() {
        let err = RpcError {
            code: -32000,
            message: "Service not found".to_string(),
            data: None,
        };
        assert_eq!(err.to_string(), "[-32000] Service not found");
    }
}