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