Skip to main content

use_qwik/
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 Qwik component name metadata.
9#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
10pub struct QwikComponentName(String);
11
12impl QwikComponentName {
13    /// Creates a `PascalCase` ASCII Qwik component name.
14    ///
15    /// # Errors
16    ///
17    /// Returns [`QwikNameError`] when `input` is not an ASCII identifier or is not `PascalCase`-shaped.
18    pub fn new(input: &str) -> Result<Self, QwikNameError> {
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 QwikComponentName {
30    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
31        formatter.write_str(self.as_str())
32    }
33}
34
35impl FromStr for QwikComponentName {
36    type Err = QwikNameError;
37
38    fn from_str(input: &str) -> Result<Self, Self::Err> {
39        Self::new(input)
40    }
41}
42
43impl TryFrom<&str> for QwikComponentName {
44    type Error = QwikNameError;
45
46    fn try_from(value: &str) -> Result<Self, Self::Error> {
47        Self::new(value)
48    }
49}
50
51/// Qwik file-kind labels.
52#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
53pub enum QwikFileKind {
54    Component,
55    Route,
56    Layout,
57    Endpoint,
58    Entry,
59    Config,
60}
61
62impl QwikFileKind {
63    /// Returns the file-kind label.
64    #[must_use]
65    pub const fn as_str(self) -> &'static str {
66        match self {
67            Self::Component => "component",
68            Self::Route => "route",
69            Self::Layout => "layout",
70            Self::Endpoint => "endpoint",
71            Self::Entry => "entry",
72            Self::Config => "config",
73        }
74    }
75}
76
77impl fmt::Display for QwikFileKind {
78    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
79        formatter.write_str(self.as_str())
80    }
81}
82
83impl FromStr for QwikFileKind {
84    type Err = QwikNameError;
85
86    fn from_str(input: &str) -> Result<Self, Self::Err> {
87        match normalized_label(input)?.as_str() {
88            "component" => Ok(Self::Component),
89            "route" => Ok(Self::Route),
90            "layout" => Ok(Self::Layout),
91            "endpoint" => Ok(Self::Endpoint),
92            "entry" => Ok(Self::Entry),
93            "config" => Ok(Self::Config),
94            _ => Err(QwikNameError::UnknownLabel),
95        }
96    }
97}
98
99/// Qwik directory labels.
100#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
101pub enum QwikDirectoryKind {
102    Routes,
103    Components,
104    Public,
105    Src,
106    Server,
107    Lib,
108}
109
110impl QwikDirectoryKind {
111    /// Returns the directory label.
112    #[must_use]
113    pub const fn as_str(self) -> &'static str {
114        match self {
115            Self::Routes => "routes",
116            Self::Components => "components",
117            Self::Public => "public",
118            Self::Src => "src",
119            Self::Server => "server",
120            Self::Lib => "lib",
121        }
122    }
123}
124
125impl fmt::Display for QwikDirectoryKind {
126    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
127        formatter.write_str(self.as_str())
128    }
129}
130
131impl FromStr for QwikDirectoryKind {
132    type Err = QwikNameError;
133
134    fn from_str(input: &str) -> Result<Self, Self::Err> {
135        match normalized_label(input)?.as_str() {
136            "routes" => Ok(Self::Routes),
137            "components" => Ok(Self::Components),
138            "public" => Ok(Self::Public),
139            "src" => Ok(Self::Src),
140            "server" => Ok(Self::Server),
141            "lib" => Ok(Self::Lib),
142            _ => Err(QwikNameError::UnknownLabel),
143        }
144    }
145}
146
147/// Qwik optimizer mode labels.
148#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
149pub enum QwikOptimizerMode {
150    Development,
151    Production,
152    Library,
153}
154
155impl QwikOptimizerMode {
156    /// Returns the optimizer mode label.
157    #[must_use]
158    pub const fn as_str(self) -> &'static str {
159        match self {
160            Self::Development => "development",
161            Self::Production => "production",
162            Self::Library => "library",
163        }
164    }
165}
166
167impl fmt::Display for QwikOptimizerMode {
168    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
169        formatter.write_str(self.as_str())
170    }
171}
172
173impl FromStr for QwikOptimizerMode {
174    type Err = QwikNameError;
175
176    fn from_str(input: &str) -> Result<Self, Self::Err> {
177        match normalized_label(input)?.as_str() {
178            "development" | "dev" => Ok(Self::Development),
179            "production" | "prod" => Ok(Self::Production),
180            "library" | "lib" => Ok(Self::Library),
181            _ => Err(QwikNameError::UnknownLabel),
182        }
183    }
184}
185
186/// Qwik City route-kind labels.
187#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
188pub enum QwikCityRouteKind {
189    Page,
190    Layout,
191    Endpoint,
192    Plugin,
193    Middleware,
194}
195
196impl QwikCityRouteKind {
197    /// Returns the Qwik City route-kind label.
198    #[must_use]
199    pub const fn as_str(self) -> &'static str {
200        match self {
201            Self::Page => "page",
202            Self::Layout => "layout",
203            Self::Endpoint => "endpoint",
204            Self::Plugin => "plugin",
205            Self::Middleware => "middleware",
206        }
207    }
208}
209
210impl fmt::Display for QwikCityRouteKind {
211    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
212        formatter.write_str(self.as_str())
213    }
214}
215
216impl FromStr for QwikCityRouteKind {
217    type Err = QwikNameError;
218
219    fn from_str(input: &str) -> Result<Self, Self::Err> {
220        match normalized_label(input)?.as_str() {
221            "page" => Ok(Self::Page),
222            "layout" => Ok(Self::Layout),
223            "endpoint" => Ok(Self::Endpoint),
224            "plugin" => Ok(Self::Plugin),
225            "middleware" => Ok(Self::Middleware),
226            _ => Err(QwikNameError::UnknownLabel),
227        }
228    }
229}
230
231/// Common Qwik config file labels.
232#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
233pub enum QwikConfigFile {
234    ViteConfigTs,
235    QwikCityPlan,
236}
237
238impl QwikConfigFile {
239    /// Returns the config file label.
240    #[must_use]
241    pub const fn as_str(self) -> &'static str {
242        match self {
243            Self::ViteConfigTs => "vite.config.ts",
244            Self::QwikCityPlan => "qwik-city-plan",
245        }
246    }
247}
248
249impl fmt::Display for QwikConfigFile {
250    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
251        formatter.write_str(self.as_str())
252    }
253}
254
255impl FromStr for QwikConfigFile {
256    type Err = QwikNameError;
257
258    fn from_str(input: &str) -> Result<Self, Self::Err> {
259        match normalized_label(input)?.as_str() {
260            "viteconfigts" | "vite.config.ts" => Ok(Self::ViteConfigTs),
261            "qwikcityplan" => Ok(Self::QwikCityPlan),
262            _ => Err(QwikNameError::UnknownLabel),
263        }
264    }
265}
266
267/// Error returned when Qwik metadata is invalid.
268#[derive(Clone, Debug, Eq, PartialEq)]
269pub enum QwikNameError {
270    Identifier(JsIdentifierError),
271    NotPascalCase,
272    Empty,
273    UnknownLabel,
274}
275
276impl fmt::Display for QwikNameError {
277    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
278        match self {
279            Self::Identifier(error) => write!(formatter, "{error}"),
280            Self::NotPascalCase => {
281                formatter.write_str("Qwik component name must be `PascalCase`-shaped")
282            }
283            Self::Empty => formatter.write_str("Qwik metadata label cannot be empty"),
284            Self::UnknownLabel => formatter.write_str("unknown Qwik metadata label"),
285        }
286    }
287}
288
289impl Error for QwikNameError {
290    fn source(&self) -> Option<&(dyn Error + 'static)> {
291        match self {
292            Self::Identifier(error) => Some(error),
293            Self::NotPascalCase | Self::Empty | Self::UnknownLabel => None,
294        }
295    }
296}
297
298fn validate_pascal_case(input: &str) -> Result<String, QwikNameError> {
299    let identifier = JsIdentifier::new(input).map_err(QwikNameError::Identifier)?;
300    if !identifier
301        .as_str()
302        .chars()
303        .next()
304        .is_some_and(|character| character.is_ascii_uppercase())
305    {
306        return Err(QwikNameError::NotPascalCase);
307    }
308    Ok(identifier.as_str().to_string())
309}
310
311fn normalized_label(input: &str) -> Result<String, QwikNameError> {
312    let trimmed = input.trim();
313    if trimmed.is_empty() {
314        return Err(QwikNameError::Empty);
315    }
316    Ok(trimmed
317        .chars()
318        .filter(|character| !matches!(character, '-' | '_' | ' '))
319        .flat_map(char::to_lowercase)
320        .collect())
321}
322
323#[cfg(test)]
324mod tests {
325    use super::{
326        QwikCityRouteKind, QwikComponentName, QwikConfigFile, QwikDirectoryKind, QwikFileKind,
327        QwikNameError, QwikOptimizerMode,
328    };
329
330    #[test]
331    fn validates_component_names() -> Result<(), QwikNameError> {
332        let component = QwikComponentName::new("HeroPanel")?;
333        assert_eq!(component.as_str(), "HeroPanel");
334        assert_eq!(
335            QwikComponentName::new("heroPanel"),
336            Err(QwikNameError::NotPascalCase)
337        );
338        assert!(QwikComponentName::new("hero-panel").is_err());
339        Ok(())
340    }
341
342    #[test]
343    fn parses_labels() -> Result<(), QwikNameError> {
344        assert_eq!(
345            "component".parse::<QwikFileKind>()?,
346            QwikFileKind::Component
347        );
348        assert_eq!(
349            "routes".parse::<QwikDirectoryKind>()?,
350            QwikDirectoryKind::Routes
351        );
352        assert_eq!(
353            "production".parse::<QwikOptimizerMode>()?,
354            QwikOptimizerMode::Production
355        );
356        assert_eq!(
357            "endpoint".parse::<QwikCityRouteKind>()?,
358            QwikCityRouteKind::Endpoint
359        );
360        assert_eq!(
361            "vite.config.ts".parse::<QwikConfigFile>()?,
362            QwikConfigFile::ViteConfigTs
363        );
364        assert_eq!(QwikOptimizerMode::Development.to_string(), "development");
365        Ok(())
366    }
367}