Skip to main content

kcode_k1_web_code_runtime/
lib.rs

1use kcode_k1_transaction_id::TxId;
2use kcode_k1_web_coding::{K1WebCoding, WebCodingConfig, WebCodingError, WebCodingRevisions};
3use kcode_k1_web_podman::WebPodmanConfig;
4use kcode_k1_web_projection::K1WebProjection;
5use std::collections::HashMap;
6use std::fmt::{Display, Formatter, Write};
7use std::path::PathBuf;
8use std::sync::{Arc, Mutex};
9
10type UserSlot = Arc<Mutex<Option<K1WebCoding>>>;
11
12#[derive(Clone)]
13pub struct RuntimeRevisions {
14    pub boot: String,
15    pub schema: String,
16    pub route: String,
17    pub harness: String,
18    pub check_policy: String,
19}
20
21#[derive(Clone)]
22pub struct RuntimeConfig {
23    cache_root: PathBuf,
24    projection_root: PathBuf,
25    revisions: RuntimeRevisions,
26    podman: WebPodmanConfig,
27}
28
29impl RuntimeConfig {
30    pub fn new(
31        cache_root: PathBuf,
32        projection_root: PathBuf,
33        revisions: RuntimeRevisions,
34        podman: WebPodmanConfig,
35    ) -> Self {
36        Self {
37            cache_root,
38            projection_root,
39            revisions,
40            podman,
41        }
42    }
43}
44
45#[derive(Debug)]
46pub enum RuntimeError {
47    Coding(WebCodingError),
48    LockPoisoned(String),
49}
50
51impl Display for RuntimeError {
52    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
53        match self {
54            Self::Coding(error) => Display::fmt(error, formatter),
55            Self::LockPoisoned(message) => formatter.write_str(message),
56        }
57    }
58}
59
60impl std::error::Error for RuntimeError {
61    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
62        match self {
63            Self::Coding(error) => Some(error),
64            Self::LockPoisoned(_) => None,
65        }
66    }
67}
68
69impl From<WebCodingError> for RuntimeError {
70    fn from(error: WebCodingError) -> Self {
71        Self::Coding(error)
72    }
73}
74
75struct Inner {
76    config: RuntimeConfig,
77    projection: Arc<K1WebProjection>,
78    slots: Mutex<HashMap<[u8; 12], UserSlot>>,
79}
80
81#[derive(Clone)]
82pub struct K1WebCodeRuntime {
83    inner: Arc<Inner>,
84}
85
86impl K1WebCodeRuntime {
87    pub fn new(config: RuntimeConfig, projection: Arc<K1WebProjection>) -> Self {
88        Self {
89            inner: Arc::new(Inner {
90                config,
91                projection,
92                slots: Mutex::new(HashMap::new()),
93            }),
94        }
95    }
96
97    pub fn with_user<T>(
98        &self,
99        user: [u8; 12],
100        operation: impl FnOnce(&mut K1WebCoding) -> Result<T, WebCodingError>,
101    ) -> Result<T, RuntimeError> {
102        let slot = {
103            let mut slots = self.inner.slots.lock().map_err(|_| {
104                RuntimeError::LockPoisoned("runtime slot map mutex poisoned".to_owned())
105            })?;
106            slots
107                .entry(user)
108                .or_insert_with(|| Arc::new(Mutex::new(None)))
109                .clone()
110        };
111
112        let mut coding = slot
113            .lock()
114            .map_err(|_| RuntimeError::LockPoisoned("user coding mutex poisoned".to_owned()))?;
115        if coding.is_none() {
116            *coding = Some(self.open_user(user)?);
117        }
118        operation(coding.as_mut().expect("coding was initialized")).map_err(RuntimeError::Coding)
119    }
120
121    fn open_user(&self, user: [u8; 12]) -> Result<K1WebCoding, WebCodingError> {
122        let revisions = &self.inner.config.revisions;
123        let config = WebCodingConfig::new(
124            WebCodingRevisions {
125                boot: revisions.boot.clone(),
126                schema: revisions.schema.clone(),
127                route: revisions.route.clone(),
128                harness: revisions.harness.clone(),
129                check_policy: revisions.check_policy.clone(),
130            },
131            self.inner.config.projection_root.clone(),
132            self.inner.config.podman.clone(),
133        )?;
134        K1WebCoding::open(
135            self.inner.config.cache_root.join(cache_directory(user)),
136            TxId::from_bytes(user),
137            config,
138            self.inner.projection.clone(),
139        )
140    }
141}
142
143fn cache_directory(user: [u8; 12]) -> String {
144    let mut directory = String::with_capacity(24);
145    for byte in user {
146        write!(&mut directory, "{byte:02x}").expect("writing to String cannot fail");
147    }
148    directory
149}
150
151#[cfg(test)]
152mod tests {
153    use super::cache_directory;
154
155    #[test]
156    fn cache_directory_is_exact_lowercase_user_hex() {
157        assert_eq!(
158            cache_directory([
159                0x00, 0x01, 0x0a, 0x10, 0x2f, 0x7f, 0x80, 0xab, 0xcd, 0xef, 0xf0, 0xff,
160            ]),
161            "00010a102f7f80abcdeff0ff"
162        );
163    }
164}