spikard 0.6.2 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +90 -508
- data/ext/spikard_rb/Cargo.lock +3287 -0
- data/ext/spikard_rb/Cargo.toml +1 -1
- data/ext/spikard_rb/extconf.rb +3 -3
- data/lib/spikard/app.rb +72 -49
- data/lib/spikard/background.rb +38 -7
- data/lib/spikard/testing.rb +42 -4
- data/lib/spikard/version.rb +1 -1
- data/sig/spikard.rbs +4 -0
- data/vendor/crates/spikard-bindings-shared/Cargo.toml +1 -1
- data/vendor/crates/spikard-bindings-shared/tests/config_extractor_behavior.rs +191 -0
- data/vendor/crates/spikard-core/Cargo.toml +1 -1
- data/vendor/crates/spikard-core/src/http.rs +1 -0
- data/vendor/crates/spikard-core/src/lifecycle.rs +63 -0
- data/vendor/crates/spikard-core/tests/bindings_response_tests.rs +136 -0
- data/vendor/crates/spikard-core/tests/di_dependency_defaults.rs +37 -0
- data/vendor/crates/spikard-core/tests/error_mapper.rs +761 -0
- data/vendor/crates/spikard-core/tests/parameters_edge_cases.rs +106 -0
- data/vendor/crates/spikard-core/tests/parameters_full.rs +701 -0
- data/vendor/crates/spikard-core/tests/parameters_schema_and_formats.rs +301 -0
- data/vendor/crates/spikard-core/tests/request_data_roundtrip.rs +67 -0
- data/vendor/crates/spikard-core/tests/validation_coverage.rs +250 -0
- data/vendor/crates/spikard-core/tests/validation_error_paths.rs +45 -0
- data/vendor/crates/spikard-http/Cargo.toml +1 -1
- data/vendor/crates/spikard-http/src/jsonrpc/http_handler.rs +502 -0
- data/vendor/crates/spikard-http/src/jsonrpc/method_registry.rs +648 -0
- data/vendor/crates/spikard-http/src/jsonrpc/mod.rs +58 -0
- data/vendor/crates/spikard-http/src/jsonrpc/protocol.rs +1207 -0
- data/vendor/crates/spikard-http/src/jsonrpc/router.rs +2262 -0
- data/vendor/crates/spikard-http/src/testing/test_client.rs +155 -2
- data/vendor/crates/spikard-http/src/testing.rs +171 -0
- data/vendor/crates/spikard-http/src/websocket.rs +79 -6
- data/vendor/crates/spikard-http/tests/auth_integration.rs +647 -0
- data/vendor/crates/spikard-http/tests/common/test_builders.rs +633 -0
- data/vendor/crates/spikard-http/tests/di_handler_error_responses.rs +162 -0
- data/vendor/crates/spikard-http/tests/middleware_stack_integration.rs +389 -0
- data/vendor/crates/spikard-http/tests/request_extraction_full.rs +513 -0
- data/vendor/crates/spikard-http/tests/server_auth_middleware_behavior.rs +244 -0
- data/vendor/crates/spikard-http/tests/server_configured_router_behavior.rs +200 -0
- data/vendor/crates/spikard-http/tests/server_cors_preflight.rs +82 -0
- data/vendor/crates/spikard-http/tests/server_handler_wrappers.rs +464 -0
- data/vendor/crates/spikard-http/tests/server_method_router_additional_behavior.rs +286 -0
- data/vendor/crates/spikard-http/tests/server_method_router_coverage.rs +118 -0
- data/vendor/crates/spikard-http/tests/server_middleware_behavior.rs +99 -0
- data/vendor/crates/spikard-http/tests/server_middleware_branches.rs +206 -0
- data/vendor/crates/spikard-http/tests/server_openapi_jsonrpc_static.rs +281 -0
- data/vendor/crates/spikard-http/tests/server_router_behavior.rs +121 -0
- data/vendor/crates/spikard-http/tests/sse_full_behavior.rs +584 -0
- data/vendor/crates/spikard-http/tests/sse_handler_behavior.rs +130 -0
- data/vendor/crates/spikard-http/tests/test_client_requests.rs +167 -0
- data/vendor/crates/spikard-http/tests/testing_helpers.rs +87 -0
- data/vendor/crates/spikard-http/tests/testing_module_coverage.rs +156 -0
- data/vendor/crates/spikard-http/tests/urlencoded_content_type.rs +82 -0
- data/vendor/crates/spikard-http/tests/websocket_full_behavior.rs +440 -0
- data/vendor/crates/spikard-http/tests/websocket_integration.rs +152 -0
- data/vendor/crates/spikard-rb/Cargo.toml +1 -1
- data/vendor/crates/spikard-rb/src/gvl.rs +80 -0
- data/vendor/crates/spikard-rb/src/handler.rs +12 -9
- data/vendor/crates/spikard-rb/src/lib.rs +137 -124
- data/vendor/crates/spikard-rb/src/request.rs +342 -0
- data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +1 -8
- data/vendor/crates/spikard-rb/src/server.rs +1 -8
- data/vendor/crates/spikard-rb/src/testing/client.rs +168 -9
- data/vendor/crates/spikard-rb/src/websocket.rs +119 -30
- data/vendor/crates/spikard-rb-macros/Cargo.toml +14 -0
- data/vendor/crates/spikard-rb-macros/src/lib.rs +52 -0
- metadata +44 -1
|
@@ -0,0 +1,1207 @@
|
|
|
1
|
+
//! JSON-RPC 2.0 Protocol Types
|
|
2
|
+
//!
|
|
3
|
+
//! This module provides type definitions for the JSON-RPC 2.0 specification.
|
|
4
|
+
//! See [JSON-RPC 2.0 Specification](https://www.jsonrpc.org/specification)
|
|
5
|
+
//!
|
|
6
|
+
//! # Overview
|
|
7
|
+
//!
|
|
8
|
+
//! JSON-RPC is a stateless, light-weight remote procedure call (RPC) protocol.
|
|
9
|
+
//! This module implements the complete specification including:
|
|
10
|
+
//!
|
|
11
|
+
//! - Request/Response messages
|
|
12
|
+
//! - Standard error codes
|
|
13
|
+
//! - Helper constructors for building valid messages
|
|
14
|
+
//! - Full serialization support via serde
|
|
15
|
+
|
|
16
|
+
use serde::{Deserialize, Serialize};
|
|
17
|
+
use serde_json::Value;
|
|
18
|
+
|
|
19
|
+
/// Maximum allowed length for a JSON-RPC method name
|
|
20
|
+
const MAX_METHOD_NAME_LENGTH: usize = 255;
|
|
21
|
+
|
|
22
|
+
/// Validates a JSON-RPC method name for security and correctness
|
|
23
|
+
///
|
|
24
|
+
/// This function prevents DoS attacks through method name validation by enforcing:
|
|
25
|
+
/// - Maximum length of 255 characters to prevent resource exhaustion
|
|
26
|
+
/// - Only allowed characters: alphanumeric (a-z, A-Z, 0-9), dot (.), underscore (_), hyphen (-)
|
|
27
|
+
/// - No control characters (0x00-0x1F, 0x7F) to prevent injection attacks
|
|
28
|
+
/// - No leading or trailing whitespace to ensure proper formatting
|
|
29
|
+
///
|
|
30
|
+
/// # Arguments
|
|
31
|
+
///
|
|
32
|
+
/// * `method_name` - The method name to validate
|
|
33
|
+
///
|
|
34
|
+
/// # Returns
|
|
35
|
+
///
|
|
36
|
+
/// * `Ok(())` - If the method name is valid
|
|
37
|
+
/// * `Err(String)` - If the method name is invalid with a descriptive error message
|
|
38
|
+
///
|
|
39
|
+
/// # Example
|
|
40
|
+
///
|
|
41
|
+
/// ```ignore
|
|
42
|
+
/// use spikard_http::jsonrpc::validate_method_name;
|
|
43
|
+
///
|
|
44
|
+
/// // Valid method names
|
|
45
|
+
/// assert!(validate_method_name("user.getById").is_ok());
|
|
46
|
+
/// assert!(validate_method_name("calculate_sum").is_ok());
|
|
47
|
+
/// assert!(validate_method_name("api-v1-handler").is_ok());
|
|
48
|
+
/// assert!(validate_method_name("rpc1").is_ok());
|
|
49
|
+
///
|
|
50
|
+
/// // Invalid method names
|
|
51
|
+
/// assert!(validate_method_name("").is_err()); // Empty
|
|
52
|
+
/// assert!(validate_method_name(" method").is_err()); // Leading whitespace
|
|
53
|
+
/// assert!(validate_method_name("method ").is_err()); // Trailing whitespace
|
|
54
|
+
/// assert!(validate_method_name("method\x00name").is_err()); // Control character
|
|
55
|
+
/// assert!(validate_method_name("a".repeat(256)).is_err()); // Too long
|
|
56
|
+
/// ```
|
|
57
|
+
pub fn validate_method_name(method_name: &str) -> Result<(), String> {
|
|
58
|
+
if method_name.is_empty() {
|
|
59
|
+
return Err("Method name cannot be empty".to_string());
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if method_name.starts_with(char::is_whitespace) || method_name.ends_with(char::is_whitespace) {
|
|
63
|
+
return Err("Method name cannot have leading or trailing whitespace".to_string());
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if method_name.len() > MAX_METHOD_NAME_LENGTH {
|
|
67
|
+
return Err(format!(
|
|
68
|
+
"Method name exceeds maximum length of {} characters (got {})",
|
|
69
|
+
MAX_METHOD_NAME_LENGTH,
|
|
70
|
+
method_name.len()
|
|
71
|
+
));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for ch in method_name.chars() {
|
|
75
|
+
match ch {
|
|
76
|
+
'a'..='z' | 'A'..='Z' | '0'..='9' => {}
|
|
77
|
+
'.' => {}
|
|
78
|
+
'_' => {}
|
|
79
|
+
'-' => {}
|
|
80
|
+
c if (c as u32) < 0x20 || (c as u32) == 0x7F => {
|
|
81
|
+
return Err(format!(
|
|
82
|
+
"Method name contains invalid control character: 0x{:02X}",
|
|
83
|
+
c as u32
|
|
84
|
+
));
|
|
85
|
+
}
|
|
86
|
+
c => {
|
|
87
|
+
return Err(format!(
|
|
88
|
+
"Method name contains invalid character: '{}' (0x{:02X}). \
|
|
89
|
+
Only alphanumeric, dot (.), underscore (_), and hyphen (-) are allowed",
|
|
90
|
+
c, c as u32
|
|
91
|
+
));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
Ok(())
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/// JSON-RPC 2.0 Request
|
|
100
|
+
///
|
|
101
|
+
/// Represents a JSON-RPC request method invocation with optional parameters and identifier.
|
|
102
|
+
///
|
|
103
|
+
/// # Fields
|
|
104
|
+
///
|
|
105
|
+
/// * `jsonrpc` - A String specifying the JSON-RPC version. MUST be exactly "2.0"
|
|
106
|
+
/// * `method` - A String containing the name of the method to be invoked
|
|
107
|
+
/// * `params` - Optional structured data that serves as arguments to the method.
|
|
108
|
+
/// The order of the objects in the Array is significant to the method.
|
|
109
|
+
/// * `id` - A value which is used to match the response with the request that it is replying to.
|
|
110
|
+
/// Can be a string, number, or NULL. Notifications MUST NOT include an "id".
|
|
111
|
+
///
|
|
112
|
+
/// # Example
|
|
113
|
+
///
|
|
114
|
+
/// ```ignore
|
|
115
|
+
/// use serde_json::json;
|
|
116
|
+
/// use spikard_http::jsonrpc::JsonRpcRequest;
|
|
117
|
+
///
|
|
118
|
+
/// // Request with parameters and ID
|
|
119
|
+
/// let req = JsonRpcRequest::new("add", Some(json!([1, 2])), Some(json!(1)));
|
|
120
|
+
/// assert!(!req.is_notification());
|
|
121
|
+
///
|
|
122
|
+
/// // Notification (no ID)
|
|
123
|
+
/// let notif = JsonRpcRequest::new("notify", Some(json!({})), None);
|
|
124
|
+
/// assert!(notif.is_notification());
|
|
125
|
+
/// ```
|
|
126
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
127
|
+
pub struct JsonRpcRequest {
|
|
128
|
+
/// JSON-RPC version, must be "2.0"
|
|
129
|
+
pub jsonrpc: String,
|
|
130
|
+
|
|
131
|
+
/// The name of the method to invoke
|
|
132
|
+
pub method: String,
|
|
133
|
+
|
|
134
|
+
/// Optional parameters for the method
|
|
135
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
136
|
+
pub params: Option<Value>,
|
|
137
|
+
|
|
138
|
+
/// Optional request identifier. When absent, this is a notification.
|
|
139
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
140
|
+
pub id: Option<Value>,
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
impl JsonRpcRequest {
|
|
144
|
+
/// Creates a new JSON-RPC 2.0 request
|
|
145
|
+
///
|
|
146
|
+
/// # Arguments
|
|
147
|
+
///
|
|
148
|
+
/// * `method` - The method name to invoke
|
|
149
|
+
/// * `params` - Optional parameters (can be array, object, or null)
|
|
150
|
+
/// * `id` - Optional request identifier (string, number, or null)
|
|
151
|
+
///
|
|
152
|
+
/// # Example
|
|
153
|
+
///
|
|
154
|
+
/// ```ignore
|
|
155
|
+
/// use serde_json::json;
|
|
156
|
+
/// use spikard_http::jsonrpc::JsonRpcRequest;
|
|
157
|
+
///
|
|
158
|
+
/// let req = JsonRpcRequest::new("subtract", Some(json!({"a": 5, "b": 3})), Some(json!(2)));
|
|
159
|
+
/// assert_eq!(req.method, "subtract");
|
|
160
|
+
/// assert!(!req.is_notification());
|
|
161
|
+
/// ```
|
|
162
|
+
pub fn new(method: impl Into<String>, params: Option<Value>, id: Option<Value>) -> Self {
|
|
163
|
+
Self {
|
|
164
|
+
jsonrpc: "2.0".to_string(),
|
|
165
|
+
method: method.into(),
|
|
166
|
+
params,
|
|
167
|
+
id,
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/// Checks if this request is a notification
|
|
172
|
+
///
|
|
173
|
+
/// A notification is a JSON-RPC request without an "id" field.
|
|
174
|
+
/// The server MUST NOT reply to a notification.
|
|
175
|
+
///
|
|
176
|
+
/// # Example
|
|
177
|
+
///
|
|
178
|
+
/// ```ignore
|
|
179
|
+
/// use spikard_http::jsonrpc::JsonRpcRequest;
|
|
180
|
+
///
|
|
181
|
+
/// let req = JsonRpcRequest::new("method", None, Some(serde_json::json!(1)));
|
|
182
|
+
/// assert!(!req.is_notification());
|
|
183
|
+
///
|
|
184
|
+
/// let notif = JsonRpcRequest::new("notify", None, None);
|
|
185
|
+
/// assert!(notif.is_notification());
|
|
186
|
+
/// ```
|
|
187
|
+
pub fn is_notification(&self) -> bool {
|
|
188
|
+
self.id.is_none()
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/// JSON-RPC 2.0 Success Response
|
|
193
|
+
///
|
|
194
|
+
/// Represents a successful JSON-RPC response containing the result of the method invocation.
|
|
195
|
+
///
|
|
196
|
+
/// # Fields
|
|
197
|
+
///
|
|
198
|
+
/// * `jsonrpc` - A String specifying the JSON-RPC version. MUST be exactly "2.0"
|
|
199
|
+
/// * `result` - The result of the method invocation. This MUST be null in case of an error.
|
|
200
|
+
/// * `id` - This MUST be the same id as the request it is responding to
|
|
201
|
+
///
|
|
202
|
+
/// # Example
|
|
203
|
+
///
|
|
204
|
+
/// ```ignore
|
|
205
|
+
/// use serde_json::json;
|
|
206
|
+
/// use spikard_http::jsonrpc::JsonRpcResponse;
|
|
207
|
+
///
|
|
208
|
+
/// let response = JsonRpcResponse::success(json!(19), json!(1));
|
|
209
|
+
/// assert_eq!(response.jsonrpc, "2.0");
|
|
210
|
+
/// ```
|
|
211
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
212
|
+
pub struct JsonRpcResponse {
|
|
213
|
+
/// JSON-RPC version, must be "2.0"
|
|
214
|
+
pub jsonrpc: String,
|
|
215
|
+
|
|
216
|
+
/// The result of the method invocation
|
|
217
|
+
pub result: Value,
|
|
218
|
+
|
|
219
|
+
/// The request identifier this response corresponds to
|
|
220
|
+
pub id: Value,
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
impl JsonRpcResponse {
|
|
224
|
+
/// Creates a new JSON-RPC 2.0 success response
|
|
225
|
+
///
|
|
226
|
+
/// # Arguments
|
|
227
|
+
///
|
|
228
|
+
/// * `result` - The result value from the method invocation
|
|
229
|
+
/// * `id` - The request identifier from the original request
|
|
230
|
+
///
|
|
231
|
+
/// # Example
|
|
232
|
+
///
|
|
233
|
+
/// ```ignore
|
|
234
|
+
/// use serde_json::json;
|
|
235
|
+
/// use spikard_http::jsonrpc::JsonRpcResponse;
|
|
236
|
+
///
|
|
237
|
+
/// let response = JsonRpcResponse::success(json!({"sum": 7}), json!("abc"));
|
|
238
|
+
/// assert_eq!(response.jsonrpc, "2.0");
|
|
239
|
+
/// ```
|
|
240
|
+
pub fn success(result: Value, id: Value) -> Self {
|
|
241
|
+
Self {
|
|
242
|
+
jsonrpc: "2.0".to_string(),
|
|
243
|
+
result,
|
|
244
|
+
id,
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/// JSON-RPC 2.0 Error Object
|
|
250
|
+
///
|
|
251
|
+
/// Represents a JSON-RPC error that occurred during method invocation.
|
|
252
|
+
///
|
|
253
|
+
/// # Fields
|
|
254
|
+
///
|
|
255
|
+
/// * `code` - A Number that indicates the error type that occurred
|
|
256
|
+
/// * `message` - A String providing a short description of the error
|
|
257
|
+
/// * `data` - Optional additional error information
|
|
258
|
+
///
|
|
259
|
+
/// # Standard Error Codes
|
|
260
|
+
///
|
|
261
|
+
/// - `-32700`: Parse error
|
|
262
|
+
/// - `-32600`: Invalid Request
|
|
263
|
+
/// - `-32601`: Method not found
|
|
264
|
+
/// - `-32602`: Invalid params
|
|
265
|
+
/// - `-32603`: Internal error
|
|
266
|
+
/// - `-32000 to -32099`: Server error (reserved)
|
|
267
|
+
///
|
|
268
|
+
/// # Example
|
|
269
|
+
///
|
|
270
|
+
/// ```ignore
|
|
271
|
+
/// use spikard_http::jsonrpc::{JsonRpcError, error_codes};
|
|
272
|
+
///
|
|
273
|
+
/// let err = JsonRpcError {
|
|
274
|
+
/// code: error_codes::INVALID_PARAMS,
|
|
275
|
+
/// message: "Invalid method parameters".to_string(),
|
|
276
|
+
/// data: None,
|
|
277
|
+
/// };
|
|
278
|
+
/// ```
|
|
279
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
280
|
+
pub struct JsonRpcError {
|
|
281
|
+
/// Numeric error code
|
|
282
|
+
pub code: i32,
|
|
283
|
+
|
|
284
|
+
/// Human-readable error description
|
|
285
|
+
pub message: String,
|
|
286
|
+
|
|
287
|
+
/// Optional additional error context
|
|
288
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
289
|
+
pub data: Option<Value>,
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/// JSON-RPC 2.0 Error Response
|
|
293
|
+
///
|
|
294
|
+
/// Represents a JSON-RPC response containing an error result.
|
|
295
|
+
///
|
|
296
|
+
/// # Fields
|
|
297
|
+
///
|
|
298
|
+
/// * `jsonrpc` - A String specifying the JSON-RPC version. MUST be exactly "2.0"
|
|
299
|
+
/// * `error` - An Error Object with error information
|
|
300
|
+
/// * `id` - This MUST be the same id as the request it is responding to
|
|
301
|
+
///
|
|
302
|
+
/// # Example
|
|
303
|
+
///
|
|
304
|
+
/// ```ignore
|
|
305
|
+
/// use serde_json::json;
|
|
306
|
+
/// use spikard_http::jsonrpc::{JsonRpcErrorResponse, error_codes};
|
|
307
|
+
///
|
|
308
|
+
/// let err_response = JsonRpcErrorResponse::error(
|
|
309
|
+
/// error_codes::METHOD_NOT_FOUND,
|
|
310
|
+
/// "Method not found",
|
|
311
|
+
/// json!(1)
|
|
312
|
+
/// );
|
|
313
|
+
/// assert_eq!(err_response.jsonrpc, "2.0");
|
|
314
|
+
/// ```
|
|
315
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
316
|
+
pub struct JsonRpcErrorResponse {
|
|
317
|
+
/// JSON-RPC version, must be "2.0"
|
|
318
|
+
pub jsonrpc: String,
|
|
319
|
+
|
|
320
|
+
/// Error object containing error information
|
|
321
|
+
pub error: JsonRpcError,
|
|
322
|
+
|
|
323
|
+
/// The request identifier this response corresponds to
|
|
324
|
+
pub id: Value,
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
impl JsonRpcErrorResponse {
|
|
328
|
+
/// Creates a new JSON-RPC 2.0 error response
|
|
329
|
+
///
|
|
330
|
+
/// # Arguments
|
|
331
|
+
///
|
|
332
|
+
/// * `code` - The numeric error code
|
|
333
|
+
/// * `message` - The error message
|
|
334
|
+
/// * `id` - The request identifier from the original request
|
|
335
|
+
///
|
|
336
|
+
/// # Example
|
|
337
|
+
///
|
|
338
|
+
/// ```ignore
|
|
339
|
+
/// use serde_json::json;
|
|
340
|
+
/// use spikard_http::jsonrpc::{JsonRpcErrorResponse, error_codes};
|
|
341
|
+
///
|
|
342
|
+
/// let response = JsonRpcErrorResponse::error(
|
|
343
|
+
/// error_codes::METHOD_NOT_FOUND,
|
|
344
|
+
/// "Unknown method",
|
|
345
|
+
/// json!(null)
|
|
346
|
+
/// );
|
|
347
|
+
/// assert_eq!(response.jsonrpc, "2.0");
|
|
348
|
+
/// ```
|
|
349
|
+
pub fn error(code: i32, message: impl Into<String>, id: Value) -> Self {
|
|
350
|
+
Self {
|
|
351
|
+
jsonrpc: "2.0".to_string(),
|
|
352
|
+
error: JsonRpcError {
|
|
353
|
+
code,
|
|
354
|
+
message: message.into(),
|
|
355
|
+
data: None,
|
|
356
|
+
},
|
|
357
|
+
id,
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/// Creates a new JSON-RPC 2.0 error response with additional error data
|
|
362
|
+
///
|
|
363
|
+
/// # Arguments
|
|
364
|
+
///
|
|
365
|
+
/// * `code` - The numeric error code
|
|
366
|
+
/// * `message` - The error message
|
|
367
|
+
/// * `data` - Additional context about the error
|
|
368
|
+
/// * `id` - The request identifier from the original request
|
|
369
|
+
///
|
|
370
|
+
/// # Example
|
|
371
|
+
///
|
|
372
|
+
/// ```ignore
|
|
373
|
+
/// use serde_json::json;
|
|
374
|
+
/// use spikard_http::jsonrpc::{JsonRpcErrorResponse, error_codes};
|
|
375
|
+
///
|
|
376
|
+
/// let response = JsonRpcErrorResponse::error_with_data(
|
|
377
|
+
/// error_codes::INVALID_PARAMS,
|
|
378
|
+
/// "Invalid method parameters",
|
|
379
|
+
/// json!({"reason": "Missing required field 'name'"}),
|
|
380
|
+
/// json!(1)
|
|
381
|
+
/// );
|
|
382
|
+
/// assert_eq!(response.jsonrpc, "2.0");
|
|
383
|
+
/// assert!(response.error.data.is_some());
|
|
384
|
+
/// ```
|
|
385
|
+
pub fn error_with_data(code: i32, message: impl Into<String>, data: Value, id: Value) -> Self {
|
|
386
|
+
Self {
|
|
387
|
+
jsonrpc: "2.0".to_string(),
|
|
388
|
+
error: JsonRpcError {
|
|
389
|
+
code,
|
|
390
|
+
message: message.into(),
|
|
391
|
+
data: Some(data),
|
|
392
|
+
},
|
|
393
|
+
id,
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/// JSON-RPC 2.0 Response Type
|
|
399
|
+
///
|
|
400
|
+
/// An enum that represents either a successful response or an error response.
|
|
401
|
+
/// This is useful for untagged deserialization and handling both response types uniformly.
|
|
402
|
+
///
|
|
403
|
+
/// # Variants
|
|
404
|
+
///
|
|
405
|
+
/// * `Success(JsonRpcResponse)` - A successful response with a result
|
|
406
|
+
/// * `Error(JsonRpcErrorResponse)` - An error response with error details
|
|
407
|
+
///
|
|
408
|
+
/// # Example
|
|
409
|
+
///
|
|
410
|
+
/// ```ignore
|
|
411
|
+
/// use serde_json::json;
|
|
412
|
+
/// use spikard_http::jsonrpc::{JsonRpcResponseType, JsonRpcResponse, JsonRpcErrorResponse, error_codes};
|
|
413
|
+
///
|
|
414
|
+
/// let success = JsonRpcResponseType::Success(
|
|
415
|
+
/// JsonRpcResponse::success(json!(42), json!(1))
|
|
416
|
+
/// );
|
|
417
|
+
///
|
|
418
|
+
/// let error = JsonRpcResponseType::Error(
|
|
419
|
+
/// JsonRpcErrorResponse::error(error_codes::INVALID_PARAMS, "Bad params", json!(1))
|
|
420
|
+
/// );
|
|
421
|
+
/// ```
|
|
422
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
423
|
+
#[serde(untagged)]
|
|
424
|
+
pub enum JsonRpcResponseType {
|
|
425
|
+
/// Successful response containing a result
|
|
426
|
+
Success(JsonRpcResponse),
|
|
427
|
+
|
|
428
|
+
/// Error response containing error details
|
|
429
|
+
Error(JsonRpcErrorResponse),
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/// JSON-RPC 2.0 Standard Error Codes
|
|
433
|
+
///
|
|
434
|
+
/// This module contains the standard error codes defined by the JSON-RPC 2.0 specification.
|
|
435
|
+
/// The error codes from -32768 to -32000 are reserved for JSON-RPC specification use.
|
|
436
|
+
pub mod error_codes {
|
|
437
|
+
/// Parse error
|
|
438
|
+
///
|
|
439
|
+
/// Invalid JSON was received by the server.
|
|
440
|
+
/// An error occurred on the server while parsing the JSON text.
|
|
441
|
+
pub const PARSE_ERROR: i32 = -32700;
|
|
442
|
+
|
|
443
|
+
/// Invalid Request
|
|
444
|
+
///
|
|
445
|
+
/// The JSON sent is not a valid Request object.
|
|
446
|
+
pub const INVALID_REQUEST: i32 = -32600;
|
|
447
|
+
|
|
448
|
+
/// Method not found
|
|
449
|
+
///
|
|
450
|
+
/// The method does not exist / is not available.
|
|
451
|
+
pub const METHOD_NOT_FOUND: i32 = -32601;
|
|
452
|
+
|
|
453
|
+
/// Invalid params
|
|
454
|
+
///
|
|
455
|
+
/// Invalid method parameter(s).
|
|
456
|
+
pub const INVALID_PARAMS: i32 = -32602;
|
|
457
|
+
|
|
458
|
+
/// Internal error
|
|
459
|
+
///
|
|
460
|
+
/// Internal JSON-RPC error.
|
|
461
|
+
pub const INTERNAL_ERROR: i32 = -32603;
|
|
462
|
+
|
|
463
|
+
/// Server error (base)
|
|
464
|
+
///
|
|
465
|
+
/// Server errors are reserved for implementation-defined server-errors.
|
|
466
|
+
/// The error codes from -32099 to -32000 are reserved for server error codes.
|
|
467
|
+
pub const SERVER_ERROR_BASE: i32 = -32000;
|
|
468
|
+
|
|
469
|
+
/// Server error (end of reserved range)
|
|
470
|
+
pub const SERVER_ERROR_END: i32 = -32099;
|
|
471
|
+
|
|
472
|
+
/// Helper function to check if a code is a reserved server error code
|
|
473
|
+
pub fn is_server_error(code: i32) -> bool {
|
|
474
|
+
(SERVER_ERROR_END..=SERVER_ERROR_BASE).contains(&code)
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
#[cfg(test)]
|
|
479
|
+
mod tests {
|
|
480
|
+
use super::*;
|
|
481
|
+
use serde_json::json;
|
|
482
|
+
|
|
483
|
+
#[test]
|
|
484
|
+
fn test_jsonrpc_request_creation() {
|
|
485
|
+
let req = JsonRpcRequest::new("method", Some(json!({"key": "value"})), Some(json!(1)));
|
|
486
|
+
assert_eq!(req.jsonrpc, "2.0");
|
|
487
|
+
assert_eq!(req.method, "method");
|
|
488
|
+
assert!(!req.is_notification());
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
#[test]
|
|
492
|
+
fn test_jsonrpc_notification() {
|
|
493
|
+
let notif = JsonRpcRequest::new("notify", None, None);
|
|
494
|
+
assert!(notif.is_notification());
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
#[test]
|
|
498
|
+
fn test_jsonrpc_response_success() {
|
|
499
|
+
let response = JsonRpcResponse::success(json!(42), json!(1));
|
|
500
|
+
assert_eq!(response.jsonrpc, "2.0");
|
|
501
|
+
assert_eq!(response.result, json!(42));
|
|
502
|
+
assert_eq!(response.id, json!(1));
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
#[test]
|
|
506
|
+
fn test_jsonrpc_error_response() {
|
|
507
|
+
let err = JsonRpcErrorResponse::error(error_codes::METHOD_NOT_FOUND, "Method not found", json!(1));
|
|
508
|
+
assert_eq!(err.jsonrpc, "2.0");
|
|
509
|
+
assert_eq!(err.error.code, error_codes::METHOD_NOT_FOUND);
|
|
510
|
+
assert_eq!(err.error.message, "Method not found");
|
|
511
|
+
assert!(err.error.data.is_none());
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
#[test]
|
|
515
|
+
fn test_jsonrpc_error_response_with_data() {
|
|
516
|
+
let data = json!({"reason": "Missing parameter"});
|
|
517
|
+
let err = JsonRpcErrorResponse::error_with_data(
|
|
518
|
+
error_codes::INVALID_PARAMS,
|
|
519
|
+
"Invalid parameters",
|
|
520
|
+
data.clone(),
|
|
521
|
+
json!(null),
|
|
522
|
+
);
|
|
523
|
+
assert_eq!(err.error.code, error_codes::INVALID_PARAMS);
|
|
524
|
+
assert_eq!(err.error.data, Some(data));
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
#[test]
|
|
528
|
+
fn test_error_codes_constants() {
|
|
529
|
+
assert_eq!(error_codes::PARSE_ERROR, -32700);
|
|
530
|
+
assert_eq!(error_codes::INVALID_REQUEST, -32600);
|
|
531
|
+
assert_eq!(error_codes::METHOD_NOT_FOUND, -32601);
|
|
532
|
+
assert_eq!(error_codes::INVALID_PARAMS, -32602);
|
|
533
|
+
assert_eq!(error_codes::INTERNAL_ERROR, -32603);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
#[test]
|
|
537
|
+
fn test_is_server_error() {
|
|
538
|
+
assert!(error_codes::is_server_error(-32000));
|
|
539
|
+
assert!(error_codes::is_server_error(-32050));
|
|
540
|
+
assert!(error_codes::is_server_error(-32099));
|
|
541
|
+
assert!(!error_codes::is_server_error(-32700));
|
|
542
|
+
assert!(!error_codes::is_server_error(0));
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
#[test]
|
|
546
|
+
fn test_request_serialization() {
|
|
547
|
+
let req = JsonRpcRequest::new("test", Some(json!([1, 2])), Some(json!(1)));
|
|
548
|
+
let json = serde_json::to_value(&req).unwrap();
|
|
549
|
+
assert_eq!(json["jsonrpc"], "2.0");
|
|
550
|
+
assert_eq!(json["method"], "test");
|
|
551
|
+
assert!(json["params"].is_array());
|
|
552
|
+
assert_eq!(json["id"], 1);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
#[test]
|
|
556
|
+
fn test_notification_serialization() {
|
|
557
|
+
let notif = JsonRpcRequest::new("notify", Some(json!({})), None);
|
|
558
|
+
let json = serde_json::to_value(¬if).unwrap();
|
|
559
|
+
assert!(!json.get("id").is_some() || json["id"].is_null());
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
#[test]
|
|
563
|
+
fn test_response_serialization() {
|
|
564
|
+
let resp = JsonRpcResponse::success(json!({"result": 100}), json!("string-id"));
|
|
565
|
+
let json = serde_json::to_value(&resp).unwrap();
|
|
566
|
+
assert_eq!(json["jsonrpc"], "2.0");
|
|
567
|
+
assert_eq!(json["id"], "string-id");
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
#[test]
|
|
571
|
+
fn test_error_response_serialization() {
|
|
572
|
+
let err = JsonRpcErrorResponse::error(error_codes::PARSE_ERROR, "Parse error", json!(null));
|
|
573
|
+
let json = serde_json::to_value(&err).unwrap();
|
|
574
|
+
assert_eq!(json["jsonrpc"], "2.0");
|
|
575
|
+
assert_eq!(json["error"]["code"], -32700);
|
|
576
|
+
assert_eq!(json["error"]["message"], "Parse error");
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
#[test]
|
|
580
|
+
fn test_response_type_enum() {
|
|
581
|
+
let success_resp = JsonRpcResponseType::Success(JsonRpcResponse::success(json!(1), json!(1)));
|
|
582
|
+
let error_resp = JsonRpcResponseType::Error(JsonRpcErrorResponse::error(
|
|
583
|
+
error_codes::INVALID_REQUEST,
|
|
584
|
+
"Invalid",
|
|
585
|
+
json!(1),
|
|
586
|
+
));
|
|
587
|
+
|
|
588
|
+
let _success_json = serde_json::to_value(&success_resp).unwrap();
|
|
589
|
+
let _error_json = serde_json::to_value(&error_resp).unwrap();
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
#[test]
|
|
593
|
+
fn test_validate_method_name_valid_simple() {
|
|
594
|
+
assert!(validate_method_name("test").is_ok());
|
|
595
|
+
assert!(validate_method_name("method").is_ok());
|
|
596
|
+
assert!(validate_method_name("rpc").is_ok());
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
#[test]
|
|
600
|
+
fn test_validate_method_name_valid_with_dot() {
|
|
601
|
+
assert!(validate_method_name("user.get").is_ok());
|
|
602
|
+
assert!(validate_method_name("api.v1.endpoint").is_ok());
|
|
603
|
+
assert!(validate_method_name("service.method.action").is_ok());
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
#[test]
|
|
607
|
+
fn test_validate_method_name_valid_with_underscore() {
|
|
608
|
+
assert!(validate_method_name("get_user").is_ok());
|
|
609
|
+
assert!(validate_method_name("_private_method").is_ok());
|
|
610
|
+
assert!(validate_method_name("method_v1").is_ok());
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
#[test]
|
|
614
|
+
fn test_validate_method_name_valid_with_hyphen() {
|
|
615
|
+
assert!(validate_method_name("get-user").is_ok());
|
|
616
|
+
assert!(validate_method_name("api-v1").is_ok());
|
|
617
|
+
assert!(validate_method_name("my-method-name").is_ok());
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
#[test]
|
|
621
|
+
fn test_validate_method_name_valid_with_numbers() {
|
|
622
|
+
assert!(validate_method_name("method1").is_ok());
|
|
623
|
+
assert!(validate_method_name("v2.endpoint").is_ok());
|
|
624
|
+
assert!(validate_method_name("rpc123abc").is_ok());
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
#[test]
|
|
628
|
+
fn test_validate_method_name_valid_mixed() {
|
|
629
|
+
assert!(validate_method_name("user.get_by_id").is_ok());
|
|
630
|
+
assert!(validate_method_name("api-v1.service_name").is_ok());
|
|
631
|
+
assert!(validate_method_name("Service_v1_2_3").is_ok());
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
#[test]
|
|
635
|
+
fn test_validate_method_name_valid_max_length() {
|
|
636
|
+
let max_name = "a".repeat(255);
|
|
637
|
+
assert!(validate_method_name(&max_name).is_ok());
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
#[test]
|
|
641
|
+
fn test_validate_method_name_empty() {
|
|
642
|
+
let result = validate_method_name("");
|
|
643
|
+
assert!(result.is_err());
|
|
644
|
+
assert!(result.unwrap_err().contains("cannot be empty"));
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
#[test]
|
|
648
|
+
fn test_validate_method_name_leading_space() {
|
|
649
|
+
let result = validate_method_name(" method");
|
|
650
|
+
assert!(result.is_err());
|
|
651
|
+
assert!(result.unwrap_err().contains("leading or trailing whitespace"));
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
#[test]
|
|
655
|
+
fn test_validate_method_name_trailing_space() {
|
|
656
|
+
let result = validate_method_name("method ");
|
|
657
|
+
assert!(result.is_err());
|
|
658
|
+
assert!(result.unwrap_err().contains("leading or trailing whitespace"));
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
#[test]
|
|
662
|
+
fn test_validate_method_name_leading_and_trailing_space() {
|
|
663
|
+
let result = validate_method_name(" method ");
|
|
664
|
+
assert!(result.is_err());
|
|
665
|
+
assert!(result.unwrap_err().contains("leading or trailing whitespace"));
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
#[test]
|
|
669
|
+
fn test_validate_method_name_internal_space() {
|
|
670
|
+
let result = validate_method_name("method name");
|
|
671
|
+
assert!(result.is_err());
|
|
672
|
+
assert!(result.unwrap_err().contains("invalid character"));
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
#[test]
|
|
676
|
+
fn test_validate_method_name_too_long() {
|
|
677
|
+
let too_long_name = "a".repeat(256);
|
|
678
|
+
let result = validate_method_name(&too_long_name);
|
|
679
|
+
assert!(result.is_err());
|
|
680
|
+
assert!(result.unwrap_err().contains("exceeds maximum length"));
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
#[test]
|
|
684
|
+
fn test_validate_method_name_null_byte() {
|
|
685
|
+
let result = validate_method_name("method\x00name");
|
|
686
|
+
assert!(result.is_err());
|
|
687
|
+
assert!(result.unwrap_err().contains("control character"));
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
#[test]
|
|
691
|
+
fn test_validate_method_name_newline() {
|
|
692
|
+
let result = validate_method_name("method\nname");
|
|
693
|
+
assert!(result.is_err());
|
|
694
|
+
assert!(result.unwrap_err().contains("control character"));
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
#[test]
|
|
698
|
+
fn test_validate_method_name_carriage_return() {
|
|
699
|
+
let result = validate_method_name("method\rname");
|
|
700
|
+
assert!(result.is_err());
|
|
701
|
+
assert!(result.unwrap_err().contains("control character"));
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
#[test]
|
|
705
|
+
fn test_validate_method_name_tab() {
|
|
706
|
+
let result = validate_method_name("method\tname");
|
|
707
|
+
assert!(result.is_err());
|
|
708
|
+
assert!(result.unwrap_err().contains("control character"));
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
#[test]
|
|
712
|
+
fn test_validate_method_name_delete_char() {
|
|
713
|
+
let result = validate_method_name("method\x7fname");
|
|
714
|
+
assert!(result.is_err());
|
|
715
|
+
assert!(result.unwrap_err().contains("control character"));
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
#[test]
|
|
719
|
+
fn test_validate_method_name_special_char_at_sign() {
|
|
720
|
+
let result = validate_method_name("method@name");
|
|
721
|
+
assert!(result.is_err());
|
|
722
|
+
assert!(result.unwrap_err().contains("invalid character"));
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
#[test]
|
|
726
|
+
fn test_validate_method_name_special_char_hash() {
|
|
727
|
+
let result = validate_method_name("method#name");
|
|
728
|
+
assert!(result.is_err());
|
|
729
|
+
assert!(result.unwrap_err().contains("invalid character"));
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
#[test]
|
|
733
|
+
fn test_validate_method_name_special_char_percent() {
|
|
734
|
+
let result = validate_method_name("method%name");
|
|
735
|
+
assert!(result.is_err());
|
|
736
|
+
assert!(result.unwrap_err().contains("invalid character"));
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
#[test]
|
|
740
|
+
fn test_validate_method_name_special_char_slash() {
|
|
741
|
+
let result = validate_method_name("method/name");
|
|
742
|
+
assert!(result.is_err());
|
|
743
|
+
assert!(result.unwrap_err().contains("invalid character"));
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
#[test]
|
|
747
|
+
fn test_validate_method_name_special_char_backslash() {
|
|
748
|
+
let result = validate_method_name("method\\name");
|
|
749
|
+
assert!(result.is_err());
|
|
750
|
+
assert!(result.unwrap_err().contains("invalid character"));
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
#[test]
|
|
754
|
+
fn test_validate_method_name_special_char_quote() {
|
|
755
|
+
let result = validate_method_name("method\"name");
|
|
756
|
+
assert!(result.is_err());
|
|
757
|
+
assert!(result.unwrap_err().contains("invalid character"));
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
#[test]
|
|
761
|
+
fn test_validate_method_name_dos_attack_very_long() {
|
|
762
|
+
let very_long = "a".repeat(10000);
|
|
763
|
+
let result = validate_method_name(&very_long);
|
|
764
|
+
assert!(result.is_err());
|
|
765
|
+
assert!(result.unwrap_err().contains("exceeds maximum length"));
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
#[test]
|
|
769
|
+
fn test_validate_method_name_dos_attack_control_chars() {
|
|
770
|
+
let result = validate_method_name("method\x00\x00\x00\x00");
|
|
771
|
+
assert!(result.is_err());
|
|
772
|
+
assert!(result.unwrap_err().contains("control character"));
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
#[test]
|
|
776
|
+
fn test_validate_method_name_edge_case_single_char() {
|
|
777
|
+
assert!(validate_method_name("a").is_ok());
|
|
778
|
+
assert!(validate_method_name("_").is_ok());
|
|
779
|
+
assert!(validate_method_name("-").is_ok());
|
|
780
|
+
assert!(validate_method_name(".").is_ok());
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
#[test]
|
|
784
|
+
fn test_request_with_null_id_is_notification() {
|
|
785
|
+
let json = json!({
|
|
786
|
+
"jsonrpc": "2.0",
|
|
787
|
+
"method": "notify",
|
|
788
|
+
"params": []
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
let request: JsonRpcRequest = serde_json::from_value(json).unwrap();
|
|
792
|
+
|
|
793
|
+
assert!(request.is_notification());
|
|
794
|
+
assert_eq!(request.method, "notify");
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
#[test]
|
|
798
|
+
fn test_request_with_string_id_preserved() {
|
|
799
|
+
let json = json!({
|
|
800
|
+
"jsonrpc": "2.0",
|
|
801
|
+
"method": "add",
|
|
802
|
+
"params": [1, 2],
|
|
803
|
+
"id": "abc-123"
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
let request: JsonRpcRequest = serde_json::from_value(json).unwrap();
|
|
807
|
+
|
|
808
|
+
assert!(!request.is_notification());
|
|
809
|
+
assert_eq!(request.id, Some(json!("abc-123")));
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
#[test]
|
|
813
|
+
fn test_request_with_zero_id_valid() {
|
|
814
|
+
let json = json!({
|
|
815
|
+
"jsonrpc": "2.0",
|
|
816
|
+
"method": "test",
|
|
817
|
+
"id": 0
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
let request: JsonRpcRequest = serde_json::from_value(json).unwrap();
|
|
821
|
+
|
|
822
|
+
assert!(!request.is_notification());
|
|
823
|
+
assert_eq!(request.id, Some(json!(0)));
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
#[test]
|
|
827
|
+
fn test_request_with_negative_id_valid() {
|
|
828
|
+
let json = json!({
|
|
829
|
+
"jsonrpc": "2.0",
|
|
830
|
+
"method": "test",
|
|
831
|
+
"id": -999
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
let request: JsonRpcRequest = serde_json::from_value(json).unwrap();
|
|
835
|
+
|
|
836
|
+
assert!(!request.is_notification());
|
|
837
|
+
assert_eq!(request.id, Some(json!(-999)));
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
#[test]
|
|
841
|
+
fn test_request_without_jsonrpc_field_invalid() {
|
|
842
|
+
let json = json!({
|
|
843
|
+
"method": "test",
|
|
844
|
+
"id": 1
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
let result: serde_json::Result<JsonRpcRequest> = serde_json::from_value(json);
|
|
848
|
+
|
|
849
|
+
assert!(result.is_err());
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
#[test]
|
|
853
|
+
fn test_response_preserves_id_type_numeric() {
|
|
854
|
+
let response = JsonRpcResponse::success(json!(42), json!(999));
|
|
855
|
+
let serialized = serde_json::to_value(&response).unwrap();
|
|
856
|
+
|
|
857
|
+
assert_eq!(serialized["id"], 999);
|
|
858
|
+
assert!(serialized["id"].is_number());
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
#[test]
|
|
862
|
+
fn test_error_response_never_has_result_field() {
|
|
863
|
+
let err = JsonRpcErrorResponse::error(error_codes::INVALID_PARAMS, "Bad params", json!(1));
|
|
864
|
+
let serialized = serde_json::to_value(&err).unwrap();
|
|
865
|
+
|
|
866
|
+
assert!(serialized.get("result").is_none());
|
|
867
|
+
assert!(serialized.get("error").is_some());
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
#[test]
|
|
871
|
+
fn test_success_response_never_has_error_field() {
|
|
872
|
+
let success = JsonRpcResponse::success(json!({"data": 123}), json!(1));
|
|
873
|
+
let serialized = serde_json::to_value(&success).unwrap();
|
|
874
|
+
|
|
875
|
+
assert!(serialized.get("error").is_none());
|
|
876
|
+
assert!(serialized.get("result").is_some());
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
#[test]
|
|
880
|
+
fn test_params_array_type() {
|
|
881
|
+
let json = json!({
|
|
882
|
+
"jsonrpc": "2.0",
|
|
883
|
+
"method": "sum",
|
|
884
|
+
"params": [1, 2, 3, 4, 5],
|
|
885
|
+
"id": 1
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
let request: JsonRpcRequest = serde_json::from_value(json).unwrap();
|
|
889
|
+
|
|
890
|
+
assert!(request.params.is_some());
|
|
891
|
+
let params = request.params.unwrap();
|
|
892
|
+
assert!(params.is_array());
|
|
893
|
+
let arr = params.as_array().unwrap();
|
|
894
|
+
assert_eq!(arr.len(), 5);
|
|
895
|
+
assert_eq!(arr[0], json!(1));
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
#[test]
|
|
899
|
+
fn test_params_object_type() {
|
|
900
|
+
let json = json!({
|
|
901
|
+
"jsonrpc": "2.0",
|
|
902
|
+
"method": "subtract",
|
|
903
|
+
"params": {"a": 5, "b": 3},
|
|
904
|
+
"id": 2
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
let request: JsonRpcRequest = serde_json::from_value(json).unwrap();
|
|
908
|
+
|
|
909
|
+
assert!(request.params.is_some());
|
|
910
|
+
let params = request.params.unwrap();
|
|
911
|
+
assert!(params.is_object());
|
|
912
|
+
let obj = params.as_object().unwrap();
|
|
913
|
+
assert_eq!(obj.get("a"), Some(&json!(5)));
|
|
914
|
+
assert_eq!(obj.get("b"), Some(&json!(3)));
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
#[test]
|
|
918
|
+
fn test_params_null_type() {
|
|
919
|
+
let json_no_params = json!({
|
|
920
|
+
"jsonrpc": "2.0",
|
|
921
|
+
"method": "test",
|
|
922
|
+
"id": 3
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
let request: JsonRpcRequest = serde_json::from_value(json_no_params).unwrap();
|
|
926
|
+
assert!(request.params.is_none());
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
#[test]
|
|
930
|
+
fn test_params_primitive_string() {
|
|
931
|
+
let json = json!({
|
|
932
|
+
"jsonrpc": "2.0",
|
|
933
|
+
"method": "echo",
|
|
934
|
+
"params": "hello world",
|
|
935
|
+
"id": 4
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
let request: JsonRpcRequest = serde_json::from_value(json).unwrap();
|
|
939
|
+
|
|
940
|
+
assert!(request.params.is_some());
|
|
941
|
+
assert_eq!(request.params.unwrap(), json!("hello world"));
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
#[test]
|
|
945
|
+
fn test_params_primitive_number() {
|
|
946
|
+
let json = json!({
|
|
947
|
+
"jsonrpc": "2.0",
|
|
948
|
+
"method": "increment",
|
|
949
|
+
"params": 42,
|
|
950
|
+
"id": 5
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
let request: JsonRpcRequest = serde_json::from_value(json).unwrap();
|
|
954
|
+
|
|
955
|
+
assert!(request.params.is_some());
|
|
956
|
+
assert_eq!(request.params.unwrap(), json!(42));
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
#[test]
|
|
960
|
+
fn test_params_deeply_nested() {
|
|
961
|
+
let json = json!({
|
|
962
|
+
"jsonrpc": "2.0",
|
|
963
|
+
"method": "process",
|
|
964
|
+
"params": {
|
|
965
|
+
"level1": {
|
|
966
|
+
"level2": {
|
|
967
|
+
"level3": {
|
|
968
|
+
"level4": {
|
|
969
|
+
"level5": {
|
|
970
|
+
"level6": {
|
|
971
|
+
"level7": {
|
|
972
|
+
"level8": {
|
|
973
|
+
"level9": {
|
|
974
|
+
"level10": "deep value"
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
},
|
|
985
|
+
"id": 6
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
let request: JsonRpcRequest = serde_json::from_value(json).unwrap();
|
|
989
|
+
|
|
990
|
+
assert!(request.params.is_some());
|
|
991
|
+
let deep = request.params.unwrap();
|
|
992
|
+
assert!(
|
|
993
|
+
deep["level1"]["level2"]["level3"]["level4"]["level5"]["level6"]["level7"]["level8"]["level9"]["level10"]
|
|
994
|
+
.is_string()
|
|
995
|
+
);
|
|
996
|
+
assert_eq!(
|
|
997
|
+
deep["level1"]["level2"]["level3"]["level4"]["level5"]["level6"]["level7"]["level8"]["level9"]["level10"],
|
|
998
|
+
"deep value"
|
|
999
|
+
);
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
#[test]
|
|
1003
|
+
fn test_params_unicode_strings() {
|
|
1004
|
+
let json = json!({
|
|
1005
|
+
"jsonrpc": "2.0",
|
|
1006
|
+
"method": "translate",
|
|
1007
|
+
"params": {
|
|
1008
|
+
"emoji": "Hello 👋 World 🌍",
|
|
1009
|
+
"rtl": "שלום עולם",
|
|
1010
|
+
"cjk": "你好世界",
|
|
1011
|
+
"special": "café ñ ü"
|
|
1012
|
+
},
|
|
1013
|
+
"id": 7
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
let request: JsonRpcRequest = serde_json::from_value(json).unwrap();
|
|
1017
|
+
|
|
1018
|
+
let params = request.params.unwrap();
|
|
1019
|
+
assert_eq!(params["emoji"], "Hello 👋 World 🌍");
|
|
1020
|
+
assert_eq!(params["rtl"], "שלום עולם");
|
|
1021
|
+
assert_eq!(params["cjk"], "你好世界");
|
|
1022
|
+
assert_eq!(params["special"], "café ñ ü");
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
#[test]
|
|
1026
|
+
fn test_response_result_with_null_valid() {
|
|
1027
|
+
let response = JsonRpcResponse::success(json!(null), json!(1));
|
|
1028
|
+
let serialized = serde_json::to_value(&response).unwrap();
|
|
1029
|
+
|
|
1030
|
+
assert_eq!(serialized["jsonrpc"], "2.0");
|
|
1031
|
+
assert_eq!(serialized["id"], 1);
|
|
1032
|
+
assert_eq!(serialized["result"], json!(null));
|
|
1033
|
+
assert!(serialized.get("error").is_none());
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
#[test]
|
|
1037
|
+
fn test_response_result_with_false_valid() {
|
|
1038
|
+
let response = JsonRpcResponse::success(json!(false), json!(2));
|
|
1039
|
+
let serialized = serde_json::to_value(&response).unwrap();
|
|
1040
|
+
|
|
1041
|
+
assert_eq!(serialized["jsonrpc"], "2.0");
|
|
1042
|
+
assert_eq!(serialized["id"], 2);
|
|
1043
|
+
assert_eq!(serialized["result"], json!(false));
|
|
1044
|
+
assert!(serialized.get("error").is_none());
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
#[test]
|
|
1048
|
+
fn test_response_result_with_zero_valid() {
|
|
1049
|
+
let response = JsonRpcResponse::success(json!(0), json!(3));
|
|
1050
|
+
let serialized = serde_json::to_value(&response).unwrap();
|
|
1051
|
+
|
|
1052
|
+
assert_eq!(serialized["jsonrpc"], "2.0");
|
|
1053
|
+
assert_eq!(serialized["id"], 3);
|
|
1054
|
+
assert_eq!(serialized["result"], json!(0));
|
|
1055
|
+
assert!(serialized.get("error").is_none());
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
#[test]
|
|
1059
|
+
fn test_response_result_with_empty_object_valid() {
|
|
1060
|
+
let response = JsonRpcResponse::success(json!({}), json!(4));
|
|
1061
|
+
let serialized = serde_json::to_value(&response).unwrap();
|
|
1062
|
+
|
|
1063
|
+
assert_eq!(serialized["jsonrpc"], "2.0");
|
|
1064
|
+
assert_eq!(serialized["id"], 4);
|
|
1065
|
+
assert_eq!(serialized["result"], json!({}));
|
|
1066
|
+
assert!(serialized.get("error").is_none());
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
#[test]
|
|
1070
|
+
fn test_response_result_with_empty_array_valid() {
|
|
1071
|
+
let response = JsonRpcResponse::success(json!([]), json!(5));
|
|
1072
|
+
let serialized = serde_json::to_value(&response).unwrap();
|
|
1073
|
+
|
|
1074
|
+
assert_eq!(serialized["jsonrpc"], "2.0");
|
|
1075
|
+
assert_eq!(serialized["id"], 5);
|
|
1076
|
+
assert_eq!(serialized["result"], json!([]));
|
|
1077
|
+
assert!(serialized.get("error").is_none());
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
#[test]
|
|
1081
|
+
fn test_error_code_parse_error() {
|
|
1082
|
+
let err = JsonRpcErrorResponse::error(error_codes::PARSE_ERROR, "Parse error", json!(null));
|
|
1083
|
+
let serialized = serde_json::to_value(&err).unwrap();
|
|
1084
|
+
|
|
1085
|
+
assert_eq!(serialized["error"]["code"], -32700);
|
|
1086
|
+
assert!(serialized.get("result").is_none());
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
#[test]
|
|
1090
|
+
fn test_error_code_roundtrip() {
|
|
1091
|
+
let codes = vec![
|
|
1092
|
+
error_codes::PARSE_ERROR,
|
|
1093
|
+
error_codes::INVALID_REQUEST,
|
|
1094
|
+
error_codes::METHOD_NOT_FOUND,
|
|
1095
|
+
error_codes::INVALID_PARAMS,
|
|
1096
|
+
error_codes::INTERNAL_ERROR,
|
|
1097
|
+
];
|
|
1098
|
+
|
|
1099
|
+
for code in codes {
|
|
1100
|
+
let err = JsonRpcErrorResponse::error(code, "Test error", json!(1));
|
|
1101
|
+
let serialized = serde_json::to_value(&err).unwrap();
|
|
1102
|
+
let deserialized: JsonRpcErrorResponse = serde_json::from_value(serialized).unwrap();
|
|
1103
|
+
|
|
1104
|
+
assert_eq!(deserialized.error.code, code);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
#[test]
|
|
1109
|
+
fn test_notification_has_no_id_field() {
|
|
1110
|
+
let notif = JsonRpcRequest::new("notify", None, None);
|
|
1111
|
+
let serialized = serde_json::to_value(¬if).unwrap();
|
|
1112
|
+
|
|
1113
|
+
assert!(serialized.get("id").is_none());
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
#[test]
|
|
1117
|
+
fn test_id_preservation_in_batch() {
|
|
1118
|
+
let json_batch = json!([
|
|
1119
|
+
{
|
|
1120
|
+
"jsonrpc": "2.0",
|
|
1121
|
+
"method": "method1",
|
|
1122
|
+
"id": "string-id"
|
|
1123
|
+
},
|
|
1124
|
+
{
|
|
1125
|
+
"jsonrpc": "2.0",
|
|
1126
|
+
"method": "method2",
|
|
1127
|
+
"id": 42
|
|
1128
|
+
},
|
|
1129
|
+
{
|
|
1130
|
+
"jsonrpc": "2.0",
|
|
1131
|
+
"method": "method3"
|
|
1132
|
+
}
|
|
1133
|
+
]);
|
|
1134
|
+
|
|
1135
|
+
let batch: Vec<JsonRpcRequest> = serde_json::from_value(json_batch).unwrap();
|
|
1136
|
+
|
|
1137
|
+
assert_eq!(batch.len(), 3);
|
|
1138
|
+
assert_eq!(batch[0].id, Some(json!("string-id")));
|
|
1139
|
+
assert_eq!(batch[1].id, Some(json!(42)));
|
|
1140
|
+
assert_eq!(batch[2].id, None);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
#[test]
|
|
1144
|
+
fn test_mixed_id_types_in_batch() {
|
|
1145
|
+
let responses = vec![
|
|
1146
|
+
JsonRpcResponse::success(json!(100), json!("id1")),
|
|
1147
|
+
JsonRpcResponse::success(json!(200), json!(2)),
|
|
1148
|
+
JsonRpcResponse::success(json!(300), json!(null)),
|
|
1149
|
+
];
|
|
1150
|
+
|
|
1151
|
+
for resp in responses {
|
|
1152
|
+
let serialized = serde_json::to_value(&resp).unwrap();
|
|
1153
|
+
let deserialized: JsonRpcResponse = serde_json::from_value(serialized).unwrap();
|
|
1154
|
+
assert_eq!(deserialized.jsonrpc, "2.0");
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
#[test]
|
|
1159
|
+
fn test_large_numeric_id() {
|
|
1160
|
+
let large_id = i64::MAX;
|
|
1161
|
+
let json = json!({
|
|
1162
|
+
"jsonrpc": "2.0",
|
|
1163
|
+
"method": "test",
|
|
1164
|
+
"id": large_id
|
|
1165
|
+
});
|
|
1166
|
+
|
|
1167
|
+
let request: JsonRpcRequest = serde_json::from_value(json).unwrap();
|
|
1168
|
+
|
|
1169
|
+
assert_eq!(request.id, Some(json!(large_id)));
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
#[test]
|
|
1173
|
+
fn test_error_always_has_code() {
|
|
1174
|
+
let err = JsonRpcErrorResponse::error(error_codes::METHOD_NOT_FOUND, "Not found", json!(1));
|
|
1175
|
+
let serialized = serde_json::to_value(&err).unwrap();
|
|
1176
|
+
|
|
1177
|
+
assert!(serialized["error"].get("code").is_some());
|
|
1178
|
+
assert_eq!(serialized["error"]["code"], -32601);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
#[test]
|
|
1182
|
+
fn test_error_always_has_message() {
|
|
1183
|
+
let err = JsonRpcErrorResponse::error(error_codes::INVALID_PARAMS, "Invalid parameters", json!(2));
|
|
1184
|
+
let serialized = serde_json::to_value(&err).unwrap();
|
|
1185
|
+
|
|
1186
|
+
assert!(serialized["error"].get("message").is_some());
|
|
1187
|
+
assert_eq!(serialized["error"]["message"], "Invalid parameters");
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
#[test]
|
|
1191
|
+
fn test_error_data_optional() {
|
|
1192
|
+
let err_without_data = JsonRpcErrorResponse::error(error_codes::INTERNAL_ERROR, "Internal error", json!(3));
|
|
1193
|
+
let serialized_without = serde_json::to_value(&err_without_data).unwrap();
|
|
1194
|
+
|
|
1195
|
+
assert!(serialized_without["error"].get("data").is_none());
|
|
1196
|
+
|
|
1197
|
+
let err_with_data = JsonRpcErrorResponse::error_with_data(
|
|
1198
|
+
error_codes::INTERNAL_ERROR,
|
|
1199
|
+
"Internal error",
|
|
1200
|
+
json!({"details": "something went wrong"}),
|
|
1201
|
+
json!(4),
|
|
1202
|
+
);
|
|
1203
|
+
let serialized_with = serde_json::to_value(&err_with_data).unwrap();
|
|
1204
|
+
|
|
1205
|
+
assert!(serialized_with["error"].get("data").is_some());
|
|
1206
|
+
}
|
|
1207
|
+
}
|