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 QwikComponentName(String);
11
12impl QwikComponentName {
13 pub fn new(input: &str) -> Result<Self, QwikNameError> {
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 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#[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 #[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#[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 #[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#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
149pub enum QwikOptimizerMode {
150 Development,
151 Production,
152 Library,
153}
154
155impl QwikOptimizerMode {
156 #[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#[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 #[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#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
233pub enum QwikConfigFile {
234 ViteConfigTs,
235 QwikCityPlan,
236}
237
238impl QwikConfigFile {
239 #[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#[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}