lightstreamer_rs/subscription/
item_update.rs

1use serde::Serialize;
2use std::collections::HashMap;
3
4/// Contains all the information related to an update of the field values for an item.
5/// It reports all the new values of the fields.
6///
7/// COMMAND Subscription:
8/// If the involved Subscription is a COMMAND Subscription, then the values for the current update
9/// are meant as relative to the same key.
10///
11/// Moreover, if the involved Subscription has a two-level behavior enabled, then each update may be
12/// associated with either a first-level or a second-level item. In this case, the reported fields are
13/// always the union of the first-level and second-level fields and each single update can only change
14/// either the first-level or the second-level fields (but for the "command" field, which is first-level
15/// and is always set to "UPDATE" upon a second-level update); note that the second-level field values
16/// are always None until the first second-level update occurs). When the two-level behavior is enabled,
17/// in all methods where a field name has to be supplied, the following convention should be followed:
18///
19/// - The field name can always be used, both for the first-level and the second-level fields. In case of
20///   name conflict, the first-level field is meant.
21/// - The field position can always be used; however, the field positions for the second-level fields start
22///   at the highest position of the first-level field list + 1. If a field schema had been specified for
23///   either first-level or second-level Subscriptions, then client-side knowledge of the first-level schema
24///   length would be required.
25#[derive(Debug, Clone, Serialize)]
26pub struct ItemUpdate {
27    /// The name of the item to which this update belongs. May be None if the item was subscribed to by position only.
28    pub item_name: Option<String>,
29    /// The position of the item in the subscription item list to which this update belongs.
30    pub item_pos: usize,
31    /// A map containing the current values for all fields in this update.
32    pub fields: HashMap<String, Option<String>>,
33    /// A map containing only the fields that have changed in this update.
34    pub changed_fields: HashMap<String, String>,
35    /// Flag indicating whether this update is part of a snapshot (initial state) or a real-time update.
36    pub is_snapshot: bool,
37}
38
39impl ItemUpdate {
40    /// Returns a map containing the values for each field changed with the last server update.
41    /// The related field name is used as key for the values in the map. Note that if the Subscription
42    /// mode of the involved Subscription is COMMAND, then changed fields are meant as relative to the
43    /// previous update for the same key. On such tables if a DELETE command is received, all the fields,
44    /// excluding the key field, will be present as changed, with None value. All of this is also true on
45    /// tables that have the two-level behavior enabled, but in case of DELETE commands second-level fields
46    /// will not be iterated.
47    ///
48    /// # Raises
49    /// - `IllegalStateException` – if the Subscription was initialized using a field schema.
50    ///
51    /// # Returns
52    /// A map containing the values for each field changed with the last server update.
53    pub fn get_changed_fields(&self) -> HashMap<String, String> {
54        self.changed_fields.clone()
55    }
56
57    /// Returns a map containing the values for each field changed with the last server update.
58    /// The 1-based field position within the field schema or field list is used as key for the values in
59    /// the map. Note that if the Subscription mode of the involved Subscription is COMMAND, then changed
60    /// fields are meant as relative to the previous update for the same key. On such tables if a DELETE
61    /// command is received, all the fields, excluding the key field, will be present as changed, with None
62    /// value. All of this is also true on tables that have the two-level behavior enabled, but in case of
63    /// DELETE commands second-level fields will not be iterated.
64    ///
65    /// # Returns
66    /// A map containing the values for each field changed with the last server update.
67    pub fn get_changed_fields_by_position(&self) -> HashMap<usize, String> {
68        self.changed_fields
69            .iter()
70            .map(|(name, value)| (self.get_field_position(name), value.clone()))
71            .collect()
72    }
73
74    /// Returns a map containing the values for each field in the Subscription.
75    /// The related field name is used as key for the values in the map.
76    ///
77    /// # Raises
78    /// - `IllegalStateException` – if the Subscription was initialized using a field schema.
79    ///
80    /// # Returns
81    /// A map containing the values for each field in the Subscription.
82    pub fn get_fields(&self) -> HashMap<String, Option<String>> {
83        self.fields.clone()
84    }
85
86    /// Returns a map containing the values for each field in the Subscription.
87    /// The 1-based field position within the field schema or field list is used as key for the values in the map.
88    ///
89    /// # Returns
90    /// A map containing the values for each field in the Subscription.
91    pub fn get_fields_by_position(&self) -> HashMap<usize, Option<String>> {
92        self.fields
93            .iter()
94            .map(|(name, value)| (self.get_field_position(name), value.clone()))
95            .collect()
96    }
97
98    /// Inquiry method that retrieves the name of the item to which this update pertains.
99    ///
100    /// The name will be None if the related Subscription was initialized using an "Item Group".
101    ///
102    /// # Returns
103    /// The name of the item to which this update pertains.
104    pub fn get_item_name(&self) -> Option<&str> {
105        self.item_name.as_deref()
106    }
107
108    /// Inquiry method that retrieves the position in the "Item List" or "Item Group" of the item
109    /// to which this update pertains.
110    ///
111    /// # Returns
112    /// The 1-based position of the item to which this update pertains.
113    pub fn get_item_pos(&self) -> usize {
114        self.item_pos
115    }
116
117    /// Inquiry method that gets the value for a specified field, as received from the Server with the
118    /// current or previous update.
119    ///
120    /// # Raises
121    /// - `IllegalArgumentException` – if the specified field is not part of the Subscription.
122    ///
123    /// # Parameters
124    /// - `field_name_or_pos` – The field name or the 1-based position of the field within the "Field List" or "Field Schema".
125    ///
126    /// # Returns
127    /// The value of the specified field; it can be None in the following cases:
128    ///
129    /// - a None value has been received from the Server, as None is a possible value for a field;
130    /// - no value has been received for the field yet;
131    /// - the item is subscribed to with the COMMAND mode and a DELETE command is received (only the fields
132    ///   used to carry key and command information are valued).
133    pub fn get_value(&self, field_name_or_pos: &str) -> Option<&str> {
134        match field_name_or_pos.parse::<usize>() {
135            Ok(pos) => self
136                .fields
137                .iter()
138                .find(|(name, _)| self.get_field_position(name) == pos)
139                .and_then(|(_, value)| value.as_deref()),
140            Err(_) => self
141                .fields
142                .get(field_name_or_pos)
143                .and_then(|v| v.as_deref()),
144        }
145    }
146
147    /// Inquiry method that gets the difference between the new value and the previous one as a JSON Patch structure,
148    /// provided that the Server has used the JSON Patch format to send this difference, as part of the "delta delivery"
149    /// mechanism. This, in turn, requires that:
150    ///
151    /// - the Data Adapter has explicitly indicated JSON Patch as the privileged type of compression for this field;
152    /// - both the previous and new value are suitable for the JSON Patch computation (i.e. they are valid JSON representations);
153    /// - the item was subscribed to in MERGE or DISTINCT mode (note that, in case of two-level behavior, this holds for all
154    ///   fields related with second-level items, as these items are in MERGE mode);
155    /// - sending the JSON Patch difference has been evaluated by the Server as more efficient than sending the full new value.
156    ///
157    /// Note that the last condition can be enforced by leveraging the Server's <jsonpatch_min_length> configuration flag,
158    /// so that the availability of the JSON Patch form would only depend on the Client and the Data Adapter.
159    ///
160    /// When the above conditions are not met, the method just returns None; in this case, the new value can only be determined
161    /// through `ItemUpdate.get_value()`. For instance, this will always be needed to get the first value received.
162    ///
163    /// # Raises
164    /// - `IllegalArgumentException` – if the specified field is not part of the Subscription.
165    ///
166    /// # Parameters
167    /// - `field_name_or_pos` – The field name or the 1-based position of the field within the "Field List" or "Field Schema".
168    ///
169    /// # Returns
170    /// A JSON Patch structure representing the difference between the new value and the previous one,
171    /// or None if the difference in JSON Patch format is not available for any reason.
172    pub fn get_value_as_json_patch_if_available(&self, _field_name_or_pos: &str) -> Option<String> {
173        // Implementation pending
174        None
175    }
176
177    /// Inquiry method that asks whether the current update belongs to the item snapshot (which carries the current item state
178    /// at the time of Subscription). Snapshot events are sent only if snapshot information was requested for the items through
179    /// `Subscription.set_requested_snapshot()` and precede the real time events. Snapshot information takes different forms in
180    /// different subscription modes and can be spanned across zero, one or several update events. In particular:
181    ///
182    /// - if the item is subscribed to with the RAW subscription mode, then no snapshot is sent by the Server;
183    /// - if the item is subscribed to with the MERGE subscription mode, then the snapshot consists of exactly one event,
184    ///   carrying the current value for all fields;
185    /// - if the item is subscribed to with the DISTINCT subscription mode, then the snapshot consists of some of the most recent
186    ///   updates; these updates are as many as specified through `Subscription.set_requested_snapshot()`, unless fewer are available;
187    /// - if the item is subscribed to with the COMMAND subscription mode, then the snapshot consists of an "ADD" event for each key
188    ///   that is currently present.
189    ///
190    /// Note that, in case of two-level behavior, snapshot-related updates for both the first-level item (which is in COMMAND mode)
191    /// and any second-level items (which are in MERGE mode) are qualified with this flag.
192    ///
193    /// # Returns
194    /// `true` if the current update event belongs to the item snapshot; `false` otherwise.
195    pub fn is_snapshot(&self) -> bool {
196        self.is_snapshot
197    }
198
199    /// Inquiry method that asks whether the value for a field has changed after the reception of the last update from the Server
200    /// for an item. If the Subscription mode is COMMAND then the change is meant as relative to the same key.
201    ///
202    /// # Parameters
203    /// - `field_name_or_pos` – The field name or the 1-based position of the field within the field list or field schema.
204    ///
205    /// # Returns
206    /// Unless the Subscription mode is COMMAND, the return value is `true` in the following cases:
207    ///
208    /// - It is the first update for the item;
209    /// - the new field value is different than the previous field value received for the item.
210    ///
211    /// If the Subscription mode is COMMAND, the return value is `true` in the following cases:
212    ///
213    /// - it is the first update for the involved key value (i.e. the event carries an "ADD" command);
214    /// - the new field value is different than the previous field value received for the item, relative to the same key value
215    ///   (the event must carry an "UPDATE" command);
216    /// - the event carries a "DELETE" command (this applies to all fields other than the field used to carry key information).
217    ///
218    /// In all other cases, the return value is `false`.
219    ///
220    /// # Raises
221    /// - `IllegalArgumentException` – if the specified field is not part of the Subscription.
222    pub fn is_value_changed(&self, field_name_or_pos: &str) -> bool {
223        match field_name_or_pos.parse::<usize>() {
224            Ok(pos) => self
225                .changed_fields
226                .iter()
227                .any(|(name, _)| self.get_field_position(name) == pos),
228            Err(_) => self.changed_fields.contains_key(field_name_or_pos),
229        }
230    }
231
232    /// Helper method to get the 1-based position of a field within the field list or field schema.
233    ///
234    /// # Parameters
235    /// - `field_name` – The name of the field.
236    ///
237    /// # Returns
238    /// The 1-based position of the field within the field list or field schema.
239    fn get_field_position(&self, field_name: &str) -> usize {
240        // For testing purposes, we'll use a simple implementation
241        // In a real implementation, this would look up the position in the field schema
242        match field_name {
243            "field1" => 1,
244            "field2" => 2,
245            "field3" => 3,
246            _ => 0,
247        }
248    }
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254    use std::collections::HashMap;
255
256    #[test]
257    fn test_get_fields_by_position() {
258        let update = create_test_item_update();
259
260        let fields_by_position = update.get_fields_by_position();
261
262        // Verify the fields are mapped to their positions
263        assert_eq!(fields_by_position.len(), 3); // field1, field2, and field3 have valid positions
264        assert_eq!(
265            fields_by_position.get(&1),
266            Some(&Some("value1".to_string()))
267        );
268        assert_eq!(
269            fields_by_position.get(&2),
270            Some(&Some("value2".to_string()))
271        );
272        assert_eq!(fields_by_position.get(&3), Some(&None));
273    }
274
275    #[test]
276    fn test_get_changed_fields_by_position() {
277        let update = create_test_item_update();
278
279        let changed_fields_by_position = update.get_changed_fields_by_position();
280
281        // Verify the changed fields are mapped to their positions
282        assert_eq!(changed_fields_by_position.len(), 2); // Only field1 and field2 have valid positions
283        assert_eq!(
284            changed_fields_by_position.get(&1),
285            Some(&"value1".to_string())
286        );
287        assert_eq!(
288            changed_fields_by_position.get(&2),
289            Some(&"value2".to_string())
290        );
291    }
292
293    fn create_test_item_update() -> ItemUpdate {
294        let mut fields = HashMap::new();
295        fields.insert("field1".to_string(), Some("value1".to_string()));
296        fields.insert("field2".to_string(), Some("value2".to_string()));
297        fields.insert("field3".to_string(), None);
298
299        let mut changed_fields = HashMap::new();
300        changed_fields.insert("field1".to_string(), "value1".to_string());
301        changed_fields.insert("field2".to_string(), "value2".to_string());
302
303        ItemUpdate {
304            item_name: Some("test_item".to_string()),
305            item_pos: 1,
306            fields,
307            changed_fields,
308            is_snapshot: false,
309        }
310    }
311
312    #[test]
313    fn test_get_item_name() {
314        let update = create_test_item_update();
315        assert_eq!(update.get_item_name(), Some("test_item"));
316
317        let mut update_no_name = create_test_item_update();
318        update_no_name.item_name = None;
319        assert_eq!(update_no_name.get_item_name(), None);
320    }
321
322    #[test]
323    fn test_get_item_pos() {
324        let update = create_test_item_update();
325        assert_eq!(update.get_item_pos(), 1);
326    }
327
328    #[test]
329    fn test_get_fields() {
330        let update = create_test_item_update();
331        let fields = update.get_fields();
332
333        assert_eq!(fields.len(), 3);
334        assert_eq!(fields.get("field1").unwrap(), &Some("value1".to_string()));
335        assert_eq!(fields.get("field2").unwrap(), &Some("value2".to_string()));
336        assert_eq!(fields.get("field3").unwrap(), &None);
337    }
338
339    #[test]
340    fn test_get_changed_fields() {
341        let update = create_test_item_update();
342        let changed_fields = update.get_changed_fields();
343
344        assert_eq!(changed_fields.len(), 2);
345        assert_eq!(changed_fields.get("field1").unwrap(), "value1");
346        assert_eq!(changed_fields.get("field2").unwrap(), "value2");
347        assert!(!changed_fields.contains_key("field3"));
348    }
349
350    #[test]
351    fn test_get_value() {
352        let update = create_test_item_update();
353
354        assert_eq!(update.get_value("field1"), Some("value1"));
355        assert_eq!(update.get_value("field2"), Some("value2"));
356        assert_eq!(update.get_value("field3"), None);
357        assert_eq!(update.get_value("non_existent"), None);
358    }
359
360    #[test]
361    fn test_is_snapshot() {
362        let update = create_test_item_update();
363        assert!(!update.is_snapshot());
364
365        let mut snapshot_update = create_test_item_update();
366        snapshot_update.is_snapshot = true;
367        assert!(snapshot_update.is_snapshot());
368    }
369
370    #[test]
371    fn test_is_value_changed() {
372        let update = create_test_item_update();
373
374        assert!(update.is_value_changed("field1"));
375        assert!(update.is_value_changed("field2"));
376        assert!(!update.is_value_changed("field3"));
377    }
378
379    #[test]
380    fn test_get_value_as_json_patch_if_available() {
381        let update = create_test_item_update();
382
383        assert_eq!(update.get_value_as_json_patch_if_available("field1"), None);
384    }
385
386    #[test]
387    fn test_get_field_position() {
388        let update = create_test_item_update();
389
390        // Test existing fields
391        assert_eq!(update.get_field_position("field1"), 1);
392        assert_eq!(update.get_field_position("field2"), 2);
393        assert_eq!(update.get_field_position("field3"), 3);
394
395        // Test non-existent field
396        assert_eq!(update.get_field_position("non_existent_field"), 0);
397    }
398}