Skip to main content

sim_table_remote/
site.rs

1use std::sync::Arc;
2
3use sim_kernel::{Consistency, Cx, Error, EvalReply, Expr, Result, Symbol, Value};
4use sim_lib_server::{
5    EvalSite, FrameKind, ServerAddress, ServerFrame, eval_request_from_frame,
6    server_frame_from_reply,
7};
8use sim_table_core::{TableOp, TableOpError, decode_table_op};
9
10/// Wraps `inner` so table operations dispatch against the table `root`.
11pub fn wrap_remote_table_site(inner: Arc<dyn EvalSite>, root: Value) -> Arc<dyn EvalSite> {
12    Arc::new(RemoteTableSite { inner, root })
13}
14
15/// [`EvalSite`] adapter that serves table operations from a local `root` table.
16#[derive(Clone)]
17pub struct RemoteTableSite {
18    inner: Arc<dyn EvalSite>,
19    root: Value,
20}
21
22impl RemoteTableSite {
23    fn current_dir(&self, cx: &mut Cx, path: &[Symbol]) -> Result<Value> {
24        let mut current = self.root.clone();
25        for segment in path {
26            let dir = current.object().as_dir().ok_or(Error::TypeMismatch {
27                expected: "directory table",
28                found: "non-directory",
29            })?;
30            current = dir.opendir(cx, segment.clone())?.ok_or_else(|| {
31                Error::Eval(format!("table/remote: missing path segment {segment}"))
32            })?;
33        }
34        Ok(current)
35    }
36
37    fn parse_call(expr: Expr) -> Result<(Symbol, Vec<Expr>)> {
38        match expr {
39            Expr::Call { operator, args } => match *operator {
40                Expr::Symbol(symbol) => Ok((symbol, args)),
41                _ => Err(Error::TypeMismatch {
42                    expected: "symbol operator",
43                    found: "non-symbol",
44                }),
45            },
46            _ => Err(Error::TypeMismatch {
47                expected: "call",
48                found: "non-call",
49            }),
50        }
51    }
52
53    fn parse_path(expr: &Expr) -> Result<Vec<Symbol>> {
54        let Expr::List(items) = expr else {
55            return Err(Error::TypeMismatch {
56                expected: "path list",
57                found: "non-list",
58            });
59        };
60        items
61            .iter()
62            .map(|item| match item {
63                Expr::Symbol(symbol) => Ok(symbol.clone()),
64                _ => Err(Error::TypeMismatch {
65                    expected: "path symbol",
66                    found: "non-symbol",
67                }),
68            })
69            .collect()
70    }
71
72    /// The arity-error message for a known wire op name, preserving the exact
73    /// strings the per-op dispatch used before sharing the `TableOp` codec.
74    fn arity_message(name: &str) -> String {
75        match name {
76            "get" => "table/get expects path and key".to_owned(),
77            "set" => "table/set expects path, key, and value".to_owned(),
78            "has" => "table/has expects path and key".to_owned(),
79            "del" => "table/del expects path and key".to_owned(),
80            "keys" => "table/keys expects only a path".to_owned(),
81            "entries" => "table/entries expects only a path".to_owned(),
82            "len" => "table/len expects only a path".to_owned(),
83            "clear" => "table/clear expects only a path".to_owned(),
84            "mkdir" => "table/mkdir expects path and name".to_owned(),
85            "opendir" => "table/opendir expects path and name".to_owned(),
86            "rmdir" => "table/rmdir expects path and name".to_owned(),
87            "dir?" => "table/dir? expects path and name".to_owned(),
88            other => format!("table/{other}: malformed request"),
89        }
90    }
91
92    fn child_token(path: &[Symbol], name: &Symbol) -> String {
93        let mut segments = path.iter().map(Symbol::to_string).collect::<Vec<_>>();
94        segments.push(name.to_string());
95        format!("/{}", segments.join("/"))
96    }
97
98    fn reply_value(
99        &self,
100        cx: &mut Cx,
101        frame: &ServerFrame,
102        value: Value,
103        consistency: Consistency,
104    ) -> Result<ServerFrame> {
105        let reply = EvalReply {
106            value,
107            diagnostics: cx.take_diagnostics(),
108            trace: None,
109        };
110        server_frame_from_reply(cx, &self.reply_codec(frame), reply, consistency)
111    }
112
113    fn reply_codec(&self, frame: &ServerFrame) -> Symbol {
114        if let Some(hint) = &frame.envelope.reply_codec_hint
115            && self.inner.codecs().iter().any(|codec| codec == hint)
116        {
117            return hint.clone();
118        }
119        frame.codec.clone()
120    }
121
122    fn answer_table_request(&self, cx: &mut Cx, frame: ServerFrame) -> Result<Option<ServerFrame>> {
123        if frame.kind != FrameKind::Request {
124            return Ok(None);
125        }
126        let consistency = frame.envelope.consistency;
127        let request = eval_request_from_frame(cx, &frame)?;
128        let (operator, args) = Self::parse_call(request.expr)?;
129        if operator.namespace.as_deref() != Some("table") {
130            return Ok(None);
131        }
132        let [path_expr, rest @ ..] = args.as_slice() else {
133            return Err(Error::Eval(
134                "table/remote: missing path argument".to_owned(),
135            ));
136        };
137        let path = Self::parse_path(path_expr)?;
138
139        // Parse the op against the one shared `table/<op>` vocabulary. The path
140        // is transport context, so we decode a Call carrying just `rest`. An
141        // unknown table op falls through to the inner site exactly as before; a
142        // malformed-but-known op reproduces the original arity/arg error.
143        let op = match decode_table_op(&Expr::Call {
144            operator: Box::new(Expr::Symbol(operator.clone())),
145            args: rest.to_vec(),
146        }) {
147            Ok(op) => op,
148            Err(TableOpError::UnknownOp(_) | TableOpError::NotATableCall) => return Ok(None),
149            Err(TableOpError::BadArity(name)) => {
150                return Err(Error::Eval(Self::arity_message(&name)));
151            }
152            Err(TableOpError::BadArg(_)) => {
153                return Err(Error::TypeMismatch {
154                    expected: "symbol",
155                    found: "non-symbol",
156                });
157            }
158        };
159
160        let current = self.current_dir(cx, &path)?;
161        let table = current
162            .object()
163            .as_table_impl()
164            .ok_or(Error::TypeMismatch {
165                expected: "table",
166                found: "non-table",
167            })?;
168        let value = match op {
169            TableOp::Get(key) => table.get(cx, key),
170            TableOp::Set(key, value) => {
171                table.set(cx, key, cx.factory().expr(value)?)?;
172                cx.factory().nil()
173            }
174            TableOp::Has(key) => {
175                let present = table.has(cx, key)?;
176                cx.factory().bool(present)
177            }
178            TableOp::Delete(key) => table.del(cx, key),
179            TableOp::Keys => {
180                let keys = table.keys(cx)?;
181                let values = keys
182                    .into_iter()
183                    .map(|symbol| cx.factory().symbol(symbol))
184                    .collect::<Result<Vec<_>>>()?;
185                cx.factory().list(values)
186            }
187            TableOp::Entries => {
188                let entries = table.entries(cx)?;
189                cx.factory().table(entries)
190            }
191            TableOp::Len => {
192                let len = table.len(cx)?;
193                cx.factory()
194                    .number_literal(Symbol::qualified("numbers", "f64"), len.to_string())
195            }
196            TableOp::Clear => {
197                table.clear(cx)?;
198                cx.factory().nil()
199            }
200            TableOp::Mkdir(name) => {
201                let dir = current.object().as_dir().ok_or(Error::TypeMismatch {
202                    expected: "directory table",
203                    found: "non-directory",
204                })?;
205                let _ = dir.mkdir(cx, name.clone())?;
206                cx.factory().string(Self::child_token(&path, &name))
207            }
208            TableOp::Opendir(name) => {
209                let dir = current.object().as_dir().ok_or(Error::TypeMismatch {
210                    expected: "directory table",
211                    found: "non-directory",
212                })?;
213                match dir.opendir(cx, name.clone())? {
214                    Some(_) => cx.factory().string(Self::child_token(&path, &name)),
215                    None => cx.factory().nil(),
216                }
217            }
218            TableOp::Rmdir(name) => {
219                let dir = current.object().as_dir().ok_or(Error::TypeMismatch {
220                    expected: "directory table",
221                    found: "non-directory",
222                })?;
223                dir.rmdir(cx, name)
224            }
225            TableOp::IsDir(name) => {
226                let dir = current.object().as_dir().ok_or(Error::TypeMismatch {
227                    expected: "directory table",
228                    found: "non-directory",
229                })?;
230                let is_dir = dir.is_dir(cx, name)?;
231                cx.factory().bool(is_dir)
232            }
233        }?;
234        Ok(Some(self.reply_value(cx, &frame, value, consistency)?))
235    }
236}
237
238impl EvalSite for RemoteTableSite {
239    fn site_kind(&self) -> &'static str {
240        "remote-table"
241    }
242
243    fn address(&self) -> &ServerAddress {
244        self.inner.address()
245    }
246
247    fn codecs(&self) -> &[Symbol] {
248        self.inner.codecs()
249    }
250
251    fn answer(&self, cx: &mut Cx, frame: ServerFrame) -> Result<ServerFrame> {
252        if let Some(reply) = self.answer_table_request(cx, frame.clone())? {
253            return Ok(reply);
254        }
255        self.inner.answer(cx, frame)
256    }
257
258    fn answer_with_timeout(
259        &self,
260        cx: &mut Cx,
261        frame: ServerFrame,
262        timeout: Option<std::time::Duration>,
263    ) -> Result<ServerFrame> {
264        if let Some(reply) = self.answer_table_request(cx, frame.clone())? {
265            return Ok(reply);
266        }
267        self.inner.answer_with_timeout(cx, frame, timeout)
268    }
269
270    fn close_connection(&self, cx: &mut Cx) -> Result<()> {
271        self.inner.close_connection(cx)
272    }
273
274    fn as_any(&self) -> &dyn std::any::Any {
275        self
276    }
277}