@@ -31,19 +31,15 @@ pub(crate) async fn parse_jsonrpc_http_request<C: AsyncRead + AsyncWrite + Unpin
3131
3232#[ derive( Debug , Clone , PartialEq , Eq ) ]
3333pub struct JsonRpcRequestInfo {
34- pub method : Option < String > ,
35- pub params : HashMap < String , String > ,
34+ pub calls : Vec < JsonRpcCallInfo > ,
35+ pub is_batch : bool ,
3636 pub error : Option < String > ,
3737}
3838
39- /// Returns true if the parsed request's method matches the given `rpc_method` rule pattern.
40- ///
41- /// An empty `rpc_method` pattern matches any method.
42- pub fn rpc_method_rule_matches ( info : & JsonRpcRequestInfo , rpc_method : & str ) -> bool {
43- if rpc_method. is_empty ( ) {
44- return true ;
45- }
46- info. method . as_deref ( ) == Some ( rpc_method)
39+ #[ derive( Debug , Clone , PartialEq , Eq ) ]
40+ pub struct JsonRpcCallInfo {
41+ pub method : String ,
42+ pub params : HashMap < String , String > ,
4743}
4844
4945/// Parse a JSON-RPC 2.0 request body and extract the `method` field.
@@ -53,25 +49,60 @@ pub fn rpc_method_rule_matches(info: &JsonRpcRequestInfo, rpc_method: &str) -> b
5349pub fn parse_jsonrpc_body ( body : & [ u8 ] ) -> JsonRpcRequestInfo {
5450 let Ok ( value) = serde_json:: from_slice :: < serde_json:: Value > ( body) else {
5551 return JsonRpcRequestInfo {
56- method : None ,
57- params : HashMap :: new ( ) ,
52+ calls : Vec :: new ( ) ,
53+ is_batch : false ,
5854 error : Some ( "invalid JSON" . to_string ( ) ) ,
5955 } ;
6056 } ;
61- let Some ( method) = value. get ( "method" ) . and_then ( |m| m. as_str ( ) ) else {
57+
58+ if let serde_json:: Value :: Array ( items) = value {
59+ if items. is_empty ( ) {
60+ return JsonRpcRequestInfo {
61+ calls : Vec :: new ( ) ,
62+ is_batch : true ,
63+ error : Some ( "empty batch" . to_string ( ) ) ,
64+ } ;
65+ }
66+ let mut calls = Vec :: new ( ) ;
67+ for item in & items {
68+ let Some ( call) = parse_jsonrpc_call ( item) else {
69+ return JsonRpcRequestInfo {
70+ calls : Vec :: new ( ) ,
71+ is_batch : true ,
72+ error : Some ( "batch item missing or non-string 'method' field" . to_string ( ) ) ,
73+ } ;
74+ } ;
75+ calls. push ( call) ;
76+ }
77+ return JsonRpcRequestInfo {
78+ calls,
79+ is_batch : true ,
80+ error : None ,
81+ } ;
82+ }
83+
84+ let Some ( call) = parse_jsonrpc_call ( & value) else {
6285 return JsonRpcRequestInfo {
63- method : None ,
64- params : HashMap :: new ( ) ,
86+ calls : Vec :: new ( ) ,
87+ is_batch : false ,
6588 error : Some ( "missing or non-string 'method' field" . to_string ( ) ) ,
6689 } ;
6790 } ;
6891 JsonRpcRequestInfo {
69- method : Some ( method. to_string ( ) ) ,
92+ calls : vec ! [ call] ,
93+ is_batch : false ,
94+ error : None ,
95+ }
96+ }
97+
98+ fn parse_jsonrpc_call ( value : & serde_json:: Value ) -> Option < JsonRpcCallInfo > {
99+ let method = value. get ( "method" ) . and_then ( |m| m. as_str ( ) ) ?;
100+ Some ( JsonRpcCallInfo {
101+ method : method. to_string ( ) ,
70102 params : value
71103 . get ( "params" )
72104 . map_or_else ( HashMap :: new, flatten_jsonrpc_params) ,
73- error : None ,
74- }
105+ } )
75106}
76107
77108fn flatten_jsonrpc_params ( value : & serde_json:: Value ) -> HashMap < String , String > {
@@ -113,39 +144,45 @@ mod tests {
113144 fn parses_method_from_request_body ( ) {
114145 let body = br#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}"# ;
115146 let info = parse_jsonrpc_body ( body) ;
116- assert_eq ! ( info. method. as_deref( ) , Some ( "initialize" ) ) ;
147+ assert_eq ! (
148+ info. calls. first( ) . map( |call| call. method. as_str( ) ) ,
149+ Some ( "initialize" )
150+ ) ;
151+ assert_eq ! ( info. calls. len( ) , 1 ) ;
152+ assert ! ( !info. is_batch) ;
117153 assert ! ( info. error. is_none( ) ) ;
118154 }
119155
120156 #[ test]
121157 fn flattens_object_params_for_policy_matching ( ) {
122158 let body = br#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"submit_report","arguments":{"scope":"workspace/main"}}}"# ;
123159 let info = parse_jsonrpc_body ( body) ;
160+ let params = & info. calls . first ( ) . expect ( "single request call" ) . params ;
124161 assert_eq ! (
125- info . params. get( "name" ) . map( String :: as_str) ,
162+ params. get( "name" ) . map( String :: as_str) ,
126163 Some ( "submit_report" )
127164 ) ;
128165 assert_eq ! (
129- info . params. get( "arguments.scope" ) . map( String :: as_str) ,
166+ params. get( "arguments.scope" ) . map( String :: as_str) ,
130167 Some ( "workspace/main" )
131168 ) ;
132169 }
133170
134171 #[ test]
135- fn rpc_method_rule_empty_matches_any ( ) {
136- let info = parse_jsonrpc_body ( br#"{"jsonrpc":"2.0","id":1,"method":"tools/call"}"# ) ;
137- assert ! ( rpc_method_rule_matches ( & info , "" ) ) ;
138- }
139-
140- # [ test ]
141- fn rpc_method_rule_matches_exact_method ( ) {
142- let info = parse_jsonrpc_body ( br#"{"jsonrpc":"2.0","id":1,"method":"initialize"}"# ) ;
143- assert ! ( rpc_method_rule_matches ( & info, "initialize" ) ) ;
144- }
145-
146- # [ test ]
147- fn rpc_method_rule_does_not_match_different_method ( ) {
148- let info = parse_jsonrpc_body ( br#"{"jsonrpc":"2.0","id":1,"method":"tools/call"}"# ) ;
149- assert ! ( !rpc_method_rule_matches ( & info , "initialize" ) ) ;
172+ fn parses_valid_batch_without_error ( ) {
173+ let body = br#"[
174+ {"jsonrpc":"2.0","id":1,"method":"tools/list"},
175+ {"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"read_status"} }
176+ ]"# ;
177+ let info = parse_jsonrpc_body ( body ) ;
178+ assert ! ( info . error . is_none ( ) ) ;
179+ assert ! ( info . is_batch ) ;
180+ assert_eq ! ( info. calls . len ( ) , 2 ) ;
181+ assert_eq ! ( info . calls [ 0 ] . method , "tools/list" ) ;
182+ assert_eq ! ( info . calls [ 1 ] . method , "tools/call" ) ;
183+ assert_eq ! (
184+ info . calls [ 1 ] . params . get ( "name" ) . map ( String :: as_str ) ,
185+ Some ( "read_status" )
186+ ) ;
150187 }
151188}
0 commit comments