Skip to main content

dataflow_rs/engine/
message.rs

1use crate::engine::error::{DataflowError, ErrorInfo};
2use chrono::{DateTime, Utc};
3use datavalue::OwnedDataValue;
4use serde::{Deserialize, Serialize};
5use serde_json::Value as JsonValue;
6use std::sync::Arc;
7use uuid::Uuid;
8
9/// A message flowing through the dataflow engine.
10///
11/// Construct via [`Message::builder`] for the full API, or use the shortcuts
12/// [`Message::new`] (already-owned `Arc<OwnedDataValue>` payload — the perf
13/// path) and [`Message::from_value`] (bridges from `serde_json::Value`).
14///
15/// `context` is held as an [`OwnedDataValue`] tree (not `serde_json::Value`)
16/// so the JSONLogic evaluator can borrow it into its arena via
17/// `OwnedDataValue::to_arena` with a single deep walk in, and project the
18/// result back via `DataValue::to_owned` with a single deep walk out — no
19/// `serde_json::Value` round-trip in the hot path. The on-the-wire JSON
20/// shape is preserved by datavalue's native `Serialize` / `Deserialize`
21/// impls.
22///
23/// Every other field is encapsulated — read via `id()`, `payload()`,
24/// `audit_trail()`, `errors()`, `capture_changes()`; mutate `errors` via
25/// [`Message::add_error`]; mutate `context` via [`crate::TaskContext::set`].
26/// Direct mutation of `audit_trail` is engine-internal.
27/// Internal id storage for [`Message`]. The default UUID v7 id is written
28/// into a fixed inline buffer (36 hyphenated lowercase ASCII bytes) — no
29/// per-message heap allocation; a caller-supplied id keeps its `String`.
30/// Private repr: every public surface exposes it as `&str` via
31/// [`Message::id`], and the serialized wire shape (a JSON string) is
32/// unchanged.
33#[derive(Clone)]
34pub(crate) enum MessageId {
35    Uuid([u8; 36]),
36    Custom(String),
37}
38
39impl MessageId {
40    /// Generate a fresh UUID v7 id directly into the inline buffer.
41    fn new_uuid_v7() -> Self {
42        let mut buf = [0u8; 36];
43        Uuid::now_v7().hyphenated().encode_lower(&mut buf);
44        Self::Uuid(buf)
45    }
46
47    fn as_str(&self) -> &str {
48        match self {
49            // encode_lower fills all 36 bytes with hyphenated lowercase
50            // ASCII, so the buffer is always valid UTF-8.
51            Self::Uuid(buf) => std::str::from_utf8(buf).expect("uuid ids are ascii"),
52            Self::Custom(s) => s,
53        }
54    }
55}
56
57impl std::fmt::Debug for MessageId {
58    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59        std::fmt::Debug::fmt(self.as_str(), f)
60    }
61}
62
63#[derive(Debug, Clone)]
64pub struct Message {
65    pub(crate) id: MessageId,
66    pub(crate) payload: Arc<OwnedDataValue>,
67    /// Unified context containing `data`, `metadata`, and `temp_data` keys.
68    /// Always an `OwnedDataValue::Object`; the engine populates the three
69    /// top-level keys at construction. Public for read access (tests do
70    /// `message.context["data"]["x"]` lookups); inside handlers prefer
71    /// [`crate::TaskContext::set`] which records audit-trail changes.
72    pub context: OwnedDataValue,
73    pub(crate) audit_trail: Vec<AuditTrail>,
74    /// Errors that occurred during message processing. Read via
75    /// `errors()`, append via `add_error()`.
76    pub(crate) errors: Vec<ErrorInfo>,
77    /// When `true` (default), built-in functions emit per-write `Change`
78    /// entries into `audit_trail`, capturing `old_value` and `new_value` deep
79    /// clones. When `false`, `AuditTrail` entries are still recorded
80    /// (workflow_id, task_id, status, timestamp) but `changes` is empty —
81    /// the bulk-pipeline fast path. UI debug consumers should leave this at
82    /// `true`. Wire shape is unchanged either way.
83    pub(crate) capture_changes: bool,
84}
85
86// Custom Serialize: stable wire format ({id, payload, context, audit_trail, errors}).
87// `capture_changes` is an in-memory hint only — never serialized.
88impl Serialize for Message {
89    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
90    where
91        S: serde::Serializer,
92    {
93        use serde::ser::SerializeStruct;
94        let mut state = serializer.serialize_struct("Message", 5)?;
95        state.serialize_field("id", &self.id.as_str())?;
96        state.serialize_field("payload", &self.payload)?;
97        state.serialize_field("context", &self.context)?;
98        state.serialize_field("audit_trail", &self.audit_trail)?;
99        state.serialize_field("errors", &self.errors)?;
100        state.end()
101    }
102}
103
104// Custom Deserialize: mirrors the Serialize shape; no cache field to seed.
105// `capture_changes` defaults to `true` for back-compat.
106impl<'de> Deserialize<'de> for Message {
107    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
108    where
109        D: serde::Deserializer<'de>,
110    {
111        #[derive(Deserialize)]
112        struct MessageData {
113            id: String,
114            payload: Arc<OwnedDataValue>,
115            context: OwnedDataValue,
116            audit_trail: Vec<AuditTrail>,
117            errors: Vec<ErrorInfo>,
118        }
119
120        let data = MessageData::deserialize(deserializer)?;
121        Ok(Message {
122            id: MessageId::Custom(data.id),
123            payload: data.payload,
124            context: data.context,
125            audit_trail: data.audit_trail,
126            errors: data.errors,
127            capture_changes: true,
128        })
129    }
130}
131
132impl Message {
133    /// Start building a message. The recommended constructor — chains
134    /// `.id(...)`, `.payload(...)` / `.payload_json(...)`, and
135    /// `.capture_changes(...)` calls, then `.build()`.
136    pub fn builder() -> MessageBuilder {
137        MessageBuilder::new()
138    }
139
140    /// Construct a message from an already-owned payload `Arc`. The perf
141    /// path: zero `serde_json::Value` walk, one Arc refcount bump per
142    /// message. Use this from a hot loop with a payload `Arc` shared across
143    /// messages (e.g. a benchmark harness or an HTTP handler that receives
144    /// already-parsed payloads).
145    pub fn new(payload: Arc<OwnedDataValue>) -> Self {
146        Self {
147            // UUID v7: ms-precision timestamp in the high bits, random tail.
148            // Time-ordered and sortable — better for databases/logs than v4
149            // (random-only) and the same `rng` backend cost. Encoded into the
150            // inline id buffer — no `String` allocation per message.
151            id: MessageId::new_uuid_v7(),
152            payload,
153            context: empty_context(),
154            audit_trail: vec![],
155            errors: vec![],
156            capture_changes: true,
157        }
158    }
159
160    /// Construct a message from a `serde_json::Value` payload. Convenience
161    /// for code that already speaks serde_json; goes through the
162    /// `OwnedDataValue::from(&Value)` bridge (one deep walk).
163    pub fn from_value(payload: &JsonValue) -> Self {
164        Self::new(Arc::new(OwnedDataValue::from(payload)))
165    }
166
167    /// Construct a message from a JSON payload string. Parses with
168    /// `serde_json` and bridges into `OwnedDataValue`. Returns
169    /// `DataflowError::Deserialization` on parse failure.
170    pub fn from_json_str(payload: &str) -> crate::engine::error::Result<Self> {
171        let value: JsonValue = serde_json::from_str(payload).map_err(DataflowError::from_serde)?;
172        Ok(Self::from_value(&value))
173    }
174
175    /// Add an error to the message
176    pub fn add_error(&mut self, error: ErrorInfo) {
177        self.errors.push(error);
178    }
179
180    /// Check if message has errors
181    pub fn has_errors(&self) -> bool {
182        !self.errors.is_empty()
183    }
184
185    /// Message id (UUID v7 string by default; caller-supplied if set via
186    /// [`MessageBuilder::id`]).
187    #[inline]
188    pub fn id(&self) -> &str {
189        self.id.as_str()
190    }
191
192    /// Original payload as the engine received it. Immutable for the
193    /// lifetime of the message — the engine reads it through this Arc and
194    /// copies into `context` only as needed by handlers.
195    #[inline]
196    pub fn payload(&self) -> &OwnedDataValue {
197        &self.payload
198    }
199
200    /// The shared payload `Arc` itself. Useful when forwarding the same
201    /// payload to multiple messages without recloning the underlying
202    /// `OwnedDataValue` tree.
203    #[inline]
204    pub fn payload_arc(&self) -> &Arc<OwnedDataValue> {
205        &self.payload
206    }
207
208    /// Audit-trail entries recorded by the engine, one per task that ran
209    /// (skipped tasks are absent unless `Trace` mode is on).
210    #[inline]
211    pub fn audit_trail(&self) -> &[AuditTrail] {
212        &self.audit_trail
213    }
214
215    /// Errors collected while processing — both validation failures and
216    /// task errors that the workflow swallowed via `continue_on_error`.
217    #[inline]
218    pub fn errors(&self) -> &[ErrorInfo] {
219        &self.errors
220    }
221
222    /// Whether per-write `Change` capture is on. When `false`, audit-trail
223    /// entries are still emitted but their `changes` lists are empty —
224    /// the bulk-pipeline fast path.
225    #[inline]
226    pub fn capture_changes(&self) -> bool {
227        self.capture_changes
228    }
229
230    /// Get a reference to the `data` field in context. Returns
231    /// `&OwnedDataValue::Null` if missing (matches `serde_json::Value`'s
232    /// `Index` fallback semantics).
233    pub fn data(&self) -> &OwnedDataValue {
234        &self.context["data"]
235    }
236
237    /// Get a reference to the `metadata` field in context.
238    pub fn metadata(&self) -> &OwnedDataValue {
239        &self.context["metadata"]
240    }
241
242    /// Get a reference to the `temp_data` field in context.
243    pub fn temp_data(&self) -> &OwnedDataValue {
244        &self.context["temp_data"]
245    }
246}
247
248/// Builder for [`Message`]. Collapses the historical
249/// `new` / `with_id` / `from_value` / `without_change_capture` four-way
250/// constructor split into a single fluent shape.
251///
252/// ```
253/// use dataflow_rs::Message;
254/// use serde_json::json;
255///
256/// // Minimal: serde_json payload, default UUID id, capture on.
257/// let m = Message::builder()
258///     .payload_json(&json!({"order": {"total": 1500}}))
259///     .build();
260/// assert!(m.id().len() > 0);
261/// assert!(m.capture_changes());
262/// ```
263#[must_use = "MessageBuilder must be `.build()` to produce a Message"]
264#[derive(Default)]
265pub struct MessageBuilder {
266    id: Option<String>,
267    payload: Option<Arc<OwnedDataValue>>,
268    capture_changes: Option<bool>,
269}
270
271impl MessageBuilder {
272    /// Create an empty builder. Equivalent to [`MessageBuilder::default`].
273    pub fn new() -> Self {
274        Self::default()
275    }
276
277    /// Caller-supplied id (typically a correlation id from upstream).
278    /// Defaults to a freshly-generated UUID v7.
279    pub fn id(mut self, id: impl Into<String>) -> Self {
280        self.id = Some(id.into());
281        self
282    }
283
284    /// Already-owned payload `Arc` — zero serde_json walk, refcount-only
285    /// share. Mutually exclusive with [`Self::payload_json`]; whichever is
286    /// called last wins.
287    pub fn payload(mut self, payload: Arc<OwnedDataValue>) -> Self {
288        self.payload = Some(payload);
289        self
290    }
291
292    /// Construct the payload from a `serde_json::Value`. Goes through the
293    /// `OwnedDataValue::from(&Value)` bridge (one deep walk).
294    pub fn payload_json(mut self, payload: &JsonValue) -> Self {
295        self.payload = Some(Arc::new(OwnedDataValue::from(payload)));
296        self
297    }
298
299    /// When `false`, built-in functions skip per-write `Change` capture —
300    /// audit-trail entries are still recorded but their `changes` list is
301    /// empty. Defaults to `true`.
302    pub fn capture_changes(mut self, on: bool) -> Self {
303        self.capture_changes = Some(on);
304        self
305    }
306
307    /// Finalize. Defaults: id = UUID v7, payload = `OwnedDataValue::Null`,
308    /// capture_changes = `true`.
309    pub fn build(self) -> Message {
310        Message {
311            id: self
312                .id
313                .map(MessageId::Custom)
314                .unwrap_or_else(MessageId::new_uuid_v7),
315            payload: self
316                .payload
317                .unwrap_or_else(|| Arc::new(OwnedDataValue::Null)),
318            context: empty_context(),
319            audit_trail: vec![],
320            errors: vec![],
321            capture_changes: self.capture_changes.unwrap_or(true),
322        }
323    }
324}
325
326/// Build the canonical empty context shape used by `Message::new` and
327/// `MessageBuilder::build`.
328fn empty_context() -> OwnedDataValue {
329    OwnedDataValue::Object(vec![
330        ("data".to_string(), OwnedDataValue::Object(Vec::new())),
331        ("metadata".to_string(), OwnedDataValue::Object(Vec::new())),
332        ("temp_data".to_string(), OwnedDataValue::Object(Vec::new())),
333    ])
334}
335
336#[derive(Serialize, Deserialize, Debug, Clone)]
337pub struct AuditTrail {
338    pub workflow_id: Arc<str>,
339    pub task_id: Arc<str>,
340    pub timestamp: DateTime<Utc>,
341    pub changes: Vec<Change>,
342    pub status: usize,
343}
344
345/// A single recorded mutation in the audit trail.
346///
347/// `old_value` and `new_value` are owned `OwnedDataValue`s rather than
348/// `Arc<OwnedDataValue>` — eliminates one heap allocation per Change on the
349/// hot path. External consumers that need to share a `Change` across threads
350/// can wrap it themselves; in-process pipelines (audit-on map mappings) don't
351/// pay the Arc cost they were never going to use.
352#[derive(Serialize, Deserialize, Debug, Clone)]
353pub struct Change {
354    pub path: Arc<str>,
355    pub old_value: OwnedDataValue,
356    pub new_value: OwnedDataValue,
357}
358
359#[cfg(test)]
360mod tests {
361    use super::*;
362
363    #[test]
364    fn from_json_str_parses_valid_payload() {
365        let msg =
366            Message::from_json_str(r#"{"order": {"total": 42}}"#).expect("valid JSON should parse");
367        let payload_json = serde_json::to_value(msg.payload()).unwrap();
368        assert_eq!(payload_json, serde_json::json!({"order": {"total": 42}}));
369    }
370
371    #[test]
372    fn from_json_str_rejects_malformed_payload() {
373        let err = Message::from_json_str("{ not json").expect_err("malformed input should fail");
374        assert!(matches!(err, DataflowError::Deserialization(_)));
375    }
376}