1use std::path::PathBuf;
11
12use lumen_script::{ScriptCommand, ScriptNs, ScriptPrelude, ScriptSig, ScriptValue};
13use serde::de::DeserializeOwned;
14use serde::{Deserialize, Serialize};
15
16use crate::Error;
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct InitCx {
21 pub app_dir: PathBuf,
24 pub app_id: String,
27 pub headless: bool,
29 pub hot_reload: bool,
32 pub lumen_version: String,
34 config_toml: String,
37}
38
39impl InitCx {
40 pub fn new(
42 app_dir: PathBuf,
43 app_id: String,
44 headless: bool,
45 hot_reload: bool,
46 lumen_version: String,
47 config_toml: String,
48 ) -> Self {
49 InitCx {
50 app_dir,
51 app_id,
52 headless,
53 hot_reload,
54 lumen_version,
55 config_toml,
56 }
57 }
58
59 pub fn config<T: DeserializeOwned>(&self) -> Result<T, Error> {
62 toml::from_str(&self.config_toml).map_err(Error::from)
63 }
64}
65
66#[derive(Debug, Clone, Default, Serialize, Deserialize)]
68pub struct Manifest {
69 pub fns: Vec<FnDecl>,
73 pub preludes: Vec<ScriptPrelude>,
76 pub capabilities: Vec<String>,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct FnDecl {
84 pub name: String,
86 pub ns: ScriptNs,
89 pub sig: ScriptSig,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct Call {
96 pub index: u32,
98 pub args: Vec<ScriptValue>,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct CallOut {
105 pub ret: Result<ScriptValue, String>,
109 pub commands: Vec<ScriptCommand>,
112}
113
114pub use lumen_script::PluginEvent;
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123 use crate::codec;
124
125 fn cx(config_toml: &str) -> InitCx {
126 InitCx::new(
127 PathBuf::from("/app"),
128 "demo".to_string(),
129 true,
130 false,
131 "0.1.0".to_string(),
132 config_toml.to_string(),
133 )
134 }
135
136 #[test]
137 fn init_cx_config_deserializes_the_table_and_reports_type_errors() {
138 let cx = cx("poll_ms = 250");
139 let table: toml::Table = cx.config().unwrap();
140 assert_eq!(table.get("poll_ms").and_then(|v| v.as_integer()), Some(250));
141
142 #[derive(Deserialize, Debug)]
143 struct Wrong {
144 #[allow(dead_code)]
145 poll_ms: String,
146 }
147 let err = cx.config::<Wrong>().unwrap_err();
148 assert!(err.message.contains("invalid type"), "{}", err.message);
149 }
150
151 #[test]
152 fn an_absent_config_table_is_the_types_empty_view() {
153 #[derive(Deserialize, Debug, Default)]
154 #[serde(default)]
155 struct Cfg {
156 poll_ms: u64,
157 }
158 assert_eq!(cx("").config::<Cfg>().unwrap().poll_ms, 0);
159 }
160
161 #[test]
162 fn the_context_survives_the_boundary() {
163 let bytes = codec::encode(&cx("poll_ms = 250")).unwrap();
164 let back: InitCx = codec::decode(&bytes).unwrap();
165 assert_eq!(back.app_id, "demo");
166 assert!(back.headless);
167 assert_eq!(
168 back.config::<toml::Table>()
169 .unwrap()
170 .get("poll_ms")
171 .and_then(|v| v.as_integer()),
172 Some(250)
173 );
174 }
175
176 #[test]
177 fn a_call_and_its_outcome_survive_the_boundary() {
178 let call = Call {
179 index: 3,
180 args: vec![ScriptValue::Str("x".into()), ScriptValue::I64(7)],
181 };
182 let back: Call = codec::decode(&codec::encode(&call).unwrap()).unwrap();
183 assert_eq!(back.index, 3);
184 assert_eq!(back.args[1], ScriptValue::I64(7));
185
186 let out = CallOut {
187 ret: Err("no such device".to_string()),
188 commands: vec![ScriptCommand::Print("tried".into())],
189 };
190 let back: CallOut = codec::decode(&codec::encode(&out).unwrap()).unwrap();
191 assert_eq!(back.ret, Err("no such device".to_string()));
192 assert_eq!(back.commands.len(), 1);
193 }
194
195 #[test]
196 fn an_event_survives_the_boundary() {
197 let event = PluginEvent::Call {
198 event: "on_file_changed".to_string(),
199 key: "watcher".to_string(),
200 fallback: String::new(),
201 args: vec![ScriptValue::Bool(true)],
202 };
203 let back: PluginEvent = codec::decode(&codec::encode(&event).unwrap()).unwrap();
204 assert!(matches!(back, PluginEvent::Call { event, .. } if event == "on_file_changed"));
205 }
206}