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 Algorithm {
123 qname: String,
125 args: Vec<WireArgType>,
127 yields: Vec<String>,
130 },
131}
132
133fn default_proc_mode() -> String {
134 "read".to_owned()
135}
136
137impl RegistrationEntry {
138 #[must_use]
140 pub fn qname(&self) -> &str {
141 match self {
142 Self::Scalar { qname, .. }
143 | Self::Aggregate { qname, .. }
144 | Self::Procedure { qname, .. }
145 | Self::Algorithm { qname, .. } => qname,
146 }
147 }
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
152#[serde(deny_unknown_fields)]
153pub struct RegistrationManifest {
154 pub entries: Vec<RegistrationEntry>,
156}
157
158pub fn parse_manifest_json(bytes: &[u8]) -> Result<ExtismPluginManifest, ExtismError> {
166 serde_json::from_slice(bytes)
167 .map_err(|e| ExtismError::ManifestInvalid(format!("json parse: {e}")))
168}
169
170pub fn parse_registration_json(bytes: &[u8]) -> Result<RegistrationManifest, ExtismError> {
178 serde_json::from_slice(bytes)
179 .map_err(|e| ExtismError::OutputDecode(format!("register json parse: {e}")))
180}
181
182pub fn read_manifest_export(
194 plugin: &mut extism::Plugin,
195) -> Result<ExtismPluginManifest, ExtismError> {
196 let bytes = read_required_export(plugin, "manifest")?;
197 parse_manifest_json(&bytes)
198}
199
200fn read_required_export(plugin: &mut extism::Plugin, export: &str) -> Result<Vec<u8>, ExtismError> {
210 if !plugin.function_exists(export) {
211 return Err(ExtismError::InvalidPlugin(format!(
212 "plugin does not export required `{export}` function"
213 )));
214 }
215 plugin
216 .call::<&str, &[u8]>(export, "")
217 .map(<[u8]>::to_vec)
218 .map_err(|e| ExtismError::InvalidPlugin(format!("call {export}: {e}")))
219}
220
221pub fn read_register_export(
234 plugin: &mut extism::Plugin,
235) -> Result<RegistrationManifest, ExtismError> {
236 let bytes = read_required_export(plugin, "register")?;
237 parse_registration_json(&bytes)
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243
244 #[test]
245 fn parses_minimal_manifest() {
246 let json = br#"{"id":"a.b","version":"0.0.1"}"#;
247 let m = parse_manifest_json(json).unwrap();
248 assert_eq!(m.id, "a.b");
249 assert_eq!(m.version, "0.0.1");
250 assert!(m.capabilities.is_empty());
251 assert!(m.fuel_per_call.is_none());
252 }
253
254 #[test]
255 fn parses_manifest_with_resource_limits() {
256 let json = br#"{
257 "id": "a.b",
258 "version": "0.0.1",
259 "capabilities": ["filesystem"],
260 "fuel_per_call": 1000,
261 "memory_max_pages": 4,
262 "timeout_ms": 500
263 }"#;
264 let m = parse_manifest_json(json).unwrap();
265 assert_eq!(m.fuel_per_call, Some(1000));
266 assert_eq!(m.memory_max_pages, Some(4));
267 assert_eq!(m.timeout_ms, Some(500));
268 assert!(m.declared_capability_set().contains_variant(
269 &uni_plugin::Capability::Filesystem {
270 read: vec![],
271 write: vec![],
272 }
273 ));
274 }
275
276 #[test]
277 fn rejects_unknown_manifest_field() {
278 let json = br#"{"id":"a.b","version":"0.0.1","mystery":"surprise"}"#;
279 let err = parse_manifest_json(json).unwrap_err();
280 assert!(matches!(err, ExtismError::ManifestInvalid(_)));
281 }
282
283 #[test]
284 fn parses_empty_registration() {
285 let json = br#"{"entries":[]}"#;
286 let r = parse_registration_json(json).unwrap();
287 assert!(r.entries.is_empty());
288 }
289
290 #[test]
291 fn parses_scalar_registration_entry() {
292 let json = br#"{
293 "entries": [{
294 "kind": "scalar",
295 "qname": "geo.haversine",
296 "signature": {
297 "args": [
298 {"kind":"primitive","arrow":"float64"},
299 {"kind":"primitive","arrow":"float64"},
300 {"kind":"primitive","arrow":"float64"},
301 {"kind":"primitive","arrow":"float64"}
302 ],
303 "returns": {"kind":"primitive","arrow":"float64"}
304 }
305 }]
306 }"#;
307 let r = parse_registration_json(json).unwrap();
308 assert_eq!(r.entries.len(), 1);
309 match &r.entries[0] {
310 RegistrationEntry::Scalar { qname, signature } => {
311 assert_eq!(qname, "geo.haversine");
312 assert_eq!(signature.args.len(), 4);
313 assert_eq!(signature.volatility, "immutable");
314 assert_eq!(signature.null_handling, "propagate");
315 assert!(matches!(
316 signature.returns,
317 WireArgType::Primitive { ref arrow } if arrow == "float64"
318 ));
319 }
320 other => panic!("expected Scalar, got: {other:?}"),
321 }
322 }
323
324 #[test]
325 fn parses_aggregate_registration_entry() {
326 let json = br#"{
327 "entries": [{
328 "kind": "aggregate",
329 "qname": "stats.weighted_mean",
330 "signature": {
331 "args": [
332 {"kind":"primitive","arrow":"float64"},
333 {"kind":"primitive","arrow":"float64"}
334 ],
335 "returns": {"kind":"primitive","arrow":"float64"},
336 "volatility": "stable"
337 },
338 "state": {"kind":"primitive","arrow":"binary"}
339 }]
340 }"#;
341 let r = parse_registration_json(json).unwrap();
342 match &r.entries[0] {
343 RegistrationEntry::Aggregate {
344 qname,
345 signature,
346 state,
347 } => {
348 assert_eq!(qname, "stats.weighted_mean");
349 assert_eq!(signature.volatility, "stable");
350 assert!(matches!(state, WireArgType::Primitive { arrow } if arrow == "binary"));
351 }
352 other => panic!("expected Aggregate, got: {other:?}"),
353 }
354 }
355
356 #[test]
357 fn parses_procedure_registration_entry() {
358 let json = br#"{
359 "entries": [{
360 "kind": "procedure",
361 "qname": "myorg.scan",
362 "args": [{"kind":"primitive","arrow":"utf8"}],
363 "yields": [
364 {"kind":"primitive","arrow":"int64"},
365 {"kind":"cypher_value"}
366 ],
367 "mode": "write"
368 }]
369 }"#;
370 let r = parse_registration_json(json).unwrap();
371 match &r.entries[0] {
372 RegistrationEntry::Procedure {
373 qname,
374 args,
375 yields,
376 mode,
377 } => {
378 assert_eq!(qname, "myorg.scan");
379 assert_eq!(args.len(), 1);
380 assert_eq!(yields.len(), 2);
381 assert_eq!(mode, "write");
382 assert!(matches!(yields[1], WireArgType::CypherValue));
383 }
384 other => panic!("expected Procedure, got: {other:?}"),
385 }
386 }
387
388 #[test]
389 fn procedure_mode_defaults_to_read() {
390 let json = br#"{
391 "entries": [{
392 "kind": "procedure",
393 "qname": "myorg.scan",
394 "args": [],
395 "yields": []
396 }]
397 }"#;
398 let r = parse_registration_json(json).unwrap();
399 match &r.entries[0] {
400 RegistrationEntry::Procedure { mode, .. } => assert_eq!(mode, "read"),
401 _ => unreachable!(),
402 }
403 }
404
405 #[test]
406 fn registration_entry_exposes_qname() {
407 let e = RegistrationEntry::Scalar {
408 qname: "x.y".to_owned(),
409 signature: WireFnSignature {
410 args: vec![],
411 returns: WireArgType::CypherValue,
412 volatility: "immutable".to_owned(),
413 null_handling: "propagate".to_owned(),
414 },
415 };
416 assert_eq!(e.qname(), "x.y");
417 }
418
419 #[test]
420 fn rejects_unknown_registration_kind() {
421 let json = br#"{"entries":[{"kind":"telegraphic","qname":"x"}]}"#;
422 let err = parse_registration_json(json).unwrap_err();
423 assert!(matches!(err, ExtismError::OutputDecode(_)));
424 }
425
426 #[test]
427 fn parses_vector_and_variadic_argtypes() {
428 let json = br#"{
429 "entries": [{
430 "kind": "scalar",
431 "qname": "vec.norm",
432 "signature": {
433 "args": [
434 {"kind":"vector","len":128,"element":"float32"},
435 {"kind":"variadic","inner":{"kind":"primitive","arrow":"int64"}}
436 ],
437 "returns": {"kind":"primitive","arrow":"float32"}
438 }
439 }]
440 }"#;
441 let r = parse_registration_json(json).unwrap();
442 match &r.entries[0] {
443 RegistrationEntry::Scalar { signature, .. } => {
444 assert!(matches!(
445 signature.args[0],
446 WireArgType::Vector { len: 128, ref element } if element == "float32"
447 ));
448 assert!(matches!(
449 signature.args[1],
450 WireArgType::Variadic { ref inner } if matches!(
451 **inner,
452 WireArgType::Primitive { ref arrow } if arrow == "int64"
453 )
454 ));
455 }
456 _ => unreachable!(),
457 }
458 }
459}