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}