harn_vm/
host_attachments.rs1use std::sync::{Arc, OnceLock, RwLock};
9
10#[derive(Clone, Debug, PartialEq, Eq)]
11pub enum MaterializedAttachment {
12 Text(String),
13 ImageUrl(String),
14 ImageBase64 { media_type: String, data: String },
15}
16
17pub trait HostAttachmentResolver: Send + Sync {
18 fn resolve(
19 &self,
20 artifact_pointer: &str,
21 media_type: &str,
22 ) -> Result<MaterializedAttachment, String>;
23}
24
25struct Entry {
26 id: u64,
27 #[cfg(test)]
28 owner: std::thread::ThreadId,
29 resolver: Arc<dyn HostAttachmentResolver>,
30}
31
32fn registry() -> &'static RwLock<Vec<Entry>> {
33 static REGISTRY: OnceLock<RwLock<Vec<Entry>>> = OnceLock::new();
34 REGISTRY.get_or_init(|| RwLock::new(Vec::new()))
35}
36
37#[must_use = "dropping the registration unregisters the attachment resolver"]
38pub struct HostAttachmentResolverRegistration {
39 id: u64,
40}
41
42impl Drop for HostAttachmentResolverRegistration {
43 fn drop(&mut self) {
44 if let Ok(mut entries) = registry().write() {
45 entries.retain(|entry| entry.id != self.id);
46 }
47 }
48}
49
50pub fn register_host_attachment_resolver(
51 resolver: Arc<dyn HostAttachmentResolver>,
52) -> HostAttachmentResolverRegistration {
53 use std::sync::atomic::{AtomicU64, Ordering};
54 static NEXT_ID: AtomicU64 = AtomicU64::new(1);
55 let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
56 registry()
57 .write()
58 .expect("host attachment resolver registry poisoned")
59 .push(Entry {
60 id,
61 #[cfg(test)]
62 owner: std::thread::current().id(),
63 resolver,
64 });
65 HostAttachmentResolverRegistration { id }
66}
67
68pub(crate) fn materialize(
69 artifact_pointer: &str,
70 media_type: &str,
71) -> Result<MaterializedAttachment, String> {
72 let resolver = registry()
73 .read()
74 .map_err(|_| "host attachment resolver registry poisoned".to_string())?
75 .iter()
76 .rev()
77 .find(|entry| {
78 #[cfg(test)]
79 {
80 entry.owner == std::thread::current().id()
81 }
82 #[cfg(not(test))]
83 {
84 let _ = entry;
85 true
86 }
87 })
88 .map(|entry| Arc::clone(&entry.resolver));
89 match resolver {
90 Some(resolver) => resolver.resolve(artifact_pointer, media_type),
91 None => Err("no host attachment resolver registered".to_string()),
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 struct TextResolver;
100
101 impl HostAttachmentResolver for TextResolver {
102 fn resolve(
103 &self,
104 pointer: &str,
105 _media_type: &str,
106 ) -> Result<MaterializedAttachment, String> {
107 Ok(MaterializedAttachment::Text(format!("resolved:{pointer}")))
108 }
109 }
110
111 #[test]
112 fn registration_is_scoped() {
113 assert!(materialize("artifact:one", "text/plain").is_err());
114 {
115 let _registration = register_host_attachment_resolver(Arc::new(TextResolver));
116 assert_eq!(
117 materialize("artifact:one", "text/plain").unwrap(),
118 MaterializedAttachment::Text("resolved:artifact:one".into())
119 );
120 }
121 assert!(materialize("artifact:one", "text/plain").is_err());
122 }
123}