Skip to main content

stdiobus_core/
message.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026-present Raman Marozau <raman@worktif.com>
3// Copyright (c) 2026-present stdiobus contributors
4
5//! JSON-RPC message types for stdio_bus protocol
6
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9
10/// JSON-RPC 2.0 request
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct JsonRpcRequest {
13    pub jsonrpc: String,
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub id: Option<Value>,
16    pub method: String,
17    #[serde(default, skip_serializing_if = "Option::is_none")]
18    pub params: Option<Value>,
19    #[serde(rename = "sessionId", skip_serializing_if = "Option::is_none")]
20    pub session_id: Option<String>,
21    #[serde(rename = "agentId", skip_serializing_if = "Option::is_none")]
22    pub agent_id: Option<String>,
23    #[serde(rename = "_ext", skip_serializing_if = "Option::is_none")]
24    pub extensions: Option<Value>,
25}
26
27impl JsonRpcRequest {
28    /// Create a new request
29    pub fn new(method: impl Into<String>, params: Option<Value>) -> Self {
30        Self {
31            jsonrpc: "2.0".to_string(),
32            id: Some(Value::String(uuid::Uuid::new_v4().to_string())),
33            method: method.into(),
34            params,
35            session_id: None,
36            agent_id: None,
37            extensions: None,
38        }
39    }
40
41    /// Create a notification (no id, no response expected)
42    pub fn notification(method: impl Into<String>, params: Option<Value>) -> Self {
43        Self {
44            jsonrpc: "2.0".to_string(),
45            id: None,
46            method: method.into(),
47            params,
48            session_id: None,
49            agent_id: None,
50            extensions: None,
51        }
52    }
53
54    /// Set session ID
55    pub fn with_session_id(mut self, session_id: impl Into<String>) -> Self {
56        self.session_id = Some(session_id.into());
57        self
58    }
59
60    /// Set agent ID for routing
61    pub fn with_agent_id(mut self, agent_id: impl Into<String>) -> Self {
62        self.agent_id = Some(agent_id.into());
63        self
64    }
65
66    /// Set extensions
67    pub fn with_extensions(mut self, extensions: Value) -> Self {
68        self.extensions = Some(extensions);
69        self
70    }
71
72    /// Check if this is a notification (no id)
73    pub fn is_notification(&self) -> bool {
74        self.id.is_none()
75    }
76}
77
78/// JSON-RPC 2.0 error object
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct JsonRpcError {
81    pub code: i32,
82    pub message: String,
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub data: Option<Value>,
85}
86
87impl JsonRpcError {
88    /// Standard parse error
89    pub fn parse_error() -> Self {
90        Self {
91            code: -32700,
92            message: "Parse error".to_string(),
93            data: None,
94        }
95    }
96
97    /// Standard invalid request
98    pub fn invalid_request() -> Self {
99        Self {
100            code: -32600,
101            message: "Invalid Request".to_string(),
102            data: None,
103        }
104    }
105
106    /// Standard method not found
107    pub fn method_not_found() -> Self {
108        Self {
109            code: -32601,
110            message: "Method not found".to_string(),
111            data: None,
112        }
113    }
114
115    /// Standard internal error
116    pub fn internal_error() -> Self {
117        Self {
118            code: -32603,
119            message: "Internal error".to_string(),
120            data: None,
121        }
122    }
123}
124
125/// JSON-RPC 2.0 response
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct JsonRpcResponse {
128    pub jsonrpc: String,
129    pub id: Value,
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub result: Option<Value>,
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub error: Option<JsonRpcError>,
134    #[serde(rename = "sessionId", skip_serializing_if = "Option::is_none")]
135    pub session_id: Option<String>,
136    #[serde(rename = "_ext", skip_serializing_if = "Option::is_none")]
137    pub extensions: Option<Value>,
138}
139
140impl JsonRpcResponse {
141    /// Create a success response
142    pub fn success(id: Value, result: Value) -> Self {
143        Self {
144            jsonrpc: "2.0".to_string(),
145            id,
146            result: Some(result),
147            error: None,
148            session_id: None,
149            extensions: None,
150        }
151    }
152
153    /// Create an error response
154    pub fn error(id: Value, error: JsonRpcError) -> Self {
155        Self {
156            jsonrpc: "2.0".to_string(),
157            id,
158            result: None,
159            error: Some(error),
160            session_id: None,
161            extensions: None,
162        }
163    }
164
165    /// Check if response is an error
166    pub fn is_error(&self) -> bool {
167        self.error.is_some()
168    }
169}
170
171/// Generic JSON-RPC message (can be request or response)
172#[derive(Debug, Clone, Serialize, Deserialize)]
173#[serde(untagged)]
174pub enum JsonRpcMessage {
175    Request(JsonRpcRequest),
176    Response(JsonRpcResponse),
177}
178
179impl JsonRpcMessage {
180    /// Parse from JSON string
181    pub fn parse(json: &str) -> Result<Self, serde_json::Error> {
182        serde_json::from_str(json)
183    }
184
185    /// Serialize to JSON string
186    pub fn to_json(&self) -> Result<String, serde_json::Error> {
187        serde_json::to_string(self)
188    }
189
190    /// Get the message ID if present
191    pub fn id(&self) -> Option<&Value> {
192        match self {
193            Self::Request(req) => req.id.as_ref(),
194            Self::Response(res) => Some(&res.id),
195        }
196    }
197}