1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
6pub struct TokenName(String);
7
8impl TokenName {
9 pub fn new(value: impl Into<String>) -> Self {
10 Self(value.into())
11 }
12
13 pub fn as_str(&self) -> &str {
14 &self.0
15 }
16}
17
18impl AsRef<str> for TokenName {
19 fn as_ref(&self) -> &str {
20 self.as_str()
21 }
22}
23
24#[derive(Debug, Clone, PartialEq, Eq, Hash)]
26pub struct TokenPath(Vec<TokenName>);
27
28impl TokenPath {
29 pub fn new(segments: Vec<TokenName>) -> Self {
30 Self(segments)
31 }
32
33 pub fn from_segments<I, S>(segments: I) -> Self
34 where
35 I: IntoIterator<Item = S>,
36 S: Into<String>,
37 {
38 Self(segments.into_iter().map(TokenName::new).collect())
39 }
40
41 pub fn segments(&self) -> &[TokenName] {
42 &self.0
43 }
44
45 pub fn is_empty(&self) -> bool {
46 self.0.is_empty()
47 }
48
49 pub fn len(&self) -> usize {
50 self.0.len()
51 }
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
56pub enum TokenCategory {
57 Color,
58 Typography,
59 Spacing,
60 Radius,
61 Shadow,
62 Motion,
63 Breakpoint,
64 Layer,
65 Component,
66 Semantic,
67}
68
69impl TokenCategory {
70 pub fn as_str(self) -> &'static str {
71 match self {
72 Self::Color => "color",
73 Self::Typography => "typography",
74 Self::Spacing => "spacing",
75 Self::Radius => "radius",
76 Self::Shadow => "shadow",
77 Self::Motion => "motion",
78 Self::Breakpoint => "breakpoint",
79 Self::Layer => "layer",
80 Self::Component => "component",
81 Self::Semantic => "semantic",
82 }
83 }
84}
85
86#[derive(Debug, Clone, PartialEq, Eq, Hash)]
88pub struct TokenReference {
89 path: TokenPath,
90}
91
92impl TokenReference {
93 pub fn new(path: TokenPath) -> Self {
94 Self { path }
95 }
96
97 pub fn path(&self) -> &TokenPath {
98 &self.path
99 }
100}
101
102#[derive(Debug, Clone, PartialEq, Eq, Hash)]
104pub enum TokenValue {
105 Text(String),
106 Integer(i64),
107 Boolean(bool),
108 Reference(TokenReference),
109}
110
111impl TokenValue {
112 pub fn text(value: impl Into<String>) -> Self {
113 Self::Text(value.into())
114 }
115}
116
117#[derive(Debug, Clone, PartialEq, Eq, Hash)]
119pub struct DesignToken {
120 name: TokenName,
121 category: TokenCategory,
122 value: TokenValue,
123 path: Option<TokenPath>,
124}
125
126impl DesignToken {
127 pub fn new(name: TokenName, category: TokenCategory, value: TokenValue) -> Self {
128 Self {
129 name,
130 category,
131 value,
132 path: None,
133 }
134 }
135
136 pub fn with_path(mut self, path: TokenPath) -> Self {
137 self.path = Some(path);
138 self
139 }
140
141 pub fn name(&self) -> &TokenName {
142 &self.name
143 }
144
145 pub fn category(&self) -> TokenCategory {
146 self.category
147 }
148
149 pub fn value(&self) -> &TokenValue {
150 &self.value
151 }
152
153 pub fn path(&self) -> Option<&TokenPath> {
154 self.path.as_ref()
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use super::{DesignToken, TokenCategory, TokenName, TokenPath, TokenReference, TokenValue};
161
162 #[test]
163 fn creates_token_categories_and_values() {
164 let token = DesignToken::new(
165 TokenName::new("primary"),
166 TokenCategory::Color,
167 TokenValue::text("#3366cc"),
168 );
169
170 assert_eq!(token.name().as_str(), "primary");
171 assert_eq!(token.category().as_str(), "color");
172 assert_eq!(token.value(), &TokenValue::Text(String::from("#3366cc")));
173 }
174
175 #[test]
176 fn creates_paths_and_references() {
177 let path = TokenPath::from_segments(["color", "background", "primary"]);
178 let reference = TokenReference::new(path.clone());
179 let token = DesignToken::new(
180 TokenName::new("surface"),
181 TokenCategory::Semantic,
182 TokenValue::Reference(reference),
183 )
184 .with_path(path);
185
186 assert_eq!(token.path().map(TokenPath::len), Some(3));
187 assert!(!token.path().is_some_and(TokenPath::is_empty));
188 }
189}