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}