Skip to main content

use_wasm_component/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7/// Error returned when Component Model names are invalid.
8#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
9pub enum ComponentNameError {
10    /// The supplied value was empty.
11    Empty,
12    /// The supplied value does not match this crate's conservative name rules.
13    Invalid,
14    /// The supplied item kind label was not recognized.
15    UnknownKind,
16}
17
18impl fmt::Display for ComponentNameError {
19    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
20        match self {
21            Self::Empty => formatter.write_str("Component Model name cannot be empty"),
22            Self::Invalid => formatter.write_str("invalid Component Model name"),
23            Self::UnknownKind => formatter.write_str("unknown Component Model item kind"),
24        }
25    }
26}
27
28impl Error for ComponentNameError {}
29
30fn is_component_segment(value: &str) -> bool {
31    let mut characters = value.chars();
32    let Some(first) = characters.next() else {
33        return false;
34    };
35    (first.is_ascii_alphabetic() || first == '_')
36        && characters
37            .all(|character| character.is_ascii_alphanumeric() || matches!(character, '_' | '-'))
38}
39
40fn validate_component_name(value: &str) -> Result<&str, ComponentNameError> {
41    let trimmed = value.trim();
42    if trimmed.is_empty() {
43        return Err(ComponentNameError::Empty);
44    }
45    if is_component_segment(trimmed) {
46        Ok(trimmed)
47    } else {
48        Err(ComponentNameError::Invalid)
49    }
50}
51
52fn validate_package_reference(value: &str) -> Result<&str, ComponentNameError> {
53    let trimmed = value.trim();
54    if trimmed.is_empty() {
55        return Err(ComponentNameError::Empty);
56    }
57    let without_version = trimmed.split_once('@').map_or(trimmed, |(name, _)| name);
58    let mut parts = without_version.split(':');
59    let Some(namespace) = parts.next() else {
60        return Err(ComponentNameError::Invalid);
61    };
62    let Some(name) = parts.next() else {
63        return Err(ComponentNameError::Invalid);
64    };
65    if parts.next().is_some() || !is_component_segment(namespace) || !is_component_segment(name) {
66        return Err(ComponentNameError::Invalid);
67    }
68    Ok(trimmed)
69}
70
71macro_rules! component_name_newtype {
72    ($name:ident, $validator:path) => {
73        #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
74        pub struct $name(String);
75
76        impl $name {
77            /// Creates a validated Component Model name wrapper.
78            pub fn new(value: impl AsRef<str>) -> Result<Self, ComponentNameError> {
79                $validator(value.as_ref()).map(|value| Self(value.to_owned()))
80            }
81
82            /// Returns the stored name.
83            #[must_use]
84            pub fn as_str(&self) -> &str {
85                &self.0
86            }
87
88            /// Consumes the wrapper and returns the stored name.
89            #[must_use]
90            pub fn into_string(self) -> String {
91                self.0
92            }
93        }
94
95        impl AsRef<str> for $name {
96            fn as_ref(&self) -> &str {
97                self.as_str()
98            }
99        }
100
101        impl fmt::Display for $name {
102            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
103                formatter.write_str(self.as_str())
104            }
105        }
106
107        impl FromStr for $name {
108            type Err = ComponentNameError;
109
110            fn from_str(value: &str) -> Result<Self, Self::Err> {
111                Self::new(value)
112            }
113        }
114
115        impl TryFrom<&str> for $name {
116            type Error = ComponentNameError;
117
118            fn try_from(value: &str) -> Result<Self, Self::Error> {
119                Self::new(value)
120            }
121        }
122    };
123}
124
125component_name_newtype!(ComponentName, validate_component_name);
126component_name_newtype!(WorldName, validate_component_name);
127component_name_newtype!(InterfaceName, validate_component_name);
128component_name_newtype!(PackageReference, validate_package_reference);
129
130/// Component Model import/export item kind.
131#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
132pub enum ComponentItemKind {
133    /// Function item.
134    #[default]
135    Function,
136    /// Type item.
137    Type,
138    /// Interface item.
139    Interface,
140    /// Instance item.
141    Instance,
142    /// Nested component item.
143    Component,
144    /// Resource item.
145    Resource,
146    /// Value item.
147    Value,
148}
149
150impl ComponentItemKind {
151    /// Returns the stable kind label.
152    #[must_use]
153    pub const fn as_str(self) -> &'static str {
154        match self {
155            Self::Function => "function",
156            Self::Type => "type",
157            Self::Interface => "interface",
158            Self::Instance => "instance",
159            Self::Component => "component",
160            Self::Resource => "resource",
161            Self::Value => "value",
162        }
163    }
164}
165
166impl fmt::Display for ComponentItemKind {
167    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
168        formatter.write_str(self.as_str())
169    }
170}
171
172impl FromStr for ComponentItemKind {
173    type Err = ComponentNameError;
174
175    fn from_str(value: &str) -> Result<Self, Self::Err> {
176        let trimmed = value.trim();
177        if trimmed.is_empty() {
178            return Err(ComponentNameError::Empty);
179        }
180        match trimmed.to_ascii_lowercase().as_str() {
181            "function" | "func" => Ok(Self::Function),
182            "type" => Ok(Self::Type),
183            "interface" => Ok(Self::Interface),
184            "instance" => Ok(Self::Instance),
185            "component" => Ok(Self::Component),
186            "resource" => Ok(Self::Resource),
187            "value" => Ok(Self::Value),
188            _ => Err(ComponentNameError::UnknownKind),
189        }
190    }
191}
192
193/// Component import metadata.
194#[derive(Clone, Debug, Eq, Hash, PartialEq)]
195pub struct ComponentImport {
196    name: InterfaceName,
197    kind: ComponentItemKind,
198}
199
200impl ComponentImport {
201    /// Creates component import metadata.
202    #[must_use]
203    pub const fn new(name: InterfaceName, kind: ComponentItemKind) -> Self {
204        Self { name, kind }
205    }
206
207    /// Returns the import name.
208    #[must_use]
209    pub const fn name(&self) -> &InterfaceName {
210        &self.name
211    }
212
213    /// Returns the import kind.
214    #[must_use]
215    pub const fn kind(&self) -> ComponentItemKind {
216        self.kind
217    }
218}
219
220/// Component export metadata.
221#[derive(Clone, Debug, Eq, Hash, PartialEq)]
222pub struct ComponentExport {
223    name: InterfaceName,
224    kind: ComponentItemKind,
225}
226
227impl ComponentExport {
228    /// Creates component export metadata.
229    #[must_use]
230    pub const fn new(name: InterfaceName, kind: ComponentItemKind) -> Self {
231        Self { name, kind }
232    }
233
234    /// Returns the export name.
235    #[must_use]
236    pub const fn name(&self) -> &InterfaceName {
237        &self.name
238    }
239
240    /// Returns the export kind.
241    #[must_use]
242    pub const fn kind(&self) -> ComponentItemKind {
243        self.kind
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use super::{
250        ComponentImport, ComponentItemKind, ComponentNameError, InterfaceName, PackageReference,
251        WorldName,
252    };
253
254    #[test]
255    fn validates_component_names() {
256        let world = WorldName::new("cli").expect("valid world");
257        let package = PackageReference::new("wasi:cli@0.2.0").expect("valid package");
258
259        assert_eq!(world.as_str(), "cli");
260        assert_eq!(package.as_str(), "wasi:cli@0.2.0");
261        assert_eq!(WorldName::new("bad name"), Err(ComponentNameError::Invalid));
262    }
263
264    #[test]
265    fn parses_item_kinds_and_metadata() {
266        let kind = "interface"
267            .parse::<ComponentItemKind>()
268            .expect("known kind");
269        let import = ComponentImport::new(
270            InterfaceName::new("filesystem").expect("valid interface"),
271            kind,
272        );
273
274        assert_eq!(kind.to_string(), "interface");
275        assert_eq!(import.name().as_str(), "filesystem");
276        assert_eq!(import.kind(), ComponentItemKind::Interface);
277    }
278}