Skip to main content

use_solid/
lib.rs

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/// Validated `SolidJS` component name metadata.
9#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
10pub struct SolidComponentName(String);
11
12impl SolidComponentName {
13    /// Creates a `PascalCase` ASCII `SolidJS` component name.
14    ///
15    /// # Errors
16    ///
17    /// Returns [`SolidNameError`] when `input` is not an ASCII identifier or is not `PascalCase`-shaped.
18    pub fn new(input: &str) -> Result<Self, SolidNameError> {
19        validate_pascal_case(input).map(Self)
20    }
21
22    /// Returns the component name.
23    #[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/// Validated `SolidJS` signal name metadata.
52#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
53pub struct SolidSignalName(String);
54
55impl SolidSignalName {
56    /// Creates ASCII `SolidJS` signal name metadata.
57    ///
58    /// # Errors
59    ///
60    /// Returns [`SolidNameError`] when `input` is not an ASCII JavaScript identifier.
61    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    /// Returns the signal name.
67    #[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/// `SolidJS` file-kind labels.
96#[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    /// Returns the file-kind label.
108    #[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/// `SolidJS` primitive-kind labels.
144#[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    /// Returns the primitive-kind label.
157    #[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/// `SolidJS` JSX runtime labels.
195#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
196pub enum SolidJsxRuntime {
197    Classic,
198    Automatic,
199}
200
201impl SolidJsxRuntime {
202    /// Returns the JSX runtime label.
203    #[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/// Error returned when `SolidJS` metadata is invalid.
231#[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}