crdhcpc 0.1.1

Standalone DHCP Client for Linux with DHCPv4, DHCPv6, PXE, and Dynamic DNS support
Documentation
//! JSON-RPC 2.0 protocol implementation
//!
//! Implements the JSON-RPC 2.0 specification for communication between
//! the web server and daemon over Unix socket.

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

/// Request ID type
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(untagged)]
pub enum RequestId {
    String(String),
    Number(i64),
    Null,
}

impl From<String> for RequestId {
    fn from(s: String) -> Self {
        RequestId::String(s)
    }
}

impl From<i64> for RequestId {
    fn from(n: i64) -> Self {
        RequestId::Number(n)
    }
}

impl From<RequestId> for Value {
    fn from(id: RequestId) -> Self {
        match id {
            RequestId::String(s) => Value::String(s),
            RequestId::Number(n) => Value::Number(n.into()),
            RequestId::Null => Value::Null,
        }
    }
}

/// JSON-RPC 2.0 Request
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcRequest {
    pub jsonrpc: String,
    pub method: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub params: Option<Value>,
    pub id: Value,
}

impl JsonRpcRequest {
    pub fn new(method: impl Into<String>, params: Option<Value>, id: Value) -> Self {
        Self {
            jsonrpc: "2.0".to_string(),
            method: method.into(),
            params,
            id,
        }
    }
}

/// JSON-RPC 2.0 Response (Success)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcResponse {
    pub jsonrpc: String,
    pub result: Value,
    pub id: Value,
}

impl JsonRpcResponse {
    pub fn new(result: Value, id: Value) -> Self {
        Self {
            jsonrpc: "2.0".to_string(),
            result,
            id,
        }
    }
}

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

/// JSON-RPC 2.0 Error Response
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcErrorResponse {
    pub jsonrpc: String,
    pub error: JsonRpcError,
    pub id: Value,
}

impl JsonRpcErrorResponse {
    pub fn new(error: JsonRpcError, id: Value) -> Self {
        Self {
            jsonrpc: "2.0".to_string(),
            error,
            id,
        }
    }
}

/// JSON-RPC 2.0 Notification (no response expected)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcNotification {
    pub jsonrpc: String,
    pub method: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub params: Option<Value>,
}

impl JsonRpcNotification {
    pub fn new(method: impl Into<String>, params: Option<Value>) -> Self {
        Self {
            jsonrpc: "2.0".to_string(),
            method: method.into(),
            params,
        }
    }
}

/// Batch request/response
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(transparent)]
pub struct JsonRpcBatch(pub Vec<JsonRpcMessage>);

/// JSON-RPC version constant
pub const JSON_RPC_VERSION: &str = "2.0";

/// Standard JSON-RPC error codes
#[allow(dead_code)]
pub mod 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;

    // Custom error codes (implementation-defined)
    pub const UNAUTHORIZED: i32 = -32000;
    pub const FORBIDDEN: i32 = -32001;
    pub const NOT_FOUND: i32 = -32002;
    pub const RATE_LIMITED: i32 = -32003;
    pub const TIMEOUT: i32 = -32004;
    pub const PLUGIN_ERROR: i32 = -32005;
    pub const ACL_DENIED: i32 = -32006;
}

/// JSON-RPC message (either request, response, or notification)
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum JsonRpcMessage {
    Request(JsonRpcRequest),
    Notification(JsonRpcNotification),
    Response(JsonRpcResponse),
    ErrorResponse(JsonRpcErrorResponse),
}

impl JsonRpcMessage {
    /// Check if this is a request that expects a response
    pub fn expects_response(&self) -> bool {
        matches!(self, JsonRpcMessage::Request(_))
    }

    /// Get the request ID if this is a request or response
    pub fn id(&self) -> Option<&Value> {
        match self {
            JsonRpcMessage::Request(req) => Some(&req.id),
            JsonRpcMessage::Response(resp) => Some(&resp.id),
            JsonRpcMessage::ErrorResponse(err) => Some(&err.id),
            JsonRpcMessage::Notification(_) => None,
        }
    }

    /// Get the method name if this is a request or notification
    pub fn method(&self) -> Option<&str> {
        match self {
            JsonRpcMessage::Request(req) => Some(&req.method),
            JsonRpcMessage::Notification(notif) => Some(&notif.method),
            _ => None,
        }
    }
}

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

    #[test]
    fn test_request_serialization() {
        let req = JsonRpcRequest::new(
            "network.getInterfaces",
            Some(json!({"filter": "up"})),
            json!(1),
        );
        let serialized = serde_json::to_string(&req).unwrap();
        assert!(serialized.contains("\"jsonrpc\":\"2.0\""));
        assert!(serialized.contains("\"method\":\"network.getInterfaces\""));
    }

    #[test]
    fn test_response_serialization() {
        let resp = JsonRpcResponse::new(json!({"status": "ok"}), json!(1));
        let serialized = serde_json::to_string(&resp).unwrap();
        assert!(serialized.contains("\"jsonrpc\":\"2.0\""));
        assert!(serialized.contains("\"result\""));
    }

    #[test]
    fn test_error_response() {
        let error = JsonRpcError {
            code: error_codes::METHOD_NOT_FOUND,
            message: "Method not found".to_string(),
            data: None,
        };
        let resp = JsonRpcErrorResponse::new(error, json!(1));
        let serialized = serde_json::to_string(&resp).unwrap();
        assert!(serialized.contains("\"error\""));
        assert!(serialized.contains("-32601"));
    }
}