1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::fmt;
5use std::error::Error;
6use use_js_identifier::{JsIdentifier, JsIdentifierError};
7
8#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
10pub struct VueComponentName(String);
11
12impl VueComponentName {
13 pub fn new(input: &str) -> Result<Self, VueNameError> {
19 let trimmed = input.trim();
20 if trimmed.is_empty() {
21 return Err(VueNameError::Empty);
22 }
23 if trimmed.chars().any(char::is_whitespace) {
24 return Err(VueNameError::ContainsWhitespace);
25 }
26 if trimmed.contains('-') {
27 if trimmed.split('-').any(str::is_empty) {
28 return Err(VueNameError::InvalidKebabName);
29 }
30 return Ok(Self(trimmed.to_string()));
31 }
32
33 let identifier = JsIdentifier::new(trimmed).map_err(VueNameError::Identifier)?;
34 if !identifier
35 .as_str()
36 .chars()
37 .next()
38 .is_some_and(|character| character.is_ascii_uppercase())
39 {
40 return Err(VueNameError::NotComponentName);
41 }
42 Ok(Self(identifier.as_str().to_string()))
43 }
44
45 #[must_use]
47 pub fn as_str(&self) -> &str {
48 &self.0
49 }
50}
51
52#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
54pub enum VueFileKind {
55 Component,
56 Composable,
57 Store,
58 Page,
59 Layout,
60}
61
62#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
64pub enum VueApiStyle {
65 Options,
66 Composition,
67 ScriptSetup,
68}
69
70impl VueApiStyle {
71 #[must_use]
73 pub const fn as_str(self) -> &'static str {
74 match self {
75 Self::Options => "options",
76 Self::Composition => "composition",
77 Self::ScriptSetup => "script-setup",
78 }
79 }
80}
81
82#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
84pub struct VueDirectiveName(String);
85
86impl VueDirectiveName {
87 pub fn new(input: &str) -> Result<Self, VueNameError> {
93 let trimmed_input = input.trim();
94 let trimmed = trimmed_input.strip_prefix("v-").unwrap_or(trimmed_input);
95 if trimmed.is_empty() {
96 return Err(VueNameError::Empty);
97 }
98 if trimmed.chars().any(char::is_whitespace) {
99 return Err(VueNameError::ContainsWhitespace);
100 }
101 Ok(Self(trimmed.to_string()))
102 }
103
104 #[must_use]
106 pub fn as_str(&self) -> &str {
107 &self.0
108 }
109}
110
111#[derive(Clone, Debug, Eq, PartialEq)]
113pub enum VueNameError {
114 Empty,
115 ContainsWhitespace,
116 Identifier(JsIdentifierError),
117 NotComponentName,
118 InvalidKebabName,
119}
120
121impl fmt::Display for VueNameError {
122 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
123 match self {
124 Self::Empty => formatter.write_str("Vue metadata name cannot be empty"),
125 Self::ContainsWhitespace => {
126 formatter.write_str("Vue metadata name cannot contain whitespace")
127 }
128 Self::Identifier(error) => write!(formatter, "invalid JavaScript identifier: {error}"),
129 Self::NotComponentName => formatter
130 .write_str("Vue component name must be PascalCase-shaped or kebab-case-shaped"),
131 Self::InvalidKebabName => {
132 formatter.write_str("Vue kebab-case name contains an empty segment")
133 }
134 }
135 }
136}
137
138impl Error for VueNameError {
139 fn source(&self) -> Option<&(dyn Error + 'static)> {
140 match self {
141 Self::Identifier(error) => Some(error),
142 Self::Empty
143 | Self::ContainsWhitespace
144 | Self::NotComponentName
145 | Self::InvalidKebabName => None,
146 }
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::{VueApiStyle, VueComponentName, VueDirectiveName, VueNameError};
153
154 #[test]
155 fn validates_component_names() -> Result<(), VueNameError> {
156 assert_eq!(VueComponentName::new("UserCard")?.as_str(), "UserCard");
157 assert_eq!(VueComponentName::new("user-card")?.as_str(), "user-card");
158 assert_eq!(
159 VueComponentName::new("user--card"),
160 Err(VueNameError::InvalidKebabName)
161 );
162 Ok(())
163 }
164
165 #[test]
166 fn validates_directive_names() -> Result<(), VueNameError> {
167 let directive = VueDirectiveName::new("v-focus")?;
168 assert_eq!(directive.as_str(), "focus");
169 assert_eq!(VueApiStyle::ScriptSetup.as_str(), "script-setup");
170 Ok(())
171 }
172}