Skip to main content

sparkplug_b/
model.rs

1//! The Sparkplug B payload object model (spec §6.4 “Payload Component
2//! Definitions”). Plain data structs — no Java-style getters/setters; values
3//! are typed via the enums in [`crate::value`].
4
5use bytes::Bytes;
6
7use crate::datatype::DataType;
8use crate::error::{Result, SparkplugError};
9use crate::value::{DataSetValue, MetricValue, ParameterValue, PropertyValue};
10
11/// A complete Sparkplug B payload (the body of an N/D BIRTH/DATA/DEATH/CMD message).
12#[derive(Clone, Debug, PartialEq, Default)]
13pub struct Payload {
14    /// Message send time, epoch milliseconds UTC.
15    pub timestamp: Option<u64>,
16    /// The metrics carried by this payload.
17    pub metrics: Vec<Metric>,
18    /// The Sparkplug sequence number (`0..=255`); absent on NDEATH/NCMD/DCMD.
19    pub seq: Option<u8>,
20    /// Optional schema UUID; also the `SPBV1.0_COMPRESSED` sentinel for the
21    /// compression envelope.
22    pub uuid: Option<String>,
23    /// Optional opaque body (used by the compression envelope).
24    pub body: Option<Bytes>,
25}
26
27impl Payload {
28    /// Create an empty payload.
29    #[must_use]
30    pub fn new() -> Self {
31        Self::default()
32    }
33
34    /// Builder-style setter for the payload timestamp.
35    #[must_use]
36    pub fn with_timestamp(mut self, ts: u64) -> Self {
37        self.timestamp = Some(ts);
38        self
39    }
40
41    /// Builder-style setter for the sequence number.
42    #[must_use]
43    pub fn with_seq(mut self, seq: u8) -> Self {
44        self.seq = Some(seq);
45        self
46    }
47
48    /// Append a metric, consuming and returning `self` for chaining.
49    #[must_use]
50    pub fn with_metric(mut self, metric: Metric) -> Self {
51        self.metrics.push(metric);
52        self
53    }
54}
55
56/// A single named/aliased typed value — the unit of all Sparkplug data.
57///
58/// `name` and `alias` are both optional: BIRTH metrics carry the name (and,
59/// when aliasing, an alias); DATA metrics may carry only the alias
60/// (`tck-id-payloads-alias-data-cmd-requirement`).
61#[derive(Clone, Debug, PartialEq)]
62pub struct Metric {
63    /// Metric name (required on BIRTH unless aliasing — `tck-id-payloads-name-requirement`).
64    pub name: Option<String>,
65    /// Metric alias (unique per Edge Node — `tck-id-payloads-alias-uniqueness`).
66    pub alias: Option<u64>,
67    /// Acquisition timestamp, epoch milliseconds UTC.
68    pub timestamp: Option<u64>,
69    /// The typed value (carries the datatype; `Null` carries the declared type).
70    pub value: MetricValue,
71    /// Whether this is historical data (should not update the live tag).
72    pub is_historical: Option<bool>,
73    /// Whether this value should not be stored as a tag.
74    pub is_transient: Option<bool>,
75    /// Optional metadata (esp. for `Bytes`/`File`/multi-part transfers).
76    pub metadata: Option<MetaData>,
77    /// Optional property set (engineering units, quality, …).
78    pub properties: Option<PropertySet>,
79}
80
81impl Metric {
82    /// A named metric with the given value and no other fields set.
83    #[must_use]
84    pub fn new(name: impl Into<String>, value: MetricValue) -> Self {
85        Self {
86            name: Some(name.into()),
87            alias: None,
88            timestamp: None,
89            value,
90            is_historical: None,
91            is_transient: None,
92            metadata: None,
93            properties: None,
94        }
95    }
96
97    /// An alias-only metric (no name), as used in DATA/CMD messages.
98    #[must_use]
99    pub fn aliased(alias: u64, value: MetricValue) -> Self {
100        Self {
101            name: None,
102            alias: Some(alias),
103            timestamp: None,
104            value,
105            is_historical: None,
106            is_transient: None,
107            metadata: None,
108            properties: None,
109        }
110    }
111
112    /// Builder-style setter for the alias.
113    #[must_use]
114    pub fn with_alias(mut self, alias: u64) -> Self {
115        self.alias = Some(alias);
116        self
117    }
118
119    /// Builder-style setter for the timestamp (epoch ms).
120    #[must_use]
121    pub fn with_timestamp(mut self, ts: u64) -> Self {
122        self.timestamp = Some(ts);
123        self
124    }
125
126    /// Builder-style setter for the property set.
127    #[must_use]
128    pub fn with_properties(mut self, props: PropertySet) -> Self {
129        self.properties = Some(props);
130        self
131    }
132
133    /// The datatype this metric declares on the wire.
134    #[must_use]
135    pub fn datatype(&self) -> DataType {
136        self.value.datatype()
137    }
138}
139
140/// Optional per-metric metadata, especially for `File`/`Bytes`/multi-part transfers.
141#[derive(Clone, Debug, PartialEq, Default)]
142pub struct MetaData {
143    /// Whether the payload is one part of a multi-part transfer.
144    pub is_multi_part: Option<bool>,
145    /// Content/media type.
146    pub content_type: Option<String>,
147    /// File/string/multi-part size in bytes.
148    pub size: Option<u64>,
149    /// Multi-part sequence number (distinct from the payload-level Sparkplug seq).
150    pub seq: Option<u64>,
151    /// File name (for `File` metrics).
152    pub file_name: Option<String>,
153    /// File type (e.g. `xml`, `json`).
154    pub file_type: Option<String>,
155    /// MD5 of the data.
156    pub md5: Option<String>,
157    /// Free-form description.
158    pub description: Option<String>,
159}
160
161/// A set of named properties attached to a metric. Order is preserved (it is
162/// significant on the wire, where keys and values are parallel arrays).
163#[derive(Clone, Debug, PartialEq, Default)]
164pub struct PropertySet {
165    /// The (key, value) entries, in wire order.
166    pub entries: Vec<(String, PropertyValue)>,
167}
168
169impl PropertySet {
170    /// An empty property set.
171    #[must_use]
172    pub fn new() -> Self {
173        Self::default()
174    }
175
176    /// Append a property, returning `self` for chaining.
177    #[must_use]
178    pub fn with(mut self, key: impl Into<String>, value: PropertyValue) -> Self {
179        self.entries.push((key.into(), value));
180        self
181    }
182
183    /// Look up a property value by key (first match).
184    #[must_use]
185    pub fn get(&self, key: &str) -> Option<&PropertyValue> {
186        self.entries.iter().find(|(k, _)| k == key).map(|(_, v)| v)
187    }
188
189    /// Number of properties.
190    #[must_use]
191    pub fn len(&self) -> usize {
192        self.entries.len()
193    }
194
195    /// Whether the set is empty.
196    #[must_use]
197    pub fn is_empty(&self) -> bool {
198        self.entries.is_empty()
199    }
200}
201
202/// A list of property sets (`PropertySetList` datatype).
203#[derive(Clone, Debug, PartialEq, Default)]
204pub struct PropertySetList {
205    /// The property sets.
206    pub sets: Vec<PropertySet>,
207}
208
209/// A column-oriented table value (the `DataSet` datatype).
210///
211/// Construct via [`DataSet::new`], which validates that `columns`, `types`, and
212/// every row share the same width (`tck-id-payloads-dataset-*`).
213#[derive(Clone, Debug, PartialEq)]
214pub struct DataSet {
215    columns: Vec<String>,
216    types: Vec<DataType>,
217    rows: Vec<Vec<DataSetValue>>,
218}
219
220impl DataSet {
221    /// Build and validate a DataSet.
222    ///
223    /// # Errors
224    /// Returns [`SparkplugError::DataSetShape`] if `columns.len() != types.len()`,
225    /// any column type is not a basic scalar, or any row's width differs from
226    /// the column count.
227    pub fn new(
228        columns: Vec<String>,
229        types: Vec<DataType>,
230        rows: Vec<Vec<DataSetValue>>,
231    ) -> Result<Self> {
232        if columns.len() != types.len() {
233            return Err(SparkplugError::DataSetShape(format!(
234                "{} columns but {} types",
235                columns.len(),
236                types.len()
237            )));
238        }
239        for ty in &types {
240            if !ty.is_basic() {
241                return Err(SparkplugError::DataSetShape(format!(
242                    "column type {ty:?} is not a basic scalar type"
243                )));
244            }
245        }
246        for (i, row) in rows.iter().enumerate() {
247            if row.len() != columns.len() {
248                return Err(SparkplugError::DataSetShape(format!(
249                    "row {i} has {} cells but there are {} columns",
250                    row.len(),
251                    columns.len()
252                )));
253            }
254            // Each non-null cell's type must match its column type, so a value
255            // can never be decoded under a different type than it was built with.
256            for (col, (cell, col_ty)) in row.iter().zip(&types).enumerate() {
257                if let Some(cell_ty) = cell.datatype()
258                    && cell_ty != *col_ty
259                {
260                    return Err(SparkplugError::DataSetShape(format!(
261                        "row {i} column {col}: cell type {cell_ty:?} does not match column type {col_ty:?}"
262                    )));
263                }
264            }
265        }
266        Ok(Self {
267            columns,
268            types,
269            rows,
270        })
271    }
272
273    /// The number of columns (`num_of_columns` on the wire).
274    #[must_use]
275    pub fn num_of_columns(&self) -> u64 {
276        self.columns.len() as u64
277    }
278
279    /// The column names.
280    #[must_use]
281    pub fn columns(&self) -> &[String] {
282        &self.columns
283    }
284
285    /// The per-column datatypes.
286    #[must_use]
287    pub fn types(&self) -> &[DataType] {
288        &self.types
289    }
290
291    /// The rows (each row has one [`DataSetValue`] per column).
292    #[must_use]
293    pub fn rows(&self) -> &[Vec<DataSetValue>] {
294        &self.rows
295    }
296}
297
298/// A user-defined type (UDT) definition or instance (the `Template` datatype).
299#[derive(Clone, Debug, PartialEq, Default)]
300pub struct Template {
301    /// Optional template version string.
302    pub version: Option<String>,
303    /// Reference to the definition's name — set on instances, omitted on
304    /// definitions (`tck-id-payloads-template-ref-*`).
305    pub template_ref: Option<String>,
306    /// `true` for a definition, `false` for an instance
307    /// (`tck-id-payloads-template-is-definition`).
308    pub is_definition: bool,
309    /// Member metrics.
310    pub metrics: Vec<Metric>,
311    /// Template parameters.
312    pub parameters: Vec<Parameter>,
313}
314
315/// A named, typed template parameter.
316#[derive(Clone, Debug, PartialEq)]
317pub struct Parameter {
318    /// Parameter name (required — `tck-id-payloads-template-parameter-name-required`).
319    pub name: String,
320    /// Parameter datatype (a basic scalar — `tck-id-payloads-template-parameter-type-value`).
321    pub datatype: DataType,
322    /// Parameter value; a definition may omit it.
323    pub value: Option<ParameterValue>,
324}