use-design-token 0.1.0

Framework-neutral design-token primitives for RustUse
Documentation
#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]

/// A design-token name segment.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
pub struct TokenName(String);

impl TokenName {
    pub fn new(value: impl Into<String>) -> Self {
        Self(value.into())
    }

    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl AsRef<str> for TokenName {
    fn as_ref(&self) -> &str {
        self.as_str()
    }
}

/// A hierarchical token path such as `color.background.primary`.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct TokenPath(Vec<TokenName>);

impl TokenPath {
    pub fn new(segments: Vec<TokenName>) -> Self {
        Self(segments)
    }

    pub fn from_segments<I, S>(segments: I) -> Self
    where
        I: IntoIterator<Item = S>,
        S: Into<String>,
    {
        Self(segments.into_iter().map(TokenName::new).collect())
    }

    pub fn segments(&self) -> &[TokenName] {
        &self.0
    }

    pub fn is_empty(&self) -> bool {
        self.0.is_empty()
    }

    pub fn len(&self) -> usize {
        self.0.len()
    }
}

/// Broad design-token categories.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
pub enum TokenCategory {
    Color,
    Typography,
    Spacing,
    Radius,
    Shadow,
    Motion,
    Breakpoint,
    Layer,
    Component,
    Semantic,
}

impl TokenCategory {
    pub fn as_str(self) -> &'static str {
        match self {
            Self::Color => "color",
            Self::Typography => "typography",
            Self::Spacing => "spacing",
            Self::Radius => "radius",
            Self::Shadow => "shadow",
            Self::Motion => "motion",
            Self::Breakpoint => "breakpoint",
            Self::Layer => "layer",
            Self::Component => "component",
            Self::Semantic => "semantic",
        }
    }
}

/// A reference to another design token.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct TokenReference {
    path: TokenPath,
}

impl TokenReference {
    pub fn new(path: TokenPath) -> Self {
        Self { path }
    }

    pub fn path(&self) -> &TokenPath {
        &self.path
    }
}

/// A simple design-token value.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum TokenValue {
    Text(String),
    Integer(i64),
    Boolean(bool),
    Reference(TokenReference),
}

impl TokenValue {
    pub fn text(value: impl Into<String>) -> Self {
        Self::Text(value.into())
    }
}

/// Primitive design-token metadata.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct DesignToken {
    name: TokenName,
    category: TokenCategory,
    value: TokenValue,
    path: Option<TokenPath>,
}

impl DesignToken {
    pub fn new(name: TokenName, category: TokenCategory, value: TokenValue) -> Self {
        Self {
            name,
            category,
            value,
            path: None,
        }
    }

    pub fn with_path(mut self, path: TokenPath) -> Self {
        self.path = Some(path);
        self
    }

    pub fn name(&self) -> &TokenName {
        &self.name
    }

    pub fn category(&self) -> TokenCategory {
        self.category
    }

    pub fn value(&self) -> &TokenValue {
        &self.value
    }

    pub fn path(&self) -> Option<&TokenPath> {
        self.path.as_ref()
    }
}

#[cfg(test)]
mod tests {
    use super::{DesignToken, TokenCategory, TokenName, TokenPath, TokenReference, TokenValue};

    #[test]
    fn creates_token_categories_and_values() {
        let token = DesignToken::new(
            TokenName::new("primary"),
            TokenCategory::Color,
            TokenValue::text("#3366cc"),
        );

        assert_eq!(token.name().as_str(), "primary");
        assert_eq!(token.category().as_str(), "color");
        assert_eq!(token.value(), &TokenValue::Text(String::from("#3366cc")));
    }

    #[test]
    fn creates_paths_and_references() {
        let path = TokenPath::from_segments(["color", "background", "primary"]);
        let reference = TokenReference::new(path.clone());
        let token = DesignToken::new(
            TokenName::new("surface"),
            TokenCategory::Semantic,
            TokenValue::Reference(reference),
        )
        .with_path(path);

        assert_eq!(token.path().map(TokenPath::len), Some(3));
        assert!(!token.path().is_some_and(TokenPath::is_empty));
    }
}