use core::fmt;
use oxrdf::NamedNode;
use sha2::{Digest, Sha256};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AddressError {
pub text: String,
pub reason: String,
}
impl fmt::Display for AddressError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "not an actor address: {:?} ({})", self.text, self.reason)
}
}
impl std::error::Error for AddressError {}
#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Address(NamedNode);
impl fmt::Debug for Address {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "<{}>", self.0.as_str())
}
}
impl fmt::Display for Address {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.0.as_str())
}
}
impl Address {
pub fn parse(text: &str) -> Result<Self, AddressError> {
NamedNode::new(text).map(Address).map_err(|e| AddressError {
text: text.to_owned(),
reason: e.to_string(),
})
}
pub fn virtual_actor(chart_type: &str, key: &str) -> Result<Self, AddressError> {
Address::parse(chart_type)?;
let mut s = String::with_capacity(chart_type.len() + key.len() + 1);
s.push_str(chart_type);
s.push('/');
percent_encode_into(key, &mut s);
Address::parse(&s)
}
pub fn invoked(parent: &Address, invokeid: &str) -> Address {
Address(parent.derived("invoke", invokeid))
}
pub fn as_str(&self) -> &str {
self.0.as_str()
}
pub fn node(&self) -> &NamedNode {
&self.0
}
pub fn scheme(&self) -> &str {
self.as_str().split(':').next().unwrap_or_default()
}
fn derived(&self, kind: &str, part: &str) -> NamedNode {
let mut s = String::with_capacity(self.as_str().len() + kind.len() + part.len() + 2);
s.push_str(self.as_str());
s.push('/');
s.push_str(kind);
s.push('/');
percent_encode_into(part, &mut s);
NamedNode::new_unchecked(s)
}
pub fn entry_iri(&self, position: u64) -> NamedNode {
self.derived("entry", &position.to_string())
}
pub fn snapshot_iri(&self, position: u64) -> NamedNode {
self.derived("snapshot", &position.to_string())
}
pub fn dead_letter_iri(&self, message_id: &str) -> NamedNode {
self.derived("dead-letter", &short_digest(message_id))
}
pub fn dead_letter_graph(&self) -> NamedNode {
NamedNode::new_unchecked(format!("{}/dead-letters", self.as_str()))
}
pub fn mailbox_graph(&self) -> NamedNode {
NamedNode::new_unchecked(format!("{}/mailbox", self.as_str()))
}
pub fn mailbox_row_iri(&self, seq: u64, envelope: &str) -> NamedNode {
self.derived("mailbox", &format!("{seq}-{}", short_digest(envelope)))
}
pub fn outbox_graph(&self) -> NamedNode {
NamedNode::new_unchecked(format!("{}/outbox", self.as_str()))
}
pub fn correlation_id(&self, position: u64, k: usize) -> String {
format!("{}/request/{position}.{k}", self.as_str())
}
}
pub fn input_iri(entry: &NamedNode) -> NamedNode {
NamedNode::new_unchecked(format!("{}/input", entry.as_str()))
}
pub fn send_iri(entry: &NamedNode, k: usize) -> NamedNode {
NamedNode::new_unchecked(format!("{}/send/{k}", entry.as_str()))
}
pub fn timer_iri(entry: &NamedNode, send_id: &str) -> NamedNode {
let mut s = format!("{}/timer/", entry.as_str());
percent_encode_into(send_id, &mut s);
NamedNode::new_unchecked(s)
}
pub fn percent_encode_into(text: &str, out: &mut String) {
for b in text.bytes() {
if b.is_ascii_alphanumeric() || matches!(b, b'-' | b'.' | b'_' | b'~') {
out.push(char::from(b));
} else {
out.push('%');
out.push(char::from(HEX[usize::from(b >> 4)]));
out.push(char::from(HEX[usize::from(b & 0xF)]));
}
}
}
pub fn percent_decode(text: &str) -> Option<String> {
let bytes = text.as_bytes();
let mut out = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'%' {
let hex = text.get(i + 1..i + 3)?;
out.push(u8::from_str_radix(hex, 16).ok()?);
i += 3;
} else {
out.push(bytes[i]);
i += 1;
}
}
String::from_utf8(out).ok()
}
const HEX: &[u8; 16] = b"0123456789ABCDEF";
pub fn short_digest(text: &str) -> String {
hex(&Sha256::digest(text.as_bytes())[..16])
}
pub fn hex(bytes: &[u8]) -> String {
const L: &[u8; 16] = b"0123456789abcdef";
let mut s = String::with_capacity(bytes.len() * 2);
for b in bytes {
s.push(char::from(L[usize::from(b >> 4)]));
s.push(char::from(L[usize::from(b & 0xF)]));
}
s
}
pub fn seed_of(entry: &NamedNode) -> u64 {
let d = Sha256::digest(entry.as_str().as_bytes());
let mut b = [0u8; 8];
b.copy_from_slice(&d[..8]);
u64::from_be_bytes(b)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn percent_round_trip() {
let mut s = String::new();
percent_encode_into("a b/ü#", &mut s);
assert_eq!(s, "a%20b%2F%C3%BC%23");
assert_eq!(percent_decode(&s).unwrap(), "a b/ü#");
}
#[test]
fn derived_iris_are_stable_and_distinct() {
let a = Address::parse("urn:vertep:test:a").unwrap();
assert_eq!(a.entry_iri(3).as_str(), "urn:vertep:test:a/entry/3");
assert_ne!(a.entry_iri(3), a.snapshot_iri(3));
let e = a.entry_iri(3);
assert_eq!(
timer_iri(&e, "r 1").as_str(),
"urn:vertep:test:a/entry/3/timer/r%201"
);
assert_eq!(seed_of(&e), seed_of(&a.entry_iri(3)));
assert_ne!(seed_of(&e), seed_of(&a.entry_iri(4)));
assert_eq!(a.scheme(), "urn");
}
}