lightstreamer_client/
item_update.rs

1use std::collections::HashMap;
2
3use serde::Serialize;
4
5/// Contains all the information related to an update of the field values for an item.
6/// It reports all the new values of the fields.
7///
8/// COMMAND Subscription:
9/// If the involved Subscription is a COMMAND Subscription, then the values for the current update
10/// are meant as relative to the same key.
11///
12/// Moreover, if the involved Subscription has a two-level behavior enabled, then each update may be
13/// associated with either a first-level or a second-level item. In this case, the reported fields are
14/// always the union of the first-level and second-level fields and each single update can only change
15/// either the first-level or the second-level fields (but for the "command" field, which is first-level
16/// and is always set to "UPDATE" upon a second-level update); note that the second-level field values
17/// are always None until the first second-level update occurs). When the two-level behavior is enabled,
18/// in all methods where a field name has to be supplied, the following convention should be followed:
19///
20/// - The field name can always be used, both for the first-level and the second-level fields. In case of
21///   name conflict, the first-level field is meant.
22/// - The field position can always be used; however, the field positions for the second-level fields start
23///   at the highest position of the first-level field list + 1. If a field schema had been specified for
24///   either first-level or second-level Subscriptions, then client-side knowledge of the first-level schema
25///   length would be required.
26#[derive(Debug, Clone, Serialize)]
27pub struct ItemUpdate {
28    pub item_name: Option<String>,
29    pub item_pos: usize,
30    pub fields: HashMap<String, Option<String>>,
31    pub changed_fields: HashMap<String, String>,
32    pub is_snapshot: bool,
33}
34
35impl ItemUpdate {
36    /// Returns a map containing the values for each field changed with the last server update.
37    /// The related field name is used as key for the values in the map. Note that if the Subscription
38    /// mode of the involved Subscription is COMMAND, then changed fields are meant as relative to the
39    /// previous update for the same key. On such tables if a DELETE command is received, all the fields,
40    /// excluding the key field, will be present as changed, with None value. All of this is also true on
41    /// tables that have the two-level behavior enabled, but in case of DELETE commands second-level fields
42    /// will not be iterated.
43    ///
44    /// # Raises
45    /// - `IllegalStateException` – if the Subscription was initialized using a field schema.
46    ///
47    /// # Returns
48    /// A map containing the values for each field changed with the last server update.
49    pub fn get_changed_fields(&self) -> HashMap<String, String> {
50        self.changed_fields.clone()
51    }
52
53    /// Returns a map containing the values for each field changed with the last server update.
54    /// The 1-based field position within the field schema or field list is used as key for the values in
55    /// the map. Note that if the Subscription mode of the involved Subscription is COMMAND, then changed
56    /// fields are meant as relative to the previous update for the same key. On such tables if a DELETE
57    /// command is received, all the fields, excluding the key field, will be present as changed, with None
58    /// value. All of this is also true on tables that have the two-level behavior enabled, but in case of
59    /// DELETE commands second-level fields will not be iterated.
60    ///
61    /// # Returns
62    /// A map containing the values for each field changed with the last server update.
63    pub fn get_changed_fields_by_position(&self) -> HashMap<usize, String> {
64        self.changed_fields
65            .iter()
66            .map(|(name, value)| (self.get_field_position(name), value.clone()))
67            .collect()
68    }
69
70    /// Returns a map containing the values for each field in the Subscription.
71    /// The related field name is used as key for the values in the map.
72    ///
73    /// # Raises
74    /// - `IllegalStateException` – if the Subscription was initialized using a field schema.
75    ///
76    /// # Returns
77    /// A map containing the values for each field in the Subscription.
78    pub fn get_fields(&self) -> HashMap<String, Option<String>> {
79        self.fields.clone()
80    }
81
82    /// Returns a map containing the values for each field in the Subscription.
83    /// The 1-based field position within the field schema or field list is used as key for the values in the map.
84    ///
85    /// # Returns
86    /// A map containing the values for each field in the Subscription.
87    pub fn get_fields_by_position(&self) -> HashMap<usize, Option<String>> {
88        self.fields
89            .iter()
90            .map(|(name, value)| (self.get_field_position(name), value.clone()))
91            .collect()
92    }
93
94    /// Inquiry method that retrieves the name of the item to which this update pertains.
95    ///
96    /// The name will be None if the related Subscription was initialized using an "Item Group".
97    ///
98    /// # Returns
99    /// The name of the item to which this update pertains.
100    pub fn get_item_name(&self) -> Option<&str> {
101        self.item_name.as_deref()
102    }
103
104    /// Inquiry method that retrieves the position in the "Item List" or "Item Group" of the item
105    /// to which this update pertains.
106    ///
107    /// # Returns
108    /// The 1-based position of the item to which this update pertains.
109    pub fn get_item_pos(&self) -> usize {
110        self.item_pos
111    }
112
113    /// Inquiry method that gets the value for a specified field, as received from the Server with the
114    /// current or previous update.
115    ///
116    /// # Raises
117    /// - `IllegalArgumentException` – if the specified field is not part of the Subscription.
118    ///
119    /// # Parameters
120    /// - `field_name_or_pos` – The field name or the 1-based position of the field within the "Field List" or "Field Schema".
121    ///
122    /// # Returns
123    /// The value of the specified field; it can be None in the following cases:
124    ///
125    /// - a None value has been received from the Server, as None is a possible value for a field;
126    /// - no value has been received for the field yet;
127    /// - the item is subscribed to with the COMMAND mode and a DELETE command is received (only the fields
128    ///   used to carry key and command information are valued).
129    pub fn get_value(&self, field_name_or_pos: &str) -> Option<&str> {
130        match field_name_or_pos.parse::<usize>() {
131            Ok(pos) => self
132                .fields
133                .iter()
134                .find(|(name, _)| self.get_field_position(name) == pos)
135                .and_then(|(_, value)| value.as_deref()),
136            Err(_) => self
137                .fields
138                .get(field_name_or_pos)
139                .and_then(|v| v.as_deref()),
140        }
141    }
142
143    /// Inquiry method that gets the difference between the new value and the previous one as a JSON Patch structure,
144    /// provided that the Server has used the JSON Patch format to send this difference, as part of the "delta delivery"
145    /// mechanism. This, in turn, requires that:
146    ///
147    /// - the Data Adapter has explicitly indicated JSON Patch as the privileged type of compression for this field;
148    /// - both the previous and new value are suitable for the JSON Patch computation (i.e. they are valid JSON representations);
149    /// - the item was subscribed to in MERGE or DISTINCT mode (note that, in case of two-level behavior, this holds for all
150    ///   fields related with second-level items, as these items are in MERGE mode);
151    /// - sending the JSON Patch difference has been evaluated by the Server as more efficient than sending the full new value.
152    ///
153    /// Note that the last condition can be enforced by leveraging the Server's <jsonpatch_min_length> configuration flag,
154    /// so that the availability of the JSON Patch form would only depend on the Client and the Data Adapter.
155    ///
156    /// When the above conditions are not met, the method just returns None; in this case, the new value can only be determined
157    /// through `ItemUpdate.get_value()`. For instance, this will always be needed to get the first value received.
158    ///
159    /// # Raises
160    /// - `IllegalArgumentException` – if the specified field is not part of the Subscription.
161    ///
162    /// # Parameters
163    /// - `field_name_or_pos` – The field name or the 1-based position of the field within the "Field List" or "Field Schema".
164    ///
165    /// # Returns
166    /// A JSON Patch structure representing the difference between the new value and the previous one,
167    /// or None if the difference in JSON Patch format is not available for any reason.
168    pub fn get_value_as_json_patch_if_available(&self, _field_name_or_pos: &str) -> Option<String> {
169        // Implementation pending
170        None
171    }
172
173    /// Inquiry method that asks whether the current update belongs to the item snapshot (which carries the current item state
174    /// at the time of Subscription). Snapshot events are sent only if snapshot information was requested for the items through
175    /// `Subscription.set_requested_snapshot()` and precede the real time events. Snapshot information takes different forms in
176    /// different subscription modes and can be spanned across zero, one or several update events. In particular:
177    ///
178    /// - if the item is subscribed to with the RAW subscription mode, then no snapshot is sent by the Server;
179    /// - if the item is subscribed to with the MERGE subscription mode, then the snapshot consists of exactly one event,
180    ///   carrying the current value for all fields;
181    /// - if the item is subscribed to with the DISTINCT subscription mode, then the snapshot consists of some of the most recent
182    ///   updates; these updates are as many as specified through `Subscription.set_requested_snapshot()`, unless fewer are available;
183    /// - if the item is subscribed to with the COMMAND subscription mode, then the snapshot consists of an "ADD" event for each key
184    ///   that is currently present.
185    ///
186    /// Note that, in case of two-level behavior, snapshot-related updates for both the first-level item (which is in COMMAND mode)
187    /// and any second-level items (which are in MERGE mode) are qualified with this flag.
188    ///
189    /// # Returns
190    /// `true` if the current update event belongs to the item snapshot; `false` otherwise.
191    pub fn is_snapshot(&self) -> bool {
192        self.is_snapshot
193    }
194
195    /// Inquiry method that asks whether the value for a field has changed after the reception of the last update from the Server
196    /// for an item. If the Subscription mode is COMMAND then the change is meant as relative to the same key.
197    ///
198    /// # Parameters
199    /// - `field_name_or_pos` – The field name or the 1-based position of the field within the field list or field schema.
200    ///
201    /// # Returns
202    /// Unless the Subscription mode is COMMAND, the return value is `true` in the following cases:
203    ///
204    /// - It is the first update for the item;
205    /// - the new field value is different than the previous field value received for the item.
206    ///
207    /// If the Subscription mode is COMMAND, the return value is `true` in the following cases:
208    ///
209    /// - it is the first update for the involved key value (i.e. the event carries an "ADD" command);
210    /// - the new field value is different than the previous field value received for the item, relative to the same key value
211    ///   (the event must carry an "UPDATE" command);
212    /// - the event carries a "DELETE" command (this applies to all fields other than the field used to carry key information).
213    ///
214    /// In all other cases, the return value is `false`.
215    ///
216    /// # Raises
217    /// - `IllegalArgumentException` – if the specified field is not part of the Subscription.
218    pub fn is_value_changed(&self, field_name_or_pos: &str) -> bool {
219        match field_name_or_pos.parse::<usize>() {
220            Ok(pos) => self
221                .changed_fields
222                .iter()
223                .any(|(name, _)| self.get_field_position(name) == pos),
224            Err(_) => self.changed_fields.contains_key(field_name_or_pos),
225        }
226    }
227
228    /// Helper method to get the 1-based position of a field within the field list or field schema.
229    ///
230    /// # Parameters
231    /// - `field_name` – The name of the field.
232    ///
233    /// # Returns
234    /// The 1-based position of the field within the field list or field schema.
235    fn get_field_position(&self, _field_name: &str) -> usize {
236        // Implementation pending
237        // This method should return the 1-based position of the field based on the field list or field schema
238        // If the field is not found, it should raise an IllegalArgumentException
239        unimplemented!()
240    }
241}