1use std::sync::Arc;
17
18use lumen_ir::addon::{Addon, AddonFunction};
19
20use crate::ScriptTy;
21use crate::script_fn::{ScriptFn, ScriptFnBody, ScriptNs, ScriptParam, ScriptResult, ScriptSig};
22
23pub const TAG_PARAM: &str = "tag";
25
26pub fn error_event(event: &str) -> String {
28 format!("{event}_error")
29}
30
31pub fn signature(addon: &Addon, function: &AddonFunction) -> Result<ScriptSig, String> {
38 let ty = |spelling: &str, what: &str| -> Result<ScriptTy, String> {
39 spelling.parse::<ScriptTy>().map_err(|e| {
40 format!(
41 "add-on '{}': {}::{} {what}: {e}",
42 addon.name, addon.namespace, function.name
43 )
44 })
45 };
46 let mut params = Vec::with_capacity(function.params.len() + 1);
47 for param in &function.params {
48 params.push(ScriptParam {
49 name: param.name.clone(),
50 ty: ty(¶m.ty, &format!("parameter `{}`", param.name))?,
51 });
52 }
53 let ret = if function.event.is_some() {
54 params.push(ScriptParam {
55 name: TAG_PARAM.to_owned(),
56 ty: ScriptTy::Str,
57 });
58 ScriptTy::Unit
59 } else if function.returns.is_empty() {
60 ScriptTy::Unit
61 } else {
62 ty(&function.returns, "return")?
63 };
64 Ok(ScriptSig {
65 min_arity: params.len(),
66 params,
67 ret,
68 variadic: false,
69 doc: function.doc.clone(),
70 })
71}
72
73pub fn script_fns(
82 addon: &Addon,
83 mut body: impl FnMut(usize, &AddonFunction) -> ScriptFnBody,
84) -> Result<Vec<ScriptFn>, String> {
85 let mut fns = Vec::with_capacity(addon.functions.len());
86 for (index, function) in addon.functions.iter().enumerate() {
87 fns.push(ScriptFn {
88 name: function.name.clone(),
89 ns: ScriptNs::Named(addon.namespace.clone()),
90 sig: signature(addon, function)?,
91 body: body(index, function),
92 });
93 }
94 Ok(fns)
95}
96
97pub type OnBrowserOnlyCall = Arc<dyn Fn(&str) + Send + Sync>;
99
100pub fn browser_only_fns(
113 addon: &Addon,
114 on_call: Option<OnBrowserOnlyCall>,
115) -> Result<Vec<ScriptFn>, String> {
116 script_fns(addon, |_, function| {
117 let qualified = format!("{}::{}", addon.namespace, function.name);
118 let on_call = on_call.clone();
119 Arc::new(move |_| -> ScriptResult {
120 if let Some(on_call) = &on_call {
121 on_call(&qualified);
122 }
123 Err(format!("{qualified} runs only in a browser"))
124 })
125 })
126}
127
128#[cfg(test)]
129mod tests {
130 use std::sync::Mutex;
131
132 use lumen_ir::addon::AddonParam;
133
134 use super::*;
135 use crate::ScriptValue;
136
137 fn echo() -> Addon {
138 Addon {
139 name: "echo".into(),
140 namespace: "echo".into(),
141 functions: vec![
142 AddonFunction {
143 name: "shout".into(),
144 params: vec![AddonParam {
145 name: "text".into(),
146 ty: "string".into(),
147 }],
148 returns: "string".into(),
149 event: None,
150 doc: "Upper-case it.".into(),
151 },
152 AddonFunction {
153 name: "later".into(),
154 params: vec![AddonParam {
155 name: "ms".into(),
156 ty: "int".into(),
157 }],
158 returns: String::new(),
159 event: Some("on_echo".into()),
160 doc: String::new(),
161 },
162 ],
163 elements: Vec::new(),
164 }
165 }
166
167 #[test]
168 fn a_function_binds_under_the_addons_namespace_with_its_declared_types() {
169 let fns = script_fns(&echo(), |_, _| Arc::new(|_| Ok(ScriptValue::Unit))).unwrap();
170 assert_eq!(fns[0].name, "shout");
171 assert_eq!(fns[0].ns, ScriptNs::Named("echo".into()));
172 assert_eq!(fns[0].sig.params[0].ty, ScriptTy::Str);
173 assert_eq!(fns[0].sig.ret, ScriptTy::Str);
174 assert_eq!(fns[0].sig.doc, "Upper-case it.");
175 }
176
177 #[test]
178 fn an_asynchronous_function_takes_a_tag_and_returns_nothing() {
179 let fns = script_fns(&echo(), |_, _| Arc::new(|_| Ok(ScriptValue::Unit))).unwrap();
180 let params: Vec<(&str, &ScriptTy)> = fns[1]
181 .sig
182 .params
183 .iter()
184 .map(|p| (p.name.as_str(), &p.ty))
185 .collect();
186 assert_eq!(
187 params,
188 [("ms", &ScriptTy::Int), (TAG_PARAM, &ScriptTy::Str)]
189 );
190 assert_eq!(fns[1].sig.ret, ScriptTy::Unit);
191 assert_eq!(fns[1].sig.min_arity, 2);
192 }
193
194 #[test]
195 fn a_function_that_declares_no_return_returns_nothing() {
196 let mut addon = echo();
197 addon.functions[0].returns = String::new();
198 let fns = script_fns(&addon, |_, _| Arc::new(|_| Ok(ScriptValue::Unit))).unwrap();
199 assert_eq!(fns[0].sig.ret, ScriptTy::Unit);
200 assert_eq!(fns[0].sig.min_arity, 1);
201 }
202
203 #[test]
204 fn an_asynchronous_failure_arrives_as_the_error_event() {
205 assert_eq!(error_event("on_echo"), "on_echo_error");
206 }
207
208 #[test]
209 fn a_type_no_host_reads_names_the_function() {
210 let mut addon = echo();
211 addon.functions[0].params[0].ty = "text".into();
212 let err = script_fns(&addon, |_, _| Arc::new(|_| Ok(ScriptValue::Unit))).unwrap_err();
213 assert!(err.contains("echo::shout parameter `text`"), "{err}");
214 }
215
216 #[test]
217 fn a_browser_only_call_raises_and_is_heard() {
218 let heard = Arc::new(Mutex::new(Vec::new()));
219 let sink = Arc::clone(&heard);
220 let fns = browser_only_fns(
221 &echo(),
222 Some(Arc::new(move |name: &str| {
223 sink.lock().unwrap().push(name.to_owned());
224 })),
225 )
226 .unwrap();
227 let (ret, _) = fns[0].invoke(&[ScriptValue::Str("hi".into())]);
228 assert_eq!(ret, Err("echo::shout runs only in a browser".to_owned()));
229 assert_eq!(*heard.lock().unwrap(), ["echo::shout"]);
230 }
231}