1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6use use_js_identifier::{JsIdentifier, JsIdentifierError};
7
8#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
10pub struct SolidComponentName(String);
11
12impl SolidComponentName {
13 pub fn new(input: &str) -> Result<Self, SolidNameError> {
19 validate_pascal_case(input).map(Self)
20 }
21
22 #[must_use]
24 pub fn as_str(&self) -> &str {
25 &self.0
26 }
27}
28
29impl fmt::Display for SolidComponentName {
30 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
31 formatter.write_str(self.as_str())
32 }
33}
34
35impl FromStr for SolidComponentName {
36 type Err = SolidNameError;
37
38 fn from_str(input: &str) -> Result<Self, Self::Err> {
39 Self::new(input)
40 }
41}
42
43impl TryFrom<&str> for SolidComponentName {
44 type Error = SolidNameError;
45
46 fn try_from(value: &str) -> Result<Self, Self::Error> {
47 Self::new(value)
48 }
49}
50
51#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
53pub struct SolidSignalName(String);
54
55impl SolidSignalName {
56 pub fn new(input: &str) -> Result<Self, SolidNameError> {
62 let identifier = JsIdentifier::new(input).map_err(SolidNameError::Identifier)?;
63 Ok(Self(identifier.as_str().to_string()))
64 }
65
66 #[must_use]
68 pub fn as_str(&self) -> &str {
69 &self.0
70 }
71}
72
73impl fmt::Display for SolidSignalName {
74 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
75 formatter.write_str(self.as_str())
76 }
77}
78
79impl FromStr for SolidSignalName {
80 type Err = SolidNameError;
81
82 fn from_str(input: &str) -> Result<Self, Self::Err> {
83 Self::new(input)
84 }
85}
86
87impl TryFrom<&str> for SolidSignalName {
88 type Error = SolidNameError;
89
90 fn try_from(value: &str) -> Result<Self, Self::Error> {
91 Self::new(value)
92 }
93}
94
95#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
97pub enum SolidFileKind {
98 Component,
99 Signal,
100 Resource,
101 Store,
102 Route,
103 Context,
104}
105
106impl SolidFileKind {
107 #[must_use]
109 pub const fn as_str(self) -> &'static str {
110 match self {
111 Self::Component => "component",
112 Self::Signal => "signal",
113 Self::Resource => "resource",
114 Self::Store => "store",
115 Self::Route => "route",
116 Self::Context => "context",
117 }
118 }
119}
120
121impl fmt::Display for SolidFileKind {
122 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
123 formatter.write_str(self.as_str())
124 }
125}
126
127impl FromStr for SolidFileKind {
128 type Err = SolidNameError;
129
130 fn from_str(input: &str) -> Result<Self, Self::Err> {
131 match normalized_label(input)?.as_str() {
132 "component" => Ok(Self::Component),
133 "signal" => Ok(Self::Signal),
134 "resource" => Ok(Self::Resource),
135 "store" => Ok(Self::Store),
136 "route" => Ok(Self::Route),
137 "context" => Ok(Self::Context),
138 _ => Err(SolidNameError::UnknownLabel),
139 }
140 }
141}
142
143#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
145pub enum SolidPrimitiveKind {
146 Signal,
147 Memo,
148 Effect,
149 Resource,
150 Store,
151 Context,
152 Root,
153}
154
155impl SolidPrimitiveKind {
156 #[must_use]
158 pub const fn as_str(self) -> &'static str {
159 match self {
160 Self::Signal => "signal",
161 Self::Memo => "memo",
162 Self::Effect => "effect",
163 Self::Resource => "resource",
164 Self::Store => "store",
165 Self::Context => "context",
166 Self::Root => "root",
167 }
168 }
169}
170
171impl fmt::Display for SolidPrimitiveKind {
172 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
173 formatter.write_str(self.as_str())
174 }
175}
176
177impl FromStr for SolidPrimitiveKind {
178 type Err = SolidNameError;
179
180 fn from_str(input: &str) -> Result<Self, Self::Err> {
181 match normalized_label(input)?.as_str() {
182 "signal" => Ok(Self::Signal),
183 "memo" => Ok(Self::Memo),
184 "effect" => Ok(Self::Effect),
185 "resource" => Ok(Self::Resource),
186 "store" => Ok(Self::Store),
187 "context" => Ok(Self::Context),
188 "root" => Ok(Self::Root),
189 _ => Err(SolidNameError::UnknownLabel),
190 }
191 }
192}
193
194#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
196pub enum SolidJsxRuntime {
197 Classic,
198 Automatic,
199}
200
201impl SolidJsxRuntime {
202 #[must_use]
204 pub const fn as_str(self) -> &'static str {
205 match self {
206 Self::Classic => "classic",
207 Self::Automatic => "automatic",
208 }
209 }
210}
211
212impl fmt::Display for SolidJsxRuntime {
213 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
214 formatter.write_str(self.as_str())
215 }
216}
217
218impl FromStr for SolidJsxRuntime {
219 type Err = SolidNameError;
220
221 fn from_str(input: &str) -> Result<Self, Self::Err> {
222 match normalized_label(input)?.as_str() {
223 "classic" => Ok(Self::Classic),
224 "automatic" | "auto" => Ok(Self::Automatic),
225 _ => Err(SolidNameError::UnknownLabel),
226 }
227 }
228}
229
230#[derive(Clone, Debug, Eq, PartialEq)]
232pub enum SolidNameError {
233 Identifier(JsIdentifierError),
234 NotPascalCase,
235 Empty,
236 UnknownLabel,
237}
238
239impl fmt::Display for SolidNameError {
240 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
241 match self {
242 Self::Identifier(error) => write!(formatter, "{error}"),
243 Self::NotPascalCase => {
244 formatter.write_str("Solid component name must be `PascalCase`-shaped")
245 }
246 Self::Empty => formatter.write_str("Solid metadata label cannot be empty"),
247 Self::UnknownLabel => formatter.write_str("unknown Solid metadata label"),
248 }
249 }
250}
251
252impl Error for SolidNameError {
253 fn source(&self) -> Option<&(dyn Error + 'static)> {
254 match self {
255 Self::Identifier(error) => Some(error),
256 Self::NotPascalCase | Self::Empty | Self::UnknownLabel => None,
257 }
258 }
259}
260
261fn validate_pascal_case(input: &str) -> Result<String, SolidNameError> {
262 let identifier = JsIdentifier::new(input).map_err(SolidNameError::Identifier)?;
263 if !identifier
264 .as_str()
265 .chars()
266 .next()
267 .is_some_and(|character| character.is_ascii_uppercase())
268 {
269 return Err(SolidNameError::NotPascalCase);
270 }
271 Ok(identifier.as_str().to_string())
272}
273
274fn normalized_label(input: &str) -> Result<String, SolidNameError> {
275 let trimmed = input.trim();
276 if trimmed.is_empty() {
277 return Err(SolidNameError::Empty);
278 }
279 Ok(trimmed
280 .chars()
281 .filter(|character| !matches!(character, '-' | '_' | ' '))
282 .flat_map(char::to_lowercase)
283 .collect())
284}
285
286#[cfg(test)]
287mod tests {
288 use super::{
289 SolidComponentName, SolidFileKind, SolidJsxRuntime, SolidNameError, SolidPrimitiveKind,
290 SolidSignalName,
291 };
292
293 #[test]
294 fn validates_component_names() -> Result<(), SolidNameError> {
295 let component = SolidComponentName::new("CounterPanel")?;
296 assert_eq!(component.as_str(), "CounterPanel");
297 assert_eq!(
298 SolidComponentName::new("counterPanel"),
299 Err(SolidNameError::NotPascalCase)
300 );
301 assert!(SolidComponentName::new("counter-panel").is_err());
302 Ok(())
303 }
304
305 #[test]
306 fn validates_signal_names() -> Result<(), SolidNameError> {
307 let signal = SolidSignalName::new("count")?;
308 assert_eq!(signal.as_str(), "count");
309 assert!(SolidSignalName::new("count-value").is_err());
310 Ok(())
311 }
312
313 #[test]
314 fn parses_labels() -> Result<(), SolidNameError> {
315 assert_eq!(
316 "component".parse::<SolidFileKind>()?,
317 SolidFileKind::Component
318 );
319 assert_eq!(
320 "signal".parse::<SolidPrimitiveKind>()?,
321 SolidPrimitiveKind::Signal
322 );
323 assert_eq!(
324 "automatic".parse::<SolidJsxRuntime>()?,
325 SolidJsxRuntime::Automatic
326 );
327 assert_eq!(SolidPrimitiveKind::Memo.to_string(), "memo");
328 Ok(())
329 }
330}