1use serde::{Deserialize, Serialize};
20
21use crate::error::ExtismError;
22use crate::loader::ExtismPluginManifest;
23
24#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
32#[serde(deny_unknown_fields)]
33pub struct WireFnSignature {
34 pub args: Vec<WireArgType>,
36 pub returns: WireArgType,
38 #[serde(default = "default_volatility")]
41 pub volatility: String,
42 #[serde(default = "default_null_handling")]
44 pub null_handling: String,
45}
46
47fn default_volatility() -> String {
48 "immutable".to_owned()
49}
50
51fn default_null_handling() -> String {
52 "propagate".to_owned()
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
62#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
63pub enum WireArgType {
64 Primitive {
66 arrow: String,
68 },
69 CypherValue,
71 Vector {
73 len: usize,
75 element: String,
77 },
78 Variadic {
80 inner: Box<WireArgType>,
82 },
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
87#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
88pub enum RegistrationEntry {
89 Scalar {
91 qname: String,
93 signature: WireFnSignature,
95 },
96 Aggregate {
99 qname: String,
101 signature: WireFnSignature,
103 state: WireArgType,
106 },
107 Procedure {
109 qname: String,
111 args: Vec<WireArgType>,
113 yields: Vec<WireArgType>,
115 #[serde(default = "default_proc_mode")]
117 mode: String,
118 },
119}
120
121fn default_proc_mode() -> String {
122 "read".to_owned()
123}
124
125impl RegistrationEntry {
126 #[must_use]
128 pub fn qname(&self) -> &str {
129 match self {
130 Self::Scalar { qname, .. }
131 | Self::Aggregate { qname, .. }
132 | Self::Procedure { qname, .. } => qname,
133 }
134 }
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
139#[serde(deny_unknown_fields)]
140pub struct RegistrationManifest {
141 pub entries: Vec<RegistrationEntry>,
143}
144
145pub fn parse_manifest_json(bytes: &[u8]) -> Result<ExtismPluginManifest, ExtismError> {
153 serde_json::from_slice(bytes)
154 .map_err(|e| ExtismError::ManifestInvalid(format!("json parse: {e}")))
155}
156
157pub fn parse_registration_json(bytes: &[u8]) -> Result<RegistrationManifest, ExtismError> {
165 serde_json::from_slice(bytes)
166 .map_err(|e| ExtismError::OutputDecode(format!("register json parse: {e}")))
167}
168
169pub fn read_manifest_export(
181 plugin: &mut extism::Plugin,
182) -> Result<ExtismPluginManifest, ExtismError> {
183 let bytes = read_required_export(plugin, "manifest")?;
184 parse_manifest_json(&bytes)
185}
186
187fn read_required_export(plugin: &mut extism::Plugin, export: &str) -> Result<Vec<u8>, ExtismError> {
197 if !plugin.function_exists(export) {
198 return Err(ExtismError::InvalidPlugin(format!(
199 "plugin does not export required `{export}` function"
200 )));
201 }
202 plugin
203 .call::<&str, &[u8]>(export, "")
204 .map(<[u8]>::to_vec)
205 .map_err(|e| ExtismError::InvalidPlugin(format!("call {export}: {e}")))
206}
207
208pub fn read_register_export(
221 plugin: &mut extism::Plugin,
222) -> Result<RegistrationManifest, ExtismError> {
223 let bytes = read_required_export(plugin, "register")?;
224 parse_registration_json(&bytes)
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230
231 #[test]
232 fn parses_minimal_manifest() {
233 let json = br#"{"id":"a.b","version":"0.0.1"}"#;
234 let m = parse_manifest_json(json).unwrap();
235 assert_eq!(m.id, "a.b");
236 assert_eq!(m.version, "0.0.1");
237 assert!(m.capabilities.is_empty());
238 assert!(m.fuel_per_call.is_none());
239 }
240
241 #[test]
242 fn parses_manifest_with_resource_limits() {
243 let json = br#"{
244 "id": "a.b",
245 "version": "0.0.1",
246 "capabilities": ["filesystem"],
247 "fuel_per_call": 1000,
248 "memory_max_pages": 4,
249 "timeout_ms": 500
250 }"#;
251 let m = parse_manifest_json(json).unwrap();
252 assert_eq!(m.fuel_per_call, Some(1000));
253 assert_eq!(m.memory_max_pages, Some(4));
254 assert_eq!(m.timeout_ms, Some(500));
255 assert!(m.declared_capability_set().contains_variant(
256 &uni_plugin::Capability::Filesystem {
257 read: vec![],
258 write: vec![],
259 }
260 ));
261 }
262
263 #[test]
264 fn rejects_unknown_manifest_field() {
265 let json = br#"{"id":"a.b","version":"0.0.1","mystery":"surprise"}"#;
266 let err = parse_manifest_json(json).unwrap_err();
267 assert!(matches!(err, ExtismError::ManifestInvalid(_)));
268 }
269
270 #[test]
271 fn parses_empty_registration() {
272 let json = br#"{"entries":[]}"#;
273 let r = parse_registration_json(json).unwrap();
274 assert!(r.entries.is_empty());
275 }
276
277 #[test]
278 fn parses_scalar_registration_entry() {
279 let json = br#"{
280 "entries": [{
281 "kind": "scalar",
282 "qname": "geo.haversine",
283 "signature": {
284 "args": [
285 {"kind":"primitive","arrow":"float64"},
286 {"kind":"primitive","arrow":"float64"},
287 {"kind":"primitive","arrow":"float64"},
288 {"kind":"primitive","arrow":"float64"}
289 ],
290 "returns": {"kind":"primitive","arrow":"float64"}
291 }
292 }]
293 }"#;
294 let r = parse_registration_json(json).unwrap();
295 assert_eq!(r.entries.len(), 1);
296 match &r.entries[0] {
297 RegistrationEntry::Scalar { qname, signature } => {
298 assert_eq!(qname, "geo.haversine");
299 assert_eq!(signature.args.len(), 4);
300 assert_eq!(signature.volatility, "immutable");
301 assert_eq!(signature.null_handling, "propagate");
302 assert!(matches!(
303 signature.returns,
304 WireArgType::Primitive { ref arrow } if arrow == "float64"
305 ));
306 }
307 other => panic!("expected Scalar, got: {other:?}"),
308 }
309 }
310
311 #[test]
312 fn parses_aggregate_registration_entry() {
313 let json = br#"{
314 "entries": [{
315 "kind": "aggregate",
316 "qname": "stats.weighted_mean",
317 "signature": {
318 "args": [
319 {"kind":"primitive","arrow":"float64"},
320 {"kind":"primitive","arrow":"float64"}
321 ],
322 "returns": {"kind":"primitive","arrow":"float64"},
323 "volatility": "stable"
324 },
325 "state": {"kind":"primitive","arrow":"binary"}
326 }]
327 }"#;
328 let r = parse_registration_json(json).unwrap();
329 match &r.entries[0] {
330 RegistrationEntry::Aggregate {
331 qname,
332 signature,
333 state,
334 } => {
335 assert_eq!(qname, "stats.weighted_mean");
336 assert_eq!(signature.volatility, "stable");
337 assert!(matches!(state, WireArgType::Primitive { arrow } if arrow == "binary"));
338 }
339 other => panic!("expected Aggregate, got: {other:?}"),
340 }
341 }
342
343 #[test]
344 fn parses_procedure_registration_entry() {
345 let json = br#"{
346 "entries": [{
347 "kind": "procedure",
348 "qname": "myorg.scan",
349 "args": [{"kind":"primitive","arrow":"utf8"}],
350 "yields": [
351 {"kind":"primitive","arrow":"int64"},
352 {"kind":"cypher_value"}
353 ],
354 "mode": "write"
355 }]
356 }"#;
357 let r = parse_registration_json(json).unwrap();
358 match &r.entries[0] {
359 RegistrationEntry::Procedure {
360 qname,
361 args,
362 yields,
363 mode,
364 } => {
365 assert_eq!(qname, "myorg.scan");
366 assert_eq!(args.len(), 1);
367 assert_eq!(yields.len(), 2);
368 assert_eq!(mode, "write");
369 assert!(matches!(yields[1], WireArgType::CypherValue));
370 }
371 other => panic!("expected Procedure, got: {other:?}"),
372 }
373 }
374
375 #[test]
376 fn procedure_mode_defaults_to_read() {
377 let json = br#"{
378 "entries": [{
379 "kind": "procedure",
380 "qname": "myorg.scan",
381 "args": [],
382 "yields": []
383 }]
384 }"#;
385 let r = parse_registration_json(json).unwrap();
386 match &r.entries[0] {
387 RegistrationEntry::Procedure { mode, .. } => assert_eq!(mode, "read"),
388 _ => unreachable!(),
389 }
390 }
391
392 #[test]
393 fn registration_entry_exposes_qname() {
394 let e = RegistrationEntry::Scalar {
395 qname: "x.y".to_owned(),
396 signature: WireFnSignature {
397 args: vec![],
398 returns: WireArgType::CypherValue,
399 volatility: "immutable".to_owned(),
400 null_handling: "propagate".to_owned(),
401 },
402 };
403 assert_eq!(e.qname(), "x.y");
404 }
405
406 #[test]
407 fn rejects_unknown_registration_kind() {
408 let json = br#"{"entries":[{"kind":"telegraphic","qname":"x"}]}"#;
409 let err = parse_registration_json(json).unwrap_err();
410 assert!(matches!(err, ExtismError::OutputDecode(_)));
411 }
412
413 #[test]
414 fn parses_vector_and_variadic_argtypes() {
415 let json = br#"{
416 "entries": [{
417 "kind": "scalar",
418 "qname": "vec.norm",
419 "signature": {
420 "args": [
421 {"kind":"vector","len":128,"element":"float32"},
422 {"kind":"variadic","inner":{"kind":"primitive","arrow":"int64"}}
423 ],
424 "returns": {"kind":"primitive","arrow":"float32"}
425 }
426 }]
427 }"#;
428 let r = parse_registration_json(json).unwrap();
429 match &r.entries[0] {
430 RegistrationEntry::Scalar { signature, .. } => {
431 assert!(matches!(
432 signature.args[0],
433 WireArgType::Vector { len: 128, ref element } if element == "float32"
434 ));
435 assert!(matches!(
436 signature.args[1],
437 WireArgType::Variadic { ref inner } if matches!(
438 **inner,
439 WireArgType::Primitive { ref arrow } if arrow == "int64"
440 )
441 ));
442 }
443 _ => unreachable!(),
444 }
445 }
446}