Skip to main content

surrealism_runtime/
exports.rs

1//! Exports manifest: function signatures baked into `.surli` archives.
2//!
3//! Stored as `surrealism/exports.toml` inside the package. Args and returns
4//! use hex-encoded Kind blobs for stable serialization across SDK versions.
5
6use serde::{Deserialize, Serialize};
7use surrealdb_types::Kind;
8use surrealism_types::err::{PrefixErr, SurrealismResult};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct ExportsManifest {
12	pub functions: Vec<FunctionExport>,
13}
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct FunctionExport {
17	/// `None` for the default export, `Some("name")` for named exports.
18	#[serde(default, skip_serializing_if = "Option::is_none")]
19	pub name: Option<String>,
20	/// Named argument list: each entry is `(arg_name, kind)`.
21	#[serde(with = "hex_argument_list")]
22	pub args: Vec<(String, Kind)>,
23	#[serde(with = "hex_kind")]
24	pub returns: Kind,
25	#[serde(default, skip_serializing_if = "Option::is_none")]
26	pub args_text: Option<Vec<String>>,
27	#[serde(default, skip_serializing_if = "Option::is_none")]
28	pub returns_text: Option<String>,
29	/// Whether this function may perform writes. Opt-in via `#[surrealism(writeable)]`.
30	/// Defaults to `false` (read-only) for backward compatibility.
31	#[serde(default)]
32	pub writeable: bool,
33	/// Human-readable comment for this function, aligned with SurrealQL's `COMMENT`
34	/// clause. Sourced from Rust doc comments or `#[surrealism(comment = "...")]`.
35	#[serde(default, skip_serializing_if = "Option::is_none")]
36	pub comment: Option<String>,
37}
38
39impl FunctionExport {
40	pub fn args_display(&self) -> String {
41		self.args_text.as_ref().map(|v| v.join(", ")).unwrap_or_else(|| {
42			self.args
43				.iter()
44				.map(|(name, kind)| format!("{name}: {kind}"))
45				.collect::<Vec<_>>()
46				.join(", ")
47		})
48	}
49
50	pub fn returns_display(&self) -> String {
51		self.returns_text
52			.as_deref()
53			.map(|s| s.to_string())
54			.unwrap_or_else(|| format!("{}", self.returns))
55	}
56}
57
58impl ExportsManifest {
59	/// Create an empty manifest. Used during the build step before signatures
60	/// have been extracted.
61	pub fn empty() -> Self {
62		Self {
63			functions: Vec::new(),
64		}
65	}
66
67	pub fn parse(s: &str) -> SurrealismResult<Self> {
68		toml::from_str(s).prefix_err(|| "Failed to parse exports manifest")
69	}
70
71	pub fn to_toml(&self) -> SurrealismResult<String> {
72		toml::to_string(self).prefix_err(|| "Failed to serialize exports manifest")
73	}
74
75	/// Look up a function by name. `None` matches the default export.
76	pub fn get_signature(&self, name: Option<&str>) -> Option<&FunctionExport> {
77		self.functions.iter().find(|f| f.name.as_deref() == name)
78	}
79}
80
81mod hex_kind {
82	use serde::{Deserialize, Deserializer, Serializer};
83	use surrealdb_types::Kind;
84
85	pub fn serialize<S: Serializer>(kind: &Kind, serializer: S) -> Result<S::Ok, S::Error> {
86		let bytes = surrealdb_types::encode_kind(kind).map_err(serde::ser::Error::custom)?;
87		serializer.serialize_str(&hex::encode(bytes))
88	}
89
90	pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Kind, D::Error> {
91		let s = String::deserialize(deserializer)?;
92		let bytes = hex::decode(s).map_err(serde::de::Error::custom)?;
93		surrealdb_types::decode_kind(&bytes).map_err(serde::de::Error::custom)
94	}
95}
96
97mod hex_argument_list {
98	use serde::{Deserialize, Deserializer, Serializer};
99	use surrealdb_types::Kind;
100
101	pub fn serialize<S: Serializer>(
102		args: &[(String, Kind)],
103		serializer: S,
104	) -> Result<S::Ok, S::Error> {
105		let pairs: Vec<(&str, Kind)> = args.iter().map(|(n, k)| (n.as_str(), k.clone())).collect();
106		let bytes =
107			surrealdb_types::encode_argument_list(&pairs).map_err(serde::ser::Error::custom)?;
108		serializer.serialize_str(&hex::encode(bytes))
109	}
110
111	pub fn deserialize<'de, D: Deserializer<'de>>(
112		deserializer: D,
113	) -> Result<Vec<(String, Kind)>, D::Error> {
114		let s = String::deserialize(deserializer)?;
115		let bytes = hex::decode(s).map_err(serde::de::Error::custom)?;
116		surrealdb_types::decode_argument_list(&bytes).map_err(serde::de::Error::custom)
117	}
118}
119
120#[cfg(test)]
121mod tests {
122	use super::*;
123
124	#[test]
125	fn roundtrip_manifest() {
126		let manifest = ExportsManifest {
127			functions: vec![
128				FunctionExport {
129					name: None,
130					args: vec![("value".to_string(), Kind::Int)],
131					returns: Kind::Bool,
132					args_text: Some(vec!["value: int".to_string()]),
133					returns_text: Some("bool".to_string()),
134					writeable: false,
135					comment: Some("Checks whether a value is valid.".to_string()),
136				},
137				FunctionExport {
138					name: Some("foo::bar".to_string()),
139					args: vec![
140						("input".to_string(), Kind::String),
141						("tags".to_string(), Kind::Array(Box::new(Kind::String), None)),
142					],
143					returns: Kind::Object,
144					args_text: None,
145					returns_text: None,
146					writeable: true,
147					comment: None,
148				},
149			],
150		};
151
152		let toml_str = manifest.to_toml().unwrap();
153		let parsed = ExportsManifest::parse(&toml_str).unwrap();
154		assert_eq!(manifest.functions.len(), parsed.functions.len());
155
156		assert!(parsed.functions[0].name.is_none());
157		assert_eq!(parsed.functions[0].args, vec![("value".to_string(), Kind::Int)]);
158		assert_eq!(parsed.functions[0].returns, Kind::Bool);
159		assert!(!parsed.functions[0].writeable);
160		assert_eq!(
161			parsed.functions[0].comment.as_deref(),
162			Some("Checks whether a value is valid.")
163		);
164
165		assert_eq!(parsed.functions[1].name.as_deref(), Some("foo::bar"));
166		assert_eq!(parsed.functions[1].args.len(), 2);
167		assert_eq!(parsed.functions[1].args[0].0, "input");
168		assert_eq!(parsed.functions[1].args[1].0, "tags");
169		assert_eq!(parsed.functions[1].returns, Kind::Object);
170		assert!(parsed.functions[1].writeable);
171		assert!(parsed.functions[1].comment.is_none());
172	}
173
174	#[test]
175	fn get_signature_default() {
176		let manifest = ExportsManifest {
177			functions: vec![FunctionExport {
178				name: None,
179				args: vec![("n".to_string(), Kind::Int)],
180				returns: Kind::Bool,
181				args_text: None,
182				returns_text: None,
183				writeable: false,
184				comment: None,
185			}],
186		};
187
188		assert!(manifest.get_signature(None).is_some());
189		assert!(manifest.get_signature(Some("nonexistent")).is_none());
190	}
191
192	#[test]
193	fn hex_roundtrip() {
194		let bytes = vec![0x0c, 0x00, 0xff, 0xab];
195		let encoded = hex::encode(&bytes);
196		assert_eq!(encoded, "0c00ffab");
197		let decoded = hex::decode(&encoded).unwrap();
198		assert_eq!(bytes, decoded);
199	}
200}