1use core::fmt;
12use std::collections::BTreeSet;
13
14use oxrdf::vocab::xsd;
15use oxrdf::{Graph, Literal, NamedNode, NamedNodeRef, Term, TermRef, Triple};
16use serde::{Deserialize, Serialize};
17use sha2::{Digest, Sha256};
18use vertep_core::datamodel::{Event, EventType, Value};
19use vertep_store::reification::Annotation;
20use vertep_store::{Entry, JournalRecord};
21
22use crate::address::{self, Address, hex};
23use crate::chart::UnhandledPolicy;
24use crate::json;
25use crate::vocab::{self as vt, node};
26
27#[derive(Clone, Debug, PartialEq, Eq)]
29pub struct DecodeError(pub String);
30
31impl fmt::Display for DecodeError {
32 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33 write!(f, "malformed journal record: {}", self.0)
34 }
35}
36
37impl std::error::Error for DecodeError {}
38
39#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
42pub struct ReminderSpec {
43 pub name: String,
45 pub period_ms: i64,
47}
48
49#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
52pub struct TimerRecord {
53 pub iri: String,
56 pub actor: String,
58 pub send_id: String,
60 pub due_ms: i64,
63 pub event: Event,
65 pub recipient: String,
67 pub reminder: Option<ReminderSpec>,
69 pub correlation: Option<String>,
71}
72
73impl TimerRecord {
74 pub fn message_id(&self, n: u64) -> String {
76 if self.reminder.is_some() {
77 format!("{}/{n}", self.iri)
78 } else {
79 self.iri.clone()
80 }
81 }
82
83 pub fn due_of(&self, n: u64) -> i64 {
86 match &self.reminder {
87 Some(r) => self.due_ms.saturating_add(
88 r.period_ms
89 .saturating_mul(i64::try_from(n).unwrap_or(i64::MAX)),
90 ),
91 None => self.due_ms,
92 }
93 }
94}
95
96#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
98pub struct SendRecord {
99 pub iri: String,
101 pub send_id: String,
103 pub event: Event,
105 pub target: Option<String>,
107 pub recipient: Option<String>,
109 pub type_: String,
111 pub correlation: Option<String>,
113}
114
115#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
117pub struct InputRecord {
118 pub event: Event,
120 pub message_id: String,
122 pub correlation: Option<String>,
124 pub timer: Option<String>,
126}
127
128#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
131pub struct SnapshotData {
132 pub iri: String,
134 pub actor: String,
136 pub position: u64,
138 pub version: String,
140 pub digest: String,
142 pub session: String,
144 pub datamodel: String,
146 pub runtime: String,
148}
149
150#[derive(Clone, Debug, Default, PartialEq)]
152pub struct Effects {
153 pub sends: Vec<SendRecord>,
155 pub timers: Vec<TimerRecord>,
157 pub cancels: Vec<String>,
159 pub invokes: Vec<String>,
161 pub requests: Vec<String>,
163 pub answers: Option<String>,
165 pub transitions: Vec<String>,
167 pub terminated: bool,
169 pub unhandled: Option<UnhandledPolicy>,
171}
172
173#[derive(Clone, Debug, PartialEq)]
175pub enum EntryBody {
176 Input {
178 input: InputRecord,
180 effects: Effects,
182 },
183 Migration {
185 migration: String,
187 from: String,
189 before: SnapshotData,
191 after: SnapshotData,
193 },
194 Fork {
196 source: String,
198 position: u64,
200 snapshot: SnapshotData,
202 },
203}
204
205#[derive(Clone, Debug, PartialEq)]
210pub struct EntryData {
211 pub actor: Address,
213 pub position: u64,
215 pub version: String,
217 pub message_id: String,
219 pub timestamp_ms: i64,
221 pub digest: String,
223 pub body: EntryBody,
225}
226
227pub fn digest(active: &[String], datamodel: &Value, terminated: bool) -> String {
230 let mut h = Sha256::new();
231 for s in active {
232 h.update(s.as_bytes());
233 h.update([0u8]);
234 }
235 h.update([1u8]);
236 h.update(json::to_text(datamodel).as_bytes());
237 h.update([u8::from(terminated)]);
238 hex(&h.finalize())
239}
240
241fn int(n: i64) -> Literal {
242 Literal::new_typed_literal(n.to_string(), xsd::INTEGER)
243}
244
245fn uint(n: u64) -> Literal {
246 Literal::new_typed_literal(n.to_string(), xsd::INTEGER)
247}
248
249fn boolean(b: bool) -> Literal {
250 Literal::new_typed_literal(if b { "true" } else { "false" }, xsd::BOOLEAN)
251}
252
253fn str_lit(s: &str) -> Literal {
254 Literal::new_simple_literal(s)
255}
256
257struct Out<'a> {
259 g: &'a mut Vec<Triple>,
260}
261
262impl Out<'_> {
263 fn add(&mut self, s: &NamedNode, p: &str, o: impl Into<Term>) {
264 self.g.push(Triple::new(s.clone(), node(p), o.into()));
265 }
266
267 fn event(&mut self, s: &NamedNode, e: &Event) {
268 self.add(s, vt::EVENT_NAME, str_lit(&e.name));
269 self.add(s, vt::EVENT_TYPE, str_lit(event_type_name(e.type_)));
270 if let Some(x) = &e.sendid {
271 self.add(s, vt::SEND_ID, str_lit(x));
272 }
273 if let Some(x) = &e.origin {
274 self.add(s, vt::ORIGIN, str_lit(x));
275 }
276 if let Some(x) = &e.origintype {
277 self.add(s, vt::ORIGIN_TYPE, str_lit(x));
278 }
279 if e.originverified {
281 self.add(s, vt::ORIGIN_VERIFIED, boolean(true));
282 }
283 if let Some(x) = &e.invokeid {
284 self.add(s, vt::INVOKE_ID, str_lit(x));
285 }
286 self.add(s, vt::PAYLOAD, str_lit(&json::to_text(&e.data)));
287 }
288
289 fn timer(&mut self, t: &TimerRecord) {
290 let s = NamedNode::new_unchecked(&t.iri);
291 self.add(
292 &s,
293 vt::RDF_TYPE,
294 node(if t.reminder.is_some() {
295 vt::REMINDER
296 } else {
297 vt::TIMER
298 }),
299 );
300 self.add(&s, vt::ACTOR, NamedNode::new_unchecked(&t.actor));
301 self.add(&s, vt::SEND_ID, str_lit(&t.send_id));
302 self.add(&s, vt::DUE_AT, int(t.due_ms));
303 self.add(&s, vt::RECIPIENT, NamedNode::new_unchecked(&t.recipient));
304 self.add(&s, vt::MESSAGE_ID, str_lit(&t.message_id(0)));
305 self.add(
306 &s,
307 vt::ENVELOPE,
308 str_lit(&serde_json::to_string(t).unwrap_or_default()),
309 );
310 if let Some(r) = &t.reminder {
311 self.add(&s, vt::REMINDER_NAME, str_lit(&r.name));
312 self.add(&s, vt::PERIOD, int(r.period_ms));
313 }
314 if let Some(c) = &t.correlation {
315 self.add(&s, vt::CORRELATION_ID, str_lit(c));
316 }
317 self.add(&s, vt::EVENT_NAME, str_lit(&t.event.name));
318 }
319
320 fn snapshot(&mut self, s: &SnapshotData) {
321 let n = NamedNode::new_unchecked(&s.iri);
322 self.add(&n, vt::RDF_TYPE, node(vt::SNAPSHOT));
323 self.add(&n, vt::SNAPSHOT_OF, NamedNode::new_unchecked(&s.actor));
324 self.add(&n, vt::POSITION, uint(s.position));
325 self.add(&n, vt::CHART_VERSION, NamedNode::new_unchecked(&s.version));
326 self.add(&n, vt::CONFIGURATION_DIGEST, str_lit(&s.digest));
327 self.add(&n, vt::SESSION_STATE, str_lit(&s.session));
328 self.add(&n, vt::DATAMODEL_STATE, str_lit(&s.datamodel));
329 self.add(&n, vt::RUNTIME_STATE, str_lit(&s.runtime));
330 }
331}
332
333pub fn snapshot_triples(s: &SnapshotData) -> Vec<Triple> {
335 let mut g = Vec::new();
336 Out { g: &mut g }.snapshot(s);
337 g
338}
339
340pub fn timer_triples(t: &TimerRecord) -> Vec<Triple> {
342 let mut g = Vec::new();
343 Out { g: &mut g }.timer(t);
344 g
345}
346
347fn event_type_name(t: EventType) -> &'static str {
348 match t {
349 EventType::External => "external",
350 EventType::Internal => "internal",
351 EventType::Platform => "platform",
352 }
353}
354
355fn event_type_of(s: &str) -> EventType {
356 match s {
357 "internal" => EventType::Internal,
358 "platform" => EventType::Platform,
359 _ => EventType::External,
360 }
361}
362
363impl EntryData {
364 pub fn iri(&self) -> NamedNode {
366 self.actor.entry_iri(self.position)
367 }
368
369 pub fn effects(&self) -> Option<&Effects> {
371 match &self.body {
372 EntryBody::Input { effects, .. } => Some(effects),
373 _ => None,
374 }
375 }
376
377 pub fn to_entry(&self) -> Entry {
379 let e = self.iri();
380 let mut g = Vec::new();
381 let mut o = Out { g: &mut g };
382 o.add(
383 &e,
384 vt::CHART_VERSION,
385 NamedNode::new_unchecked(&self.version),
386 );
387 o.add(&e, vt::MESSAGE_ID, str_lit(&self.message_id));
388 o.add(&e, vt::TIMESTAMP, int(self.timestamp_ms));
389 o.add(&e, vt::CONFIGURATION_DIGEST, str_lit(&self.digest));
390 match &self.body {
391 EntryBody::Input { input, effects } => {
392 o.add(&e, vt::RDF_TYPE, node(vt::INPUT_ENTRY));
393 o.add(&e, vt::RDF_TYPE, node(vt::MACROSTEP));
394 let i = address::input_iri(&e);
395 o.add(&e, vt::INPUT, i.clone());
396 o.add(&i, vt::RDF_TYPE, node(vt::INPUT_EVENT));
397 o.add(&i, vt::MESSAGE_ID, str_lit(&input.message_id));
398 o.event(&i, &input.event);
399 if let Some(c) = &input.correlation {
400 o.add(&i, vt::CORRELATION_ID, str_lit(c));
401 }
402 if let Some(t) = &input.timer {
403 o.add(&e, vt::FIRED_TIMER, NamedNode::new_unchecked(t));
404 }
405 for (k, s) in effects.sends.iter().enumerate() {
406 let sn = NamedNode::new_unchecked(&s.iri);
407 o.add(&e, vt::SEND, sn.clone());
408 o.add(&sn, vt::RDF_TYPE, node(vt::OUTGOING_SEND));
409 o.add(&sn, vt::ORDER, uint(k as u64));
410 o.add(&sn, vt::MESSAGE_ID, str_lit(&s.iri));
411 o.add(&sn, vt::SEND_TYPE, str_lit(&s.type_));
412 if let Some(t) = &s.target {
413 o.add(&sn, vt::SEND_TARGET, str_lit(t));
414 }
415 if let Some(r) = &s.recipient {
416 o.add(&sn, vt::RECIPIENT, NamedNode::new_unchecked(r));
417 }
418 if let Some(c) = &s.correlation {
419 o.add(&sn, vt::CORRELATION_ID, str_lit(c));
420 }
421 o.add(&sn, vt::EVENT_NAME, str_lit(&s.event.name));
422 o.add(&sn, vt::PAYLOAD, str_lit(&json::to_text(&s.event.data)));
423 o.add(
424 &sn,
425 vt::ENVELOPE,
426 str_lit(&serde_json::to_string(s).unwrap_or_default()),
427 );
428 }
429 for t in &effects.timers {
430 o.add(&e, vt::SCHEDULES, NamedNode::new_unchecked(&t.iri));
431 o.timer(t);
432 o.add(
433 &NamedNode::new_unchecked(&t.iri),
434 vt::SCHEDULED_BY,
435 e.clone(),
436 );
437 }
438 for c in &effects.cancels {
439 o.add(&e, vt::CANCELS, NamedNode::new_unchecked(c));
440 }
441 if !effects.invokes.is_empty() {
442 let arr = format!("[{}]", effects.invokes.join(","));
443 o.add(&e, vt::INVOKES, str_lit(&arr));
444 }
445 for r in &effects.requests {
446 o.add(&e, vt::REQUESTS, str_lit(r));
447 }
448 if let Some(a) = &effects.answers {
449 o.add(&e, vt::ANSWERS, str_lit(a));
450 }
451 if effects.terminated {
452 o.add(&e, vt::TERMINATED_HERE, boolean(true));
453 }
454 if let Some(p) = effects.unhandled {
455 o.add(&e, vt::UNHANDLED, str_lit(p.as_str()));
456 }
457 for t in &effects.transitions {
458 o.add(&e, vt::TOOK_TRANSITION, NamedNode::new_unchecked(t));
459 }
460 }
461 EntryBody::Migration {
462 migration,
463 from,
464 before,
465 after,
466 } => {
467 o.add(&e, vt::RDF_TYPE, node(vt::MIGRATION_ENTRY));
468 o.add(&e, vt::MIGRATION_P, NamedNode::new_unchecked(migration));
469 o.add(
470 &NamedNode::new_unchecked(migration),
471 vt::RDF_TYPE,
472 node(vt::MIGRATION),
473 );
474 o.add(&e, vt::FROM_VERSION, NamedNode::new_unchecked(from));
475 o.add(&e, vt::TO_VERSION, NamedNode::new_unchecked(&self.version));
476 o.add(
477 &e,
478 vt::SNAPSHOT_BEFORE,
479 NamedNode::new_unchecked(&before.iri),
480 );
481 o.add(&e, vt::SNAPSHOT_AFTER, NamedNode::new_unchecked(&after.iri));
482 o.snapshot(before);
483 o.snapshot(after);
484 }
485 EntryBody::Fork {
486 source,
487 position,
488 snapshot,
489 } => {
490 o.add(&e, vt::RDF_TYPE, node(vt::FORK_ENTRY));
491 o.add(&e, vt::FORK_OF, NamedNode::new_unchecked(source));
492 o.add(&e, vt::FORK_POSITION, uint(*position));
493 o.add(&e, vt::SNAPSHOT_P, NamedNode::new_unchecked(&snapshot.iri));
494 o.snapshot(snapshot);
495 }
496 }
497 Entry::new(e, g)
498 }
499
500 pub fn annotation_graph(&self) -> NamedNode {
502 NamedNode::new_unchecked(format!("{}/annotations", self.iri().as_str()))
503 }
504
505 pub fn annotations(&self) -> Vec<Annotation> {
511 let e = self.iri();
512 let g = self.annotation_graph();
513 self.effects()
514 .map(|eff| {
515 eff.transitions
516 .iter()
517 .map(|t| {
518 Annotation::new(
519 g.clone(),
520 Triple::new(
521 self.actor.node().clone(),
522 node(vt::TOOK_TRANSITION),
523 NamedNode::new_unchecked(t),
524 ),
525 )
526 .with(node(vt::CAUSED_BY), e.clone())
527 })
528 .collect()
529 })
530 .unwrap_or_default()
531 }
532
533 pub fn from_record(actor: &Address, rec: &JournalRecord) -> Result<Self, DecodeError> {
539 let g = rec.entry.triples();
540 let e = rec.entry.name().clone();
541 let r = Reader { g };
542 let version = r.iri(&e, vt::CHART_VERSION)?;
543 let message_id = r.string(&e, vt::MESSAGE_ID)?;
544 let timestamp_ms = r.int(&e, vt::TIMESTAMP)?;
545 let digest = r.string(&e, vt::CONFIGURATION_DIGEST)?;
546 let body = if r.has_type(&e, vt::MIGRATION_ENTRY) {
547 let before = r.iri(&e, vt::SNAPSHOT_BEFORE)?;
548 let after = r.iri(&e, vt::SNAPSHOT_AFTER)?;
549 EntryBody::Migration {
550 migration: r.iri(&e, vt::MIGRATION_P)?,
551 from: r.iri(&e, vt::FROM_VERSION)?,
552 before: r.snapshot(&NamedNode::new_unchecked(before))?,
553 after: r.snapshot(&NamedNode::new_unchecked(after))?,
554 }
555 } else if r.has_type(&e, vt::FORK_ENTRY) {
556 let s = r.iri(&e, vt::SNAPSHOT_P)?;
557 EntryBody::Fork {
558 source: r.iri(&e, vt::FORK_OF)?,
559 position: u64::try_from(r.int(&e, vt::FORK_POSITION)?)
560 .map_err(|_| DecodeError("negative fork position".into()))?,
561 snapshot: r.snapshot(&NamedNode::new_unchecked(s))?,
562 }
563 } else {
564 let i = NamedNode::new_unchecked(r.iri(&e, vt::INPUT)?);
565 let input = InputRecord {
566 event: r.event(&i)?,
567 message_id: r.string(&i, vt::MESSAGE_ID)?,
568 correlation: r.opt_string(&i, vt::CORRELATION_ID),
569 timer: r.opt_iri(&e, vt::FIRED_TIMER),
570 };
571 let mut sends = Vec::new();
572 for s in r.iris(&e, vt::SEND) {
573 let sn = NamedNode::new_unchecked(&s);
574 let order = r.int(&sn, vt::ORDER)?;
575 let j = r.string(&sn, vt::ENVELOPE)?;
576 let rec = serde_json::from_str::<SendRecord>(&j)
577 .map_err(|x| DecodeError(format!("send {s}: {x}")))?;
578 sends.push((order, rec));
579 }
580 sends.sort_by_key(|(o, _)| *o);
581 let mut timers = Vec::new();
582 for t in r.iris(&e, vt::SCHEDULES) {
583 let tn = NamedNode::new_unchecked(&t);
584 let j = r.string(&tn, vt::ENVELOPE)?;
585 timers.push(
586 serde_json::from_str::<TimerRecord>(&j)
587 .map_err(|x| DecodeError(format!("timer {t}: {x}")))?,
588 );
589 }
590 timers.sort_by(|a, b| a.iri.cmp(&b.iri));
591 let mut cancels = r.iris(&e, vt::CANCELS);
592 cancels.sort();
593 let invokes = match r.opt_string(&e, vt::INVOKES) {
594 Some(arr) => serde_json::from_str::<Vec<serde_json::Value>>(&arr)
595 .map_err(|x| DecodeError(format!("invokes: {x}")))?
596 .into_iter()
597 .map(|v| v.to_string())
598 .collect(),
599 None => Vec::new(),
600 };
601 let mut requests = r.strings(&e, vt::REQUESTS);
602 requests.sort();
603 let mut transitions = r.objects_iri(&e, vt::TOOK_TRANSITION);
604 transitions.sort();
605 EntryBody::Input {
606 input,
607 effects: Effects {
608 sends: sends.into_iter().map(|(_, s)| s).collect(),
609 timers,
610 cancels,
611 invokes,
612 requests,
613 answers: r.opt_string(&e, vt::ANSWERS),
614 transitions,
615 terminated: r.opt_bool(&e, vt::TERMINATED_HERE),
616 unhandled: r
617 .opt_string(&e, vt::UNHANDLED)
618 .and_then(|p| UnhandledPolicy::parse(&p)),
619 },
620 }
621 };
622 Ok(EntryData {
623 actor: actor.clone(),
624 position: rec.position.get(),
625 version,
626 message_id,
627 timestamp_ms,
628 digest,
629 body,
630 })
631 }
632}
633
634pub(crate) struct Reader<'a> {
636 pub(crate) g: &'a Graph,
637}
638
639impl Reader<'_> {
640 fn objects<'b>(
641 &'b self,
642 s: &'b NamedNode,
643 p: &'static str,
644 ) -> impl Iterator<Item = TermRef<'b>> + 'b {
645 let p = NamedNodeRef::new_unchecked(p);
646 self.g.objects_for_subject_predicate(s, p)
647 }
648
649 pub(crate) fn has_type(&self, s: &NamedNode, class: &str) -> bool {
650 self.objects(s, vt::RDF_TYPE)
651 .any(|o| matches!(o, TermRef::NamedNode(n) if n.as_str() == class))
652 }
653
654 pub(crate) fn opt_string(&self, s: &NamedNode, p: &'static str) -> Option<String> {
655 self.objects(s, p).find_map(|o| match o {
656 TermRef::Literal(l) => Some(l.value().to_owned()),
657 _ => None,
658 })
659 }
660
661 pub(crate) fn string(&self, s: &NamedNode, p: &'static str) -> Result<String, DecodeError> {
662 self.opt_string(s, p)
663 .ok_or_else(|| DecodeError(format!("{s} has no {p}")))
664 }
665
666 fn strings(&self, s: &NamedNode, p: &'static str) -> Vec<String> {
667 self.objects(s, p)
668 .filter_map(|o| match o {
669 TermRef::Literal(l) => Some(l.value().to_owned()),
670 _ => None,
671 })
672 .collect()
673 }
674
675 pub(crate) fn int(&self, s: &NamedNode, p: &'static str) -> Result<i64, DecodeError> {
676 self.string(s, p)?
677 .parse()
678 .map_err(|_| DecodeError(format!("{s} {p} is not an integer")))
679 }
680
681 fn opt_bool(&self, s: &NamedNode, p: &'static str) -> bool {
682 self.opt_string(s, p)
683 .is_some_and(|v| v == "true" || v == "1")
684 }
685
686 pub(crate) fn opt_iri(&self, s: &NamedNode, p: &'static str) -> Option<String> {
687 self.objects(s, p).find_map(|o| match o {
688 TermRef::NamedNode(n) => Some(n.as_str().to_owned()),
689 _ => None,
690 })
691 }
692
693 pub(crate) fn iri(&self, s: &NamedNode, p: &'static str) -> Result<String, DecodeError> {
694 self.opt_iri(s, p)
695 .ok_or_else(|| DecodeError(format!("{s} has no {p}")))
696 }
697
698 fn iris(&self, s: &NamedNode, p: &'static str) -> Vec<String> {
699 self.objects(s, p)
700 .filter_map(|o| match o {
701 TermRef::NamedNode(n) => Some(n.as_str().to_owned()),
702 _ => None,
703 })
704 .collect()
705 }
706
707 fn objects_iri(&self, s: &NamedNode, p: &'static str) -> Vec<String> {
708 let set: BTreeSet<String> = self.iris(s, p).into_iter().collect();
709 set.into_iter().collect()
710 }
711
712 fn event(&self, s: &NamedNode) -> Result<Event, DecodeError> {
713 let data = json::from_text(&self.string(s, vt::PAYLOAD)?)
714 .map_err(|e| DecodeError(format!("{s} payload: {e}")))?;
715 Ok(Event {
716 name: self.string(s, vt::EVENT_NAME)?,
717 type_: event_type_of(&self.opt_string(s, vt::EVENT_TYPE).unwrap_or_default()),
718 sendid: self.opt_string(s, vt::SEND_ID),
719 origin: self.opt_string(s, vt::ORIGIN),
720 origintype: self.opt_string(s, vt::ORIGIN_TYPE),
721 originverified: self.opt_bool(s, vt::ORIGIN_VERIFIED),
722 invokeid: self.opt_string(s, vt::INVOKE_ID),
723 data,
724 })
725 }
726
727 pub(crate) fn snapshot(&self, n: &NamedNode) -> Result<SnapshotData, DecodeError> {
728 Ok(SnapshotData {
729 iri: n.as_str().to_owned(),
730 actor: self.iri(n, vt::SNAPSHOT_OF)?,
731 position: u64::try_from(self.int(n, vt::POSITION)?)
732 .map_err(|_| DecodeError("negative snapshot position".into()))?,
733 version: self.iri(n, vt::CHART_VERSION)?,
734 digest: self.string(n, vt::CONFIGURATION_DIGEST)?,
735 session: self.string(n, vt::SESSION_STATE)?,
736 datamodel: self.string(n, vt::DATAMODEL_STATE)?,
737 runtime: self.string(n, vt::RUNTIME_STATE)?,
738 })
739 }
740}
741
742#[cfg(test)]
743mod tests {
744 use super::*;
745 use vertep_store::Position;
746
747 fn sample() -> EntryData {
748 let a = Address::parse("urn:vertep:test:actor").unwrap();
749 let e = a.entry_iri(1);
750 EntryData {
751 actor: a.clone(),
752 position: 1,
753 version: "urn:vertep:chart:sha256:00".into(),
754 message_id: "m1".into(),
755 timestamp_ms: 1_000,
756 digest: "d".into(),
757 body: EntryBody::Input {
758 input: InputRecord {
759 event: Event {
760 name: "go".into(),
761 data: Value::Object(vec![("x".into(), Value::Number(1.0))]),
762 origin: Some("urn:vertep:test:sender".into()),
763 ..Event::default()
764 },
765 message_id: "m1".into(),
766 correlation: None,
767 timer: None,
768 },
769 effects: Effects {
770 sends: vec![SendRecord {
771 iri: address::send_iri(&e, 0).as_str().into(),
772 send_id: "s1".into(),
773 event: Event::named("out", EventType::External),
774 target: Some("urn:vertep:test:b".into()),
775 recipient: Some("urn:vertep:test:b".into()),
776 type_: "scxml".into(),
777 correlation: None,
778 }],
779 timers: vec![TimerRecord {
780 iri: address::timer_iri(&e, "r1").as_str().into(),
781 actor: a.as_str().into(),
782 send_id: "r1".into(),
783 due_ms: 6_000,
784 event: Event::named("remind", EventType::External),
785 recipient: a.as_str().into(),
786 reminder: None,
787 correlation: None,
788 }],
789 transitions: vec!["urn:vertep:chart:sha256:00#s.a:t0".into()],
790 ..Effects::default()
791 },
792 },
793 }
794 }
795
796 #[test]
797 fn entry_round_trips_through_rdf() {
798 let d = sample();
799 let entry = d
800 .to_entry()
801 .stamp(d.actor.node(), Position::new(1))
802 .unwrap();
803 let rec = JournalRecord {
804 position: Position::new(1),
805 entry,
806 };
807 assert_eq!(EntryData::from_record(&d.actor, &rec).unwrap(), d);
808 assert_eq!(d.annotations().len(), 1);
809 }
810
811 #[test]
813 fn verified_origin_round_trips_through_the_journal() {
814 let mut d = sample();
815 let EntryBody::Input { input, .. } = &mut d.body else {
816 unreachable!()
817 };
818 input.event = Event::named("go", EventType::External)
819 .with_verified_origin("did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK");
820 let entry = d
821 .to_entry()
822 .stamp(d.actor.node(), Position::new(1))
823 .unwrap();
824 let rec = JournalRecord {
825 position: Position::new(1),
826 entry,
827 };
828 let back = EntryData::from_record(&d.actor, &rec).unwrap();
829 assert_eq!(back, d);
830 let EntryBody::Input { input, .. } = &back.body else {
831 unreachable!()
832 };
833 assert!(input.event.originverified);
834 }
835
836 #[test]
837 fn digest_depends_on_configuration_and_data() {
838 let a = digest(&["s1".into()], &Value::Undefined, false);
839 assert_ne!(a, digest(&["s2".into()], &Value::Undefined, false));
840 assert_ne!(a, digest(&["s1".into()], &Value::Null, false));
841 assert_eq!(a, digest(&["s1".into()], &Value::Undefined, false));
842 }
843}