Skip to main content

sim_lib_server/
address.rs

1use std::path::PathBuf;
2
3use sim_kernel::{Cx, Error, Expr, Result, Symbol, Value};
4
5/// Location of a SIM server endpoint, parsed from an address expression.
6///
7/// Identifies where eval/agent traffic is served -- in-process, over a
8/// transport, through an integration, or as a pipeline of stages.
9#[derive(Clone, Debug, PartialEq, Eq)]
10pub enum ServerAddress {
11    /// The current process; no transport is involved.
12    Local,
13    /// A peer thread within the same process.
14    InProcess {
15        /// Identifier of the target thread.
16        thread: u64,
17    },
18    /// A coroutine within the local scheduler.
19    Coroutine {
20        /// Identifier of the target coroutine.
21        id: u64,
22    },
23    /// A TCP endpoint.
24    Tcp {
25        /// Host name or address to connect to.
26        host: String,
27        /// TCP port number.
28        port: u16,
29    },
30    /// A Unix domain socket.
31    Unix {
32        /// Filesystem path of the socket.
33        path: PathBuf,
34    },
35    /// A wasm guest region.
36    Wasm {
37        /// Name of the target wasm region.
38        region: String,
39    },
40    /// An HTTP endpoint.
41    Http {
42        /// URL to reach the endpoint.
43        url: String,
44    },
45    /// A WebSocket endpoint.
46    Ws {
47        /// URL to reach the endpoint.
48        url: String,
49    },
50    /// A server-sent events endpoint.
51    Sse {
52        /// URL to reach the endpoint.
53        url: String,
54    },
55    /// An SMTP mail endpoint.
56    Smtp {
57        /// Mail address to send to.
58        address: String,
59    },
60    /// An IMAP mailbox endpoint.
61    Imap {
62        /// Mail address of the account.
63        address: String,
64        /// Mailbox name to read.
65        mailbox: String,
66    },
67    /// A Telegram bot chat endpoint.
68    Telegram {
69        /// Identifier of the target chat.
70        chat_id: String,
71        /// Bot account used to send.
72        bot: String,
73    },
74    /// A Matrix room endpoint.
75    Matrix {
76        /// Identifier of the target room.
77        room_id: String,
78    },
79    /// Standard input as a source.
80    Stdin,
81    /// A file followed for appended lines.
82    FileTail {
83        /// Filesystem path of the file to tail.
84        path: PathBuf,
85    },
86    /// A cron schedule that triggers serving.
87    Cron {
88        /// Cron schedule specification.
89        spec: String,
90    },
91    /// An inbound webhook route.
92    Webhook {
93        /// Route the webhook is mounted at.
94        route: String,
95    },
96    /// A named agent endpoint.
97    Agent {
98        /// Identifier or name of the target agent.
99        agent: String,
100    },
101    /// A sequence of addresses chained as stages.
102    Pipeline {
103        /// Ordered stages making up the pipeline.
104        steps: Vec<ServerAddress>,
105    },
106    /// A wildcard address matching any endpoint.
107    Any,
108}
109
110impl ServerAddress {
111    /// Returns `true` when the address denotes a non-local endpoint.
112    ///
113    /// `Local` and `Any` are local; a `Pipeline` is remote-like if any of its
114    /// stages is.
115    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    /// Parses a [`ServerAddress`] from an address expression.
124    ///
125    /// A bare symbol selects `local`, `stdin`, or `any`; a list or vector is
126    /// read as a kind symbol followed by key/value option pairs.
127    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    /// Returns the symbol naming this address kind (e.g. `tcp`, `pipeline`).
147    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    /// Builds a runtime table value describing the address.
173    ///
174    /// The table carries a `kind` entry plus one entry per field of the
175    /// variant; a `Pipeline` nests its stages as a list of such tables.
176    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    /// Returns `true` when a transport is implemented for this address kind.
258    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    /// Succeeds when a transport exists for this address, else returns an error.
277    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}