1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
9pub enum ComponentNameError {
10 Empty,
12 Invalid,
14 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 pub fn new(value: impl AsRef<str>) -> Result<Self, ComponentNameError> {
79 $validator(value.as_ref()).map(|value| Self(value.to_owned()))
80 }
81
82 #[must_use]
84 pub fn as_str(&self) -> &str {
85 &self.0
86 }
87
88 #[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#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
132pub enum ComponentItemKind {
133 #[default]
135 Function,
136 Type,
138 Interface,
140 Instance,
142 Component,
144 Resource,
146 Value,
148}
149
150impl ComponentItemKind {
151 #[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#[derive(Clone, Debug, Eq, Hash, PartialEq)]
195pub struct ComponentImport {
196 name: InterfaceName,
197 kind: ComponentItemKind,
198}
199
200impl ComponentImport {
201 #[must_use]
203 pub const fn new(name: InterfaceName, kind: ComponentItemKind) -> Self {
204 Self { name, kind }
205 }
206
207 #[must_use]
209 pub const fn name(&self) -> &InterfaceName {
210 &self.name
211 }
212
213 #[must_use]
215 pub const fn kind(&self) -> ComponentItemKind {
216 self.kind
217 }
218}
219
220#[derive(Clone, Debug, Eq, Hash, PartialEq)]
222pub struct ComponentExport {
223 name: InterfaceName,
224 kind: ComponentItemKind,
225}
226
227impl ComponentExport {
228 #[must_use]
230 pub const fn new(name: InterfaceName, kind: ComponentItemKind) -> Self {
231 Self { name, kind }
232 }
233
234 #[must_use]
236 pub const fn name(&self) -> &InterfaceName {
237 &self.name
238 }
239
240 #[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}