Skip to main content

sim_table_remote/
remote_dir.rs

1use std::{sync::Arc, time::Duration};
2
3use sim_kernel::{
4    Consistency, Cx, Error, EvalMode, EvalRequest, Expr, Object, ObjectEncode, ObjectEncoding,
5    Result, Symbol, Table, Value, capability::eval_remote_capability, id::CORE_TABLE_CLASS_ID,
6    object::ClassRef, table::Dir,
7};
8use sim_lib_server::{EvalSite, eval_reply_from_frame, server_frame_from_request};
9use sim_table_core::{TableOp, encode_table_op};
10
11use crate::{citizen::remote_dir_class_symbol, table_remote_capability};
12
13/// SIM table directory whose rows live on a remote [`EvalSite`].
14#[derive(Clone)]
15pub struct RemoteDir {
16    site: Arc<dyn EvalSite>,
17    path: Vec<Symbol>,
18    codec: Symbol,
19}
20
21impl RemoteDir {
22    /// Creates a directory rooted at `site` using `codec`.
23    ///
24    /// Returns an error when `codec` is not among the codecs the site supports.
25    pub fn new(site: Arc<dyn EvalSite>, codec: Symbol) -> Result<Self> {
26        if !site.codecs().iter().any(|candidate| candidate == &codec) {
27            return Err(Error::Eval(format!(
28                "table/remote: codec {codec} is not supported by site {}",
29                site.site_kind()
30            )));
31        }
32        Ok(Self {
33            site,
34            path: Vec::new(),
35            codec,
36        })
37    }
38
39    fn with_path(&self, path: Vec<Symbol>) -> Self {
40        Self {
41            site: self.site.clone(),
42            path,
43            codec: self.codec.clone(),
44        }
45    }
46
47    fn path_expr(&self) -> Expr {
48        Expr::List(self.path.iter().cloned().map(Expr::Symbol).collect())
49    }
50
51    fn descriptor_path(&self) -> Vec<String> {
52        self.path
53            .iter()
54            .map(|segment| segment.name.to_string())
55            .collect()
56    }
57
58    fn remote_request(&self, op: &TableOp) -> EvalRequest {
59        // The shared codec produces the `table/<wire>` operator and the op's own
60        // args; the path is transport context prepended ahead of them, leaving
61        // the on-wire Call byte-identical to the hand-built form.
62        let Expr::Call { operator, args } = encode_table_op(op) else {
63            unreachable!("encode_table_op always yields a Call");
64        };
65        let mut call_args = Vec::with_capacity(args.len() + 1);
66        call_args.push(self.path_expr());
67        call_args.extend(args);
68        EvalRequest {
69            expr: Expr::Call {
70                operator,
71                args: call_args,
72            },
73            mode: EvalMode::Eval,
74            result_shape: None,
75            answer_limit: None,
76            stream_buffer: None,
77            stream: false,
78            required_capabilities: Vec::new(),
79            deadline: Some(Duration::from_secs(5)),
80            consistency: Consistency::RemoteOnly,
81            trace: false,
82        }
83    }
84
85    fn call(&self, cx: &mut Cx, op: &TableOp) -> Result<Value> {
86        cx.require(&eval_remote_capability())?;
87        let frame = server_frame_from_request(cx, &self.codec, self.remote_request(op))?;
88        let reply = self.site.answer(cx, frame)?;
89        Ok(eval_reply_from_frame(cx, &reply)?.value)
90    }
91}
92
93impl Object for RemoteDir {
94    fn display(&self, _cx: &mut Cx) -> Result<String> {
95        if self.path.is_empty() {
96            Ok(format!("table/remote[{}:/]", self.site.site_kind()))
97        } else {
98            let suffix = self
99                .path
100                .iter()
101                .map(|segment| segment.to_string())
102                .collect::<Vec<_>>()
103                .join("/");
104            Ok(format!("table/remote[{}:/{suffix}]", self.site.site_kind()))
105        }
106    }
107
108    fn as_any(&self) -> &dyn std::any::Any {
109        self
110    }
111}
112
113impl sim_kernel::ObjectCompat for RemoteDir {
114    fn class(&self, cx: &mut Cx) -> Result<ClassRef> {
115        let symbol = remote_dir_class_symbol();
116        if let Some(value) = cx.registry().class_by_symbol(&symbol) {
117            return Ok(value.clone());
118        }
119        let symbol = Symbol::qualified("core", "Table");
120        if let Some(value) = cx.registry().class_by_symbol(&symbol) {
121            return Ok(value.clone());
122        }
123        cx.factory().class_stub(CORE_TABLE_CLASS_ID, symbol)
124    }
125    fn as_expr(&self, cx: &mut Cx) -> Result<Expr> {
126        self.as_table_expr(cx)
127    }
128    fn truth(&self, cx: &mut Cx) -> Result<bool> {
129        Ok(!self.is_empty(cx)?)
130    }
131    fn as_table_impl(&self) -> Option<&dyn Table> {
132        Some(self)
133    }
134    fn as_dir(&self) -> Option<&dyn Dir> {
135        Some(self)
136    }
137    fn as_object_encoder(&self) -> Option<&dyn ObjectEncode> {
138        Some(self)
139    }
140}
141
142impl ObjectEncode for RemoteDir {
143    fn object_encoding(&self, _cx: &mut Cx) -> Result<ObjectEncoding> {
144        Ok(ObjectEncoding::Constructor {
145            class: remote_dir_class_symbol(),
146            args: vec![
147                Expr::Symbol(Symbol::new("v0")),
148                Expr::String(self.site.site_kind().to_owned()),
149                Expr::Symbol(self.codec.clone()),
150                sim_table_core::citizen_fields::path_segments::encode(&self.descriptor_path()),
151            ],
152        })
153    }
154}
155
156impl sim_citizen::Citizen for RemoteDir {
157    fn citizen_symbol() -> Symbol {
158        remote_dir_class_symbol()
159    }
160
161    fn citizen_version() -> u32 {
162        0
163    }
164
165    fn citizen_arity() -> usize {
166        3
167    }
168
169    fn citizen_fields() -> &'static [&'static str] {
170        &["site_kind", "codec", "path"]
171    }
172}
173
174impl Table for RemoteDir {
175    fn backend_symbol(&self) -> Symbol {
176        Symbol::qualified("table", "remote")
177    }
178
179    fn get(&self, cx: &mut Cx, key: Symbol) -> Result<Value> {
180        self.call(cx, &TableOp::Get(key))
181    }
182
183    fn set(&self, cx: &mut Cx, key: Symbol, value: Value) -> Result<()> {
184        let expr = value.object().as_expr(cx)?;
185        self.call(cx, &TableOp::Set(key, expr)).map(|_| ())
186    }
187
188    fn has(&self, cx: &mut Cx, key: Symbol) -> Result<bool> {
189        self.call(cx, &TableOp::Has(key))?.object().truth(cx)
190    }
191
192    fn del(&self, cx: &mut Cx, key: Symbol) -> Result<Value> {
193        self.call(cx, &TableOp::Delete(key))
194    }
195
196    fn keys(&self, cx: &mut Cx) -> Result<Vec<Symbol>> {
197        let reply = self.call(cx, &TableOp::Keys)?;
198        let list = reply.object().as_list().ok_or(Error::TypeMismatch {
199            expected: "list",
200            found: "non-list",
201        })?;
202        list.to_vec(cx, None)?
203            .into_iter()
204            .map(|value| match value.object().as_expr(cx)? {
205                Expr::Symbol(symbol) => Ok(symbol),
206                _ => Err(Error::TypeMismatch {
207                    expected: "symbol",
208                    found: "non-symbol",
209                }),
210            })
211            .collect()
212    }
213
214    fn entries(&self, cx: &mut Cx) -> Result<Vec<(Symbol, Value)>> {
215        self.call(cx, &TableOp::Entries)?
216            .object()
217            .as_table_impl()
218            .ok_or(Error::TypeMismatch {
219                expected: "table",
220                found: "non-table",
221            })?
222            .entries(cx)
223    }
224
225    fn len(&self, cx: &mut Cx) -> Result<usize> {
226        match self.call(cx, &TableOp::Len)?.object().as_expr(cx)? {
227            Expr::Number(number) => number
228                .canonical
229                .parse::<usize>()
230                .map_err(|err| Error::Eval(format!("table/remote: invalid len reply: {err}"))),
231            Expr::String(text) => text
232                .parse::<usize>()
233                .map_err(|err| Error::Eval(format!("table/remote: invalid len reply: {err}"))),
234            _ => Err(Error::TypeMismatch {
235                expected: "number",
236                found: "non-number",
237            }),
238        }
239    }
240
241    fn clear(&self, cx: &mut Cx) -> Result<()> {
242        self.call(cx, &TableOp::Clear).map(|_| ())
243    }
244}
245
246impl Dir for RemoteDir {
247    fn mkdir(&self, cx: &mut Cx, name: Symbol) -> Result<Value> {
248        self.call(cx, &TableOp::Mkdir(name.clone()))?;
249        let mut path = self.path.clone();
250        path.push(name);
251        cx.factory().opaque(Arc::new(self.with_path(path)))
252    }
253
254    fn opendir(&self, cx: &mut Cx, name: Symbol) -> Result<Option<Value>> {
255        let reply = self.call(cx, &TableOp::Opendir(name.clone()))?;
256        if matches!(reply.object().as_expr(cx)?, Expr::Nil) {
257            return Ok(None);
258        }
259        let mut path = self.path.clone();
260        path.push(name);
261        Ok(Some(cx.factory().opaque(Arc::new(self.with_path(path)))?))
262    }
263
264    fn rmdir(&self, cx: &mut Cx, name: Symbol) -> Result<Value> {
265        self.call(cx, &TableOp::Rmdir(name))
266    }
267
268    fn is_dir(&self, cx: &mut Cx, name: Symbol) -> Result<bool> {
269        self.call(cx, &TableOp::IsDir(name))?.object().truth(cx)
270    }
271}
272
273/// Builds a [`RemoteDir`] over `site`/`codec` as an opaque runtime [`Value`].
274///
275/// Requires the remote-table capability gate.
276pub fn remote_dir_value(cx: &mut Cx, site: Arc<dyn EvalSite>, codec: Symbol) -> Result<Value> {
277    cx.require(&table_remote_capability())?;
278    cx.factory().opaque(Arc::new(RemoteDir::new(site, codec)?))
279}