wasmcloud_core/
wit.rs

1//! Reusable functionality related to [WebAssembly Interface types ("WIT")][wit]
2//!
3//! [wit]: <https://github.com/WebAssembly/component-model/blob/main/design/mvp/WIT.md>
4
5use std::collections::HashMap;
6
7use anyhow::{bail, Context as _, Result};
8use semver::Version;
9use serde::ser::SerializeMap;
10use serde::{Deserialize, Serialize, Serializer};
11
12use crate::{WitFunction, WitInterface, WitNamespace, WitPackage};
13
14/// Representation of maps (AKA associative arrays) that are usable from WIT
15///
16/// This representation is required because WIT does not natively
17/// have support for a map type, so we must use a list of tuples
18pub type WitMap<T> = Vec<(String, T)>;
19
20pub(crate) fn serialize_wit_map<S: Serializer, T>(
21    map: &WitMap<T>,
22    serializer: S,
23) -> std::result::Result<S::Ok, S::Error>
24where
25    T: Serialize,
26{
27    let mut seq = serializer.serialize_map(Some(map.len()))?;
28    for (key, val) in map {
29        seq.serialize_entry(key, val)?;
30    }
31    seq.end()
32}
33
34pub(crate) fn deserialize_wit_map<'de, D: serde::Deserializer<'de>, T>(
35    deserializer: D,
36) -> std::result::Result<WitMap<T>, D::Error>
37where
38    T: Deserialize<'de>,
39{
40    let values = HashMap::<String, T>::deserialize(deserializer)?;
41    Ok(values.into_iter().collect())
42}
43
44#[derive(Debug, Clone, Eq, Hash, PartialEq)]
45/// Call target identifier, which is equivalent to a WIT specification, which
46/// can identify an interface being called and optionally a specific function on that interface.
47pub struct CallTargetInterface {
48    /// WIT namespace (ex. `wasi` in `wasi:keyvalue/readwrite.get`)
49    pub namespace: String,
50    /// WIT package name (ex. `keyvalue` in `wasi:keyvalue/readwrite.get`)
51    pub package: String,
52    /// WIT interface (ex. `readwrite` in `wasi:keyvalue/readwrite.get`)
53    pub interface: String,
54}
55
56impl CallTargetInterface {
57    /// Returns the 3-tuple of (namespace, package, interface) for this interface
58    #[must_use]
59    pub fn as_parts(&self) -> (&str, &str, &str) {
60        (&self.namespace, &self.package, &self.interface)
61    }
62
63    /// Returns the fully qualified WIT interface in the form `namespace:package/interface`
64    pub fn as_instance(&self) -> String {
65        format!("{}:{}/{}", self.namespace, self.package, self.interface)
66    }
67
68    /// Build a [`CallTargetInterface`] from constituent parts
69    #[must_use]
70    pub fn from_parts((ns, pkg, iface): (&str, &str, &str)) -> Self {
71        Self {
72            namespace: ns.into(),
73            package: pkg.into(),
74            interface: iface.into(),
75        }
76    }
77
78    /// Build a target interface from a given operation
79    pub fn from_operation(operation: impl AsRef<str>) -> Result<Self> {
80        let operation = operation.as_ref();
81        let (wit_ns, wit_pkg, wit_iface, _) = parse_wit_meta_from_operation(operation)?;
82        Ok(CallTargetInterface::from_parts((
83            &wit_ns, &wit_pkg, &wit_iface,
84        )))
85    }
86}
87
88/// Parse a sufficiently specified WIT operation/method into constituent parts.
89///
90///
91/// # Errors
92///
93/// Returns `Err` if the operation is not of the form "&lt;package&gt;:&lt;ns&gt;/&lt;interface&gt;.&lt;function&gt;"
94///
95/// # Example
96///
97/// ```
98/// # use wasmcloud_core::parse_wit_meta_from_operation;
99/// let (wit_ns, wit_pkg, wit_iface, wit_fn) = parse_wit_meta_from_operation("wasmcloud:bus/guest-config").unwrap();
100/// # assert_eq!(wit_ns, "wasmcloud".to_string());
101/// # assert_eq!(wit_pkg, "bus".to_string());
102/// # assert_eq!(wit_iface, "guest-config".to_string());
103/// # assert_eq!(wit_fn, None);
104/// let (wit_ns, wit_pkg, wit_iface, wit_fn) = parse_wit_meta_from_operation("wasmcloud:bus/guest-config.get").unwrap();
105/// # assert_eq!(wit_ns, "wasmcloud".to_string());
106/// # assert_eq!(wit_pkg, "bus".to_string());
107/// # assert_eq!(wit_iface, "guest-config".to_string());
108/// # assert_eq!(wit_fn, Some("get".to_string()));
109/// ```
110pub fn parse_wit_meta_from_operation(
111    operation: impl AsRef<str>,
112) -> Result<(WitNamespace, WitPackage, WitInterface, Option<WitFunction>)> {
113    let operation = operation.as_ref();
114    let (ns_and_pkg, interface_and_func) = operation
115        .rsplit_once('/')
116        .context("failed to parse operation")?;
117    let (wit_ns, wit_pkg) = ns_and_pkg
118        .rsplit_once(':')
119        .context("failed to parse operation for WIT ns/pkg")?;
120    let (wit_iface, wit_fn) = match interface_and_func.split_once('.') {
121        Some((iface, func)) => (iface, Some(func.to_string())),
122        None => (interface_and_func, None),
123    };
124    Ok((wit_ns.into(), wit_pkg.into(), wit_iface.into(), wit_fn))
125}
126
127type WitInformationTuple = (
128    WitNamespace,
129    Vec<WitPackage>,
130    Option<Vec<WitInterface>>,
131    Option<WitFunction>,
132    Option<Version>,
133);
134
135/// Parse a WIT package name into constituent parts.
136///
137/// This function is `parse_wit_meta_from_operation` but differs
138/// in that it allows more portions to be missing, and handles more use cases,
139/// like operations on resources
140///
141/// This formulation should *also* support future nested package/interface features in the WIT spec.
142///
143/// # Errors
144///
145/// Returns `Err` if the operation is not of the form "&lt;package&gt;:&lt;ns&gt;/&lt;interface&gt;.&lt;function&gt;"
146///
147/// # Example
148///
149/// ```
150/// # use semver::Version;
151/// # use wasmcloud_core::parse_wit_package_name;
152/// let (ns, packages, interfaces, func, version) = parse_wit_package_name("wasi:http").unwrap();
153/// # assert_eq!(ns, "wasi".to_string());
154/// # assert_eq!(packages, vec!["http".to_string()]);
155/// # assert_eq!(interfaces, None);
156/// # assert_eq!(func, None);
157/// # assert_eq!(version, None);
158/// let (ns, packages, interfaces, func, version) = parse_wit_package_name("wasi:http@0.2.2").unwrap();
159/// # assert_eq!(ns, "wasi".to_string());
160/// # assert_eq!(packages, vec!["http".to_string()]);
161/// # assert_eq!(interfaces, None);
162/// # assert_eq!(func, None);
163/// # assert_eq!(version, Version::parse("0.2.2").ok());
164/// let (ns, packages, interfaces, func, version) = parse_wit_package_name("wasmcloud:bus/guest-config").unwrap();
165/// # assert_eq!(ns, "wasmcloud");
166/// # assert_eq!(packages, vec!["bus".to_string()]);
167/// # assert_eq!(interfaces, Some(vec!["guest-config".to_string()]));
168/// # assert_eq!(func, None);
169/// # assert_eq!(version, None);
170/// let (ns, packages, interfaces, func, version) = parse_wit_package_name("wasmcloud:bus/guest-config.get").unwrap();
171/// # assert_eq!(ns, "wasmcloud");
172/// # assert_eq!(packages, vec!["bus".to_string()]);
173/// # assert_eq!(interfaces, Some(vec!["guest-config".to_string()]));
174/// # assert_eq!(func, Some("get".to_string()));
175/// # assert_eq!(version, None);
176/// let (ns, packages, interfaces, func, version) = parse_wit_package_name("wasi:http/incoming-handler@0.2.0").unwrap();
177/// # assert_eq!(ns, "wasi".to_string());
178/// # assert_eq!(packages, vec!["http".to_string()]);
179/// # assert_eq!(interfaces, Some(vec!["incoming-handler".to_string()]));
180/// # assert_eq!(func, None);
181/// # assert_eq!(version, Version::parse("0.2.0").ok());
182/// let (ns, packages, interfaces, func, version) = parse_wit_package_name("wasi:keyvalue/atomics.increment@0.2.0-draft").unwrap();
183/// # assert_eq!(ns, "wasi".to_string());
184/// # assert_eq!(packages, vec!["keyvalue".to_string()]);
185/// # assert_eq!(interfaces, Some(vec!["atomics".to_string()]));
186/// # assert_eq!(func, Some("increment".to_string()));
187/// # assert_eq!(version, Version::parse("0.2.0-draft").ok());
188/// ```
189pub fn parse_wit_package_name(p: impl AsRef<str>) -> Result<WitInformationTuple> {
190    let p = p.as_ref();
191    // If there's a version, we can strip it off first and parse it
192    let (rest, version) = match p.rsplit_once('@') {
193        Some((rest, version)) => (
194            rest,
195            Some(
196                Version::parse(version)
197                    .map_err(|e| anyhow::anyhow!(e))
198                    .with_context(|| {
199                        format!("failed to parse version from wit package name [{p}]")
200                    })?,
201            ),
202        ),
203        None => (p, None),
204    };
205
206    // Read to the first '/' which should mark the first package
207    let (ns_and_pkg, interface_and_func) = match rest.rsplit_once('/') {
208        Some((ns_and_pkg, interface_and_func)) => (ns_and_pkg, Some(interface_and_func)),
209        None => (rest, None),
210    };
211
212    // Read all packages
213    let ns_pkg_split = ns_and_pkg.split(':').collect::<Vec<&str>>();
214    let (ns, packages) = match ns_pkg_split[..] {
215        [] => bail!("invalid package name, missing namespace & package"),
216        [_] => bail!("invalid package name, invalid package"),
217        [ns, ref packages @ ..] => (ns, packages),
218    };
219
220    // Read all interfaces
221    let (mut interfaces, iface_with_fn) = match interface_and_func
222        .unwrap_or_default()
223        .split('/')
224        .filter(|v| !v.is_empty())
225        .collect::<Vec<&str>>()[..]
226    {
227        [] => (None, None),
228        [iface] => (Some(vec![]), Some(iface)),
229        [iface, f] => (Some(vec![iface]), Some(f)),
230        [ref ifaces @ .., f] => (Some(Vec::from(ifaces)), Some(f)),
231    };
232
233    let func = match iface_with_fn {
234        Some(iface_with_fn) => match iface_with_fn.split_once('.') {
235            Some((iface, f)) => {
236                if let Some(ref mut interfaces) = interfaces {
237                    interfaces.push(iface);
238                };
239                Some(f)
240            }
241            None => {
242                if let Some(ref mut interfaces) = interfaces {
243                    interfaces.push(iface_with_fn);
244                };
245                None
246            }
247        },
248        None => None,
249    };
250
251    Ok((
252        ns.into(),
253        packages
254            .iter()
255            .map(|v| String::from(*v))
256            .collect::<Vec<_>>(),
257        interfaces.map(|v| v.into_iter().map(String::from).collect::<Vec<_>>()),
258        func.map(String::from),
259        version,
260    ))
261}
262
263// TODO(joonas): Remove these once doctests are run as part of CI.
264#[cfg(test)]
265mod test {
266    use semver::Version;
267
268    use super::parse_wit_package_name;
269    #[test]
270    fn test_parse_wit_package_name() {
271        let (ns, packages, interfaces, func, version) =
272            parse_wit_package_name("wasi:http").expect("should have parsed'wasi:http'");
273        assert_eq!(ns, "wasi".to_string());
274        assert_eq!(packages, vec!["http".to_string()]);
275        assert_eq!(interfaces, None);
276        assert_eq!(func, None);
277        assert_eq!(version, None);
278
279        let (ns, packages, interfaces, func, version) = parse_wit_package_name("wasi:http@0.2.2")
280            .expect("should have parsed 'wasi:http@0.2.2'");
281        assert_eq!(ns, "wasi".to_string());
282        assert_eq!(packages, vec!["http".to_string()]);
283        assert_eq!(interfaces, None);
284        assert_eq!(func, None);
285        assert_eq!(version, Version::parse("0.2.2").ok());
286
287        let (ns, packages, interfaces, func, version) =
288            parse_wit_package_name("wasmcloud:bus/guest-config")
289                .expect("should have parsed 'wasmcloud:bus/guest-config'");
290        assert_eq!(ns, "wasmcloud");
291        assert_eq!(packages, vec!["bus".to_string()]);
292        assert_eq!(interfaces, Some(vec!["guest-config".to_string()]));
293        assert_eq!(func, None);
294        assert_eq!(version, None);
295
296        let (ns, packages, interfaces, func, version) =
297            parse_wit_package_name("wasmcloud:bus/guest-config.get")
298                .expect("should have parsed 'wasmcloud:bus/guest-config.get'");
299        assert_eq!(ns, "wasmcloud");
300        assert_eq!(packages, vec!["bus".to_string()]);
301        assert_eq!(interfaces, Some(vec!["guest-config".to_string()]));
302        assert_eq!(func, Some("get".to_string()));
303        assert_eq!(version, None);
304
305        let (ns, packages, interfaces, func, version) =
306            parse_wit_package_name("wasi:http/incoming-handler@0.2.0")
307                .expect("should have parsed 'wasi:http/incoming-handler@0.2.0'");
308        assert_eq!(ns, "wasi".to_string());
309        assert_eq!(packages, vec!["http".to_string()]);
310        assert_eq!(interfaces, Some(vec!["incoming-handler".to_string()]));
311        assert_eq!(func, None);
312        assert_eq!(version, Version::parse("0.2.0").ok());
313
314        let (ns, packages, interfaces, func, version) =
315            parse_wit_package_name("wasi:keyvalue/atomics.increment@0.2.0-draft")
316                .expect("should have parsed 'wasi:keyvalue/atomics.increment@0.2.0-draft'");
317        assert_eq!(ns, "wasi".to_string());
318        assert_eq!(packages, vec!["keyvalue".to_string()]);
319        assert_eq!(interfaces, Some(vec!["atomics".to_string()]));
320        assert_eq!(func, Some("increment".to_string()));
321        assert_eq!(version, Version::parse("0.2.0-draft").ok());
322    }
323}