introspect_core/struct/
field.rs

1//! Rust struct fields.
2
3mod builder;
4
5pub use builder::Builder;
6
7/// An error related to a [`Field`].
8pub enum Error {
9    /// Encountered an unsupported expression for a documentation attribute.
10    UnsupportedExpression(syn::Expr),
11
12    /// Encountered an unsupported expression literal for a documentation attribute.
13    UnsupportedExpressionLiteral(syn::ExprLit),
14}
15
16impl std::fmt::Debug for Error {
17    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18        match self {
19            Self::UnsupportedExpression(_) => f.debug_tuple("UnsupportedExpression").finish(),
20            Self::UnsupportedExpressionLiteral(_) => {
21                f.debug_tuple("UnsupportedExpressionLiteral").finish()
22            }
23        }
24    }
25}
26
27impl std::fmt::Display for Error {
28    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        match self {
30            Error::UnsupportedExpression(_) => {
31                write!(f, "unsupported doc attribute expression")
32            }
33            Error::UnsupportedExpressionLiteral(_) => {
34                write!(f, "unsupported doc attribute literal")
35            }
36        }
37    }
38}
39
40impl std::error::Error for Error {}
41
42/// A [`Result`](std::result::Result) with an [`Error`].
43pub type Result<T> = std::result::Result<T, Error>;
44
45/// A Rust struct field.
46#[derive(Debug)]
47pub struct Field {
48    /// An identifier for the field, if it exists.
49    identifier: Option<String>,
50
51    /// The documentation for the field, if it exists.
52    documentation: Option<String>,
53}
54
55impl Field {
56    /// Creates a new [`Field`].
57    ///
58    /// # Examples
59    ///
60    /// ```
61    /// use introspect_core as core;
62    ///
63    /// let field = core::r#struct::Field::new(
64    ///     Some(String::from("Name")),
65    ///     Some(String::from("Documentation."))
66    /// );
67    /// ```
68    pub fn new(identifier: Option<String>, documentation: Option<String>) -> Self {
69        Self {
70            identifier,
71            documentation,
72        }
73    }
74
75    /// Gets the identifier of the [`Field`] by reference.
76    ///
77    /// # Examples
78    ///
79    /// ```
80    /// use introspect_core as core;
81    ///
82    /// let field = core::r#struct::field::Builder::default()
83    ///                 .identifier("Name")
84    ///                 .documentation("Documentation.")
85    ///                 .build();
86    ///
87    /// assert_eq!(field.identifier(), Some("Name"));
88    /// ```
89    pub fn identifier(&self) -> Option<&str> {
90        self.identifier.as_deref()
91    }
92
93    /// Gets the documentation of the [`Field`] by reference.
94    ///
95    /// # Examples
96    ///
97    /// ```
98    /// use introspect_core as core;
99    ///
100    /// let field = core::r#struct::field::Builder::default()
101    ///                 .identifier("Name")
102    ///                 .documentation("Documentation.")
103    ///                 .build();
104    ///
105    /// assert_eq!(field.documentation(), Some("Documentation."));
106    /// ```
107    pub fn documentation(&self) -> Option<&str> {
108        self.documentation.as_deref()
109    }
110}
111
112impl std::fmt::Display for Field {
113    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114        write!(f, "::introspect::r#struct::Field::new(")?;
115
116        match self.identifier.as_ref() {
117            Some(identifier) => write!(f, "Some(r#\"{}\"#.into())", identifier)?,
118            None => write!(f, "None")?,
119        };
120
121        write!(f, ", ")?;
122
123        match self.documentation.as_ref() {
124            Some(documentation) => write!(f, "Some(r#\"{}\"#.into())", documentation)?,
125            None => write!(f, "None")?,
126        };
127
128        write!(f, ")")
129    }
130}
131
132impl TryFrom<&syn::Field> for Field {
133    type Error = Error;
134
135    fn try_from(value: &syn::Field) -> Result<Self> {
136        let documentation = value
137            .attrs
138            .iter()
139            .filter_map(|attr| match attr.meta.require_name_value() {
140                Ok(v) => Some(v),
141                Err(_) => None,
142            })
143            .filter_map(|field| {
144                field
145                    .path
146                    .get_ident()
147                    .map(|ident| (ident, field.value.clone()))
148            })
149            .filter(|(ident, _)| *ident == "doc")
150            .map(|(_, expr)| match expr {
151                syn::Expr::Lit(expr_lit) => match expr_lit.lit {
152                    syn::Lit::Str(lit_str) => Ok(lit_str.value().trim().to_string()),
153                    _ => Err(Error::UnsupportedExpressionLiteral(expr_lit)),
154                },
155                _ => Err(Error::UnsupportedExpression(expr)),
156            })
157            .collect::<Result<Vec<String>>>()?
158            .join("\n");
159
160        Ok(Self {
161            identifier: value.ident.as_ref().map(|ident| ident.to_string()),
162            documentation: match documentation.is_empty() {
163                true => None,
164                false => Some(documentation),
165            },
166        })
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    #[test]
175    fn display_identifier_and_documentation() {
176        let field = Field::new(
177            Some(String::from("Name")),
178            Some(String::from("Documentation.")),
179        );
180
181        assert_eq!(
182            field.to_string(),
183            "::introspect::r#struct::Field::new(Some(r#\"Name\"#.into()), Some(r#\"Documentation.\"#.into()))"
184        )
185    }
186
187    #[test]
188    fn display_only_identifier() {
189        let field = Field::new(Some(String::from("Name")), None);
190
191        assert_eq!(
192            field.to_string(),
193            "::introspect::r#struct::Field::new(Some(r#\"Name\"#.into()), None)"
194        )
195    }
196
197    #[test]
198    fn display_only_documentation() {
199        let field = Field::new(None, Some(String::from("Documentation.")));
200
201        assert_eq!(
202            field.to_string(),
203            "::introspect::r#struct::Field::new(None, Some(r#\"Documentation.\"#.into()))"
204        )
205    }
206
207    #[test]
208    fn display_neither() {
209        let field = Field::new(None, None);
210
211        assert_eq!(
212            field.to_string(),
213            "::introspect::r#struct::Field::new(None, None)"
214        )
215    }
216}