vector_config_macros/ast/
util.rs

1use darling::{ast::NestedMeta, error::Accumulator};
2use quote::{ToTokens, quote};
3use serde_derive_internals::{Ctxt, attr as serde_attr};
4use syn::{
5    Attribute, Expr, ExprLit, ExprPath, Lit, Meta, MetaNameValue, punctuated::Punctuated,
6    spanned::Spanned, token::Comma,
7};
8
9const ERR_FIELD_MISSING_DESCRIPTION: &str = "field must have a description -- i.e. `/// This is a widget...` or `#[configurable(description = \"...\")] -- or derive it from the underlying type of the field by specifying `#[configurable(derived)]`";
10const ERR_FIELD_IMPLICIT_TRANSPARENT: &str =
11    "field in a newtype wrapper should not be manually marked as `derived`/`transparent`";
12
13pub fn try_extract_doc_title_description(
14    attributes: &[Attribute],
15) -> (Option<String>, Option<String>) {
16    // Scrape all the attributes that have the `doc` path, which will be used for holding doc
17    // comments that we're interested in utilizing, and extract their value.
18    let doc_comments = attributes
19        .iter()
20        // We only care about `doc` attributes.
21        .filter(|attribute| attribute.path().is_ident("doc"))
22        // Extract the value of the attribute if it's in the form of `doc = "..."`.
23        .filter_map(|attribute| match &attribute.meta {
24            Meta::NameValue(MetaNameValue {
25                value:
26                    Expr::Lit(ExprLit {
27                        lit: Lit::Str(s), ..
28                    }),
29                ..
30            }) => Some(s.value()),
31            _ => None,
32        })
33        .collect::<Vec<_>>();
34
35    // If there were no doc comments, then we have no title/description to try and extract.
36    if doc_comments.is_empty() {
37        return (None, None);
38    }
39
40    // We emulate what `cargo doc` does, which is that if you have a doc comment with a bunch of
41    // text, then an empty line, and then more text, it considers the first chunk the title, and
42    // the second chunk the description.
43    //
44    // If there's no empty line, then we just consider it all the description.
45    //
46    // The grouping logic of `group_doc_lines` lets us determine which scenario we're dealing with
47    // based on the number of grouped lines.
48    let mut grouped_lines = group_doc_lines(&doc_comments);
49    match grouped_lines.len() {
50        // No title or description.
51        0 => (None, None),
52        // Just a single grouped line/paragraph, so we emit that as the description.
53        1 => (None, none_if_empty(grouped_lines.remove(0))),
54        // Two or more grouped lines/paragraphs, so the first one is the title, and the rest are the
55        // description, which we concatenate together with newlines, since the description at least
56        // needs to be a single string.
57        _ => {
58            let title = grouped_lines.remove(0);
59            let description = grouped_lines.join("\n\n");
60
61            (none_if_empty(title), none_if_empty(description))
62        }
63    }
64}
65
66fn group_doc_lines(ungrouped: &[String]) -> Vec<String> {
67    // When we write a doc comment in Rust, it typically ends up looking something like this:
68    //
69    // /// A helper for XYZ.
70    // ///
71    // /// This helper works in the following way, and so on and so forth.
72    // ///
73    // /// This separate paragraph explains a different, but related, aspect
74    // /// of the helper.
75    //
76    // To humans, this format is natural and we see it and read it as three paragraphs. Once those
77    // doc comments are processed and we get them in a procedural macro, they look like this,
78    // though:
79    //
80    // #[doc = " A helper for XYZ."]
81    // #[doc = ""]
82    // #[doc = " This helper works in the following way, and so on and so forth."]
83    // #[doc = ""]
84    // #[doc = " This separate paragraph explains a different, but related, aspect"]
85    // #[doc = " of the helper."]
86    //
87    // What we want to do is actually parse this as three paragraphs, with the individual lines of
88    // each paragraph merged together as a single string, and extraneous whitespace removed, such
89    // that we should end up with a vector of strings that looks like:
90    //
91    // - "A helper for XYZ."
92    // - "This helper works in the following way, and so on and so forth."
93    // - "This separate paragraph explains a different, but related, aspect\n of the helper."
94
95    // TODO: Markdown link reference definitions (LFDs) -- e.g. `[foo]: https://zombohtml5.com` --
96    // have to be on their own line, which is a little annoying because ideally we want to remove
97    // the newlines between lines that simply get line wrapped, such that in the above example.
98    // While that extra newline towards the end of the third line/paragraph is extraneous, because
99    // it represents a forced line break which is imposing some measure of stylistic license, we
100    // _do_ need line breaks to stay in place so that LFDs stay on their own line, otherwise it
101    // seems like Markdown parsers will treat them as free-form text.
102    //
103    // I'm not sure if we'll want to go as far as trying to parse each line specifically as an LFD,
104    // for the purpose of controlling how we add/remove linebreaks... but it's something we'll
105    // likely want/need to eventually figure out.
106
107    let mut buffer = String::new();
108    let mut grouped = ungrouped.iter().fold(Vec::new(), |mut grouped, line| {
109        match line.as_str() {
110            // Full line breaks -- i.e. `#[doc = ""]` -- will be empty strings, which is our
111            // signal to consume our buffer and emit it as a grouped line/paragraph.
112            "" => {
113                if !buffer.is_empty() {
114                    let trimmed = buffer.trim().to_string();
115                    grouped.push(trimmed);
116
117                    buffer.clear();
118                }
119            }
120            // The line actually has some content, so just append it to our string buffer after
121            // dropping the leading space, if one exists.
122            s => {
123                buffer.push_str(s.strip_prefix(' ').unwrap_or(s));
124                buffer.push('\n');
125            }
126        };
127
128        grouped
129    });
130
131    // If we have anything left in the buffer, consume it as a grouped line/paragraph.
132    if !buffer.is_empty() {
133        let trimmed = buffer.trim().to_string();
134        grouped.push(trimmed);
135    }
136
137    grouped
138}
139
140fn none_if_empty(s: String) -> Option<String> {
141    if s.is_empty() { None } else { Some(s) }
142}
143
144pub fn err_field_missing_description<T: Spanned>(field: &T) -> darling::Error {
145    darling::Error::custom(ERR_FIELD_MISSING_DESCRIPTION).with_span(field)
146}
147
148pub fn err_field_implicit_transparent<T: Spanned>(field: &T) -> darling::Error {
149    darling::Error::custom(ERR_FIELD_IMPLICIT_TRANSPARENT).with_span(field)
150}
151
152pub fn get_serde_default_value<S: ToTokens>(
153    source: &S,
154    default: &serde_attr::Default,
155) -> Option<ExprPath> {
156    match default {
157        serde_attr::Default::None => None,
158        serde_attr::Default::Default => {
159            let qualified_path = syn::parse2(quote! {
160                <#source as ::std::default::Default>::default
161            })
162            .expect("should not fail to parse qualified default path");
163            Some(qualified_path)
164        }
165        serde_attr::Default::Path(path) => Some(path.clone()),
166    }
167}
168
169pub fn err_serde_failed(context: Ctxt) -> darling::Error {
170    context
171        .check()
172        .map_err(|errs| darling::Error::multiple(errs.into_iter().map(Into::into).collect()))
173        .expect_err("serde error context should not be empty")
174}
175
176pub trait DarlingResultIterator<I> {
177    fn collect_darling_results(self, accumulator: &mut Accumulator) -> Vec<I>;
178}
179
180impl<I, T> DarlingResultIterator<I> for T
181where
182    T: Iterator<Item = Result<I, darling::Error>>,
183{
184    fn collect_darling_results(self, accumulator: &mut Accumulator) -> Vec<I> {
185        self.filter_map(|result| accumulator.handle(result))
186            .collect()
187    }
188}
189
190/// Checks if the path matches `other`.
191///
192/// If a valid ident can be constructed from the path, and the ident's value matches `other`,
193/// `true` is returned. Otherwise, `false` is returned.
194fn path_matches<S: AsRef<str>>(path: &syn::Path, other: S) -> bool {
195    path.get_ident().filter(|i| *i == &other).is_some()
196}
197
198/// Tries to find a specific attribute with a specific name/value pair.
199///
200/// Only works with derive macro helper attributes, and not raw name/value attributes such as
201/// `#[path = "..."]`.
202///
203/// If an attribute with a path matching `attr_name`, and a meta name/value pair with a name
204/// matching `name_key` is found, `Some(path)` is returned, representing the value of the name/value pair.
205///
206/// If no attribute matches, or if the given name/value pair is not found, `None` is returned.
207fn find_name_value_attribute(
208    attributes: &[syn::Attribute],
209    attr_name: &str,
210    name_key: &str,
211) -> Option<Lit> {
212    attributes
213        .iter()
214        // Only take attributes whose name matches `attr_name`.
215        .filter(|attr| path_matches(attr.path(), attr_name))
216        // Derive macro helper attributes will always be in the list form.
217        .flat_map(|attr| match &attr.meta {
218            Meta::List(ml) => ml
219                .parse_args_with(Punctuated::<NestedMeta, Comma>::parse_terminated)
220                .map(|nested| nested.into_iter())
221                // If parsing fails, return an empty iterator. By this point, `serde` has already
222                // emitted its own error, so we don't want to duplicate any error emission here.
223                .unwrap_or_else(|_| Punctuated::<NestedMeta, Comma>::new().into_iter()),
224            // Non-list attributes cannot contain nested meta items; return empty iterator.
225            _ => Punctuated::<NestedMeta, Comma>::new().into_iter(),
226        })
227        // For each nested meta item in the list, find any that are name/value pairs where the
228        // name matches `name_key`, and return their value.
229        .find_map(|nm| match nm {
230            NestedMeta::Meta(meta) => match meta {
231                Meta::NameValue(nv) if path_matches(&nv.path, name_key) => match nv.value {
232                    Expr::Lit(ExprLit { lit, .. }) => Some(lit),
233                    _ => None,
234                },
235                _ => None,
236            },
237            _ => None,
238        })
239}
240
241/// Checks whether an attribute list contains a flag-style entry.
242///
243/// For example, this returns true when `attributes` contains something like `#[serde(untagged)]`
244/// when called with `attr_name = "serde"` and `flag_name = "untagged"`.
245pub(crate) fn has_flag_attribute(
246    attributes: &[syn::Attribute],
247    attr_name: &str,
248    flag_name: &str,
249) -> bool {
250    attributes
251        .iter()
252        .filter(|attr| path_matches(attr.path(), attr_name))
253        .filter_map(|attr| match &attr.meta {
254            Meta::List(ml) => ml
255                .parse_args_with(Punctuated::<NestedMeta, Comma>::parse_terminated)
256                .map(|nested| nested.into_iter())
257                .ok(),
258            _ => None,
259        })
260        .flatten()
261        .any(|nm| matches!(nm, NestedMeta::Meta(Meta::Path(ref path)) if path_matches(path, flag_name)))
262}
263
264/// Tries to find a delegated (de)serialization type from attributes.
265///
266/// In some cases, the `serde_with` crate, more specifically the `serde_as` attribute macro, may be
267/// used to help (de)serialize a field/container with type A via a (de)implementation on type B, in order to
268/// provide more ergonomic (de)serialization of values that can represent type A without needing to
269/// explicitly match type A when (de)serialized. This is similar to `serde`'s existing support for
270/// "remote" types but is taken further with a more generic and extensible approach.
271///
272/// This, however, presents an issue because while normally we can handle scenarios like
273/// `#[serde(from = "...")]` and its siblings, `serde_as` depends on `#[serde(with = "...")]` and
274/// the fact that it simply constructs a path to the (de)serialize methods, rather than always
275/// needing to explicitly reference a type. This means that we cannot simply grab the value of the
276/// `with` name/value pair blindly, and assume if there's a value that a delegated/remote type is in
277/// play... it could be a module path, too.
278///
279/// This method looks for two indicators to understand when it should be able to extract the
280/// delegated type:
281///
282/// - `#[serde(with = "...")]` is present
283/// - `#[serde_as(as = "...")]` is present
284///
285/// When both of these are true, we can rely on the fact that the value of `with` will be a valid
286/// type path, and usable like a virtual newtype, which is where we use the type specified for
287/// `try_from`/`from`/`into` for the delegated (de)serialization type of a container itself.
288///
289/// If we find both of those attribute name/value pairs, and the value of `with` can be parsed
290/// successfully as a type path, `Some(...)` is returned, contained the type. Otherwise, `None` is
291/// returned.
292pub fn find_delegated_serde_deser_ty(attributes: &[syn::Attribute]) -> Option<syn::Type> {
293    // Make sure `#[serde_as(as = "...")]` is present.
294    find_name_value_attribute(attributes, "serde_as", "r#as")
295        // Make sure `#[serde(with = "...")]` is present, and grab its value.
296        .and_then(|_| find_name_value_attribute(attributes, "serde", "with"))
297        // Try and parse the value as a type path.
298        .and_then(|with| match with {
299            Lit::Str(s) => s.parse::<syn::Type>().ok(),
300            _ => None,
301        })
302}