vector_config_macros/ast/
variant.rs

1use darling::{FromAttributes, error::Accumulator, util::Flag};
2use proc_macro2::{Ident, TokenStream};
3use quote::ToTokens;
4use serde_derive_internals::ast as serde_ast;
5
6use super::{
7    Field, LazyCustomAttribute, Metadata, Style, Tagging,
8    util::{DarlingResultIterator, has_flag_attribute, try_extract_doc_title_description},
9};
10
11/// A variant in an enum.
12pub struct Variant<'a> {
13    original: &'a syn::Variant,
14    name: String,
15    attrs: Attributes,
16    fields: Vec<Field<'a>>,
17    style: Style,
18    tagging: Tagging,
19}
20
21impl<'a> Variant<'a> {
22    /// Creates a new `Variant<'a>` from the `serde`-derived information about the given variant.
23    pub fn from_ast(
24        serde: &serde_ast::Variant<'a>,
25        tagging: Tagging,
26        is_virtual_newtype: bool,
27    ) -> darling::Result<Variant<'a>> {
28        let original = serde.original;
29        let name = serde.attrs.name().deserialize_name().to_string();
30        let style = serde.style.into();
31        let is_newtype_wrapper_field = style == Style::Newtype;
32
33        let attrs = Attributes::from_attributes(&original.attrs)
34            .and_then(|attrs| attrs.finalize(serde, &original.attrs))?;
35
36        let mut accumulator = Accumulator::default();
37        let fields = serde
38            .fields
39            .iter()
40            .map(|field| Field::from_ast(field, is_virtual_newtype, is_newtype_wrapper_field))
41            .collect_darling_results(&mut accumulator);
42
43        // If the enum overall is tagged (internal/adjacent) serde still allows one or more
44        // variants to be explicitly marked with `#[serde(untagged)]`. In that case, the
45        // variant itself is untagged; reflect that here so schema generation matches serde.
46        let tagging = if has_flag_attribute(&original.attrs, "serde", "untagged") {
47            Tagging::None
48        } else {
49            tagging
50        };
51
52        let variant = Variant {
53            original,
54            name,
55            attrs,
56            fields,
57            style,
58            tagging,
59        };
60        accumulator.finish_with(variant)
61    }
62
63    /// Ident of the variant.
64    pub fn ident(&self) -> &Ident {
65        &self.original.ident
66    }
67
68    /// Style of the variant.
69    ///
70    /// This comes directly from `serde`, but effectively represents common terminology used outside
71    /// of `serde` when describing the shape of a data container, such as if a struct is a "tuple
72    /// struct" or a "newtype wrapper", and so on.
73    pub fn style(&self) -> Style {
74        self.style
75    }
76
77    /// Tagging configuration of the variant.
78    ///
79    /// This comes directly from `serde`. For more information on tagging, see [Enum representations][serde_tagging_docs].
80    ///
81    /// [serde_tagging_docs]: https://serde.rs/enum-representations.html
82    pub fn tagging(&self) -> &Tagging {
83        &self.tagging
84    }
85
86    /// Fields of the variant, if any.
87    pub fn fields(&self) -> &[Field<'_>] {
88        &self.fields
89    }
90
91    /// Name of the variant when deserializing.
92    ///
93    /// This may be different than the name of the variant itself depending on whether it has been
94    /// altered with `serde` helper attributes i.e. `#[serde(rename = "...")]`.
95    pub fn name(&self) -> &str {
96        self.name.as_str()
97    }
98
99    /// Title of the variant, if any.
100    ///
101    /// The title specifically refers to the headline portion of a doc comment. For example, if a
102    /// variant has the following doc comment:
103    ///
104    /// ```text
105    /// /// My special variant.
106    /// ///
107    /// /// Here's why it's special:
108    /// /// ...
109    /// SomeVariant(...),
110    /// ```
111    ///
112    /// then the title would be `My special variant`. If the doc comment only contained `My special
113    /// variant.`, then we would consider the title _empty_. See `description` for more details on
114    /// detecting titles vs descriptions.
115    pub fn title(&self) -> Option<&String> {
116        self.attrs.title.as_ref()
117    }
118
119    /// Description of the variant, if any.
120    ///
121    /// The description specifically refers to the body portion of a doc comment, or the headline if
122    /// only a headline exists.. For example, if a variant has the following doc comment:
123    ///
124    /// ```text
125    /// /// My special variant.
126    /// ///
127    /// /// Here's why it's special:
128    /// /// ...
129    /// SomeVariant(...),
130    /// ```
131    ///
132    /// then the title would be everything that comes after `My special variant`. If the doc comment
133    /// only contained `My special variant.`, then the description would be `My special variant.`,
134    /// and the title would be empty.  In this way, the description will always be some port of a
135    /// doc comment, depending on the formatting applied.
136    ///
137    /// This logic was chosen to mimic how Rust's own `rustdoc` tool works, where it will use the
138    /// "title" portion as a high-level description for an item, only showing the title and
139    /// description together when drilling down to the documentation for that specific item. JSON
140    /// Schema supports both title and description for a schema, and so we expose both.
141    pub fn description(&self) -> Option<&String> {
142        self.attrs.description.as_ref()
143    }
144
145    /// Whether or not the variant is deprecated.
146    ///
147    /// Applying the `#[configurable(deprecated)]` helper attribute will mark this variant as
148    /// deprecated from the perspective of the resulting schema. It does not interact with Rust's
149    /// standard `#[deprecated]` attribute, neither automatically applying it nor deriving the
150    /// deprecation status of a variant when it is present.
151    pub fn deprecated(&self) -> bool {
152        self.attrs.deprecated.is_present()
153    }
154
155    /// Whether or not this variant is visible during either serialization or deserialization.
156    ///
157    /// This is derived from whether any of the `serde` visibility attributes are applied: `skip`,
158    /// `skip_serializing`, and `skip_deserializing`. Unless the variant is skipped entirely, it will
159    /// be considered visible and part of the schema.
160    pub fn visible(&self) -> bool {
161        self.attrs.visible
162    }
163
164    /// Metadata (custom attributes) for the variant, if any.
165    ///
166    /// Attributes can take the shape of flags (`#[configurable(metadata(im_a_teapot))]`) or
167    /// key/value pairs (`#[configurable(metadata(status = "beta"))]`) to allow rich, semantic
168    /// metadata to be attached directly to variants.
169    pub fn metadata(&self) -> impl Iterator<Item = LazyCustomAttribute> {
170        self.attrs
171            .metadata
172            .clone()
173            .into_iter()
174            .flat_map(|metadata| metadata.attributes())
175    }
176}
177
178impl ToTokens for Variant<'_> {
179    fn to_tokens(&self, tokens: &mut TokenStream) {
180        self.original.to_tokens(tokens)
181    }
182}
183
184#[derive(Debug, Default, FromAttributes)]
185#[darling(default, attributes(configurable))]
186struct Attributes {
187    title: Option<String>,
188    description: Option<String>,
189    deprecated: Flag,
190    #[darling(skip)]
191    visible: bool,
192    #[darling(multiple)]
193    metadata: Vec<Metadata>,
194}
195
196impl Attributes {
197    fn finalize(
198        mut self,
199        variant: &serde_ast::Variant<'_>,
200        forwarded_attrs: &[syn::Attribute],
201    ) -> darling::Result<Self> {
202        // Derive any of the necessary fields from the `serde` side of things.
203        self.visible = !variant.attrs.skip_deserializing() || !variant.attrs.skip_serializing();
204
205        // We additionally attempt to extract a title/description from the forwarded doc attributes, if they exist.
206        // Whether we extract both a title and description, or just description, is documented in more detail in
207        // `try_extract_doc_title_description` itself.
208        let (doc_title, doc_description) = try_extract_doc_title_description(forwarded_attrs);
209        self.title = self.title.or(doc_title);
210        self.description = self.description.or(doc_description);
211
212        Ok(self)
213    }
214}
215
216impl PartialEq for Variant<'_> {
217    fn eq(&self, other: &Self) -> bool {
218        // Equality checking between variants is only used to drive conformance checks around making
219        // sure no duplicate variants exist when in untagged mode, so all we care about is what
220        // distinguishes a variant when it's in its serialized form, which is the shape -- struct vs
221        // tuple vs unit -- and the fields therein.
222
223        // It's suboptimal to be allocating strings for the field names here but we need the
224        // deserialized name as `serde` observes it, and this only runs at compile-time.
225        let self_fields = self
226            .fields
227            .iter()
228            .map(|field| (field.name(), field.ty()))
229            .collect::<Vec<_>>();
230        let other_fields = other
231            .fields
232            .iter()
233            .map(|field| (field.name(), field.ty()))
234            .collect::<Vec<_>>();
235
236        self.style() == other.style()
237            && self.tagging == other.tagging
238            && self_fields == other_fields
239    }
240}