Skip to main content

sim_web_shell/
cli.rs

1//! Loadable CLI claims for the web shell surfaces.
2
3use std::sync::Arc;
4
5use sim_kernel::{
6    AbiVersion, Args, CORE_FUNCTION_CLASS_ID, Callable, ClassRef, Cx, Error, Export, Expr, Lib,
7    LibManifest, LibTarget, Linker, LoadCx, Object, ObjectCompat, Result, Symbol, Value, Version,
8};
9
10/// Loadable lib that claims the `atelier` command-line verb.
11pub struct AtelierCliLib;
12
13/// Loadable lib that claims the `browse` command-line verb.
14pub struct BrowseCliLib;
15
16impl Lib for AtelierCliLib {
17    fn manifest(&self) -> LibManifest {
18        cli_manifest("atelier", "cli/main/atelier")
19    }
20
21    fn load(&self, cx: &mut LoadCx, linker: &mut Linker<'_>) -> Result<()> {
22        register_cli_entrypoint(cx, linker, "atelier")
23    }
24}
25
26impl Lib for BrowseCliLib {
27    fn manifest(&self) -> LibManifest {
28        cli_manifest("browse", "cli/main/browse")
29    }
30
31    fn load(&self, cx: &mut LoadCx, linker: &mut Linker<'_>) -> Result<()> {
32        register_cli_entrypoint(cx, linker, "browse")
33    }
34}
35
36fn cli_manifest(id: &str, entrypoint: &str) -> LibManifest {
37    LibManifest {
38        id: Symbol::new(id),
39        version: Version(env!("CARGO_PKG_VERSION").to_owned()),
40        abi: AbiVersion { major: 0, minor: 1 },
41        target: LibTarget::HostRegistered,
42        requires: Vec::new(),
43        capabilities: Vec::new(),
44        exports: vec![Export::Function {
45            symbol: symbol_from_slash(entrypoint),
46            function_id: None,
47        }],
48    }
49}
50
51fn register_cli_entrypoint(
52    cx: &mut LoadCx,
53    linker: &mut Linker<'_>,
54    verb: &'static str,
55) -> Result<()> {
56    linker.function_value(
57        Symbol::qualified("cli", format!("main/{verb}")),
58        cx.factory()
59            .opaque(Arc::new(WebShellCliEntrypoint { verb }))?,
60    )?;
61    Ok(())
62}
63
64#[derive(Clone)]
65struct WebShellCliEntrypoint {
66    verb: &'static str,
67}
68
69impl Object for WebShellCliEntrypoint {
70    fn display(&self, _cx: &mut Cx) -> Result<String> {
71        Ok(format!("#<function cli/main/{}>", self.verb))
72    }
73
74    fn as_any(&self) -> &dyn std::any::Any {
75        self
76    }
77}
78
79impl ObjectCompat for WebShellCliEntrypoint {
80    fn class(&self, cx: &mut Cx) -> Result<ClassRef> {
81        if let Some(value) = cx
82            .registry()
83            .class_by_symbol(&Symbol::qualified("core", "Function"))
84        {
85            return Ok(value.clone());
86        }
87        cx.factory().class_stub(
88            CORE_FUNCTION_CLASS_ID,
89            Symbol::qualified("core", "Function"),
90        )
91    }
92
93    fn as_callable(&self) -> Option<&dyn Callable> {
94        Some(self)
95    }
96}
97
98impl Callable for WebShellCliEntrypoint {
99    fn call(&self, cx: &mut Cx, args: Args) -> Result<Value> {
100        verify_cli_envelope(cx, &args, self.verb)?;
101        cx.factory().bool(true)
102    }
103}
104
105fn verify_cli_envelope(cx: &mut Cx, args: &Args, verb: &str) -> Result<()> {
106    let envelope = args
107        .values()
108        .first()
109        .ok_or_else(|| Error::Eval(format!("cli/main/{verb} expects a CLI envelope")))?;
110    let envelope_verb = envelope_string_field(cx, envelope, "verb")?;
111    if envelope_verb != verb {
112        return Err(Error::Eval(format!(
113            "cli/main/{verb} received verb {envelope_verb}"
114        )));
115    }
116    let payload_args = envelope_args(cx, envelope)?;
117    if payload_args.first().map(String::as_str) != Some(verb) {
118        return Err(Error::Eval(format!(
119            "cli/main/{verb} expects the first payload argument to be {verb}"
120        )));
121    }
122    Ok(())
123}
124
125fn envelope_string_field(cx: &mut Cx, envelope: &Value, field: &str) -> Result<String> {
126    let Some(table) = envelope.object().as_table_impl() else {
127        return Err(Error::Eval("CLI envelope is not a table".to_owned()));
128    };
129    match table.get(cx, Symbol::new(field))?.object().as_expr(cx)? {
130        Expr::String(text) => Ok(text),
131        Expr::Nil => Err(Error::Eval(format!("CLI envelope field {field} is nil"))),
132        other => Err(Error::Eval(format!(
133            "CLI envelope field {field} is not a string: {other:?}"
134        ))),
135    }
136}
137
138fn envelope_args(cx: &mut Cx, envelope: &Value) -> Result<Vec<String>> {
139    let Some(table) = envelope.object().as_table_impl() else {
140        return Err(Error::Eval("CLI envelope is not a table".to_owned()));
141    };
142    let value = table.get(cx, Symbol::new("args"))?;
143    let Some(list) = value.object().as_list() else {
144        return Err(Error::Eval(
145            "CLI envelope field args is not a list".to_owned(),
146        ));
147    };
148    list.to_vec(cx, Some(64))?
149        .into_iter()
150        .map(|value| match value.object().as_expr(cx)? {
151            Expr::String(text) => Ok(text),
152            other => Err(Error::Eval(format!(
153                "CLI payload argument is not a string: {other:?}"
154            ))),
155        })
156        .collect()
157}
158
159fn symbol_from_slash(text: &str) -> Symbol {
160    match text.split_once('/') {
161        Some((head, tail)) => Symbol::qualified(head, tail),
162        None => Symbol::new(text),
163    }
164}