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}