Skip to main content

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}