Skip to main content

zerodds_dcps/
sample_info.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! `SampleInfo` — per-sample metadata that `DataReader::read`/`take`
4//! deliver alongside each sample.
5//!
6//! Spec reference: OMG DDS-DCPS 1.4 §2.2.2.5.1 `SampleInfo`. The spec
7//! defines 11 fields that together describe the **statechart** of a
8//! sample:
9//!
10//! 1. `sample_state`: whether the reader has already read the sample
11//!    (`READ`) or not (`NOT_READ`).
12//! 2. `view_state`: whether the reader instance is new (`NEW`) or
13//!    already known (`NOT_NEW`).
14//! 3. `instance_state`: lifecycle state of the instance (`ALIVE`,
15//!    `NOT_ALIVE_DISPOSED`, `NOT_ALIVE_NO_WRITERS`).
16//! 4. `disposed_generation_count`: number of `NOT_ALIVE_DISPOSED → ALIVE`
17//!    transitions since the first sample of this instance.
18//! 5. `no_writers_generation_count`: number of `NOT_ALIVE_NO_WRITERS → ALIVE`
19//!    transitions since the first sample of this instance.
20//! 6. `sample_rank`: number of samples in the same instance that follow
21//!    this one in the cache (spec §2.2.2.5.1.5).
22//! 7. `generation_rank`: difference in generation counts between this
23//!    sample and the last sample of the instance in the same read set.
24//! 8. `absolute_generation_rank`: like `generation_rank`, but relative
25//!    to the **current** generation count.
26//! 9. `source_timestamp`: wall-clock time of the write operation.
27//! 10. `instance_handle`: local handle of the instance (key-based).
28//! 11. `publication_handle`: local handle of the sending DataWriter.
29//! 12. `valid_data`: `false` for dispose/unregister markers without
30//!     payload (spec §2.2.2.5.1.13).
31//!
32//! Note: the spec lists `valid_data` as the 12th field; some texts do
33//! not count it, so you will find both "11" and "12" fields in the
34//! documentation. We carry all of them.
35
36extern crate alloc;
37
38use crate::instance_handle::{HANDLE_NIL, InstanceHandle};
39use crate::time::Time;
40
41/// `SampleStateKind` (DDS 1.4 §2.2.2.5.1.1) — maintained per reader.
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
43pub enum SampleStateKind {
44    /// Sample has never been delivered to the application via
45    /// `read`/`take`.
46    NotRead,
47    /// Sample has already been delivered at least once via `read`
48    /// (after `take` it is gone, so this only matters for `read`).
49    Read,
50}
51
52impl SampleStateKind {
53    /// `true` if this is an as-yet-unread sample.
54    #[must_use]
55    pub const fn is_not_read(&self) -> bool {
56        matches!(self, Self::NotRead)
57    }
58}
59
60impl Default for SampleStateKind {
61    fn default() -> Self {
62        Self::NotRead
63    }
64}
65
66/// `ViewStateKind` (DDS 1.4 §2.2.2.5.1.2) — maintained per instance.
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
68pub enum ViewStateKind {
69    /// First sample of this instance (or first after
70    /// `NOT_ALIVE_NO_WRITERS → ALIVE`).
71    New,
72    /// Reader has already delivered samples for this instance.
73    NotNew,
74}
75
76impl Default for ViewStateKind {
77    fn default() -> Self {
78        Self::New
79    }
80}
81
82/// `InstanceStateKind` (DDS 1.4 §2.2.2.5.1.3) — maintained per instance.
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
84pub enum InstanceStateKind {
85    /// At least one DataWriter has registered the instance and it has
86    /// not been disposed.
87    Alive,
88    /// At least one DataWriter has disposed the instance.
89    NotAliveDisposed,
90    /// All DataWriters that had registered the instance have called
91    /// `unregister_instance` or are gone.
92    NotAliveNoWriters,
93}
94
95impl InstanceStateKind {
96    /// `true` if the instance is still alive.
97    #[must_use]
98    pub const fn is_alive(&self) -> bool {
99        matches!(self, Self::Alive)
100    }
101    /// `true` if the instance is either disposed or no-writers.
102    #[must_use]
103    pub const fn is_not_alive(&self) -> bool {
104        !self.is_alive()
105    }
106}
107
108impl Default for InstanceStateKind {
109    fn default() -> Self {
110        Self::Alive
111    }
112}
113
114/// Bitmask for sample-state filtering in `read`/`take` calls
115/// (DDS 1.4 §2.2.2.5.1.4 `SampleStateMask`).
116pub mod sample_state_mask {
117    /// `NOT_READ` only.
118    pub const NOT_READ: u32 = 1 << 0;
119    /// `READ` only.
120    pub const READ: u32 = 1 << 1;
121    /// `READ | NOT_READ` — both.
122    pub const ANY: u32 = NOT_READ | READ;
123}
124
125/// Bitmask for view-state filtering (§2.2.2.5.1.4 `ViewStateMask`).
126pub mod view_state_mask {
127    /// `NEW`.
128    pub const NEW: u32 = 1 << 0;
129    /// `NOT_NEW`.
130    pub const NOT_NEW: u32 = 1 << 1;
131    /// Both.
132    pub const ANY: u32 = NEW | NOT_NEW;
133}
134
135/// Bitmask for instance-state filtering (§2.2.2.5.1.4
136/// `InstanceStateMask`).
137pub mod instance_state_mask {
138    /// `ALIVE`.
139    pub const ALIVE: u32 = 1 << 0;
140    /// `NOT_ALIVE_DISPOSED`.
141    pub const NOT_ALIVE_DISPOSED: u32 = 1 << 1;
142    /// `NOT_ALIVE_NO_WRITERS`.
143    pub const NOT_ALIVE_NO_WRITERS: u32 = 1 << 2;
144    /// `NOT_ALIVE_DISPOSED | NOT_ALIVE_NO_WRITERS`.
145    pub const NOT_ALIVE: u32 = NOT_ALIVE_DISPOSED | NOT_ALIVE_NO_WRITERS;
146    /// All three.
147    pub const ANY: u32 = ALIVE | NOT_ALIVE;
148}
149
150/// `SampleInfo` (DDS 1.4 §2.2.2.5.1) — per-sample metadata.
151///
152/// Filled by the DataReader in the `take`/`read` path. Application
153/// code is meant to rely on these fields to:
154/// * distinguish new samples from re-read ones (`sample_state`),
155/// * detect new instances (`view_state == NEW`),
156/// * observe lifecycle events (`instance_state`, `valid_data == false`),
157/// * follow reordering / generation changes (`*_generation_count`,
158///   `*_rank`).
159#[derive(Debug, Clone, Copy, PartialEq, Eq)]
160pub struct SampleInfo {
161    /// Sample-State (`READ` / `NOT_READ`).
162    pub sample_state: SampleStateKind,
163    /// View-State (`NEW` / `NOT_NEW`).
164    pub view_state: ViewStateKind,
165    /// Instance-State (`ALIVE` / `NOT_ALIVE_*`).
166    pub instance_state: InstanceStateKind,
167    /// How many times the instance has gone through `NOT_ALIVE_DISPOSED → ALIVE`
168    /// since its first sample.
169    pub disposed_generation_count: i32,
170    /// How many times the instance has gone through `NOT_ALIVE_NO_WRITERS → ALIVE`
171    /// since its first sample.
172    pub no_writers_generation_count: i32,
173    /// Number of samples in the same instance that follow this one in
174    /// the cache (§2.2.2.5.1.5).
175    pub sample_rank: i32,
176    /// Difference in `generation_count` between this sample and the last
177    /// sample of the instance in the same read set.
178    pub generation_rank: i32,
179    /// Difference in `generation_count` between this sample and the
180    /// **current** generation count.
181    pub absolute_generation_rank: i32,
182    /// Wall-clock time of the `write`/`dispose`/`unregister`.
183    pub source_timestamp: Time,
184    /// Local handle of the instance.
185    pub instance_handle: InstanceHandle,
186    /// Local handle of the sending DataWriter (`HANDLE_NIL` in the
187    /// offline / unknown case).
188    pub publication_handle: InstanceHandle,
189    /// `true` if the sample carries payload; `false` for a plain
190    /// dispose/unregister marker (§2.2.2.5.1.13).
191    pub valid_data: bool,
192}
193
194impl Default for SampleInfo {
195    fn default() -> Self {
196        Self {
197            sample_state: SampleStateKind::NotRead,
198            view_state: ViewStateKind::New,
199            instance_state: InstanceStateKind::Alive,
200            disposed_generation_count: 0,
201            no_writers_generation_count: 0,
202            sample_rank: 0,
203            generation_rank: 0,
204            absolute_generation_rank: 0,
205            source_timestamp: Time::default(),
206            instance_handle: HANDLE_NIL,
207            publication_handle: HANDLE_NIL,
208            valid_data: true,
209        }
210    }
211}
212
213impl SampleInfo {
214    /// Constructs a default info block for a fresh sample of a **new**
215    /// instance with `valid_data = true`.
216    #[must_use]
217    pub fn new_alive(
218        instance: InstanceHandle,
219        publication: InstanceHandle,
220        timestamp: Time,
221    ) -> Self {
222        Self {
223            sample_state: SampleStateKind::NotRead,
224            view_state: ViewStateKind::New,
225            instance_state: InstanceStateKind::Alive,
226            instance_handle: instance,
227            publication_handle: publication,
228            source_timestamp: timestamp,
229            valid_data: true,
230            ..Self::default()
231        }
232    }
233
234    /// Checks whether this `SampleInfo` is accepted by the given state
235    /// masks (spec §2.2.2.5.3.1, `read_w_condition`).
236    #[must_use]
237    pub fn matches_states(&self, sample_mask: u32, view_mask: u32, instance_mask: u32) -> bool {
238        let sample_bit = match self.sample_state {
239            SampleStateKind::NotRead => sample_state_mask::NOT_READ,
240            SampleStateKind::Read => sample_state_mask::READ,
241        };
242        let view_bit = match self.view_state {
243            ViewStateKind::New => view_state_mask::NEW,
244            ViewStateKind::NotNew => view_state_mask::NOT_NEW,
245        };
246        let inst_bit = match self.instance_state {
247            InstanceStateKind::Alive => instance_state_mask::ALIVE,
248            InstanceStateKind::NotAliveDisposed => instance_state_mask::NOT_ALIVE_DISPOSED,
249            InstanceStateKind::NotAliveNoWriters => instance_state_mask::NOT_ALIVE_NO_WRITERS,
250        };
251        (sample_mask & sample_bit) != 0
252            && (view_mask & view_bit) != 0
253            && (instance_mask & inst_bit) != 0
254    }
255}
256
257#[cfg(test)]
258#[allow(clippy::expect_used, clippy::unwrap_used)]
259mod tests {
260    use super::*;
261
262    #[test]
263    fn defaults_are_alive_new_not_read() {
264        let info = SampleInfo::default();
265        assert_eq!(info.sample_state, SampleStateKind::NotRead);
266        assert_eq!(info.view_state, ViewStateKind::New);
267        assert_eq!(info.instance_state, InstanceStateKind::Alive);
268        assert!(info.valid_data);
269        assert_eq!(info.disposed_generation_count, 0);
270        assert_eq!(info.no_writers_generation_count, 0);
271        assert_eq!(info.instance_handle, HANDLE_NIL);
272        assert_eq!(info.publication_handle, HANDLE_NIL);
273    }
274
275    #[test]
276    fn instance_state_predicates() {
277        assert!(InstanceStateKind::Alive.is_alive());
278        assert!(!InstanceStateKind::Alive.is_not_alive());
279        assert!(InstanceStateKind::NotAliveDisposed.is_not_alive());
280        assert!(InstanceStateKind::NotAliveNoWriters.is_not_alive());
281    }
282
283    #[test]
284    fn sample_state_predicate() {
285        assert!(SampleStateKind::NotRead.is_not_read());
286        assert!(!SampleStateKind::Read.is_not_read());
287    }
288
289    #[test]
290    fn matches_states_filter() {
291        let info = SampleInfo::default();
292        assert!(info.matches_states(
293            sample_state_mask::ANY,
294            view_state_mask::ANY,
295            instance_state_mask::ANY,
296        ));
297        assert!(info.matches_states(
298            sample_state_mask::NOT_READ,
299            view_state_mask::NEW,
300            instance_state_mask::ALIVE,
301        ));
302        assert!(!info.matches_states(
303            sample_state_mask::READ,
304            view_state_mask::ANY,
305            instance_state_mask::ANY,
306        ));
307        assert!(!info.matches_states(
308            sample_state_mask::ANY,
309            view_state_mask::NOT_NEW,
310            instance_state_mask::ANY,
311        ));
312        assert!(!info.matches_states(
313            sample_state_mask::ANY,
314            view_state_mask::ANY,
315            instance_state_mask::NOT_ALIVE,
316        ));
317    }
318
319    #[test]
320    fn new_alive_constructor_sets_handles_and_timestamp() {
321        let h = InstanceHandle::from_raw(7);
322        let pub_h = InstanceHandle::from_raw(42);
323        let ts = Time::new(1, 2);
324        let info = SampleInfo::new_alive(h, pub_h, ts);
325        assert_eq!(info.instance_handle, h);
326        assert_eq!(info.publication_handle, pub_h);
327        assert_eq!(info.source_timestamp, ts);
328        assert!(info.valid_data);
329        assert_eq!(info.instance_state, InstanceStateKind::Alive);
330    }
331
332    // ---- §2.2.2.5.5 all 12 SampleInfo fields available ----
333
334    #[test]
335    fn sample_info_all_spec_fields_accessible() {
336        // Spec §2.2.2.5.5 lists 12 fields; all must be readable and
337        // settable. The test verifies field identity via concrete
338        // values.
339        let info = SampleInfo {
340            sample_state: SampleStateKind::Read,
341            view_state: ViewStateKind::NotNew,
342            instance_state: InstanceStateKind::NotAliveDisposed,
343            disposed_generation_count: 3,
344            no_writers_generation_count: 5,
345            sample_rank: 7,
346            generation_rank: 9,
347            absolute_generation_rank: 11,
348            source_timestamp: Time::new(1, 2),
349            instance_handle: InstanceHandle::from_raw(0xCAFE),
350            publication_handle: InstanceHandle::from_raw(0xBEEF),
351            valid_data: false,
352        };
353
354        assert_eq!(info.sample_state, SampleStateKind::Read);
355        assert_eq!(info.view_state, ViewStateKind::NotNew);
356        assert_eq!(info.instance_state, InstanceStateKind::NotAliveDisposed);
357        assert_eq!(info.disposed_generation_count, 3);
358        assert_eq!(info.no_writers_generation_count, 5);
359        assert_eq!(info.sample_rank, 7);
360        assert_eq!(info.generation_rank, 9);
361        assert_eq!(info.absolute_generation_rank, 11);
362        assert_eq!(info.source_timestamp, Time::new(1, 2));
363        assert_eq!(info.instance_handle, InstanceHandle::from_raw(0xCAFE));
364        assert_eq!(info.publication_handle, InstanceHandle::from_raw(0xBEEF));
365        assert!(!info.valid_data);
366    }
367
368    #[test]
369    fn sample_info_dispose_marker_has_invalid_data() {
370        // §2.2.2.5.1.13 — dispose/unregister marker has valid_data=false.
371        // Negative: a sample with valid_data=false has no payload; the
372        // caller MUST NOT evaluate sample.data.
373        let info = SampleInfo {
374            valid_data: false,
375            instance_state: InstanceStateKind::NotAliveDisposed,
376            ..SampleInfo::default()
377        };
378        assert!(!info.valid_data);
379        assert!(info.instance_state.is_not_alive());
380    }
381
382    #[test]
383    fn sample_info_three_state_dimensions_independent() {
384        // Spec §2.2.2.5.1 — three orthogonal state dimensions.
385        // matches_states must filter on each one individually.
386        let info = SampleInfo {
387            sample_state: SampleStateKind::Read,
388            view_state: ViewStateKind::NotNew,
389            instance_state: InstanceStateKind::Alive,
390            ..SampleInfo::default()
391        };
392        // Read+NotNew+Alive matched.
393        assert!(info.matches_states(
394            sample_state_mask::READ,
395            view_state_mask::NOT_NEW,
396            instance_state_mask::ALIVE,
397        ));
398        // sample_state mismatch → reject.
399        assert!(!info.matches_states(
400            sample_state_mask::NOT_READ,
401            view_state_mask::ANY,
402            instance_state_mask::ANY,
403        ));
404    }
405
406    #[test]
407    fn sample_info_generation_rank_starts_zero() {
408        // Default sample: all generation counters 0.
409        let info = SampleInfo::default();
410        assert_eq!(info.disposed_generation_count, 0);
411        assert_eq!(info.no_writers_generation_count, 0);
412        assert_eq!(info.sample_rank, 0);
413        assert_eq!(info.generation_rank, 0);
414        assert_eq!(info.absolute_generation_rank, 0);
415    }
416
417    #[test]
418    fn enum_default_impls() {
419        assert_eq!(SampleStateKind::default(), SampleStateKind::NotRead);
420        assert_eq!(ViewStateKind::default(), ViewStateKind::New);
421        assert_eq!(InstanceStateKind::default(), InstanceStateKind::Alive);
422    }
423}