Skip to main content

confium_daemon/
protocol.rs

1//! JSON-RPC 2.0 wire types.
2//!
3//! Each method handler receives an [`RpcRequest`] (params extracted by
4//! the caller into a typed `Value`) and returns either an
5//! [`RpcResponse::Ok`] with a JSON result or [`RpcResponse::Err`] with
6//! a structured [`crate::error::RpcError`].
7//!
8//! Spec: <https://www.jsonrpc.org/specification>
9//!
10//! The transport framing (length-prefixed JSON, newline-delimited JSON,
11//! etc.) is layered on top by the server loop — this module only
12//! concerns itself with the payload shape.
13
14use serde::{Deserialize, Serialize};
15use serde_json::Value;
16
17use crate::error::RpcError;
18
19/// JSON-RPC 2.0 request object.
20///
21/// `params` is left as `Value`; each handler parses it into its own
22/// concrete shape. A notification is a request with `id == None`.
23#[derive(Debug, Deserialize)]
24pub struct RpcRequest {
25    /// Protocol version. Must be `"2.0"`; other values are rejected at
26    /// the transport layer before the handler runs.
27    pub jsonrpc: String,
28
29    /// Method name. Maps into the dispatch table.
30    pub method: String,
31
32    /// Method parameters. Object form (`{"foo": 1}`) is preferred for
33    /// clarity, but positional arrays are also accepted by the spec.
34    #[serde(default)]
35    pub params: Value,
36
37    /// `None` means the request is a notification — the server replies
38    /// with no body. Notifications may not include an error reply.
39    #[serde(default, deserialize_with = "deserialize_opt_id")]
40    pub id: Option<Value>,
41}
42
43/// Allow `id` to be a number, string, or null. `null` is treated as
44/// "no id" (notification) per the spec.
45fn deserialize_opt_id<'de, D>(deserializer: D) -> std::result::Result<Option<Value>, D::Error>
46where
47    D: serde::Deserializer<'de>,
48{
49    let v = Value::deserialize(deserializer)?;
50    Ok(if v.is_null() { None } else { Some(v) })
51}
52
53impl RpcRequest {
54    /// `true` if this is a notification (no id, no response expected).
55    pub fn is_notification(&self) -> bool {
56        self.id.is_none()
57    }
58
59    /// Strictly check the `jsonrpc` field is `"2.0"`.
60    pub fn version_ok(&self) -> bool {
61        self.jsonrpc == "2.0"
62    }
63}
64
65/// JSON-RPC 2.0 response object.
66///
67/// Only the Ok / Err variants cross the wire; `InternalError` is a
68/// transport-layer failure that is converted into a serialized
69/// `RpcResponse::Err` before reaching the client.
70#[derive(Debug, Serialize)]
71#[serde(untagged)]
72pub enum RpcResponse {
73    /// Successful response.
74    Ok(RpcSuccess),
75    /// Error response.
76    Err(RpcErrorBody),
77}
78
79#[derive(Debug, Serialize)]
80pub struct RpcSuccess {
81    pub jsonrpc: &'static str,
82    pub id: Value,
83    pub result: Value,
84}
85
86#[derive(Debug, Serialize)]
87pub struct RpcErrorBody {
88    pub jsonrpc: &'static str,
89    pub id: Value,
90    pub error: RpcErrorPayload,
91}
92
93/// The body of a JSON-RPC error reply. Serializes as
94/// `{"code": ..., "message": "..."}` per the spec.
95#[derive(Debug, Serialize)]
96pub struct RpcErrorPayload {
97    pub code: i32,
98    pub message: String,
99}
100
101impl RpcResponse {
102    /// Build a success response carrying `result` as the JSON value.
103    /// `id` is the value from the matching request.
104    pub fn ok(id: Value, result: Value) -> Self {
105        RpcResponse::Ok(RpcSuccess {
106            jsonrpc: "2.0",
107            id,
108            result,
109        })
110    }
111
112    /// Build an error response carrying the given [`RpcError`].
113    /// `id` is the value from the matching request (or `Value::Null`
114    /// if the request was unparsable).
115    pub fn err(id: Value, err: RpcError) -> Self {
116        RpcResponse::Err(RpcErrorBody {
117            jsonrpc: "2.0",
118            id,
119            error: RpcErrorPayload {
120                code: err.code(),
121                message: err.to_string(),
122            },
123        })
124    }
125}
126
127/// Notification pushed to audit subscribers. The wire shape mirrors
128/// `audit::event::AuditEvent::to_json` so subscribers can read both
129/// `audit` messages and the daemon's own [`RpcRequest`] messages from
130/// the same stream.
131#[derive(Debug, Serialize)]
132pub struct AuditNotification<'a> {
133    pub jsonrpc: &'static str,
134    pub method: &'static str,
135    pub params: AuditParams<'a>,
136}
137
138#[derive(Debug, Serialize)]
139pub struct AuditParams<'a> {
140    pub ts: &'a str,
141    pub event: &'a str,
142    /// Free-form event payload. The exact keys depend on the variant —
143    /// see [`crate::methods::audit::AuditEvent`] for the canonical shape.
144    #[serde(flatten)]
145    pub fields: &'a serde_json::Map<String, Value>,
146}
147
148impl<'a> AuditNotification<'a> {
149    /// Build a notification from a serialized audit event. The caller
150    /// has already produced the JSONL record (one line, no trailing
151    /// newline); we re-wrap it into the `params` envelope.
152    pub fn from_jsonl(
153        ts: &'a str,
154        event: &'a str,
155        fields: &'a serde_json::Map<String, Value>,
156    ) -> Self {
157        AuditNotification {
158            jsonrpc: "2.0",
159            method: "audit",
160            params: AuditParams { ts, event, fields },
161        }
162    }
163}