Skip to main content

component_shape/
rust_syntax.rs

1use strum::{Display, IntoStaticStr};
2
3/// Rust type syntax stored as static metadata.
4#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5pub struct RustType(&'static str);
6
7/// Rust path syntax stored as static metadata.
8#[derive(Clone, Copy, Debug, Eq, PartialEq)]
9pub struct RustPath(&'static str);
10
11/// Rust expression syntax stored as static metadata.
12#[derive(Clone, Copy, Debug, Eq, PartialEq)]
13pub struct RustExpr(&'static str);
14
15/// Kind of Rust syntax stored as static metadata.
16#[derive(Clone, Copy, Debug, Display, Eq, IntoStaticStr, PartialEq)]
17#[strum(serialize_all = "lowercase", const_into_str)]
18pub enum RustSyntaxKind {
19    /// A Rust type.
20    Type,
21    /// A Rust path.
22    Path,
23    /// A Rust expression.
24    #[strum(to_string = "expression")]
25    Expr,
26}
27
28impl RustSyntaxKind {
29    /// Returns the stable English label for this syntax kind.
30    pub const fn label(self) -> &'static str {
31        self.into_str()
32    }
33}
34
35/// Error returned when Rust syntax metadata fails to parse.
36#[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    /// Returns the syntax kind that failed to parse.
46    pub const fn kind(&self) -> RustSyntaxKind {
47        self.kind
48    }
49
50    /// Returns the original metadata string.
51    pub fn value(&self) -> &str {
52        &self.value
53    }
54
55    /// Returns the parser error message from `syn`.
56    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    /// Validates Rust type syntax stored as static metadata.
74    ///
75    /// # Errors
76    ///
77    /// Returns [`RustSyntaxError`] when `value` is not valid Rust type syntax.
78    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    /// Validates optional Rust type syntax stored as static metadata.
84    ///
85    /// # Errors
86    ///
87    /// Returns [`RustSyntaxError`] when `value` is `Some` and the contained
88    /// string is not valid Rust type syntax.
89    pub fn new_opt(value: Option<&'static str>) -> Result<Option<Self>, RustSyntaxError> {
90        value.map(Self::new).transpose()
91    }
92
93    /// Stores Rust type syntax emitted by a trusted macro expansion.
94    pub const fn from_macro_tokens_unchecked(value: &'static str) -> Self {
95        Self(value)
96    }
97
98    /// Stores optional Rust type syntax emitted by a trusted macro expansion.
99    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    /// Returns the stored Rust type syntax.
107    pub const fn as_str(self) -> &'static str {
108        self.0
109    }
110
111    /// Parses the stored Rust type syntax.
112    ///
113    /// # Errors
114    ///
115    /// Returns [`RustSyntaxError`] when the stored string is not valid Rust type
116    /// syntax.
117    pub fn parse(self) -> Result<syn::Type, RustSyntaxError> {
118        parse_rust_syntax(RustSyntaxKind::Type, self.0)
119    }
120}
121
122impl RustPath {
123    /// Validates Rust path syntax stored as static metadata.
124    ///
125    /// # Errors
126    ///
127    /// Returns [`RustSyntaxError`] when `value` is not valid Rust path syntax.
128    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    /// Stores Rust path syntax emitted by a trusted macro expansion.
134    pub const fn from_macro_tokens_unchecked(value: &'static str) -> Self {
135        Self(value)
136    }
137
138    /// Returns the stored Rust path syntax.
139    pub const fn as_str(self) -> &'static str {
140        self.0
141    }
142
143    /// Parses the stored Rust path syntax.
144    ///
145    /// # Errors
146    ///
147    /// Returns [`RustSyntaxError`] when the stored string is not valid Rust path
148    /// syntax.
149    pub fn parse(self) -> Result<syn::Path, RustSyntaxError> {
150        parse_rust_syntax(RustSyntaxKind::Path, self.0)
151    }
152}
153
154impl RustExpr {
155    /// Validates Rust expression syntax stored as static metadata.
156    ///
157    /// # Errors
158    ///
159    /// Returns [`RustSyntaxError`] when `value` is not valid Rust expression
160    /// syntax.
161    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    /// Stores Rust expression syntax emitted by a trusted macro expansion.
167    pub const fn from_macro_tokens_unchecked(value: &'static str) -> Self {
168        Self(value)
169    }
170
171    /// Returns the stored Rust expression syntax.
172    pub const fn as_str(self) -> &'static str {
173        self.0
174    }
175
176    /// Parses the stored Rust expression syntax.
177    ///
178    /// # Errors
179    ///
180    /// Returns [`RustSyntaxError`] when the stored string is not valid Rust
181    /// expression syntax.
182    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}