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#[derive(Clone)]
18pub struct RemoteDir {
19 site: Arc<dyn EvalSite>,
20 path: Vec<Symbol>,
21 codec: Symbol,
22}
23
24impl RemoteDir {
25 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 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
276pub 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}