vertep_runtime/
address.rs1use core::fmt;
9
10use oxrdf::NamedNode;
11use sha2::{Digest, Sha256};
12
13#[derive(Clone, Debug, PartialEq, Eq)]
15pub struct AddressError {
16 pub text: String,
18 pub reason: String,
20}
21
22impl fmt::Display for AddressError {
23 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24 write!(f, "not an actor address: {:?} ({})", self.text, self.reason)
25 }
26}
27
28impl std::error::Error for AddressError {}
29
30#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
33pub struct Address(NamedNode);
34
35impl fmt::Debug for Address {
36 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37 write!(f, "<{}>", self.0.as_str())
38 }
39}
40
41impl fmt::Display for Address {
42 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43 f.write_str(self.0.as_str())
44 }
45}
46
47impl Address {
48 pub fn parse(text: &str) -> Result<Self, AddressError> {
54 NamedNode::new(text).map(Address).map_err(|e| AddressError {
55 text: text.to_owned(),
56 reason: e.to_string(),
57 })
58 }
59
60 pub fn virtual_actor(chart_type: &str, key: &str) -> Result<Self, AddressError> {
68 Address::parse(chart_type)?;
69 let mut s = String::with_capacity(chart_type.len() + key.len() + 1);
70 s.push_str(chart_type);
71 s.push('/');
72 percent_encode_into(key, &mut s);
73 Address::parse(&s)
74 }
75
76 pub fn invoked(parent: &Address, invokeid: &str) -> Address {
79 Address(parent.derived("invoke", invokeid))
80 }
81
82 pub fn as_str(&self) -> &str {
84 self.0.as_str()
85 }
86
87 pub fn node(&self) -> &NamedNode {
89 &self.0
90 }
91
92 pub fn scheme(&self) -> &str {
94 self.as_str().split(':').next().unwrap_or_default()
95 }
96
97 fn derived(&self, kind: &str, part: &str) -> NamedNode {
99 let mut s = String::with_capacity(self.as_str().len() + kind.len() + part.len() + 2);
100 s.push_str(self.as_str());
101 s.push('/');
102 s.push_str(kind);
103 s.push('/');
104 percent_encode_into(part, &mut s);
105 NamedNode::new_unchecked(s)
106 }
107
108 pub fn entry_iri(&self, position: u64) -> NamedNode {
111 self.derived("entry", &position.to_string())
112 }
113
114 pub fn snapshot_iri(&self, position: u64) -> NamedNode {
116 self.derived("snapshot", &position.to_string())
117 }
118
119 pub fn dead_letter_iri(&self, message_id: &str) -> NamedNode {
121 self.derived("dead-letter", &short_digest(message_id))
122 }
123
124 pub fn dead_letter_graph(&self) -> NamedNode {
126 NamedNode::new_unchecked(format!("{}/dead-letters", self.as_str()))
127 }
128
129 pub fn mailbox_graph(&self) -> NamedNode {
131 NamedNode::new_unchecked(format!("{}/mailbox", self.as_str()))
132 }
133
134 pub fn mailbox_row_iri(&self, seq: u64, envelope: &str) -> NamedNode {
140 self.derived("mailbox", &format!("{seq}-{}", short_digest(envelope)))
141 }
142
143 pub fn outbox_graph(&self) -> NamedNode {
145 NamedNode::new_unchecked(format!("{}/outbox", self.as_str()))
146 }
147
148 pub fn correlation_id(&self, position: u64, k: usize) -> String {
152 format!("{}/request/{position}.{k}", self.as_str())
153 }
154}
155
156pub fn input_iri(entry: &NamedNode) -> NamedNode {
158 NamedNode::new_unchecked(format!("{}/input", entry.as_str()))
159}
160
161pub fn send_iri(entry: &NamedNode, k: usize) -> NamedNode {
163 NamedNode::new_unchecked(format!("{}/send/{k}", entry.as_str()))
164}
165
166pub fn timer_iri(entry: &NamedNode, send_id: &str) -> NamedNode {
169 let mut s = format!("{}/timer/", entry.as_str());
170 percent_encode_into(send_id, &mut s);
171 NamedNode::new_unchecked(s)
172}
173
174pub fn percent_encode_into(text: &str, out: &mut String) {
176 for b in text.bytes() {
177 if b.is_ascii_alphanumeric() || matches!(b, b'-' | b'.' | b'_' | b'~') {
178 out.push(char::from(b));
179 } else {
180 out.push('%');
181 out.push(char::from(HEX[usize::from(b >> 4)]));
182 out.push(char::from(HEX[usize::from(b & 0xF)]));
183 }
184 }
185}
186
187pub fn percent_decode(text: &str) -> Option<String> {
189 let bytes = text.as_bytes();
190 let mut out = Vec::with_capacity(bytes.len());
191 let mut i = 0;
192 while i < bytes.len() {
193 if bytes[i] == b'%' {
194 let hex = text.get(i + 1..i + 3)?;
195 out.push(u8::from_str_radix(hex, 16).ok()?);
196 i += 3;
197 } else {
198 out.push(bytes[i]);
199 i += 1;
200 }
201 }
202 String::from_utf8(out).ok()
203}
204
205const HEX: &[u8; 16] = b"0123456789ABCDEF";
206
207pub fn short_digest(text: &str) -> String {
209 hex(&Sha256::digest(text.as_bytes())[..16])
210}
211
212pub fn hex(bytes: &[u8]) -> String {
214 const L: &[u8; 16] = b"0123456789abcdef";
215 let mut s = String::with_capacity(bytes.len() * 2);
216 for b in bytes {
217 s.push(char::from(L[usize::from(b >> 4)]));
218 s.push(char::from(L[usize::from(b & 0xF)]));
219 }
220 s
221}
222
223pub fn seed_of(entry: &NamedNode) -> u64 {
226 let d = Sha256::digest(entry.as_str().as_bytes());
227 let mut b = [0u8; 8];
228 b.copy_from_slice(&d[..8]);
229 u64::from_be_bytes(b)
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235
236 #[test]
237 fn percent_round_trip() {
238 let mut s = String::new();
239 percent_encode_into("a b/ü#", &mut s);
240 assert_eq!(s, "a%20b%2F%C3%BC%23");
241 assert_eq!(percent_decode(&s).unwrap(), "a b/ü#");
242 }
243
244 #[test]
245 fn derived_iris_are_stable_and_distinct() {
246 let a = Address::parse("urn:vertep:test:a").unwrap();
247 assert_eq!(a.entry_iri(3).as_str(), "urn:vertep:test:a/entry/3");
248 assert_ne!(a.entry_iri(3), a.snapshot_iri(3));
249 let e = a.entry_iri(3);
250 assert_eq!(
251 timer_iri(&e, "r 1").as_str(),
252 "urn:vertep:test:a/entry/3/timer/r%201"
253 );
254 assert_eq!(seed_of(&e), seed_of(&a.entry_iri(3)));
255 assert_ne!(seed_of(&e), seed_of(&a.entry_iri(4)));
256 assert_eq!(a.scheme(), "urn");
257 }
258}