Skip to main content

rust_eureka/response/
applications.rs

1use super::Application;
2use serde::de::{self, Deserialize, Deserializer, MapAccess, SeqAccess, Visitor};
3use serde::ser::{Serialize, SerializeStruct, Serializer};
4use std::fmt;
5
6#[derive(Debug, PartialEq)]
7pub struct Applications {
8    pub versions_delta: i16,
9    pub apps_hashcode: String,
10    pub applications: Vec<Application>,
11}
12
13// Custom deserializer to handle both single Application object and array of Applications
14fn deserialize_application_field<'de, D>(deserializer: D) -> Result<Vec<Application>, D::Error>
15where
16    D: Deserializer<'de>,
17{
18    struct ApplicationOrVec;
19
20    impl<'de> Visitor<'de> for ApplicationOrVec {
21        type Value = Vec<Application>;
22
23        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
24            formatter.write_str("application or array of applications")
25        }
26
27        fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
28        where
29            A: MapAccess<'de>,
30        {
31            let app: Application =
32                Deserialize::deserialize(de::value::MapAccessDeserializer::new(map))?;
33            Ok(vec![app])
34        }
35
36        fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
37        where
38            A: SeqAccess<'de>,
39        {
40            Deserialize::deserialize(de::value::SeqAccessDeserializer::new(seq))
41        }
42    }
43
44    deserializer.deserialize_any(ApplicationOrVec)
45}
46
47// Manual implementation of Serialize for Applications
48impl Serialize for Applications {
49    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
50    where
51        S: Serializer,
52    {
53        let mut state = serializer.serialize_struct("Applications", 3)?;
54        state.serialize_field("versions__delta", &self.versions_delta)?;
55        state.serialize_field("apps__hashcode", &self.apps_hashcode)?;
56        // Serialize as single object if only one application, array if multiple
57        if self.applications.len() == 1 {
58            state.serialize_field("application", &self.applications[0])?;
59        } else {
60            state.serialize_field("application", &self.applications)?;
61        }
62        state.end()
63    }
64}
65
66// Manual implementation of Deserialize for Applications
67impl<'de> Deserialize<'de> for Applications {
68    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
69    where
70        D: Deserializer<'de>,
71    {
72        struct ApplicationsVisitor;
73
74        impl<'de> Visitor<'de> for ApplicationsVisitor {
75            type Value = Applications;
76
77            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
78                formatter.write_str("struct Applications")
79            }
80
81            fn visit_map<V>(self, mut map: V) -> Result<Applications, V::Error>
82            where
83                V: MapAccess<'de>,
84            {
85                let mut versions_delta = None;
86                let mut apps_hashcode = None;
87                let mut applications = None;
88
89                while let Some(key) = map.next_key::<String>()? {
90                    match key.as_str() {
91                        "versions__delta" => {
92                            if versions_delta.is_some() {
93                                return Err(de::Error::duplicate_field("versions__delta"));
94                            }
95                            // servers sometimes return versions__delta as string ("1") or number (1)
96                            let val: serde_json::Value = map.next_value()?;
97                            let parsed = match val {
98                                serde_json::Value::Number(n) => n.as_i64().map(|i| i as i16),
99                                serde_json::Value::String(s) => s.parse::<i16>().ok(),
100                                _ => None,
101                            };
102                            if let Some(v) = parsed {
103                                versions_delta = Some(v);
104                            } else {
105                                return Err(de::Error::custom("Invalid versions__delta type"));
106                            }
107                        }
108                        "apps__hashcode" => {
109                            if apps_hashcode.is_some() {
110                                return Err(de::Error::duplicate_field("apps__hashcode"));
111                            }
112                            // sometimes returned as string or number; coerce to string
113                            let val: serde_json::Value = map.next_value()?;
114                            let s = match val {
115                                serde_json::Value::String(s) => s,
116                                serde_json::Value::Number(n) => n.to_string(),
117                                _ => return Err(de::Error::custom("Invalid apps__hashcode type")),
118                            };
119                            apps_hashcode = Some(s);
120                        }
121                        "application" => {
122                            if applications.is_some() {
123                                return Err(de::Error::duplicate_field("application"));
124                            }
125                            applications = Some(map.next_value_seed(ApplicationFieldSeed)?);
126                        }
127                        _ => {
128                            // Skip unknown fields
129                            let _: de::IgnoredAny = map.next_value()?;
130                        }
131                    }
132                }
133
134                let versions_delta =
135                    versions_delta.ok_or_else(|| de::Error::missing_field("versions__delta"))?;
136                let apps_hashcode =
137                    apps_hashcode.ok_or_else(|| de::Error::missing_field("apps__hashcode"))?;
138                let applications =
139                    applications.ok_or_else(|| de::Error::missing_field("application"))?;
140
141                Ok(Applications {
142                    versions_delta,
143                    apps_hashcode,
144                    applications,
145                })
146            }
147        }
148
149        struct ApplicationFieldSeed;
150
151        impl<'de> de::DeserializeSeed<'de> for ApplicationFieldSeed {
152            type Value = Vec<Application>;
153
154            fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
155            where
156                D: Deserializer<'de>,
157            {
158                deserialize_application_field(deserializer)
159            }
160        }
161
162        const FIELDS: &[&str] = &["versions__delta", "apps__hashcode", "application"];
163        deserializer.deserialize_struct("Applications", FIELDS, ApplicationsVisitor)
164    }
165}
166
167impl From<Application> for Vec<Application> {
168    fn from(v: Application) -> Self {
169        vec![v]
170    }
171}
172
173#[cfg(test)]
174pub mod tests {
175    use super::super::ActionType;
176    use super::super::Application;
177    use super::super::DataCenterInfo;
178    use super::super::DcName;
179    use super::super::Instance;
180    use super::super::LeaseInfo;
181    use super::super::Status;
182    use super::*;
183    use serde_json::{self, Map};
184
185    #[test]
186    fn test_applications_serialize() {
187        let applications = build_test_applications();
188        let result = serde_json::to_string(&applications).unwrap();
189        assert!(result.contains("\"apps__hashcode\":\"UP_1_\""));
190        assert!(result.contains("\"name\":\"INTEGRATION_TEST\""));
191    }
192
193    #[test]
194    fn test_applications_deserialize() {
195        let json = build_test_applications_json();
196        let applications = build_test_applications();
197        let result = serde_json::from_str(json.as_ref()).unwrap();
198
199        assert_eq!(applications, result)
200    }
201
202    #[test]
203    fn test_applications_multi_deserialize() {
204        let json = build_test_multi_applications_json();
205        let result: Applications = serde_json::from_str(json.as_ref()).unwrap();
206        assert_eq!(2, result.applications.len())
207    }
208
209    pub fn build_test_applications() -> Applications {
210        Applications {
211            versions_delta: 1,
212            apps_hashcode: "UP_1_".to_string(),
213            applications: vec![Application {
214                name: "INTEGRATION_TEST".to_string(),
215                instance: vec![Instance {
216                    host_name: "localhost".to_string(),
217                    app: "INTEGRATION_TEST".to_string(),
218                    ip_addr: "127.0.0.1".to_string(),
219                    status: Status::Up,
220                    overriddenstatus: Some(Status::Unknown),
221                    port: Some(7001),
222                    secure_port: Some(7002),
223                    country_id: 1,
224                    data_center_info: DataCenterInfo {
225                        name: DcName::MyOwn,
226                        metadata: None,
227                    },
228                    lease_info: Some(LeaseInfo {
229                        renewal_interval_in_secs: 30,
230                        duration_in_secs: 90,
231                        registration_timestamp: 1503701416749,
232                        last_renewal_timestamp: 1503701416749,
233                        eviction_timestamp: 0,
234                        service_up_timestamp: 1503701416464,
235                    }),
236                    metadata: Map::new(),
237                    homepage_url: "http://google.com".to_string(),
238                    status_page_url: "http://google.com".to_string(),
239                    health_check_url: "http://google.com".to_string(),
240                    vip_address: "127.0.0.1".to_string(),
241                    secure_vip_address: "127.0.0.1".to_string(),
242                    is_coordinating_discovery_server: false,
243                    last_updated_timestamp: 1503701416750,
244                    last_dirty_timestamp: 1503701416457,
245                    action_type: ActionType::Added,
246                }],
247            }],
248        }
249    }
250
251    pub fn build_test_applications_json() -> String {
252        r#"
253        {
254        "versions__delta": 1,
255        "apps__hashcode": "UP_1_",
256        "application": {
257            "name": "INTEGRATION_TEST",
258            "instance": [{
259                "hostName": "localhost",
260                "app": "INTEGRATION_TEST",
261                "ipAddr": "127.0.0.1",
262                "status": "UP",
263                "overriddenstatus": "UNKNOWN",
264                "port": {
265                    "@enabled": "true",
266                    "$": "7001"
267                },
268                "securePort": {
269                    "@enabled": "false",
270                    "$": "7002"
271                },
272                "countryId": 1,
273                "dataCenterInfo": {
274                    "name": "MyOwn"
275                },
276                "leaseInfo": {
277                    "renewalIntervalInSecs": 30,
278                    "durationInSecs": 90,
279                    "registrationTimestamp": 1503701416749,
280                    "lastRenewalTimestamp": 1503701416749,
281                    "evictionTimestamp": 0,
282                    "serviceUpTimestamp": 1503701416464
283                },
284                "metadata": {
285                    "@class": "java.util.Collections$EmptyMap"
286                },
287                "homePageUrl": "http://google.com",
288                "statusPageUrl": "http://google.com",
289                "healthCheckUrl": "http://google.com",
290                "vipAddress": "127.0.0.1",
291                "secureVipAddress": "127.0.0.1",
292                "isCoordinatingDiscoveryServer": false,
293                "lastUpdatedTimestamp": 1503701416750,
294                "lastDirtyTimestamp": 1503701416457,
295                "actionType": "ADDED"
296            }]
297        }
298        }
299        "#
300        .to_string()
301        .replace(" ", "")
302        .replace("\n", "")
303    }
304
305    pub fn build_test_multi_applications_json() -> String {
306        r#"
307        {
308        "versions__delta": 1,
309        "apps__hashcode": "UP_1_",
310        "application": [{
311            "name": "INTEGRATION_TEST",
312            "instance": [{
313                "hostName": "localhost",
314                "app": "INTEGRATION_TEST",
315                "ipAddr": "127.0.0.1",
316                "status": "UP",
317                "overriddenstatus": "UNKNOWN",
318                "port": {
319                    "@enabled": "true",
320                    "$": "7001"
321                },
322                "securePort": {
323                    "@enabled": "false",
324                    "$": "7002"
325                },
326                "countryId": 1,
327                "dataCenterInfo": {
328                    "name": "MyOwn"
329                },
330                "leaseInfo": {
331                    "renewalIntervalInSecs": 30,
332                    "durationInSecs": 90,
333                    "registrationTimestamp": 1503701416749,
334                    "lastRenewalTimestamp": 1503701416749,
335                    "evictionTimestamp": 0,
336                    "serviceUpTimestamp": 1503701416464
337                },
338                "metadata": {
339                    "@class": "java.util.Collections$EmptyMap"
340                },
341                "homePageUrl": "http://google.com",
342                "statusPageUrl": "http://google.com",
343                "healthCheckUrl": "http://google.com",
344                "vipAddress": "127.0.0.1",
345                "secureVipAddress": "127.0.0.1",
346                "isCoordinatingDiscoveryServer": false,
347                "lastUpdatedTimestamp": 1503701416750,
348                "lastDirtyTimestamp": 1503701416457,
349                "actionType": "ADDED"
350            }]
351        }, {
352            "name": "INTEGRATION_TEST2",
353            "instance": [{
354                "hostName": "localhost",
355                "app": "INTEGRATION_TEST",
356                "ipAddr": "127.0.0.1",
357                "status": "UP",
358                "overriddenstatus": "UNKNOWN",
359                "port": {
360                    "@enabled": "true",
361                    "$": "7001"
362                },
363                "securePort": {
364                    "@enabled": "false",
365                    "$": "7002"
366                },
367                "countryId": 1,
368                "dataCenterInfo": {
369                    "name": "MyOwn"
370                },
371                "leaseInfo": {
372                    "renewalIntervalInSecs": 30,
373                    "durationInSecs": 90,
374                    "registrationTimestamp": 1503701416749,
375                    "lastRenewalTimestamp": 1503701416749,
376                    "evictionTimestamp": 0,
377                    "serviceUpTimestamp": 1503701416464
378                },
379                "metadata": {
380                    "@class": "java.util.Collections$EmptyMap"
381                },
382                "homePageUrl": "http://google.com",
383                "statusPageUrl": "http://google.com",
384                "healthCheckUrl": "http://google.com",
385                "vipAddress": "127.0.0.1",
386                "secureVipAddress": "127.0.0.1",
387                "isCoordinatingDiscoveryServer": false,
388                "lastUpdatedTimestamp": 1503701416750,
389                "lastDirtyTimestamp": 1503701416457,
390                "actionType": "ADDED"
391            }]
392        }]
393        }
394        "#
395        .to_string()
396        .replace(" ", "")
397        .replace("\n", "")
398    }
399}