component_shape/
rust_syntax.rs1use strum::{Display, IntoStaticStr};
2
3#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5pub struct RustType(&'static str);
6
7#[derive(Clone, Copy, Debug, Eq, PartialEq)]
9pub struct RustPath(&'static str);
10
11#[derive(Clone, Copy, Debug, Eq, PartialEq)]
13pub struct RustExpr(&'static str);
14
15#[derive(Clone, Copy, Debug, Display, Eq, IntoStaticStr, PartialEq)]
17#[strum(serialize_all = "lowercase", const_into_str)]
18pub enum RustSyntaxKind {
19 Type,
21 Path,
23 #[strum(to_string = "expression")]
25 Expr,
26}
27
28impl RustSyntaxKind {
29 pub const fn label(self) -> &'static str {
31 self.into_str()
32 }
33}
34
35#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
37#[error("invalid Rust {kind} metadata `{value}`: {error}")]
38pub struct RustSyntaxError {
39 kind: RustSyntaxKind,
40 value: String,
41 error: String,
42}
43
44impl RustSyntaxError {
45 pub const fn kind(&self) -> RustSyntaxKind {
47 self.kind
48 }
49
50 pub fn value(&self) -> &str {
52 &self.value
53 }
54
55 pub fn source_error(&self) -> &str {
57 &self.error
58 }
59}
60
61fn parse_rust_syntax<T: syn::parse::Parse>(
62 kind: RustSyntaxKind,
63 value: &'static str,
64) -> Result<T, RustSyntaxError> {
65 syn::parse_str(value).map_err(|err| RustSyntaxError {
66 kind,
67 value: value.to_string(),
68 error: err.to_string(),
69 })
70}
71
72impl RustType {
73 pub fn new(value: &'static str) -> Result<Self, RustSyntaxError> {
79 parse_rust_syntax::<syn::Type>(RustSyntaxKind::Type, value)?;
80 Ok(Self(value))
81 }
82
83 pub fn new_opt(value: Option<&'static str>) -> Result<Option<Self>, RustSyntaxError> {
90 value.map(Self::new).transpose()
91 }
92
93 pub const fn from_macro_tokens_unchecked(value: &'static str) -> Self {
95 Self(value)
96 }
97
98 pub const fn from_macro_tokens_opt_unchecked(value: Option<&'static str>) -> Option<Self> {
100 match value {
101 Some(value) => Some(Self::from_macro_tokens_unchecked(value)),
102 None => None,
103 }
104 }
105
106 pub const fn as_str(self) -> &'static str {
108 self.0
109 }
110
111 pub fn parse(self) -> Result<syn::Type, RustSyntaxError> {
118 parse_rust_syntax(RustSyntaxKind::Type, self.0)
119 }
120}
121
122impl RustPath {
123 pub fn new(value: &'static str) -> Result<Self, RustSyntaxError> {
129 parse_rust_syntax::<syn::Path>(RustSyntaxKind::Path, value)?;
130 Ok(Self(value))
131 }
132
133 pub const fn from_macro_tokens_unchecked(value: &'static str) -> Self {
135 Self(value)
136 }
137
138 pub const fn as_str(self) -> &'static str {
140 self.0
141 }
142
143 pub fn parse(self) -> Result<syn::Path, RustSyntaxError> {
150 parse_rust_syntax(RustSyntaxKind::Path, self.0)
151 }
152}
153
154impl RustExpr {
155 pub fn new(value: &'static str) -> Result<Self, RustSyntaxError> {
162 parse_rust_syntax::<syn::Expr>(RustSyntaxKind::Expr, value)?;
163 Ok(Self(value))
164 }
165
166 pub const fn from_macro_tokens_unchecked(value: &'static str) -> Self {
168 Self(value)
169 }
170
171 pub const fn as_str(self) -> &'static str {
173 self.0
174 }
175
176 pub fn parse(self) -> Result<syn::Expr, RustSyntaxError> {
183 parse_rust_syntax(RustSyntaxKind::Expr, self.0)
184 }
185}
186
187#[cfg(test)]
188mod tests {
189 use super::{RustExpr, RustPath, RustSyntaxKind, RustType};
190
191 #[test]
192 fn rust_type_validates_and_parses_type_syntax() {
193 let ty =
194 RustType::new("std::collections::HashMap<String, usize>").expect("type should parse");
195
196 assert_eq!(ty.as_str(), "std::collections::HashMap<String, usize>");
197 assert!(matches!(ty.parse(), Ok(syn::Type::Path(_))));
198 }
199
200 #[test]
201 fn rust_type_new_opt_validates_some_values() {
202 assert_eq!(
203 RustType::new_opt(Some("Option<String>"))
204 .expect("type should parse")
205 .map(RustType::as_str),
206 Some("Option<String>")
207 );
208 assert_eq!(RustType::new_opt(None).expect("none should pass"), None);
209 }
210
211 #[test]
212 fn rust_path_validates_and_parses_path_syntax() {
213 let path = RustPath::new("crate::widgets::TextInput<String>").expect("path should parse");
214
215 assert_eq!(path.as_str(), "crate::widgets::TextInput<String>");
216 assert_eq!(
217 path.parse()
218 .expect("path should parse")
219 .segments
220 .last()
221 .expect("path should have final segment")
222 .ident,
223 "TextInput"
224 );
225 }
226
227 #[test]
228 fn rust_expr_validates_and_parses_expression_syntax() {
229 let expr = RustExpr::new("Some(Default::default())").expect("expr should parse");
230
231 assert_eq!(expr.as_str(), "Some(Default::default())");
232 assert!(matches!(expr.parse(), Ok(syn::Expr::Call(_))));
233 }
234
235 #[test]
236 fn rust_syntax_errors_record_kind_value_and_source_error() {
237 let error = RustType::new("Vec<").expect_err("invalid type should fail");
238
239 assert_eq!(error.kind(), RustSyntaxKind::Type);
240 assert_eq!(error.value(), "Vec<");
241 assert!(!error.source_error().is_empty());
242 assert!(
243 error
244 .to_string()
245 .starts_with("invalid Rust type metadata `Vec<`: ")
246 );
247 }
248
249 #[test]
250 fn unchecked_macro_constructors_do_not_validate() {
251 assert_eq!(
252 RustType::from_macro_tokens_unchecked("Vec<").as_str(),
253 "Vec<"
254 );
255 assert_eq!(
256 RustType::from_macro_tokens_opt_unchecked(Some("Vec<")).map(RustType::as_str),
257 Some("Vec<")
258 );
259 assert_eq!(
260 RustType::from_macro_tokens_opt_unchecked(std::hint::black_box(None)),
261 None
262 );
263 assert_eq!(
264 RustExpr::from_macro_tokens_unchecked(std::hint::black_box("make_value()")).as_str(),
265 "make_value()"
266 );
267 }
268
269 #[test]
270 fn rust_syntax_kind_labels_are_stable() {
271 assert_eq!(RustSyntaxKind::Type.label(), "type");
272 assert_eq!(RustSyntaxKind::Path.label(), "path");
273 assert_eq!(RustSyntaxKind::Expr.label(), "expression");
274 }
275}