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}