461 lines
14 KiB
Rust
461 lines
14 KiB
Rust
//! JSON-RPC 2.0 types for the ACP Server
|
|
//!
|
|
//! This module implements JSON-RPC 2.0 request/response types according to
|
|
//! the specification at https://www.jsonrpc.org/specification.
|
|
//!
|
|
//! Key features:
|
|
//! - Support for both numeric and string IDs
|
|
//! - Batch request/response handling
|
|
//! - Proper serialization of null vs missing fields
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::error::JsonRpcErrorObject;
|
|
|
|
/// JSON-RPC protocol version constant
|
|
pub const JSONRPC_VERSION: &str = "2.0";
|
|
|
|
/// JSON-RPC request/response identifier
|
|
///
|
|
/// According to the JSON-RPC 2.0 spec, an id can be a String, Number,
|
|
/// or Null. This type uses an untagged enum to handle both string and
|
|
/// number identifiers.
|
|
///
|
|
/// Note: The spec recommends not using Null as an id for requests.
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
#[serde(untagged)]
|
|
pub enum JsonRpcId {
|
|
/// Numeric identifier (integer)
|
|
Number(i64),
|
|
|
|
/// String identifier
|
|
String(String),
|
|
|
|
/// Null identifier (typically used in error responses for invalid requests)
|
|
Null,
|
|
}
|
|
|
|
impl From<i64> for JsonRpcId {
|
|
fn from(n: i64) -> Self {
|
|
JsonRpcId::Number(n)
|
|
}
|
|
}
|
|
|
|
impl From<String> for JsonRpcId {
|
|
fn from(s: String) -> Self {
|
|
JsonRpcId::String(s)
|
|
}
|
|
}
|
|
|
|
impl From<&str> for JsonRpcId {
|
|
fn from(s: &str) -> Self {
|
|
JsonRpcId::String(s.to_string())
|
|
}
|
|
}
|
|
|
|
/// A JSON-RPC 2.0 request object
|
|
///
|
|
/// Represents a remote procedure call with optional parameters.
|
|
/// The `id` field determines whether this is a request (with id) or
|
|
/// notification (without id).
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct JsonRpcRequest {
|
|
/// JSON-RPC protocol version (must be "2.0")
|
|
pub jsonrpc: String,
|
|
|
|
/// A String containing the name of the method to be invoked
|
|
pub method: String,
|
|
|
|
/// Optional structured value that holds the parameter values
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub params: Option<serde_json::Value>,
|
|
|
|
/// Optional identifier established by the client
|
|
///
|
|
/// If absent, the request is a notification (no response expected)
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub id: Option<JsonRpcId>,
|
|
}
|
|
|
|
impl JsonRpcRequest {
|
|
/// Create a new JSON-RPC request
|
|
pub fn new(method: impl Into<String>, params: Option<serde_json::Value>, id: JsonRpcId) -> Self {
|
|
Self {
|
|
jsonrpc: JSONRPC_VERSION.to_string(),
|
|
method: method.into(),
|
|
params,
|
|
id: Some(id),
|
|
}
|
|
}
|
|
|
|
/// Create a new JSON-RPC notification (request without id)
|
|
pub fn notification(method: impl Into<String>, params: Option<serde_json::Value>) -> Self {
|
|
Self {
|
|
jsonrpc: JSONRPC_VERSION.to_string(),
|
|
method: method.into(),
|
|
params,
|
|
id: None,
|
|
}
|
|
}
|
|
|
|
/// Check if this request is a notification (no id)
|
|
pub fn is_notification(&self) -> bool {
|
|
self.id.is_none()
|
|
}
|
|
|
|
/// Validate the request format
|
|
pub fn validate(&self) -> Result<(), String> {
|
|
if self.jsonrpc != JSONRPC_VERSION {
|
|
return Err(format!(
|
|
"Invalid JSON-RPC version: expected '{}', got '{}'",
|
|
JSONRPC_VERSION, self.jsonrpc
|
|
));
|
|
}
|
|
|
|
if self.method.is_empty() {
|
|
return Err("Method name cannot be empty".to_string());
|
|
}
|
|
|
|
// Methods starting with "rpc." are reserved for internal use
|
|
if self.method.starts_with("rpc.") {
|
|
return Err(format!(
|
|
"Method name '{}' is reserved (starts with 'rpc.')",
|
|
self.method
|
|
));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// A JSON-RPC 2.0 response object
|
|
///
|
|
/// Contains either a result (success) or an error (failure), never both.
|
|
/// The id must match the corresponding request id.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct JsonRpcResponse {
|
|
/// JSON-RPC protocol version (must be "2.0")
|
|
pub jsonrpc: String,
|
|
|
|
/// The result of the call (on success)
|
|
///
|
|
/// This member is REQUIRED on success and MUST NOT exist on error.
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub result: Option<serde_json::Value>,
|
|
|
|
/// The error object (on failure)
|
|
///
|
|
/// This member is REQUIRED on error and MUST NOT exist on success.
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub error: Option<JsonRpcErrorObject>,
|
|
|
|
/// The identifier matching the request
|
|
///
|
|
/// If there was an error detecting the id in the Request object
|
|
/// (e.g. Parse error/Invalid Request), it MUST be Null.
|
|
pub id: JsonRpcId,
|
|
}
|
|
|
|
impl JsonRpcResponse {
|
|
/// Create a successful response
|
|
pub fn success(result: serde_json::Value, id: JsonRpcId) -> Self {
|
|
Self {
|
|
jsonrpc: JSONRPC_VERSION.to_string(),
|
|
result: Some(result),
|
|
error: None,
|
|
id,
|
|
}
|
|
}
|
|
|
|
/// Create an error response
|
|
pub fn error(error: JsonRpcErrorObject, id: JsonRpcId) -> Self {
|
|
Self {
|
|
jsonrpc: JSONRPC_VERSION.to_string(),
|
|
result: None,
|
|
error: Some(error),
|
|
id,
|
|
}
|
|
}
|
|
|
|
/// Create an error response with a null id (for parse errors)
|
|
pub fn error_with_null_id(error: JsonRpcErrorObject) -> Self {
|
|
Self::error(error, JsonRpcId::Null)
|
|
}
|
|
|
|
/// Check if this response represents success
|
|
pub fn is_success(&self) -> bool {
|
|
self.result.is_some() && self.error.is_none()
|
|
}
|
|
|
|
/// Check if this response represents an error
|
|
pub fn is_error(&self) -> bool {
|
|
self.error.is_some()
|
|
}
|
|
}
|
|
|
|
/// Represents either a single request or a batch of requests
|
|
///
|
|
/// The JSON-RPC 2.0 spec allows sending multiple requests in a single
|
|
/// JSON array for batch processing.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(untagged)]
|
|
pub enum JsonRpcRequestBatch {
|
|
/// A single request
|
|
Single(JsonRpcRequest),
|
|
|
|
/// A batch of requests
|
|
Batch(Vec<JsonRpcRequest>),
|
|
}
|
|
|
|
impl JsonRpcRequestBatch {
|
|
/// Check if this is an empty batch
|
|
pub fn is_empty(&self) -> bool {
|
|
match self {
|
|
JsonRpcRequestBatch::Single(_) => false,
|
|
JsonRpcRequestBatch::Batch(batch) => batch.is_empty(),
|
|
}
|
|
}
|
|
|
|
/// Get the number of requests
|
|
pub fn len(&self) -> usize {
|
|
match self {
|
|
JsonRpcRequestBatch::Single(_) => 1,
|
|
JsonRpcRequestBatch::Batch(batch) => batch.len(),
|
|
}
|
|
}
|
|
|
|
/// Convert to a vector of requests
|
|
pub fn into_vec(self) -> Vec<JsonRpcRequest> {
|
|
match self {
|
|
JsonRpcRequestBatch::Single(req) => vec![req],
|
|
JsonRpcRequestBatch::Batch(batch) => batch,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Represents either a single response or a batch of responses
|
|
///
|
|
/// The response format must match the request format: single request
|
|
/// gets single response, batch request gets batch response.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(untagged)]
|
|
pub enum JsonRpcResponseBatch {
|
|
/// A single response
|
|
Single(JsonRpcResponse),
|
|
|
|
/// A batch of responses
|
|
Batch(Vec<JsonRpcResponse>),
|
|
}
|
|
|
|
impl JsonRpcResponseBatch {
|
|
/// Create a batch response from a vector
|
|
///
|
|
/// Returns Single if there's exactly one response, otherwise Batch.
|
|
pub fn from_vec(responses: Vec<JsonRpcResponse>) -> Self {
|
|
if responses.len() == 1 {
|
|
JsonRpcResponseBatch::Single(responses.into_iter().next().unwrap())
|
|
} else {
|
|
JsonRpcResponseBatch::Batch(responses)
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use serde_json::json;
|
|
|
|
#[test]
|
|
fn test_jsonrpc_id_number() {
|
|
let id = JsonRpcId::Number(42);
|
|
let json = serde_json::to_string(&id).unwrap();
|
|
assert_eq!(json, "42");
|
|
|
|
let parsed: JsonRpcId = serde_json::from_str(&json).unwrap();
|
|
assert_eq!(parsed, id);
|
|
}
|
|
|
|
#[test]
|
|
fn test_jsonrpc_id_string() {
|
|
let id = JsonRpcId::String("abc-123".to_string());
|
|
let json = serde_json::to_string(&id).unwrap();
|
|
assert_eq!(json, "\"abc-123\"");
|
|
|
|
let parsed: JsonRpcId = serde_json::from_str(&json).unwrap();
|
|
assert_eq!(parsed, id);
|
|
}
|
|
|
|
#[test]
|
|
fn test_jsonrpc_id_null() {
|
|
let id = JsonRpcId::Null;
|
|
let json = serde_json::to_string(&id).unwrap();
|
|
assert_eq!(json, "null");
|
|
|
|
let parsed: JsonRpcId = serde_json::from_str(&json).unwrap();
|
|
assert_eq!(parsed, id);
|
|
}
|
|
|
|
#[test]
|
|
fn test_jsonrpc_id_from_conversions() {
|
|
let id: JsonRpcId = 42i64.into();
|
|
assert_eq!(id, JsonRpcId::Number(42));
|
|
|
|
let id: JsonRpcId = "test".into();
|
|
assert_eq!(id, JsonRpcId::String("test".to_string()));
|
|
|
|
let id: JsonRpcId = String::from("owned").into();
|
|
assert_eq!(id, JsonRpcId::String("owned".to_string()));
|
|
}
|
|
|
|
#[test]
|
|
fn test_request_creation() {
|
|
let req = JsonRpcRequest::new("session.new", Some(json!({"title": "Test"})), 1.into());
|
|
assert_eq!(req.jsonrpc, "2.0");
|
|
assert_eq!(req.method, "session.new");
|
|
assert!(req.params.is_some());
|
|
assert_eq!(req.id, Some(JsonRpcId::Number(1)));
|
|
assert!(!req.is_notification());
|
|
}
|
|
|
|
#[test]
|
|
fn test_notification_creation() {
|
|
let notif = JsonRpcRequest::notification("event.ping", None);
|
|
assert!(notif.is_notification());
|
|
assert_eq!(notif.id, None);
|
|
}
|
|
|
|
#[test]
|
|
fn test_request_validation() {
|
|
let valid = JsonRpcRequest::new("test.method", None, 1.into());
|
|
assert!(valid.validate().is_ok());
|
|
|
|
// Invalid version
|
|
let mut invalid_version = valid.clone();
|
|
invalid_version.jsonrpc = "1.0".to_string();
|
|
assert!(invalid_version.validate().is_err());
|
|
|
|
// Empty method
|
|
let mut empty_method = valid.clone();
|
|
empty_method.method = String::new();
|
|
assert!(empty_method.validate().is_err());
|
|
|
|
// Reserved method
|
|
let mut reserved = valid.clone();
|
|
reserved.method = "rpc.internal".to_string();
|
|
assert!(reserved.validate().is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_request_serialization() {
|
|
let req = JsonRpcRequest::new(
|
|
"session.prompt",
|
|
Some(json!({"session_id": "abc", "content": "Hello"})),
|
|
"req-123".into(),
|
|
);
|
|
|
|
let json = serde_json::to_string(&req).unwrap();
|
|
assert!(json.contains("\"jsonrpc\":\"2.0\""));
|
|
assert!(json.contains("\"method\":\"session.prompt\""));
|
|
assert!(json.contains("\"id\":\"req-123\""));
|
|
|
|
// Deserialize back
|
|
let parsed: JsonRpcRequest = serde_json::from_str(&json).unwrap();
|
|
assert_eq!(parsed.method, "session.prompt");
|
|
}
|
|
|
|
#[test]
|
|
fn test_response_success() {
|
|
let resp = JsonRpcResponse::success(json!({"session_id": "new-123"}), 1.into());
|
|
assert!(resp.is_success());
|
|
assert!(!resp.is_error());
|
|
assert_eq!(resp.result, Some(json!({"session_id": "new-123"})));
|
|
assert!(resp.error.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_response_error() {
|
|
let error = JsonRpcErrorObject::method_not_found("unknown.method");
|
|
let resp = JsonRpcResponse::error(error, 1.into());
|
|
assert!(!resp.is_success());
|
|
assert!(resp.is_error());
|
|
assert!(resp.result.is_none());
|
|
assert!(resp.error.is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn test_response_serialization() {
|
|
// Success response
|
|
let success = JsonRpcResponse::success(json!({"ok": true}), 42.into());
|
|
let json = serde_json::to_string(&success).unwrap();
|
|
assert!(json.contains("\"result\""));
|
|
assert!(!json.contains("\"error\""));
|
|
|
|
// Error response
|
|
let error = JsonRpcResponse::error(
|
|
JsonRpcErrorObject::internal_error("Something broke"),
|
|
42.into(),
|
|
);
|
|
let json = serde_json::to_string(&error).unwrap();
|
|
assert!(!json.contains("\"result\""));
|
|
assert!(json.contains("\"error\""));
|
|
}
|
|
|
|
#[test]
|
|
fn test_batch_request_single() {
|
|
let req = JsonRpcRequest::new("test", None, 1.into());
|
|
let batch = JsonRpcRequestBatch::Single(req);
|
|
assert_eq!(batch.len(), 1);
|
|
assert!(!batch.is_empty());
|
|
|
|
let vec = batch.into_vec();
|
|
assert_eq!(vec.len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_batch_request_multiple() {
|
|
let req1 = JsonRpcRequest::new("test1", None, 1.into());
|
|
let req2 = JsonRpcRequest::new("test2", None, 2.into());
|
|
let batch = JsonRpcRequestBatch::Batch(vec![req1, req2]);
|
|
assert_eq!(batch.len(), 2);
|
|
|
|
let vec = batch.into_vec();
|
|
assert_eq!(vec.len(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_batch_request_empty() {
|
|
let batch = JsonRpcRequestBatch::Batch(vec![]);
|
|
assert!(batch.is_empty());
|
|
assert_eq!(batch.len(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_batch_request_deserialization() {
|
|
// Single request
|
|
let single_json = r#"{"jsonrpc":"2.0","method":"test","id":1}"#;
|
|
let single: JsonRpcRequestBatch = serde_json::from_str(single_json).unwrap();
|
|
assert_eq!(single.len(), 1);
|
|
|
|
// Batch request
|
|
let batch_json = r#"[{"jsonrpc":"2.0","method":"test1","id":1},{"jsonrpc":"2.0","method":"test2","id":2}]"#;
|
|
let batch: JsonRpcRequestBatch = serde_json::from_str(batch_json).unwrap();
|
|
assert_eq!(batch.len(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_batch_response_from_vec() {
|
|
// Single response becomes Single variant
|
|
let responses = vec![JsonRpcResponse::success(json!(null), 1.into())];
|
|
let batch = JsonRpcResponseBatch::from_vec(responses);
|
|
matches!(batch, JsonRpcResponseBatch::Single(_));
|
|
|
|
// Multiple responses become Batch variant
|
|
let responses = vec![
|
|
JsonRpcResponse::success(json!(null), 1.into()),
|
|
JsonRpcResponse::success(json!(null), 2.into()),
|
|
];
|
|
let batch = JsonRpcResponseBatch::from_vec(responses);
|
|
matches!(batch, JsonRpcResponseBatch::Batch(_));
|
|
}
|
|
}
|