1#![cfg(feature = "rhai-runtime")]
31
32use rhai::{AST, Dynamic, Engine, Map, Scope};
33
34use crate::error::RhaiError;
35
36#[derive(Debug, Clone, Default)]
38pub struct RhaiManifest {
39 pub id: String,
41 pub version: String,
43 pub determinism: String,
45 pub scalar_fns: Vec<ScalarEntry>,
47 pub aggregate_fns: Vec<AggregateEntry>,
49 pub procedures: Vec<ProcedureEntry>,
51}
52
53#[derive(Debug, Clone)]
55pub struct ScalarEntry {
56 pub name: String,
58 pub args: Vec<String>,
60 pub returns: String,
62 pub vectorized: bool,
65}
66
67#[derive(Debug, Clone)]
69pub struct AggregateEntry {
70 pub name: String,
74 pub args: Vec<String>,
76 pub returns: String,
78 pub state: String,
81}
82
83#[derive(Debug, Clone)]
85pub struct ProcedureEntry {
86 pub name: String,
88 pub args: Vec<String>,
90 pub yields: Vec<String>,
92 pub mode: String,
95}
96
97pub fn compile(engine: &Engine, script: &str) -> Result<AST, RhaiError> {
102 engine
103 .compile(script)
104 .map_err(|e| RhaiError::ParseFailed(format!("{e}")))
105}
106
107pub fn parse_manifest(engine: &Engine, ast: &AST) -> Result<RhaiManifest, RhaiError> {
110 let mut scope = Scope::new();
111 let dynamic: Dynamic = engine
112 .call_fn(&mut scope, ast, "uni_manifest", ())
113 .map_err(|e| RhaiError::ManifestInvalid(format!("calling uni_manifest: {e}")))?;
114
115 let map: Map = dynamic
116 .try_cast::<Map>()
117 .ok_or_else(|| RhaiError::ManifestInvalid("uni_manifest() must return a map".into()))?;
118
119 let id = required_string(&map, "id")?;
120 let version = required_string(&map, "version")?;
121 let determinism = optional_string(&map, "determinism").unwrap_or_else(|| "pure".into());
122
123 let scalar_fns = parse_scalar_entries(&map)?;
124 let aggregate_fns = parse_aggregate_entries(&map)?;
125 let procedures = parse_procedure_entries(&map)?;
126
127 Ok(RhaiManifest {
128 id,
129 version,
130 determinism,
131 scalar_fns,
132 aggregate_fns,
133 procedures,
134 })
135}
136
137fn parse_entry_array<T>(
144 map: &Map,
145 key: &str,
146 build: impl Fn(&Map) -> Result<T, RhaiError>,
147) -> Result<Vec<T>, RhaiError> {
148 let Some(arr) = map.get(key) else {
149 return Ok(vec![]);
150 };
151 let arr = arr
152 .clone()
153 .try_cast::<rhai::Array>()
154 .ok_or_else(|| RhaiError::ManifestInvalid(format!("{key} must be an array of maps")))?;
155 let mut entries = Vec::with_capacity(arr.len());
156 for d in arr {
157 let m = d
158 .try_cast::<Map>()
159 .ok_or_else(|| RhaiError::ManifestInvalid(format!("{key} entry must be a map")))?;
160 entries.push(build(&m)?);
161 }
162 Ok(entries)
163}
164
165fn parse_scalar_entries(map: &Map) -> Result<Vec<ScalarEntry>, RhaiError> {
166 parse_entry_array(map, "scalar_fns", |m| {
167 Ok(ScalarEntry {
168 name: required_string(m, "name")?,
169 args: required_string_array(m, "args")?,
170 returns: required_string(m, "returns")?,
171 vectorized: optional_bool(m, "vectorized").unwrap_or(false),
172 })
173 })
174}
175
176fn parse_aggregate_entries(map: &Map) -> Result<Vec<AggregateEntry>, RhaiError> {
177 parse_entry_array(map, "aggregate_fns", |m| {
178 Ok(AggregateEntry {
179 name: required_string(m, "name")?,
180 args: required_string_array(m, "args")?,
181 returns: required_string(m, "returns")?,
182 state: optional_string(m, "state").unwrap_or_else(|| "map".into()),
183 })
184 })
185}
186
187fn parse_procedure_entries(map: &Map) -> Result<Vec<ProcedureEntry>, RhaiError> {
188 parse_entry_array(map, "procedures", |m| {
189 Ok(ProcedureEntry {
190 name: required_string(m, "name")?,
191 args: required_string_array(m, "args")?,
192 yields: required_string_array(m, "yields")?,
193 mode: optional_string(m, "mode").unwrap_or_else(|| "read".into()),
194 })
195 })
196}
197
198fn required_string(map: &Map, key: &str) -> Result<String, RhaiError> {
199 let dyn_val = map
200 .get(key)
201 .ok_or_else(|| RhaiError::ManifestInvalid(format!("missing required field `{key}`")))?;
202 dyn_val
203 .clone()
204 .into_string()
205 .map_err(|t| RhaiError::ManifestInvalid(format!("`{key}` must be a string (got {t})")))
206}
207
208fn optional_string(map: &Map, key: &str) -> Option<String> {
209 map.get(key).and_then(|d| d.clone().into_string().ok())
210}
211
212fn optional_bool(map: &Map, key: &str) -> Option<bool> {
213 map.get(key).and_then(|d| d.as_bool().ok())
214}
215
216fn required_string_array(map: &Map, key: &str) -> Result<Vec<String>, RhaiError> {
217 let dyn_val = map
218 .get(key)
219 .ok_or_else(|| RhaiError::ManifestInvalid(format!("missing required field `{key}`")))?;
220 let arr = dyn_val
221 .clone()
222 .try_cast::<rhai::Array>()
223 .ok_or_else(|| RhaiError::ManifestInvalid(format!("`{key}` must be an array")))?;
224 let mut out = Vec::with_capacity(arr.len());
225 for (i, d) in arr.into_iter().enumerate() {
226 let s = d.into_string().map_err(|t| {
227 RhaiError::ManifestInvalid(format!("`{key}`[{i}] must be a string (got {t})"))
228 })?;
229 out.push(s);
230 }
231 Ok(out)
232}
233
234#[cfg(test)]
235mod tests {
236 use super::*;
237 use crate::engine::build_engine;
238 use crate::host_fns::RhaiHostFnRegistry;
239 use uni_plugin::CapabilitySet;
240
241 fn engine() -> Engine {
242 build_engine(&CapabilitySet::new(), &RhaiHostFnRegistry::new())
243 }
244
245 #[test]
246 fn parses_minimal_manifest() {
247 let script = r#"
248 fn uni_manifest() {
249 #{
250 id: "ai.test.min",
251 version: "0.1.0",
252 scalar_fns: [
253 #{ name: "score", args: ["float","float"], returns: "float" },
254 ],
255 }
256 }
257 fn score(x, y) { x + y }
258 "#;
259 let eng = engine();
260 let ast = compile(&eng, script).expect("compiles");
261 let m = parse_manifest(&eng, &ast).expect("parses");
262 assert_eq!(m.id, "ai.test.min");
263 assert_eq!(m.version, "0.1.0");
264 assert_eq!(m.determinism, "pure");
265 assert_eq!(m.scalar_fns.len(), 1);
266 assert_eq!(m.scalar_fns[0].name, "score");
267 assert_eq!(m.scalar_fns[0].args, vec!["float", "float"]);
268 assert_eq!(m.scalar_fns[0].returns, "float");
269 assert!(!m.scalar_fns[0].vectorized);
270 }
271
272 #[test]
273 fn missing_id_rejected() {
274 let script = r#"
275 fn uni_manifest() { #{ version: "0.1.0" } }
276 "#;
277 let eng = engine();
278 let ast = compile(&eng, script).unwrap();
279 let err = parse_manifest(&eng, &ast).unwrap_err();
280 assert!(matches!(err, RhaiError::ManifestInvalid(_)));
281 }
282
283 #[test]
284 fn parses_aggregate_and_procedure_entries() {
285 let script = r#"
286 fn uni_manifest() {
287 #{
288 id: "ai.test.agg",
289 version: "0.1.0",
290 aggregate_fns: [
291 #{ name: "stats", args: ["float"], returns: "map", state: "map" },
292 ],
293 procedures: [
294 #{ name: "rows", args: [], yields: ["int","string"], mode: "read" },
295 ],
296 }
297 }
298 "#;
299 let eng = engine();
300 let ast = compile(&eng, script).unwrap();
301 let m = parse_manifest(&eng, &ast).unwrap();
302 assert_eq!(m.aggregate_fns.len(), 1);
303 assert_eq!(m.aggregate_fns[0].name, "stats");
304 assert_eq!(m.procedures.len(), 1);
305 assert_eq!(m.procedures[0].yields, vec!["int", "string"]);
306 }
307}