11//! RPC client for high-level API requests over DDS.
22
33use serde:: { Serialize , de:: DeserializeOwned } ;
4+ use serde_json:: Value ;
45use std:: time:: { Duration , Instant } ;
56use uuid:: Uuid ;
67
@@ -23,7 +24,9 @@ impl Default for RpcClientOptions {
2324 fn default ( ) -> Self {
2425 Self {
2526 domain_id : 0 ,
26- default_timeout : Duration :: from_millis ( 1000 ) ,
27+ // 5 s is a safe default for most commands. Mode changes are slow,
28+ // so change_mode passes its own longer timeout.
29+ default_timeout : Duration :: from_secs ( 5 ) ,
2730 }
2831 }
2932}
@@ -35,6 +38,32 @@ pub struct RpcClient {
3538 default_timeout : Duration ,
3639}
3740
41+ fn parse_status_value ( value : & Value ) -> Option < i32 > {
42+ match value {
43+ Value :: Number ( n) => n. as_i64 ( ) . and_then ( |v| i32:: try_from ( v) . ok ( ) ) ,
44+ Value :: String ( s) => s. parse :: < i32 > ( ) . ok ( ) ,
45+ _ => None ,
46+ }
47+ }
48+
49+ fn parse_status_from_header ( raw_json : & str ) -> Option < i32 > {
50+ let value: Value = serde_json:: from_str ( raw_json. trim ( ) ) . ok ( ) ?;
51+ let object = value. as_object ( ) ?;
52+ object. get ( "status" ) . and_then ( parse_status_value)
53+ }
54+
55+ fn decode_response_body < R > ( body : & str ) -> std:: result:: Result < R , serde_json:: Error >
56+ where
57+ R : DeserializeOwned ,
58+ {
59+ let trimmed = body. trim ( ) ;
60+ if trimmed. is_empty ( ) {
61+ return serde_json:: from_str ( "{}" ) ;
62+ }
63+
64+ serde_json:: from_str ( trimmed)
65+ }
66+
3867impl RpcClient {
3968 pub fn new ( options : RpcClientOptions ) -> Result < Self > {
4069 let node = DdsNode :: new ( super :: DdsConfig {
@@ -102,22 +131,25 @@ impl RpcClient {
102131 continue ;
103132 }
104133
105- if response. status_code == -1 {
134+ let status_code = parse_status_from_header ( & response. header ) . unwrap_or ( 0 ) ;
135+
136+ if status_code == -1 {
106137 continue ;
107138 }
108139
109- if response. status_code != 0 {
110- return Err ( RpcError :: from_status_code (
111- response. status_code ,
112- response. body ,
113- )
114- . into ( ) ) ;
140+ if status_code != 0 {
141+ let message = if response. body . trim ( ) . is_empty ( ) {
142+ response. header
143+ } else {
144+ response. body
145+ } ;
146+ return Err ( RpcError :: from_status_code ( status_code, message) . into ( ) ) ;
115147 }
116148
117- let result: R = serde_json :: from_str ( & response. body ) . map_err ( |err| {
149+ let result: R = decode_response_body ( & response. body ) . map_err ( |err| {
118150 RpcError :: RequestFailed {
119- status : response . status_code ,
120- message : format ! ( "Failed to deserialize response: {err}" ) ,
151+ status : status_code,
152+ message : format ! ( "Failed to deserialize response body : {err}" ) ,
121153 }
122154 } ) ?;
123155
@@ -134,3 +166,42 @@ impl RpcClient {
134166 . map_err ( |err| DdsError :: ReceiveFailed ( err. to_string ( ) ) ) ?
135167 }
136168}
169+
170+ #[ cfg( test) ]
171+ mod tests {
172+ use super :: { decode_response_body, parse_status_from_header, parse_status_value} ;
173+ use serde_json:: json;
174+
175+ #[ derive( serde:: Deserialize ) ]
176+ struct EmptyResponse { }
177+
178+ #[ test]
179+ fn parse_status_from_header_reads_status_field ( ) {
180+ assert_eq ! ( parse_status_from_header( r#"{"status":0}"# ) , Some ( 0 ) ) ;
181+ assert_eq ! ( parse_status_from_header( r#"{"status":"-1"}"# ) , Some ( -1 ) ) ;
182+ }
183+
184+ #[ test]
185+ fn parse_status_value_handles_number_and_string ( ) {
186+ assert_eq ! ( parse_status_value( & json!( 0 ) ) , Some ( 0 ) ) ;
187+ assert_eq ! ( parse_status_value( & json!( "-1" ) ) , Some ( -1 ) ) ;
188+ assert_eq ! ( parse_status_value( & json!( "not-a-number" ) ) , None ) ;
189+ }
190+
191+ #[ test]
192+ fn parse_status_from_header_ignores_other_fields ( ) {
193+ assert_eq ! ( parse_status_from_header( r#"{"status_code":0}"# ) , None ) ;
194+ assert_eq ! ( parse_status_from_header( r#"{"code":0}"# ) , None ) ;
195+ }
196+
197+ #[ test]
198+ fn empty_body_deserializes_as_empty_object ( ) {
199+ let _: EmptyResponse = decode_response_body ( "" ) . expect ( "empty body should parse" ) ;
200+ }
201+
202+ #[ test]
203+ fn non_json_body_fails_deserialization ( ) {
204+ let parsed = decode_response_body :: < EmptyResponse > ( "not-json" ) ;
205+ assert ! ( parsed. is_err( ) ) ;
206+ }
207+ }
0 commit comments