use-solid 0.0.1

SolidJS 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 `SolidJS` component name metadata.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct SolidComponentName(String);

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

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

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

impl FromStr for SolidComponentName {
    type Err = SolidNameError;

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

impl TryFrom<&str> for SolidComponentName {
    type Error = SolidNameError;

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

/// Validated `SolidJS` signal name metadata.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct SolidSignalName(String);

impl SolidSignalName {
    /// Creates ASCII `SolidJS` signal name metadata.
    ///
    /// # Errors
    ///
    /// Returns [`SolidNameError`] when `input` is not an ASCII JavaScript identifier.
    pub fn new(input: &str) -> Result<Self, SolidNameError> {
        let identifier = JsIdentifier::new(input).map_err(SolidNameError::Identifier)?;
        Ok(Self(identifier.as_str().to_string()))
    }

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

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

impl FromStr for SolidSignalName {
    type Err = SolidNameError;

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

impl TryFrom<&str> for SolidSignalName {
    type Error = SolidNameError;

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

/// `SolidJS` file-kind labels.
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum SolidFileKind {
    Component,
    Signal,
    Resource,
    Store,
    Route,
    Context,
}

impl SolidFileKind {
    /// Returns the file-kind label.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Component => "component",
            Self::Signal => "signal",
            Self::Resource => "resource",
            Self::Store => "store",
            Self::Route => "route",
            Self::Context => "context",
        }
    }
}

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

impl FromStr for SolidFileKind {
    type Err = SolidNameError;

    fn from_str(input: &str) -> Result<Self, Self::Err> {
        match normalized_label(input)?.as_str() {
            "component" => Ok(Self::Component),
            "signal" => Ok(Self::Signal),
            "resource" => Ok(Self::Resource),
            "store" => Ok(Self::Store),
            "route" => Ok(Self::Route),
            "context" => Ok(Self::Context),
            _ => Err(SolidNameError::UnknownLabel),
        }
    }
}

/// `SolidJS` primitive-kind labels.
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum SolidPrimitiveKind {
    Signal,
    Memo,
    Effect,
    Resource,
    Store,
    Context,
    Root,
}

impl SolidPrimitiveKind {
    /// Returns the primitive-kind label.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Signal => "signal",
            Self::Memo => "memo",
            Self::Effect => "effect",
            Self::Resource => "resource",
            Self::Store => "store",
            Self::Context => "context",
            Self::Root => "root",
        }
    }
}

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

impl FromStr for SolidPrimitiveKind {
    type Err = SolidNameError;

    fn from_str(input: &str) -> Result<Self, Self::Err> {
        match normalized_label(input)?.as_str() {
            "signal" => Ok(Self::Signal),
            "memo" => Ok(Self::Memo),
            "effect" => Ok(Self::Effect),
            "resource" => Ok(Self::Resource),
            "store" => Ok(Self::Store),
            "context" => Ok(Self::Context),
            "root" => Ok(Self::Root),
            _ => Err(SolidNameError::UnknownLabel),
        }
    }
}

/// `SolidJS` JSX runtime labels.
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum SolidJsxRuntime {
    Classic,
    Automatic,
}

impl SolidJsxRuntime {
    /// Returns the JSX runtime label.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Classic => "classic",
            Self::Automatic => "automatic",
        }
    }
}

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

impl FromStr for SolidJsxRuntime {
    type Err = SolidNameError;

    fn from_str(input: &str) -> Result<Self, Self::Err> {
        match normalized_label(input)?.as_str() {
            "classic" => Ok(Self::Classic),
            "automatic" | "auto" => Ok(Self::Automatic),
            _ => Err(SolidNameError::UnknownLabel),
        }
    }
}

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

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

impl Error for SolidNameError {
    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, SolidNameError> {
    let identifier = JsIdentifier::new(input).map_err(SolidNameError::Identifier)?;
    if !identifier
        .as_str()
        .chars()
        .next()
        .is_some_and(|character| character.is_ascii_uppercase())
    {
        return Err(SolidNameError::NotPascalCase);
    }
    Ok(identifier.as_str().to_string())
}

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

#[cfg(test)]
mod tests {
    use super::{
        SolidComponentName, SolidFileKind, SolidJsxRuntime, SolidNameError, SolidPrimitiveKind,
        SolidSignalName,
    };

    #[test]
    fn validates_component_names() -> Result<(), SolidNameError> {
        let component = SolidComponentName::new("CounterPanel")?;
        assert_eq!(component.as_str(), "CounterPanel");
        assert_eq!(
            SolidComponentName::new("counterPanel"),
            Err(SolidNameError::NotPascalCase)
        );
        assert!(SolidComponentName::new("counter-panel").is_err());
        Ok(())
    }

    #[test]
    fn validates_signal_names() -> Result<(), SolidNameError> {
        let signal = SolidSignalName::new("count")?;
        assert_eq!(signal.as_str(), "count");
        assert!(SolidSignalName::new("count-value").is_err());
        Ok(())
    }

    #[test]
    fn parses_labels() -> Result<(), SolidNameError> {
        assert_eq!(
            "component".parse::<SolidFileKind>()?,
            SolidFileKind::Component
        );
        assert_eq!(
            "signal".parse::<SolidPrimitiveKind>()?,
            SolidPrimitiveKind::Signal
        );
        assert_eq!(
            "automatic".parse::<SolidJsxRuntime>()?,
            SolidJsxRuntime::Automatic
        );
        assert_eq!(SolidPrimitiveKind::Memo.to_string(), "memo");
        Ok(())
    }
}