harmos_domain/state/
envelope.rs1pub use crate::DefinitionId;
15
16use serde::{Deserialize, Serialize};
22use super::entry::{Metadata, Position};
24
25#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
34pub struct SystemFact {
35 pub source: FactSource,
36 pub name: String,
37 pub version: u32,
38 pub payload: Vec<u8>,
39}
40
41#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
42pub struct Envelope<P> {
43 pub position: Position,
44 pub kind: Kind,
45 pub id: DefinitionId,
46 pub version: u32,
47 pub metadata: Metadata,
48 pub payload: P,
49}
50
51#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
54pub enum Kind {
55 Transaction,
56 Record,
57}
58
59#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
60pub enum FactSource {
61 Work(String),
62 Guest(String),
63 Sidecar(String),
64}
65
66impl SystemFact {
71 pub fn identify(&self) -> (DefinitionId, u32) {
72 let id = match &self.source {
73 FactSource::Work(service) => format!("harmos:work/{service}/{}", self.name),
74 FactSource::Guest(guest) => format!("harmos:guest/{guest}/{}", self.name),
75 FactSource::Sidecar(owner) => format!("harmos:sidecar/{owner}/{}", self.name),
76 };
77
78 (DefinitionId::new(id), self.version)
79 }
80
81 pub(crate) fn validate_admission(&self) -> bool {
82 if !matches!(self.source, FactSource::Sidecar(_)) {
83 return true;
84 }
85 system_identity(&self.identify().0).is_ok_and(|value| value == Some((self.source.clone(), self.name.clone())))
86 }
87
88 pub fn restore<'de, D>(id: &DefinitionId, version: u32, payload: D) -> Result<Option<Self>, D::Error>
89 where
90 D: serde::Deserializer<'de>,
91 {
92 let Some((source, name)) = system_identity(id).map_err(serde::de::Error::custom)? else {
93 return Ok(None);
94 };
95 let payload = Vec::<u8>::deserialize(payload)?;
96
97 Ok(Some(Self { source, name, version, payload }))
98 }
99}
100
101fn system_identity(id: &DefinitionId) -> Result<Option<(FactSource, String)>, &'static str> {
106 if let Some(namespaced) = id.as_str().strip_prefix("harmos:work/") {
107 let (service, name) = namespaced.split_once('/').ok_or("a work fact id must contain its local fact name")?;
108 if service.is_empty() || name.is_empty() {
109 return Err("a work fact id must name both service and fact");
110 }
111 return Ok(Some((FactSource::Work(service.to_owned()), name.to_owned())));
112 }
113
114 if let Some(namespaced) = id.as_str().strip_prefix("harmos:guest/") {
115 let (guest, name) = namespaced.split_once('/').ok_or("a guest fact id must contain its local fact name")?;
116 if guest.is_empty() || name.is_empty() {
117 return Err("a guest fact id must name both guest and fact");
118 }
119 return Ok(Some((FactSource::Guest(guest.to_owned()), name.to_owned())));
120 }
121
122 if let Some(namespaced) = id.as_str().strip_prefix("harmos:sidecar/") {
123 let (owner, name) = namespaced.split_once('/').ok_or("a sidecar fact id must contain its local fact name")?;
124 if !crate::identity::segment(owner) || name.is_empty() {
125 return Err("a sidecar fact must name a portable artifact owner and nonempty fact");
126 }
127 return Ok(Some((FactSource::Sidecar(owner.to_owned()), name.to_owned())));
128 }
129 Ok(None)
130}
131
132#[cfg(test)]
135mod tests {
136 use super::*;
137
138 #[test]
139 fn sidecar_owner_parity() {
140 let cases: Vec<(String, bool)> = serde_json::from_str(include_str!("../../tests/fixtures/owner-identities.json")).unwrap();
141 for (owner, valid) in cases {
142 let fact = SystemFact {
143 source: FactSource::Sidecar(owner.clone()),
144 name: "result/sample".to_owned(),
145 version: 1,
146 payload: vec![0, 255, 17],
147 };
148 assert_eq!(crate::state::Prepared::system(&fact).is_ok(), valid, "admission {owner:?}");
149 let payload = serde_json::json!(fact.payload);
150 let restored = SystemFact::restore(&fact.identify().0, 1, &payload);
151 assert_eq!(restored.is_ok_and(|value| value == Some(fact)), valid, "restore {owner:?}");
153 }
154 }
155}