1use std::path::PathBuf;
2
3use sim_kernel::{Cx, Error, Expr, Result, Symbol, Value};
4
5#[derive(Clone, Debug, PartialEq, Eq)]
10pub enum ServerAddress {
11 Local,
13 InProcess {
15 thread: u64,
17 },
18 Coroutine {
20 id: u64,
22 },
23 Tcp {
25 host: String,
27 port: u16,
29 },
30 Unix {
32 path: PathBuf,
34 },
35 Wasm {
37 region: String,
39 },
40 Http {
42 url: String,
44 },
45 Ws {
47 url: String,
49 },
50 Sse {
52 url: String,
54 },
55 Smtp {
57 address: String,
59 },
60 Imap {
62 address: String,
64 mailbox: String,
66 },
67 Telegram {
69 chat_id: String,
71 bot: String,
73 },
74 Matrix {
76 room_id: String,
78 },
79 Stdin,
81 FileTail {
83 path: PathBuf,
85 },
86 Cron {
88 spec: String,
90 },
91 Webhook {
93 route: String,
95 },
96 Agent {
98 agent: String,
100 },
101 Pipeline {
103 steps: Vec<ServerAddress>,
105 },
106 Any,
108}
109
110impl ServerAddress {
111 pub fn is_remote_like(&self) -> bool {
116 match self {
117 Self::Local | Self::Any => false,
118 Self::Pipeline { steps } => steps.iter().any(Self::is_remote_like),
119 _ => true,
120 }
121 }
122
123 pub fn from_expr(expr: &Expr) -> Result<Self> {
128 match expr {
129 Expr::Symbol(symbol) => match symbol.name.as_ref() {
130 "local" => Ok(Self::Local),
131 "stdin" => Ok(Self::Stdin),
132 "any" => Ok(Self::Any),
133 _ => Err(Error::Eval(format!(
134 "unsupported server address symbol {}",
135 symbol
136 ))),
137 },
138 Expr::List(items) | Expr::Vector(items) => Self::from_items(items),
139 _ => Err(Error::TypeMismatch {
140 expected: "server address expression",
141 found: "non-address",
142 }),
143 }
144 }
145
146 pub fn kind_symbol(&self) -> Symbol {
148 Symbol::new(match self {
149 Self::Local => "local",
150 Self::InProcess { .. } => "in-process",
151 Self::Coroutine { .. } => "coroutine",
152 Self::Tcp { .. } => "tcp",
153 Self::Unix { .. } => "unix",
154 Self::Wasm { .. } => "wasm",
155 Self::Http { .. } => "http",
156 Self::Ws { .. } => "ws",
157 Self::Sse { .. } => "sse",
158 Self::Smtp { .. } => "smtp",
159 Self::Imap { .. } => "imap",
160 Self::Telegram { .. } => "telegram",
161 Self::Matrix { .. } => "matrix",
162 Self::Stdin => "stdin",
163 Self::FileTail { .. } => "file-tail",
164 Self::Cron { .. } => "cron",
165 Self::Webhook { .. } => "webhook",
166 Self::Agent { .. } => "agent",
167 Self::Pipeline { .. } => "pipeline",
168 Self::Any => "any",
169 })
170 }
171
172 pub fn as_value(&self, cx: &mut Cx) -> Result<Value> {
177 let mut entries = vec![(
178 Symbol::new("kind"),
179 cx.factory().symbol(self.kind_symbol())?,
180 )];
181 match self {
182 Self::Local | Self::Stdin | Self::Any => {}
183 Self::InProcess { thread } => {
184 entries.push((
185 Symbol::new("thread"),
186 cx.factory().string(thread.to_string())?,
187 ));
188 }
189 Self::Coroutine { id } => {
190 entries.push((Symbol::new("id"), cx.factory().string(id.to_string())?));
191 }
192 Self::Tcp { host, port } => {
193 entries.push((Symbol::new("host"), cx.factory().string(host.clone())?));
194 entries.push((Symbol::new("port"), cx.factory().string(port.to_string())?));
195 }
196 Self::Unix { path } | Self::FileTail { path } => {
197 entries.push((
198 Symbol::new("path"),
199 cx.factory().string(path.display().to_string())?,
200 ));
201 }
202 Self::Wasm { region } => {
203 entries.push((Symbol::new("region"), cx.factory().string(region.clone())?));
204 }
205 Self::Http { url } | Self::Ws { url } | Self::Sse { url } => {
206 entries.push((Symbol::new("url"), cx.factory().string(url.clone())?));
207 }
208 Self::Smtp { address } => {
209 entries.push((
210 Symbol::new("address"),
211 cx.factory().string(address.clone())?,
212 ));
213 }
214 Self::Imap { address, mailbox } => {
215 entries.push((
216 Symbol::new("address"),
217 cx.factory().string(address.clone())?,
218 ));
219 entries.push((
220 Symbol::new("mailbox"),
221 cx.factory().string(mailbox.clone())?,
222 ));
223 }
224 Self::Telegram { chat_id, bot } => {
225 entries.push((
226 Symbol::new("chat-id"),
227 cx.factory().string(chat_id.clone())?,
228 ));
229 entries.push((Symbol::new("bot"), cx.factory().string(bot.clone())?));
230 }
231 Self::Matrix { room_id } => {
232 entries.push((
233 Symbol::new("room-id"),
234 cx.factory().string(room_id.clone())?,
235 ));
236 }
237 Self::Cron { spec } => {
238 entries.push((Symbol::new("spec"), cx.factory().string(spec.clone())?));
239 }
240 Self::Webhook { route } => {
241 entries.push((Symbol::new("route"), cx.factory().string(route.clone())?));
242 }
243 Self::Agent { agent } => {
244 entries.push((Symbol::new("agent"), cx.factory().string(agent.clone())?));
245 }
246 Self::Pipeline { steps } => {
247 let values = steps
248 .iter()
249 .map(|step| step.as_value(cx))
250 .collect::<Result<Vec<_>>>()?;
251 entries.push((Symbol::new("steps"), cx.factory().list(values)?));
252 }
253 }
254 cx.factory().table(entries)
255 }
256
257 pub fn transport_available(&self) -> bool {
259 matches!(
260 self,
261 Self::Local
262 | Self::Any
263 | Self::Pipeline { .. }
264 | Self::InProcess { .. }
265 | Self::Coroutine { .. }
266 | Self::Tcp { .. }
267 | Self::Unix { .. }
268 | Self::Wasm { .. }
269 | Self::Http { .. }
270 | Self::Ws { .. }
271 | Self::Sse { .. }
272 | Self::Agent { .. }
273 )
274 }
275
276 pub fn ensure_transport_available(&self) -> Result<()> {
278 if self.transport_available() {
279 Ok(())
280 } else {
281 Err(Error::Eval(format!(
282 "no transport for address kind {}",
283 self.kind_symbol()
284 )))
285 }
286 }
287
288 fn from_items(items: &[Expr]) -> Result<Self> {
289 let Some(Expr::Symbol(kind)) = items.first() else {
290 return Err(Error::TypeMismatch {
291 expected: "address list starting with a symbol",
292 found: "non-symbol",
293 });
294 };
295 match kind.name.as_ref() {
296 "in-process" => {
297 let thread = find_u64(items, "thread")?.unwrap_or(0);
298 Ok(Self::InProcess { thread })
299 }
300 "coroutine" => {
301 let id = find_u64(items, "id")?.unwrap_or(0);
302 Ok(Self::Coroutine { id })
303 }
304 "tcp" => Ok(Self::Tcp {
305 host: find_string(items, "host")?.unwrap_or_else(|| "127.0.0.1".to_owned()),
306 port: find_u16(items, "port")?
307 .ok_or_else(|| Error::Eval("tcp address requires :port".to_owned()))?,
308 }),
309 "unix" => Ok(Self::Unix {
310 path: PathBuf::from(
311 find_string(items, "path")?
312 .ok_or_else(|| Error::Eval("unix address requires :path".to_owned()))?,
313 ),
314 }),
315 "wasm" => Ok(Self::Wasm {
316 region: find_string(items, "region")?
317 .ok_or_else(|| Error::Eval("wasm address requires :region".to_owned()))?,
318 }),
319 "http" => Ok(Self::Http {
320 url: find_string(items, "url")?
321 .ok_or_else(|| Error::Eval("http address requires :url".to_owned()))?,
322 }),
323 "ws" => Ok(Self::Ws {
324 url: find_string(items, "url")?
325 .ok_or_else(|| Error::Eval("ws address requires :url".to_owned()))?,
326 }),
327 "sse" => Ok(Self::Sse {
328 url: find_string(items, "url")?
329 .ok_or_else(|| Error::Eval("sse address requires :url".to_owned()))?,
330 }),
331 "smtp" => Ok(Self::Smtp {
332 address: find_string(items, "address")?
333 .ok_or_else(|| Error::Eval("smtp address requires :address".to_owned()))?,
334 }),
335 "imap" => Ok(Self::Imap {
336 address: find_string(items, "address")?
337 .ok_or_else(|| Error::Eval("imap address requires :address".to_owned()))?,
338 mailbox: find_string(items, "mailbox")?
339 .ok_or_else(|| Error::Eval("imap address requires :mailbox".to_owned()))?,
340 }),
341 "telegram" => Ok(Self::Telegram {
342 chat_id: find_string(items, "chat-id")?
343 .or_else(|| find_string(items, "chat").ok().flatten())
344 .ok_or_else(|| Error::Eval("telegram address requires :chat-id".to_owned()))?,
345 bot: find_string(items, "bot")?
346 .ok_or_else(|| Error::Eval("telegram address requires :bot".to_owned()))?,
347 }),
348 "matrix" => Ok(Self::Matrix {
349 room_id: find_string(items, "room-id")?
350 .or_else(|| find_string(items, "room").ok().flatten())
351 .ok_or_else(|| Error::Eval("matrix address requires :room-id".to_owned()))?,
352 }),
353 "file-tail" => {
354 Ok(Self::FileTail {
355 path: PathBuf::from(find_string(items, "path")?.ok_or_else(|| {
356 Error::Eval("file-tail address requires :path".to_owned())
357 })?),
358 })
359 }
360 "cron" => Ok(Self::Cron {
361 spec: find_string(items, "spec")?
362 .ok_or_else(|| Error::Eval("cron address requires :spec".to_owned()))?,
363 }),
364 "webhook" => Ok(Self::Webhook {
365 route: find_string(items, "route")?
366 .ok_or_else(|| Error::Eval("webhook address requires :route".to_owned()))?,
367 }),
368 "agent" => {
369 let agent = items
370 .get(1)
371 .ok_or_else(|| Error::Eval("agent address requires a target".to_owned()))?;
372 Ok(Self::Agent {
373 agent: stringy(agent)?,
374 })
375 }
376 "pipeline" => Ok(Self::Pipeline {
377 steps: items[1..]
378 .iter()
379 .map(Self::from_expr)
380 .collect::<Result<Vec<_>>>()?,
381 }),
382 other => Err(Error::Eval(format!(
383 "unsupported server address kind {other}"
384 ))),
385 }
386 }
387}
388
389fn find_expr<'a>(items: &'a [Expr], key: &str) -> Result<Option<&'a Expr>> {
390 if items.len() <= 1 {
391 return Ok(None);
392 }
393 if !(items.len() - 1).is_multiple_of(2) {
394 return Err(Error::Eval(
395 "address options must be key/value pairs".to_owned(),
396 ));
397 }
398 for pair in items[1..].chunks(2) {
399 let Expr::Symbol(symbol) = &pair[0] else {
400 return Err(Error::TypeMismatch {
401 expected: "keyword symbol",
402 found: "non-symbol",
403 });
404 };
405 let name = symbol
406 .name
407 .strip_prefix(':')
408 .unwrap_or(symbol.name.as_ref());
409 if name == key {
410 return Ok(Some(&pair[1]));
411 }
412 }
413 Ok(None)
414}
415
416fn find_string(items: &[Expr], key: &str) -> Result<Option<String>> {
417 find_expr(items, key)?.map(stringy).transpose()
418}
419
420fn find_u64(items: &[Expr], key: &str) -> Result<Option<u64>> {
421 find_expr(items, key)?.map(integer_u64).transpose()
422}
423
424fn find_u16(items: &[Expr], key: &str) -> Result<Option<u16>> {
425 find_u64(items, key)?
426 .map(|value| {
427 u16::try_from(value)
428 .map_err(|_| Error::Eval(format!("address field :{key} value {value} exceeds u16")))
429 })
430 .transpose()
431}
432
433fn stringy(expr: &Expr) -> Result<String> {
434 match expr {
435 Expr::String(text) => Ok(text.clone()),
436 Expr::Symbol(symbol) => Ok(symbol.to_string()),
437 _ => Err(Error::TypeMismatch {
438 expected: "string or symbol",
439 found: "non-string",
440 }),
441 }
442}
443
444fn integer_u64(expr: &Expr) -> Result<u64> {
445 match expr {
446 Expr::Number(number) => number.canonical.parse::<u64>().map_err(|_| {
447 Error::Eval(format!(
448 "{} is not a valid unsigned integer",
449 number.canonical
450 ))
451 }),
452 Expr::String(text) => text
453 .parse::<u64>()
454 .map_err(|_| Error::Eval(format!("{text} is not a valid unsigned integer"))),
455 _ => Err(Error::TypeMismatch {
456 expected: "integer number or string",
457 found: "non-integer",
458 }),
459 }
460}