#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::fmt;
use std::error::Error;
use use_js_identifier::{JsIdentifier, JsIdentifierError};
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct VueComponentName(String);
impl VueComponentName {
pub fn new(input: &str) -> Result<Self, VueNameError> {
let trimmed = input.trim();
if trimmed.is_empty() {
return Err(VueNameError::Empty);
}
if trimmed.chars().any(char::is_whitespace) {
return Err(VueNameError::ContainsWhitespace);
}
if trimmed.contains('-') {
if trimmed.split('-').any(str::is_empty) {
return Err(VueNameError::InvalidKebabName);
}
return Ok(Self(trimmed.to_string()));
}
let identifier = JsIdentifier::new(trimmed).map_err(VueNameError::Identifier)?;
if !identifier
.as_str()
.chars()
.next()
.is_some_and(|character| character.is_ascii_uppercase())
{
return Err(VueNameError::NotComponentName);
}
Ok(Self(identifier.as_str().to_string()))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum VueFileKind {
Component,
Composable,
Store,
Page,
Layout,
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum VueApiStyle {
Options,
Composition,
ScriptSetup,
}
impl VueApiStyle {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Options => "options",
Self::Composition => "composition",
Self::ScriptSetup => "script-setup",
}
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct VueDirectiveName(String);
impl VueDirectiveName {
pub fn new(input: &str) -> Result<Self, VueNameError> {
let trimmed_input = input.trim();
let trimmed = trimmed_input.strip_prefix("v-").unwrap_or(trimmed_input);
if trimmed.is_empty() {
return Err(VueNameError::Empty);
}
if trimmed.chars().any(char::is_whitespace) {
return Err(VueNameError::ContainsWhitespace);
}
Ok(Self(trimmed.to_string()))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum VueNameError {
Empty,
ContainsWhitespace,
Identifier(JsIdentifierError),
NotComponentName,
InvalidKebabName,
}
impl fmt::Display for VueNameError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("Vue metadata name cannot be empty"),
Self::ContainsWhitespace => {
formatter.write_str("Vue metadata name cannot contain whitespace")
}
Self::Identifier(error) => write!(formatter, "invalid JavaScript identifier: {error}"),
Self::NotComponentName => formatter
.write_str("Vue component name must be PascalCase-shaped or kebab-case-shaped"),
Self::InvalidKebabName => {
formatter.write_str("Vue kebab-case name contains an empty segment")
}
}
}
}
impl Error for VueNameError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Identifier(error) => Some(error),
Self::Empty
| Self::ContainsWhitespace
| Self::NotComponentName
| Self::InvalidKebabName => None,
}
}
}
#[cfg(test)]
mod tests {
use super::{VueApiStyle, VueComponentName, VueDirectiveName, VueNameError};
#[test]
fn validates_component_names() -> Result<(), VueNameError> {
assert_eq!(VueComponentName::new("UserCard")?.as_str(), "UserCard");
assert_eq!(VueComponentName::new("user-card")?.as_str(), "user-card");
assert_eq!(
VueComponentName::new("user--card"),
Err(VueNameError::InvalidKebabName)
);
Ok(())
}
#[test]
fn validates_directive_names() -> Result<(), VueNameError> {
let directive = VueDirectiveName::new("v-focus")?;
assert_eq!(directive.as_str(), "focus");
assert_eq!(VueApiStyle::ScriptSetup.as_str(), "script-setup");
Ok(())
}
}