wasm_component_trampoline/
path.rs

1use semver::Version;
2use snafu::{ResultExt, Snafu};
3use std::fmt::Display;
4use std::str::FromStr;
5
6/// A fully-qualified path to a WIT interface, with an optional version.
7#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
8pub struct ForeignInterfacePath {
9    package_name: String,
10    interface_name: String,
11    version: Option<Version>,
12}
13
14impl ForeignInterfacePath {
15    /// Creates a new `ForeignInterfacePath` with the given package name, interface name, and optional version.
16    #[must_use]
17    pub const fn new(
18        package_name: String,
19        interface_name: String,
20        version: Option<Version>,
21    ) -> Self {
22        ForeignInterfacePath {
23            package_name,
24            interface_name,
25            version,
26        }
27    }
28
29    /// Returns the package name component of the interface path.
30    #[must_use]
31    pub fn package_name(&self) -> &str {
32        self.package_name.as_ref()
33    }
34
35    /// Returns the interface name component of the interface path.
36    #[must_use]
37    pub fn interface_name(&self) -> &str {
38        &self.interface_name
39    }
40
41    /// Returns the version component of the interface path, if one is specified.
42    #[must_use]
43    pub fn version(&self) -> Option<&Version> {
44        self.version.as_ref()
45    }
46}
47
48impl From<ForeignInterfacePath> for InterfacePath {
49    fn from(path: ForeignInterfacePath) -> Self {
50        InterfacePath {
51            package_name: Some(path.package_name),
52            interface_name: path.interface_name,
53            version: path.version,
54        }
55    }
56}
57
58impl Display for ForeignInterfacePath {
59    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60        write!(
61            f,
62            "{}/{}{}",
63            self.package_name,
64            self.interface_name,
65            self.version
66                .as_ref()
67                .map_or(String::new(), |v| format!("@{v}"))
68        )
69    }
70}
71
72/// Represents a path to a WIT interface, which may be local (without a package name) or foreign
73/// (with a package name). The version is optional in both cases.
74#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
75pub struct InterfacePath {
76    package_name: Option<String>,
77    interface_name: String,
78    version: Option<Version>,
79}
80
81impl InterfacePath {
82    #[must_use]
83    pub const fn new(
84        package_name: Option<String>,
85        interface_name: String,
86        version: Option<Version>,
87    ) -> Self {
88        InterfacePath {
89            package_name,
90            interface_name,
91            version,
92        }
93    }
94
95    /// Returns the package name component of the interface path, if one is specified.
96    #[must_use]
97    pub fn package_name(&self) -> Option<&str> {
98        self.package_name.as_deref()
99    }
100
101    /// Returns the interface name component of the interface path.
102    #[must_use]
103    pub fn interface_name(&self) -> &str {
104        &self.interface_name
105    }
106
107    /// Returns the version component of the interface path, if one is specified.
108    #[must_use]
109    pub fn version(&self) -> Option<&Version> {
110        self.version.as_ref()
111    }
112
113    /// Converts this `InterfacePath` into a `ForeignInterfacePath`, if it has a package name,
114    /// otherwise returns `None`.
115    #[must_use]
116    pub fn into_foreign(self) -> Option<ForeignInterfacePath> {
117        Some(ForeignInterfacePath {
118            package_name: self.package_name?,
119            interface_name: self.interface_name,
120            version: self.version,
121        })
122    }
123}
124
125impl FromStr for InterfacePath {
126    type Err = InterfacePathParseError;
127
128    fn from_str(s: &str) -> Result<Self, Self::Err> {
129        // Parses the following format: "package_name/interface_name@version",
130        // where the version specifier is optional.
131
132        let parts: Vec<&str> = s.split('/').collect();
133
134        match parts.len() {
135            1 if s.contains('@') => return Err(InterfacePathParseError::FormatError),
136            1 => {
137                return Ok(Self {
138                    package_name: None,
139                    interface_name: s.to_string(),
140                    version: None,
141                });
142            }
143            2 => (), // Continue below.
144            _ => return Err(InterfacePathParseError::FormatError),
145        }
146
147        let package_name = parts[0].to_string();
148
149        let interface_parts: Vec<&str> = parts[1].split('@').collect();
150        let interface_name = interface_parts[0].to_string();
151
152        let version = if interface_parts.len() == 2 {
153            Some(
154                Version::parse(interface_parts[1])
155                    .context(interface_path_parse_error::VersionParseSnafu)?,
156            )
157        } else {
158            None
159        };
160
161        Ok(InterfacePath {
162            package_name: Some(package_name),
163            interface_name,
164            version,
165        })
166    }
167}
168
169impl Display for InterfacePath {
170    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171        write!(
172            f,
173            "{}{}{}",
174            self.package_name
175                .as_ref()
176                .map_or(String::new(), |p| format!("{p}/")),
177            self.interface_name,
178            self.version
179                .as_ref()
180                .map_or(String::new(), |v| format!("@{v}")),
181        )
182    }
183}
184
185#[derive(Snafu, Debug)]
186#[snafu(module)]
187pub enum InterfacePathParseError {
188    #[snafu(display("Invalid interface path format"))]
189    FormatError,
190
191    #[snafu(display("Invalid semantic version format: {}", source))]
192    VersionParseError { source: semver::Error },
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198    const PACKAGE: &str = "package_name/interface_name@1.0.0";
199    const INTERFACE_ONLY: &str = "interface_name";
200    const PACKAGE_WITHOUT_VERSION: &str = "package_name/interface_name";
201
202    #[test]
203    fn test_path_display() {
204        for package in [PACKAGE, PACKAGE_WITHOUT_VERSION] {
205            let path = InterfacePath::from_str(package).unwrap();
206            assert_eq!(package, format!("{path}"));
207            let foreign_path = path.clone().into_foreign().unwrap();
208            assert_eq!(package, format!("{foreign_path}"));
209        }
210
211        let interface_only = InterfacePath::from_str(INTERFACE_ONLY).unwrap();
212        assert!(interface_only.clone().into_foreign().is_none());
213    }
214
215    #[test]
216    fn test_interface_path_roundtrip() {
217        let path = InterfacePath::from_str(PACKAGE).unwrap();
218        // Convert to ForeignInterfacePath and back
219        assert_eq!(path, path.clone().into_foreign().unwrap().into());
220
221        let interface_only = InterfacePath::from_str(INTERFACE_ONLY).unwrap();
222        assert_eq!(None, interface_only.clone().into_foreign());
223
224        // Parse the string representation back into InterfacePath
225        assert_eq!(
226            path,
227            InterfacePath::new(
228                path.package_name().map(String::from),
229                path.interface_name().to_string(),
230                path.version().cloned(),
231            )
232        );
233    }
234
235    #[test]
236    fn test_foreign_interface_path_roundtrip() {
237        for package in [PACKAGE, PACKAGE_WITHOUT_VERSION] {
238            let path = InterfacePath::from_str(package).unwrap();
239            let foreign_path: ForeignInterfacePath = path.clone().into_foreign().unwrap();
240
241            assert_eq!(
242                foreign_path,
243                ForeignInterfacePath::new(
244                    path.package_name().unwrap().to_string(),
245                    path.interface_name().to_string(),
246                    path.version().cloned()
247                )
248            );
249        }
250    }
251
252    #[test]
253    fn test_foreign_interface_path() {
254        let path = InterfacePath::from_str(PACKAGE).unwrap();
255        let foreign_path: ForeignInterfacePath = path.clone().into_foreign().unwrap();
256        assert_eq!(foreign_path.package_name(), "package_name");
257        assert_eq!(foreign_path.interface_name(), "interface_name");
258        assert_eq!(
259            foreign_path.version(),
260            Some(&Version::parse("1.0.0").unwrap())
261        );
262
263        let fp_string = foreign_path.to_string();
264        assert_eq!(PACKAGE, fp_string);
265        assert_eq!(PACKAGE, InterfacePath::from(foreign_path).to_string());
266        assert_eq!(fp_string, path.to_string());
267    }
268
269    #[test]
270    fn test_interface_path_parsing() {
271        let path = InterfacePath::from_str(PACKAGE).unwrap();
272        assert_eq!(path.package_name(), Some("package_name"));
273        assert_eq!(path.interface_name(), "interface_name");
274        assert_eq!(path.version(), Some(&Version::parse("1.0.0").unwrap()));
275        assert_eq!(path.to_string(), PACKAGE);
276
277        let path = InterfacePath::from_str("interface_name").unwrap();
278        assert_eq!(path.package_name(), None);
279        assert_eq!(path.interface_name(), "interface_name");
280        assert_eq!(path.version(), None);
281        assert_eq!(path.to_string(), "interface_name");
282
283        let path = InterfacePath::from_str("package_name/interface_name").unwrap();
284        assert_eq!(path.package_name(), Some("package_name"));
285        assert_eq!(path.interface_name(), "interface_name");
286        assert_eq!(path.version(), None);
287        assert_eq!(path.to_string(), "package_name/interface_name");
288
289        let path_err = InterfacePath::from_str("package_name/interface_name/").unwrap_err();
290        assert!(matches!(path_err, InterfacePathParseError::FormatError));
291
292        let path_err = InterfacePath::from_str("package_name/interface_name@").unwrap_err();
293        assert!(matches!(
294            path_err,
295            InterfacePathParseError::VersionParseError { .. }
296        ));
297    }
298}