use-qwik 0.0.1

Qwik metadata primitives for RustUse
Documentation
#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]

use core::{fmt, str::FromStr};
use std::error::Error;
use use_js_identifier::{JsIdentifier, JsIdentifierError};

/// Validated Qwik component name metadata.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct QwikComponentName(String);

impl QwikComponentName {
    /// Creates a `PascalCase` ASCII Qwik component name.
    ///
    /// # Errors
    ///
    /// Returns [`QwikNameError`] when `input` is not an ASCII identifier or is not `PascalCase`-shaped.
    pub fn new(input: &str) -> Result<Self, QwikNameError> {
        validate_pascal_case(input).map(Self)
    }

    /// Returns the component name.
    #[must_use]
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl fmt::Display for QwikComponentName {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(self.as_str())
    }
}

impl FromStr for QwikComponentName {
    type Err = QwikNameError;

    fn from_str(input: &str) -> Result<Self, Self::Err> {
        Self::new(input)
    }
}

impl TryFrom<&str> for QwikComponentName {
    type Error = QwikNameError;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        Self::new(value)
    }
}

/// Qwik file-kind labels.
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum QwikFileKind {
    Component,
    Route,
    Layout,
    Endpoint,
    Entry,
    Config,
}

impl QwikFileKind {
    /// Returns the file-kind label.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Component => "component",
            Self::Route => "route",
            Self::Layout => "layout",
            Self::Endpoint => "endpoint",
            Self::Entry => "entry",
            Self::Config => "config",
        }
    }
}

impl fmt::Display for QwikFileKind {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(self.as_str())
    }
}

impl FromStr for QwikFileKind {
    type Err = QwikNameError;

    fn from_str(input: &str) -> Result<Self, Self::Err> {
        match normalized_label(input)?.as_str() {
            "component" => Ok(Self::Component),
            "route" => Ok(Self::Route),
            "layout" => Ok(Self::Layout),
            "endpoint" => Ok(Self::Endpoint),
            "entry" => Ok(Self::Entry),
            "config" => Ok(Self::Config),
            _ => Err(QwikNameError::UnknownLabel),
        }
    }
}

/// Qwik directory labels.
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum QwikDirectoryKind {
    Routes,
    Components,
    Public,
    Src,
    Server,
    Lib,
}

impl QwikDirectoryKind {
    /// Returns the directory label.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Routes => "routes",
            Self::Components => "components",
            Self::Public => "public",
            Self::Src => "src",
            Self::Server => "server",
            Self::Lib => "lib",
        }
    }
}

impl fmt::Display for QwikDirectoryKind {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(self.as_str())
    }
}

impl FromStr for QwikDirectoryKind {
    type Err = QwikNameError;

    fn from_str(input: &str) -> Result<Self, Self::Err> {
        match normalized_label(input)?.as_str() {
            "routes" => Ok(Self::Routes),
            "components" => Ok(Self::Components),
            "public" => Ok(Self::Public),
            "src" => Ok(Self::Src),
            "server" => Ok(Self::Server),
            "lib" => Ok(Self::Lib),
            _ => Err(QwikNameError::UnknownLabel),
        }
    }
}

/// Qwik optimizer mode labels.
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum QwikOptimizerMode {
    Development,
    Production,
    Library,
}

impl QwikOptimizerMode {
    /// Returns the optimizer mode label.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Development => "development",
            Self::Production => "production",
            Self::Library => "library",
        }
    }
}

impl fmt::Display for QwikOptimizerMode {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(self.as_str())
    }
}

impl FromStr for QwikOptimizerMode {
    type Err = QwikNameError;

    fn from_str(input: &str) -> Result<Self, Self::Err> {
        match normalized_label(input)?.as_str() {
            "development" | "dev" => Ok(Self::Development),
            "production" | "prod" => Ok(Self::Production),
            "library" | "lib" => Ok(Self::Library),
            _ => Err(QwikNameError::UnknownLabel),
        }
    }
}

/// Qwik City route-kind labels.
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum QwikCityRouteKind {
    Page,
    Layout,
    Endpoint,
    Plugin,
    Middleware,
}

impl QwikCityRouteKind {
    /// Returns the Qwik City route-kind label.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Page => "page",
            Self::Layout => "layout",
            Self::Endpoint => "endpoint",
            Self::Plugin => "plugin",
            Self::Middleware => "middleware",
        }
    }
}

impl fmt::Display for QwikCityRouteKind {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(self.as_str())
    }
}

