#![allow(dead_code)]
#![allow(unused_variables)]
use serde_json::json;
use server_less::{jsonrpc, server};
#[derive(Clone)]
struct Calculator;
#[jsonrpc]
impl Calculator {
pub fn add(&self, a: i32, b: i32) -> i32 {
a + b
}
pub fn subtract(&self, a: i32, b: i32) -> i32 {
a - b
}
pub fn multiply(&self, a: i32, b: i32) -> i32 {
a * b
}
pub fn echo(&self, message: String) -> String {
message
}
}
#[test]
fn test_jsonrpc_methods_list() {
let methods = Calculator::jsonrpc_methods();
assert!(methods.contains(&"add".to_string()));
assert!(methods.contains(&"subtract".to_string()));
assert!(methods.contains(&"multiply".to_string()));
assert!(methods.contains(&"echo".to_string()));
}
#[tokio::test]
async fn test_jsonrpc_handle_add() {
let calc = Calculator;
let request = json!({
"jsonrpc": "2.0",
"method": "add",
"params": {"a": 5, "b": 3},
"id": 1
});
let response = calc.jsonrpc_handle_async(request).await;
assert_eq!(response["jsonrpc"], "2.0");
assert_eq!(response["result"], 8);
assert_eq!(response["id"], 1);
}
#[tokio::test]
async fn test_jsonrpc_handle_subtract() {
let calc = Calculator;
let request = json!({
"jsonrpc": "2.0",
"method": "subtract",
"params": {"a": 10, "b": 4},
"id": 2
});
let response = calc.jsonrpc_handle_async(request).await;
assert_eq!(response["result"], 6);
}
#[tokio::test]
async fn test_jsonrpc_handle_string_params() {
let calc = Calculator;
let request = json!({
"jsonrpc": "2.0",
"method": "echo",
"params": {"message": "hello world"},
"id": 3
});
let response = calc.jsonrpc_handle_async(request).await;
assert_eq!(response["result"], "hello world");
}
#[tokio::test]
async fn test_jsonrpc_method_not_found() {
let calc = Calculator;
let request = json!({
"jsonrpc": "2.0",
"method": "nonexistent",
"params": {},
"id": 4
});
let response = calc.jsonrpc_handle_async(request).await;
assert!(response["error"].is_object());
assert!(
response["error"]["message"]
.as_str()
.unwrap()
.contains("not found")
);
}
#[tokio::test]
async fn test_jsonrpc_invalid_version() {
let calc = Calculator;
let request = json!({
"jsonrpc": "1.0",
"method": "add",
"params": {"a": 1, "b": 2},
"id": 5
});
let response = calc.jsonrpc_handle_async(request).await;
assert!(response["error"].is_object());
assert_eq!(response["error"]["code"], -32600);
}
#[tokio::test]
async fn test_jsonrpc_notification_no_response() {
let calc = Calculator;
let request = json!({
"jsonrpc": "2.0",
"method": "add",
"params": {"a": 1, "b": 2}
});
let response = calc.jsonrpc_handle_async(request).await;
assert!(response.is_null());
}
#[tokio::test]
async fn test_jsonrpc_batch_request() {
let calc = Calculator;
let request = json!([
{"jsonrpc": "2.0", "method": "add", "params": {"a": 1, "b": 2}, "id": 1},
{"jsonrpc": "2.0", "method": "multiply", "params": {"a": 3, "b": 4}, "id": 2}
]);
let response = calc.jsonrpc_handle_async(request).await;
assert!(response.is_array());
let arr = response.as_array().unwrap();
assert_eq!(arr.len(), 2);
assert_eq!(arr[0]["result"], 3);
assert_eq!(arr[1]["result"], 12);
}
#[tokio::test]
async fn test_jsonrpc_batch_with_notifications() {
let calc = Calculator;
let request = json!([
{"jsonrpc": "2.0", "method": "add", "params": {"a": 1, "b": 2}, "id": 1},
{"jsonrpc": "2.0", "method": "multiply", "params": {"a": 3, "b": 4}} ]);
let response = calc.jsonrpc_handle_async(request).await;
assert!(response.is_array());
let arr = response.as_array().unwrap();
assert_eq!(arr.len(), 1);
assert_eq!(arr[0]["result"], 3);
}
#[derive(Clone)]
struct AsyncService;
#[jsonrpc]
impl AsyncService {
pub async fn async_echo(&self, message: String) -> String {
tokio::time::sleep(std::time::Duration::from_millis(1)).await;
message
}
}
#[tokio::test]
async fn test_jsonrpc_async_method() {
let svc = AsyncService;
let request = json!({
"jsonrpc": "2.0",
"method": "async_echo",
"params": {"message": "async works"},
"id": 1
});
let response = svc.jsonrpc_handle_async(request).await;
assert_eq!(response["result"], "async works");
}
#[derive(Clone)]
struct CustomPathService;
#[jsonrpc(path = "/api/v1/rpc")]
impl CustomPathService {
pub fn ping(&self) -> String {
"pong".to_string()
}
}
#[test]
fn test_jsonrpc_custom_path_compiles() {
let methods = CustomPathService::jsonrpc_methods();
assert!(methods.contains(&"ping".to_string()));
}
#[test]
fn test_jsonrpc_openapi_paths_generated() {
let paths = Calculator::jsonrpc_openapi_paths();
assert_eq!(paths.len(), 1);
let rpc_path = &paths[0];
assert_eq!(rpc_path.path, "/rpc");
assert_eq!(rpc_path.method, "post");
assert!(
rpc_path
.operation
.summary
.as_ref()
.unwrap()
.contains("JSON-RPC")
);
assert!(rpc_path.operation.request_body.is_some());
assert!(rpc_path.operation.responses.contains_key("200"));
assert!(rpc_path.operation.responses.contains_key("204"));
}
#[derive(Clone)]
struct MathTools;
#[jsonrpc]
impl MathTools {
fn add(&self, a: i32, b: i32) -> i32 {
a + b
}
fn double(&self, n: i32) -> i32 {
n * 2
}
}
#[derive(Clone)]
struct StringTools;
#[jsonrpc]
impl StringTools {
fn upper(&self, s: String) -> String {
s.to_uppercase()
}
}
#[derive(Clone)]
struct JsonRpcApp {
math: MathTools,
strings: StringTools,
}
#[jsonrpc]
impl JsonRpcApp {
fn ping(&self) -> String {
"pong".to_string()
}
fn math(&self) -> &MathTools {
&self.math
}
fn strings(&self) -> &StringTools {
&self.strings
}
}
#[test]
fn test_jsonrpc_static_mount_methods_listed() {
let methods = JsonRpcApp::jsonrpc_methods();
assert!(methods.contains(&"ping".to_string()));
assert!(methods.contains(&"math.add".to_string()));
assert!(methods.contains(&"math.double".to_string()));
assert!(methods.contains(&"strings.upper".to_string()));
}
#[tokio::test]
async fn test_jsonrpc_static_mount_dispatch() {
let app = JsonRpcApp {
math: MathTools,
strings: StringTools,
};
let response = app
.jsonrpc_handle_async(json!({
"jsonrpc": "2.0",
"method": "ping",
"params": {},
"id": 1
}))
.await;
assert_eq!(response["result"], "pong");
let response = app
.jsonrpc_handle_async(json!({
"jsonrpc": "2.0",
"method": "math.add",
"params": {"a": 10, "b": 5},
"id": 2
}))
.await;
assert_eq!(response["result"], 15);
let response = app
.jsonrpc_handle_async(json!({
"jsonrpc": "2.0",
"method": "strings.upper",
"params": {"s": "hello"},
"id": 3
}))
.await;
assert_eq!(response["result"], "HELLO");
}
#[tokio::test]
async fn test_jsonrpc_static_mount_double() {
let app = JsonRpcApp {
math: MathTools,
strings: StringTools,
};
let response = app
.jsonrpc_handle_async(json!({
"jsonrpc": "2.0",
"method": "math.double",
"params": {"n": 21},
"id": 1
}))
.await;
assert_eq!(response["result"], 42);
}
#[derive(Clone)]
struct JsonRpcSlugApp {
math: MathTools,
}
#[jsonrpc]
impl JsonRpcSlugApp {
fn calc(&self, id: String) -> &MathTools {
let _ = &id;
&self.math
}
}
#[test]
fn test_jsonrpc_slug_mount_methods_listed() {
let methods = JsonRpcSlugApp::jsonrpc_methods();
assert!(methods.contains(&"calc.add".to_string()));
assert!(methods.contains(&"calc.double".to_string()));
}
#[tokio::test]
async fn test_jsonrpc_slug_mount_dispatch() {
let app = JsonRpcSlugApp { math: MathTools };
let response = app
.jsonrpc_handle_async(json!({
"jsonrpc": "2.0",
"method": "calc.add",
"params": {"id": "calc-1", "a": 3, "b": 4},
"id": 1
}))
.await;
assert_eq!(response["result"], 7);
}
#[test]
fn test_jsonrpc_mount_trait_implemented() {
use server_less::JsonRpcMount;
let methods = <MathTools as JsonRpcMount>::jsonrpc_mount_methods();
assert_eq!(methods.len(), 2);
assert!(methods.contains(&"add".to_string()));
assert!(methods.contains(&"double".to_string()));
}
#[test]
fn test_jsonrpc_mount_dispatch_sync() {
use server_less::JsonRpcMount;
let math = MathTools;
let result = math.jsonrpc_mount_dispatch("add", json!({"a": 7, "b": 3}));
assert!(result.is_ok(), "sync dispatch should succeed for sync method");
let val = result.unwrap();
assert_eq!(val, json!(10));
let result = math.jsonrpc_mount_dispatch("double", json!({"n": 6}));
assert!(result.is_ok());
assert_eq!(result.unwrap(), json!(12));
let result = math.jsonrpc_mount_dispatch("nonexistent", json!({}));
assert!(result.is_err(), "sync dispatch of unknown method should return Err");
}
#[derive(Clone)]
struct AsyncOnlyService;
#[server_less::jsonrpc]
impl AsyncOnlyService {
pub async fn only_async(&self, x: i32) -> i32 {
x * 2
}
pub fn sync_method(&self, x: i32) -> i32 {
x + 1
}
}
#[test]
fn test_jsonrpc_mount_dispatch_sync_rejects_async() {
use server_less::JsonRpcMount;
let svc = AsyncOnlyService;
let result = svc.jsonrpc_mount_dispatch("sync_method", json!({"x": 5}));
assert!(result.is_ok());
assert_eq!(result.unwrap(), json!(6));
let result = svc.jsonrpc_mount_dispatch("only_async", json!({"x": 5}));
assert!(
result.is_err(),
"async method should return Err in sync dispatch context"
);
assert!(
result.unwrap_err().contains("sync context"),
"error message should mention sync context"
);
}
#[test]
fn test_error_code_jsonrpc_code() {
use server_less::ErrorCode;
assert_eq!(ErrorCode::InvalidInput.jsonrpc_code(), -32602);
assert_eq!(ErrorCode::Internal.jsonrpc_code(), -32603);
assert_eq!(ErrorCode::NotImplemented.jsonrpc_code(), -32601);
}
#[derive(Debug, server_less::ServerlessError)]
enum RpcError {
#[error(code = InvalidInput, jsonrpc_code = -32602)]
BadParams,
#[error(code = NotFound)]
Missing,
}
#[derive(Clone)]
struct ErrorService;
#[server_less::jsonrpc]
impl ErrorService {
fn get_item(&self, id: i32) -> Result<String, RpcError> {
if id < 0 {
Err(RpcError::BadParams)
} else if id == 0 {
Err(RpcError::Missing)
} else {
Ok(format!("item-{}", id))
}
}
}
#[tokio::test]
async fn test_jsonrpc_error_code_from_serverless_error() {
let svc = ErrorService;
let response = svc
.jsonrpc_handle_async(json!({
"jsonrpc": "2.0",
"method": "get_item",
"params": {"id": -1},
"id": 1
}))
.await;
assert!(response["error"].is_object());
assert_eq!(
response["error"]["code"],
-32602,
"BadParams should produce JSON-RPC code -32602"
);
let response = svc
.jsonrpc_handle_async(json!({
"jsonrpc": "2.0",
"method": "get_item",
"params": {"id": 0},
"id": 2
}))
.await;
assert!(response["error"].is_object());
assert_eq!(
response["error"]["code"],
server_less::ErrorCode::NotFound.jsonrpc_code(),
"Missing should produce the NotFound JSON-RPC code"
);
let response = svc
.jsonrpc_handle_async(json!({
"jsonrpc": "2.0",
"method": "get_item",
"params": {"id": 42},
"id": 3
}))
.await;
assert_eq!(response["result"], "item-42");
}
#[derive(Clone)]
struct IteratorService;
#[jsonrpc]
impl IteratorService {
pub fn numbers(&self) -> impl Iterator<Item = i32> {
vec![1, 2, 3].into_iter()
}
pub fn words(&self) -> impl Iterator<Item = String> {
vec!["hello".to_string(), "world".to_string()].into_iter()
}
}
#[tokio::test]
async fn test_jsonrpc_iterator_serializes_to_array() {
let svc = IteratorService;
let request = json!({
"jsonrpc": "2.0",
"method": "numbers",
"params": {},
"id": 1
});
let response = svc.jsonrpc_handle_async(request).await;
assert_eq!(response["jsonrpc"], "2.0");
assert_eq!(response["id"], 1);
assert!(response["result"].is_array(), "iterator result must be a JSON array, got: {}", response);
assert_eq!(response["result"], json!([1, 2, 3]));
}
#[tokio::test]
async fn test_jsonrpc_iterator_strings_serializes_to_array() {
let svc = IteratorService;
let request = json!({
"jsonrpc": "2.0",
"method": "words",
"params": {},
"id": 2
});
let response = svc.jsonrpc_handle_async(request).await;
assert!(response["result"].is_array(), "iterator result must be a JSON array");
assert_eq!(response["result"], json!(["hello", "world"]));
}
#[tokio::test]
async fn test_jsonrpc_missing_required_param_returns_32602() {
let calc = Calculator;
let request = json!({
"jsonrpc": "2.0",
"method": "add",
"params": {"a": 5},
"id": 10
});
let response = calc.jsonrpc_handle_async(request).await;
assert!(response["error"].is_object(), "expected an error object, got: {}", response);
assert_eq!(
response["error"]["code"],
-32602,
"missing required param must produce -32602, got: {}",
response["error"]["code"]
);
assert!(
response["error"]["message"]
.as_str()
.unwrap_or("")
.to_lowercase()
.contains("missing"),
"error message should mention 'missing', got: {}",
response["error"]["message"]
);
}
#[tokio::test]
async fn test_jsonrpc_wrong_type_param_returns_32602() {
let calc = Calculator;
let request = json!({
"jsonrpc": "2.0",
"method": "add",
"params": {"a": "not-a-number", "b": 3},
"id": 11
});
let response = calc.jsonrpc_handle_async(request).await;
assert!(response["error"].is_object(), "expected an error object, got: {}", response);
assert_eq!(
response["error"]["code"],
-32602,
"wrong-type param must produce -32602, got: {}",
response["error"]["code"]
);
}
#[derive(Clone)]
struct OptionalParamService;
#[jsonrpc]
impl OptionalParamService {
pub fn search(&self, query: String, limit: Option<u32>) -> String {
format!("query={} limit={:?}", query, limit)
}
pub fn ping(&self) -> String {
"pong".to_string()
}
}
#[tokio::test]
async fn test_jsonrpc_optional_wrong_type_returns_32602() {
let svc = OptionalParamService;
let request = json!({
"jsonrpc": "2.0",
"method": "search",
"params": {"query": "hello", "limit": "not-a-number"},
"id": 20
});
let response = svc.jsonrpc_handle_async(request).await;
assert!(
response["error"].is_object(),
"optional param with wrong type must return an error, got: {}",
response
);
assert_eq!(
response["error"]["code"],
-32602,
"optional wrong-type must produce -32602, got: {}",
response["error"]["code"]
);
assert!(
response["error"]["message"]
.as_str()
.unwrap_or("")
.to_lowercase()
.contains("limit"),
"error message should mention 'limit', got: {}",
response["error"]["message"]
);
}
#[tokio::test]
async fn test_jsonrpc_optional_absent_is_ok() {
let svc = OptionalParamService;
let request = json!({
"jsonrpc": "2.0",
"method": "search",
"params": {"query": "hello"},
"id": 21
});
let response = svc.jsonrpc_handle_async(request).await;
assert!(response["error"].is_null(), "absent optional should succeed, got: {}", response);
assert!(response["result"].is_string(), "should return a string result, got: {}", response);
}
#[tokio::test]
async fn test_jsonrpc_unknown_param_does_not_break_dispatch() {
let svc = OptionalParamService;
let request = json!({
"jsonrpc": "2.0",
"method": "ping",
"params": {"unexpected_key": "value"},
"id": 22
});
let response = svc.jsonrpc_handle_async(request).await;
assert!(
response["error"].is_null(),
"unknown param should not cause an error, got: {}",
response
);
assert_eq!(
response["result"], "pong",
"should still return the correct result"
);
}
#[tokio::test]
async fn test_jsonrpc_unknown_extra_param_does_not_break_dispatch() {
let svc = OptionalParamService;
let request = json!({
"jsonrpc": "2.0",
"method": "search",
"params": {"query": "hello", "limit": 5, "typo_param": "oops"},
"id": 23
});
let response = svc.jsonrpc_handle_async(request).await;
assert!(
response["error"].is_null(),
"unknown extra param should not cause an error, got: {}",
response
);
assert!(response["result"].is_string(), "should still return a result");
}
#[derive(Clone)]
struct HiddenRpcService;
#[jsonrpc]
impl HiddenRpcService {
pub fn public_method(&self) -> String {
"public".to_string()
}
#[server(hidden)]
pub fn hidden_method(&self, value: i32) -> i32 {
value * 2
}
}
#[test]
fn test_jsonrpc_hidden_method_not_in_methods_list() {
let methods = HiddenRpcService::jsonrpc_methods();
assert!(methods.contains(&"public_method".to_string()));
assert!(!methods.contains(&"hidden_method".to_string()));
}
#[tokio::test]
async fn test_jsonrpc_hidden_method_still_callable() {
let svc = HiddenRpcService;
let request = json!({
"jsonrpc": "2.0",
"method": "hidden_method",
"params": {"value": 21},
"id": 1
});
let response = svc.jsonrpc_handle_async(request).await;
assert_eq!(response["result"], json!(42));
}
#[test]
fn test_jsonrpc_hidden_method_absent_from_openapi_paths() {
let paths = HiddenRpcService::jsonrpc_openapi_paths();
assert_eq!(paths.len(), 1);
let path = &paths[0];
let body = path.operation.request_body.as_ref().unwrap();
let body_str = body.to_string();
assert!(body_str.contains("public_method"), "public_method must be in OpenRPC body");
assert!(!body_str.contains("hidden_method"), "hidden_method must not be in OpenRPC body");
}