uni_plugin/wire_manifest.rs
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2024-2026 Dragonscale Team
3
4//! The wire-level argument type a plugin manifest declares.
5//!
6//! Guest plugins describe their signatures as JSON. Every loader that reads a
7//! manifest — the Extism loader and the Component-Model (`uni-plugin-wasm`)
8//! loader — has to agree on that schema, or the same manifest loads under one
9//! runtime and is rejected by the other.
10//!
11//! They did not agree. Each loader carried its own `WireArgType` and the two
12//! had drifted: Extism's had `Primitive`, `CypherValue`, `Vector` and
13//! `Variadic`; the Component-Model loader's had only the first two. Because
14//! both use `#[serde(tag = "kind", deny_unknown_fields)]`, a manifest
15//! declaring `kind: "vector"` or `kind: "variadic"` deserialized fine over
16//! Extism and failed to parse under the Component-Model loader.
17//!
18//! The type lives here so there is one schema. It carries the union of what
19//! the two loaders needed from their derives: `Serialize` (Extism round-trips
20//! manifests), `Deserialize` (both parse them), and `PartialEq + Eq` (Extism's
21//! export tests match on values).
22//!
23//! Mapping to the internal [`crate::traits::scalar::ArgType`] stays in each
24//! loader, since the error types differ.
25
26use serde::{Deserialize, Serialize};
27
28/// Wire-level argument type shipped by a plugin manifest.
29///
30/// Primitive types use the lowercase Arrow names (`"int64"`, `"float64"`,
31/// `"utf8"`, `"boolean"`, `"date64"`, `"timestamp_ms"`, `"binary"`,
32/// `"largebinary"`).
33#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
34#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
35pub enum WireArgType {
36 /// A native Arrow primitive — `kind: "primitive", arrow: "<name>"`.
37 Primitive {
38 /// Arrow primitive name.
39 arrow: String,
40 },
41 /// A `CypherValue` shipped via `LargeBinary` opaque transport.
42 CypherValue,
43 /// A fixed-size vector — `kind: "vector", len: N, element: "<arrow>"`.
44 Vector {
45 /// Number of elements per row.
46 len: usize,
47 /// Element type.
48 element: String,
49 },
50 /// Variadic — repeats `inner` zero or more times.
51 Variadic {
52 /// Inner element type.
53 inner: Box<WireArgType>,
54 },
55}
56
57#[cfg(test)]
58mod tests {
59 use super::*;
60
61 /// The shape that used to load over Extism and fail under the
62 /// Component-Model loader.
63 #[test]
64 fn vector_manifest_parses() {
65 let json = r#"{"kind":"vector","len":128,"element":"float32"}"#;
66 let parsed: WireArgType = serde_json::from_str(json).expect("vector must parse");
67 assert_eq!(
68 parsed,
69 WireArgType::Vector {
70 len: 128,
71 element: "float32".to_owned(),
72 }
73 );
74 }
75
76 #[test]
77 fn variadic_manifest_parses() {
78 let json = r#"{"kind":"variadic","inner":{"kind":"cypher_value"}}"#;
79 let parsed: WireArgType = serde_json::from_str(json).expect("variadic must parse");
80 assert_eq!(
81 parsed,
82 WireArgType::Variadic {
83 inner: Box::new(WireArgType::CypherValue),
84 }
85 );
86 }
87
88 #[test]
89 fn round_trips_through_json() {
90 for value in [
91 WireArgType::Primitive {
92 arrow: "int64".to_owned(),
93 },
94 WireArgType::CypherValue,
95 WireArgType::Vector {
96 len: 4,
97 element: "float64".to_owned(),
98 },
99 WireArgType::Variadic {
100 inner: Box::new(WireArgType::Primitive {
101 arrow: "utf8".to_owned(),
102 }),
103 },
104 ] {
105 let json = serde_json::to_string(&value).expect("serialize");
106 let back: WireArgType = serde_json::from_str(&json).expect("deserialize");
107 assert_eq!(back, value, "round-trip must be lossless for {value:?}");
108 }
109 }
110
111 #[test]
112 fn unknown_kind_is_rejected() {
113 let json = r#"{"kind":"quaternion"}"#;
114 assert!(serde_json::from_str::<WireArgType>(json).is_err());
115 }
116
117 /// `deny_unknown_fields` bites on struct variants...
118 #[test]
119 fn unknown_field_on_struct_variant_is_rejected() {
120 let json = r#"{"kind":"primitive","arrow":"int64","bogus":1}"#;
121 assert!(serde_json::from_str::<WireArgType>(json).is_err());
122 }
123
124 /// ...but NOT on unit variants. serde does not enforce
125 /// `deny_unknown_fields` for a unit variant of an internally-tagged enum,
126 /// so `cypher_value` silently tolerates junk keys. Pinned so the gap is a
127 /// known property of the wire schema rather than a surprise.
128 #[test]
129 fn unknown_field_on_unit_variant_is_tolerated() {
130 let json = r#"{"kind":"cypher_value","bogus":1}"#;
131 assert_eq!(
132 serde_json::from_str::<WireArgType>(json).expect("unit variant ignores extra keys"),
133 WireArgType::CypherValue
134 );
135 }
136}