Skip to main content

use_wasm_wit/
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 WIT text identifiers are invalid.
8#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
9pub enum WitNameError {
10    /// The supplied value was empty.
11    Empty,
12    /// The supplied value does not match this crate's conservative WIT identifier rules.
13    Invalid,
14}
15
16impl fmt::Display for WitNameError {
17    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
18        match self {
19            Self::Empty => formatter.write_str("WIT name cannot be empty"),
20            Self::Invalid => formatter.write_str("invalid WIT name"),
21        }
22    }
23}
24
25impl Error for WitNameError {}
26
27fn is_wit_segment(value: &str) -> bool {
28    let mut characters = value.chars();
29    let Some(first) = characters.next() else {
30        return false;
31    };
32    (first.is_ascii_alphabetic() || first == '_')
33        && characters
34            .all(|character| character.is_ascii_alphanumeric() || matches!(character, '_' | '-'))
35}
36
37fn validate_wit_identifier(value: &str) -> Result<&str, WitNameError> {
38    let trimmed = value.trim();
39    if trimmed.is_empty() {
40        return Err(WitNameError::Empty);
41    }
42    if is_wit_segment(trimmed) {
43        Ok(trimmed)
44    } else {
45        Err(WitNameError::Invalid)
46    }
47}
48
49fn validate_wit_package_name(value: &str) -> Result<&str, WitNameError> {
50    let trimmed = value.trim();
51    if trimmed.is_empty() {
52        return Err(WitNameError::Empty);
53    }
54    let package_without_version = trimmed.split_once('@').map_or(trimmed, |(name, _)| name);
55    let mut parts = package_without_version.split(':');
56    let Some(namespace) = parts.next() else {
57        return Err(WitNameError::Invalid);
58    };
59    let Some(name) = parts.next() else {
60        return Err(WitNameError::Invalid);
61    };
62    if parts.next().is_some() || !is_wit_segment(namespace) || !is_wit_segment(name) {
63        return Err(WitNameError::Invalid);
64    }
65    Ok(trimmed)
66}
67
68macro_rules! wit_name_newtype {
69    ($name:ident, $validator:path) => {
70        #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
71        pub struct $name(String);
72
73        impl $name {
74            /// Creates a validated WIT name wrapper.
75            pub fn new(value: impl AsRef<str>) -> Result<Self, WitNameError> {
76                $validator(value.as_ref()).map(|value| Self(value.to_owned()))
77            }
78
79            /// Returns the stored WIT name.
80            #[must_use]
81            pub fn as_str(&self) -> &str {
82                &self.0
83            }
84
85            /// Consumes the wrapper and returns the stored string.
86            #[must_use]
87            pub fn into_string(self) -> String {
88                self.0
89            }
90        }
91
92        impl AsRef<str> for $name {
93            fn as_ref(&self) -> &str {
94                self.as_str()
95            }
96        }
97
98        impl fmt::Display for $name {
99            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
100                formatter.write_str(self.as_str())
101            }
102        }
103
104        impl FromStr for $name {
105            type Err = WitNameError;
106
107            fn from_str(value: &str) -> Result<Self, Self::Err> {
108                Self::new(value)
109            }
110        }
111
112        impl TryFrom<&str> for $name {
113            type Error = WitNameError;
114
115            fn try_from(value: &str) -> Result<Self, Self::Error> {
116                Self::new(value)
117            }
118        }
119    };
120}
121
122wit_name_newtype!(WitPackageName, validate_wit_package_name);
123wit_name_newtype!(WitNamespace, validate_wit_identifier);
124wit_name_newtype!(WitInterfaceName, validate_wit_identifier);
125wit_name_newtype!(WitWorldName, validate_wit_identifier);
126wit_name_newtype!(WitTypeName, validate_wit_identifier);
127wit_name_newtype!(WitFunctionName, validate_wit_identifier);
128wit_name_newtype!(WitResourceName, validate_wit_identifier);
129
130/// Returns 'true' when a label fits the conservative WIT identifier rule.
131#[must_use]
132pub fn is_valid_wit_identifier(value: &str) -> bool {
133    validate_wit_identifier(value).is_ok()
134}
135
136#[cfg(test)]
137mod tests {
138    use super::{
139        WitFunctionName, WitNameError, WitPackageName, WitWorldName, is_valid_wit_identifier,
140    };
141
142    #[test]
143    fn validates_wit_identifiers() {
144        let world = WitWorldName::new("cli").expect("valid world");
145        let function = WitFunctionName::new("read-file").expect("valid function");
146
147        assert_eq!(world.as_str(), "cli");
148        assert_eq!(function.to_string(), "read-file");
149        assert!(is_valid_wit_identifier("resource_name"));
150        assert!(!is_valid_wit_identifier("1bad"));
151    }
152
153    #[test]
154    fn validates_package_names() {
155        let package = WitPackageName::new("wasi:filesystem@0.2.0").expect("valid package");
156
157        assert_eq!(package.as_str(), "wasi:filesystem@0.2.0");
158        assert_eq!(
159            WitPackageName::new("filesystem"),
160            Err(WitNameError::Invalid)
161        );
162    }
163}