impl FromStr for QwikCityRouteKind {
    type Err = QwikNameError;

    fn from_str(input: &str) -> Result<Self, Self::Err> {
        match normalized_label(input)?.as_str() {
            "page" => Ok(Self::Page),
            "layout" => Ok(Self::Layout),
            "endpoint" => Ok(Self::Endpoint),
            "plugin" => Ok(Self::Plugin),
            "middleware" => Ok(Self::Middleware),
            _ => Err(QwikNameError::UnknownLabel),
        }
    }
}

/// Common Qwik config file labels.
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum QwikConfigFile {
    ViteConfigTs,
    QwikCityPlan,
}

impl QwikConfigFile {
    /// Returns the config file label.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::ViteConfigTs => "vite.config.ts",
            Self::QwikCityPlan => "qwik-city-plan",
        }
    }
}

impl fmt::Display for QwikConfigFile {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(self.as_str())
    }
}

impl FromStr for QwikConfigFile {
    type Err = QwikNameError;

    fn from_str(input: &str) -> Result<Self, Self::Err> {
        match normalized_label(input)?.as_str() {
            "viteconfigts" | "vite.config.ts" => Ok(Self::ViteConfigTs),
            "qwikcityplan" => Ok(Self::QwikCityPlan),
            _ => Err(QwikNameError::UnknownLabel),
        }
    }
}

/// Error returned when Qwik metadata is invalid.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum QwikNameError {
    Identifier(JsIdentifierError),
    NotPascalCase,
    Empty,
    UnknownLabel,
}

impl fmt::Display for QwikNameError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Identifier(error) => write!(formatter, "{error}"),
            Self::NotPascalCase => {
                formatter.write_str("Qwik component name must be `PascalCase`-shaped")
            }
            Self::Empty => formatter.write_str("Qwik metadata label cannot be empty"),
            Self::UnknownLabel => formatter.write_str("unknown Qwik metadata label"),
        }
    }
}

impl Error for QwikNameError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            Self::Identifier(error) => Some(error),
            Self::NotPascalCase | Self::Empty | Self::UnknownLabel => None,
        }
    }
}

fn validate_pascal_case(input: &str) -> Result<String, QwikNameError> {
    let identifier = JsIdentifier::new(input).map_err(QwikNameError::Identifier)?;
    if !identifier
        .as_str()
        .chars()
        .next()
        .is_some_and(|character| character.is_ascii_uppercase())
    {
        return Err(QwikNameError::NotPascalCase);
    }
    Ok(identifier.as_str().to_string())
}

fn normalized_label(input: &str) -> Result<String, QwikNameError> {
    let trimmed = input.trim();
    if trimmed.is_empty() {
        return Err(QwikNameError::Empty);
    }
    Ok(trimmed
        .chars()
        .filter(|character| !matches!(character, '-' | '_' | ' '))
        .flat_map(char::to_lowercase)
        .collect())
}

#[cfg(test)]
mod tests {
    use super::{
        QwikCityRouteKind, QwikComponentName, QwikConfigFile, QwikDirectoryKind, QwikFileKind,
        QwikNameError, QwikOptimizerMode,
    };

    #[test]
    fn validates_component_names() -> Result<(), QwikNameError> {
        let component = QwikComponentName::new("HeroPanel")?;
        assert_eq!(component.as_str(), "HeroPanel");
        assert_eq!(
            QwikComponentName::new("heroPanel"),
            Err(QwikNameError::NotPascalCase)
        );
        assert!(QwikComponentName::new("hero-panel").is_err());
        Ok(())
    }

    #[test]
    fn parses_labels() -> Result<(), QwikNameError> {
        assert_eq!(
            "component".parse::<QwikFileKind>()?,
            QwikFileKind::Component
        );
        assert_eq!(
            "routes".parse::<QwikDirectoryKind>()?,
            QwikDirectoryKind::Routes
        );
        assert_eq!(
            "production".parse::<QwikOptimizerMode>()?,
            QwikOptimizerMode::Production
        );
        assert_eq!(
            "endpoint".parse::<QwikCityRouteKind>()?,
            QwikCityRouteKind::Endpoint
        );
        assert_eq!(
            "vite.config.ts".parse::<QwikConfigFile>()?,
            QwikConfigFile::ViteConfigTs
        );
        assert_eq!(QwikOptimizerMode::Development.to_string(), "development");
        Ok(())
    }
}