Skip to main content

rocketmq_remoting/protocol/header/
heartbeat_request_header.rs

1// Copyright 2023 The RocketMQ Rust Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use rocketmq_macros::RequestHeaderCodecV2;
16use serde::Deserialize;
17use serde::Serialize;
18
19use crate::rpc::rpc_request_header::RpcRequestHeader;
20
21#[derive(Serialize, Deserialize, Debug, Default, RequestHeaderCodecV2)]
22#[serde(rename_all = "camelCase")]
23pub struct HeartbeatRequestHeader {
24    #[serde(flatten)]
25    pub rpc_request: Option<RpcRequestHeader>,
26}
27
28#[cfg(test)]
29mod tests {
30    use std::collections::HashMap;
31
32    use cheetah_string::CheetahString;
33
34    use super::*;
35    use crate::protocol::command_custom_header::FromMap;
36
37    #[test]
38    fn default_initialization_creates_empty_struct() {
39        let header = HeartbeatRequestHeader::default();
40        assert!(header.rpc_request.is_none());
41    }
42
43    #[test]
44    fn serialization_with_none_rpc_request() {
45        let header = HeartbeatRequestHeader { rpc_request: None };
46        let json = serde_json::to_string(&header).unwrap();
47        assert_eq!(json, "{}");
48    }
49
50    #[test]
51    fn serialization_with_some_rpc_request() {
52        let header = HeartbeatRequestHeader {
53            rpc_request: Some(RpcRequestHeader {
54                namespace: Some(CheetahString::from("test_namespace")),
55                namespaced: Some(true),
56                broker_name: Some(CheetahString::from("test_broker")),
57                oneway: Some(false),
58            }),
59        };
60        let json = serde_json::to_string(&header).unwrap();
61        assert!(json.contains("\"namespace\":\"test_namespace\""));
62        assert!(json.contains("\"namespaced\":true"));
63        assert!(json.contains("\"brokerName\":\"test_broker\""));
64        assert!(json.contains("\"oneway\":false"));
65
66        // Should not contain a nested "rpc_request" or "rpcRequest" field
67        assert!(!json.contains("rpcRequest"));
68        assert!(!json.contains("rpc_request"));
69    }
70
71    #[test]
72    fn deserialization_with_empty_json() {
73        let json = "{}";
74        let header: HeartbeatRequestHeader = serde_json::from_str(json).unwrap();
75        // With flattened attribute, empty JSON creates Some with all None fields
76        assert!(header.rpc_request.is_some());
77        let rpc = header.rpc_request.unwrap();
78        assert!(rpc.namespace.is_none());
79        assert!(rpc.namespaced.is_none());
80        assert!(rpc.broker_name.is_none());
81        assert!(rpc.oneway.is_none());
82    }
83
84    #[test]
85    fn deserialization_with_rpc_request_fields() {
86        let json = r#"{
87            "namespace": "test_namespace",
88            "namespaced": true,
89            "brokerName": "test_broker",
90            "oneway": false
91        }"#;
92        let header: HeartbeatRequestHeader = serde_json::from_str(json).unwrap();
93        assert!(header.rpc_request.is_some());
94        let rpc = header.rpc_request.unwrap();
95        assert_eq!(rpc.namespace.unwrap(), "test_namespace");
96        assert!(rpc.namespaced.unwrap());
97        assert_eq!(rpc.broker_name.unwrap(), "test_broker");
98        assert!(!rpc.oneway.unwrap());
99    }
100
101    #[test]
102    fn deserialization_with_partial_rpc_request_fields() {
103        let json = r#"{
104            "namespace": "test_namespace",
105            "brokerName": "test_broker"
106        }"#;
107        let header: HeartbeatRequestHeader = serde_json::from_str(json).unwrap();
108        assert!(header.rpc_request.is_some());
109        let rpc = header.rpc_request.unwrap();
110        assert_eq!(rpc.namespace.unwrap(), "test_namespace");
111        assert_eq!(rpc.broker_name.unwrap(), "test_broker");
112        assert!(rpc.namespaced.is_none());
113        assert!(rpc.oneway.is_none());
114    }
115
116    #[test]
117    fn serialization_deserialization_roundtrip_with_none() {
118        let original = HeartbeatRequestHeader { rpc_request: None };
119        let json = serde_json::to_string(&original).unwrap();
120        let deserialized: HeartbeatRequestHeader = serde_json::from_str(&json).unwrap();
121        // With flattened attribute, empty JSON creates Some with all None fields
122        assert!(deserialized.rpc_request.is_some());
123        let rpc = deserialized.rpc_request.unwrap();
124        assert!(rpc.namespace.is_none());
125        assert!(rpc.namespaced.is_none());
126        assert!(rpc.broker_name.is_none());
127        assert!(rpc.oneway.is_none());
128    }
129
130    #[test]
131    fn serialization_deserialization_roundtrip_with_some() {
132        let original = HeartbeatRequestHeader {
133            rpc_request: Some(RpcRequestHeader {
134                namespace: Some(CheetahString::from("test_namespace")),
135                namespaced: Some(true),
136                broker_name: Some(CheetahString::from("test_broker")),
137                oneway: Some(false),
138            }),
139        };
140        let json = serde_json::to_string(&original).unwrap();
141        let deserialized: HeartbeatRequestHeader = serde_json::from_str(&json).unwrap();
142
143        assert!(deserialized.rpc_request.is_some());
144        let rpc = deserialized.rpc_request.unwrap();
145        assert_eq!(rpc.namespace.unwrap(), "test_namespace");
146        assert!(rpc.namespaced.unwrap());
147        assert_eq!(rpc.broker_name.unwrap(), "test_broker");
148        assert!(!rpc.oneway.unwrap());
149    }
150
151    #[test]
152    fn serialization_deserialization_roundtrip_with_partial_fields() {
153        let original = HeartbeatRequestHeader {
154            rpc_request: Some(RpcRequestHeader {
155                namespace: Some(CheetahString::from("test_namespace")),
156                namespaced: None,
157                broker_name: Some(CheetahString::from("test_broker")),
158                oneway: None,
159            }),
160        };
161        let json = serde_json::to_string(&original).unwrap();
162        let deserialized: HeartbeatRequestHeader = serde_json::from_str(&json).unwrap();
163
164        assert!(deserialized.rpc_request.is_some());
165        let rpc = deserialized.rpc_request.unwrap();
166        assert_eq!(rpc.namespace.unwrap(), "test_namespace");
167        assert_eq!(rpc.broker_name.unwrap(), "test_broker");
168        assert!(rpc.namespaced.is_none());
169        assert!(rpc.oneway.is_none());
170    }
171
172    #[test]
173    fn flattened_serde_attribute_works_correctly() {
174        let header = HeartbeatRequestHeader {
175            rpc_request: Some(RpcRequestHeader {
176                namespace: Some(CheetahString::from("ns")),
177                namespaced: Some(true),
178                broker_name: Some(CheetahString::from("broker")),
179                oneway: Some(true),
180            }),
181        };
182        let json = serde_json::to_string(&header).unwrap();
183        let value: serde_json::Value = serde_json::from_str(&json).unwrap();
184
185        // Verify fields are at top level (not nested)
186        assert!(value.get("namespace").is_some());
187        assert!(value.get("namespaced").is_some());
188        assert!(value.get("brokerName").is_some());
189        assert!(value.get("oneway").is_some());
190
191        // Verify no nested structure
192        assert!(value.get("rpcRequest").is_none());
193        assert!(value.get("rpc_request").is_none());
194    }
195
196    #[test]
197    fn from_map_with_empty_map() {
198        let map = HashMap::new();
199        let header = <HeartbeatRequestHeader as FromMap>::from(&map).unwrap();
200
201        assert!(header.rpc_request.is_some());
202        let rpc = header.rpc_request.unwrap();
203
204        assert!(rpc.namespace.is_none());
205        assert!(rpc.namespaced.is_none());
206        assert!(rpc.broker_name.is_none());
207        assert!(rpc.oneway.is_none());
208    }
209
210    #[test]
211    fn from_map_with_rpc_request_fields() {
212        let mut map = HashMap::new();
213        map.insert(CheetahString::from("namespace"), CheetahString::from("test_namespace"));
214        map.insert(CheetahString::from("namespaced"), CheetahString::from("true"));
215        map.insert(CheetahString::from("brokerName"), CheetahString::from("test_broker"));
216        map.insert(CheetahString::from("oneway"), CheetahString::from("false"));
217
218        let header = <HeartbeatRequestHeader as FromMap>::from(&map).unwrap();
219
220        assert!(header.rpc_request.is_some());
221        let rpc = header.rpc_request.unwrap();
222        assert_eq!(rpc.namespace.unwrap(), "test_namespace");
223        assert!(rpc.namespaced.unwrap());
224        assert_eq!(rpc.broker_name.unwrap(), "test_broker");
225        assert!(!rpc.oneway.unwrap());
226    }
227
228    #[test]
229    fn from_map_with_partial_fields() {
230        let mut map = HashMap::new();
231        map.insert(CheetahString::from("namespace"), CheetahString::from("test_namespace"));
232
233        let header = <HeartbeatRequestHeader as FromMap>::from(&map).unwrap();
234
235        assert!(header.rpc_request.is_some());
236        let rpc = header.rpc_request.unwrap();
237        assert_eq!(rpc.namespace.unwrap(), "test_namespace");
238        assert!(rpc.namespaced.is_none());
239        assert!(rpc.broker_name.is_none());
240        assert!(rpc.oneway.is_none());
241    }
242
243    #[test]
244    fn debug_trait_implementation() {
245        let header = HeartbeatRequestHeader {
246            rpc_request: Some(RpcRequestHeader {
247                namespace: Some(CheetahString::from("test_namespace")),
248                namespaced: Some(true),
249                broker_name: Some(CheetahString::from("test_broker")),
250                oneway: Some(false),
251            }),
252        };
253        let debug_string = format!("{:?}", header);
254
255        assert!(debug_string.contains("HeartbeatRequestHeader"));
256        assert!(debug_string.contains("rpc_request"));
257    }
258
259    #[test]
260    fn debug_trait_with_none() {
261        let header = HeartbeatRequestHeader { rpc_request: None };
262        let debug_string = format!("{:?}", header);
263
264        assert!(debug_string.contains("HeartbeatRequestHeader"));
265        assert!(debug_string.contains("None"));
266    }
267
268    #[test]
269    fn default_trait_implementation() {
270        let header = HeartbeatRequestHeader::default();
271        assert!(header.rpc_request.is_none());
272
273        // Verify it's equivalent to manual construction with None
274        let manual_header = HeartbeatRequestHeader { rpc_request: None };
275        let default_json = serde_json::to_string(&header).unwrap();
276        let manual_json = serde_json::to_string(&manual_header).unwrap();
277
278        assert_eq!(default_json, manual_json);
279    }
280
281    #[test]
282    fn complete_rpc_request_header_structure() {
283        let rpc_header = RpcRequestHeader {
284            namespace: Some(CheetahString::from("production")),
285            namespaced: Some(true),
286            broker_name: Some(CheetahString::from("broker-master")),
287            oneway: Some(false),
288        };
289
290        let header = HeartbeatRequestHeader {
291            rpc_request: Some(rpc_header),
292        };
293
294        let json = serde_json::to_string(&header).unwrap();
295        let deserialized: HeartbeatRequestHeader = serde_json::from_str(&json).unwrap();
296
297        let rpc = deserialized.rpc_request.unwrap();
298        assert_eq!(rpc.namespace.unwrap(), "production");
299        assert!(rpc.namespaced.unwrap());
300        assert_eq!(rpc.broker_name.unwrap(), "broker-master");
301        assert!(!rpc.oneway.unwrap());
302    }
303
304    #[test]
305    fn rpc_request_header_namespace_field() {
306        let header = HeartbeatRequestHeader {
307            rpc_request: Some(RpcRequestHeader {
308                namespace: Some(CheetahString::from("test_ns")),
309                namespaced: None,
310                broker_name: None,
311                oneway: None,
312            }),
313        };
314
315        assert!(header.rpc_request.is_some());
316        assert_eq!(
317            header.rpc_request.as_ref().unwrap().namespace.as_ref().unwrap(),
318            "test_ns"
319        );
320    }
321
322    #[test]
323    fn rpc_request_header_namespaced_field() {
324        let header = HeartbeatRequestHeader {
325            rpc_request: Some(RpcRequestHeader {
326                namespace: None,
327                namespaced: Some(true),
328                broker_name: None,
329                oneway: None,
330            }),
331        };
332
333        assert!(header.rpc_request.is_some());
334        assert!(header.rpc_request.as_ref().unwrap().namespaced.unwrap());
335    }
336
337    #[test]
338    fn rpc_request_header_broker_name_field() {
339        let header = HeartbeatRequestHeader {
340            rpc_request: Some(RpcRequestHeader {
341                namespace: None,
342                namespaced: None,
343                broker_name: Some(CheetahString::from("my_broker")),
344                oneway: None,
345            }),
346        };
347
348        assert!(header.rpc_request.is_some());
349        assert_eq!(
350            header.rpc_request.as_ref().unwrap().broker_name.as_ref().unwrap(),
351            "my_broker"
352        );
353    }
354
355    #[test]
356    fn rpc_request_header_oneway_field() {
357        let header = HeartbeatRequestHeader {
358            rpc_request: Some(RpcRequestHeader {
359                namespace: None,
360                namespaced: None,
361                broker_name: None,
362                oneway: Some(true),
363            }),
364        };
365
366        assert!(header.rpc_request.is_some());
367        assert!(header.rpc_request.as_ref().unwrap().oneway.unwrap());
368    }
369
370    #[test]
371    fn camel_case_field_names_in_json() {
372        let header = HeartbeatRequestHeader {
373            rpc_request: Some(RpcRequestHeader {
374                namespace: Some(CheetahString::from("ns")),
375                namespaced: Some(false),
376                broker_name: Some(CheetahString::from("broker")),
377                oneway: Some(true),
378            }),
379        };
380
381        let json = serde_json::to_string(&header).unwrap();
382
383        // Verify camelCase is used (not snake_case)
384        assert!(json.contains("brokerName"));
385        assert!(!json.contains("broker_name"));
386        assert!(json.contains("namespace"));
387        assert!(json.contains("namespaced"));
388        assert!(json.contains("oneway"));
389    }
390}