datafusion_macros/
user_doc.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18#![doc(
19    html_logo_url = "https://raw.githubusercontent.com/apache/datafusion/19fe44cf2f30cbdd63d4a4f52c74055163c6cc38/docs/logos/standalone_logo/logo_original.svg",
20    html_favicon_url = "https://raw.githubusercontent.com/apache/datafusion/19fe44cf2f30cbdd63d4a4f52c74055163c6cc38/docs/logos/standalone_logo/logo_original.svg"
21)]
22#![cfg_attr(docsrs, feature(doc_cfg))]
23
24extern crate proc_macro;
25use datafusion_expr::scalar_doc_sections::doc_sections_const;
26use proc_macro::TokenStream;
27use quote::quote;
28use syn::{parse_macro_input, DeriveInput, LitStr};
29
30/// This procedural macro is intended to parse a rust custom attribute and create user documentation
31/// from it by constructing a `DocumentBuilder()` automatically. The `Documentation` can be
32/// retrieved from the `documentation()` method
33/// declared on `AggregateUDF`, `WindowUDFImpl`, `ScalarUDFImpl` traits.
34/// For `doc_section`, this macro will try to find corresponding predefined `DocSection` by label field
35/// Predefined `DocSection` can be found in datafusion/expr/src/udf.rs
36/// Example:
37/// ```ignore
38/// #[user_doc(
39///     doc_section(label = "Time and Date Functions"),
40///     description = r"Converts a value to a date (`YYYY-MM-DD`).",
41///     syntax_example = "to_date('2017-05-31', '%Y-%m-%d')",
42///     sql_example = r#"```sql
43/// > select to_date('2023-01-31');
44/// +-----------------------------+
45/// | to_date(Utf8(\"2023-01-31\")) |
46/// +-----------------------------+
47/// | 2023-01-31                  |
48/// +-----------------------------+
49/// ```"#,
50///     standard_argument(name = "expression", prefix = "String"),
51///     argument(
52///         name = "format_n",
53///         description = r"Optional [Chrono format](https://docs.rs/chrono/latest/chrono/format/strftime/index.html) strings to use to parse the expression. Formats will be tried in the order
54///   they appear with the first successful one being returned. If none of the formats successfully parse the expression
55///   an error will be returned."
56///    )
57/// )]
58/// #[derive(Debug)]
59/// pub struct ToDateFunc {
60///     signature: Signature,
61/// }
62/// ```
63/// will generate the following code
64///
65/// ```ignore
66/// pub struct ToDateFunc {
67///     signature: Signature,
68/// }
69/// impl ToDateFunc {
70///     fn doc(&self) -> Option<&datafusion_doc::Documentation> {
71///         static DOCUMENTATION: std::sync::LazyLock<
72///             datafusion_doc::Documentation,
73///         > = std::sync::LazyLock::new(|| {
74///             datafusion_doc::Documentation::builder(
75///                     datafusion_doc::DocSection {
76///                         include: true,
77///                         label: "Time and Date Functions",
78///                         description: None,
79///                     },
80///                     r"Converts a value to a date (`YYYY-MM-DD`).".to_string(),
81///                     "to_date('2017-05-31', '%Y-%m-%d')".to_string(),
82///                 )
83///                 .with_sql_example(
84///                     r#"```sql
85/// > select to_date('2023-01-31');
86/// +-----------------------------+
87/// | to_date(Utf8(\"2023-01-31\")) |
88/// +-----------------------------+
89/// | 2023-01-31                  |
90/// +-----------------------------+
91/// ```"#,
92///                 )
93///                 .with_standard_argument("expression", "String".into())
94///                 .with_argument(
95///                     "format_n",
96///                     r"Optional [Chrono format](https://docs.rs/chrono/latest/chrono/format/strftime/index.html) strings to use to parse the expression. Formats will be tried in the order
97/// they appear with the first successful one being returned. If none of the formats successfully parse the expression
98/// an error will be returned.",
99///                 )
100///                 .build()
101///         });
102///         Some(&DOCUMENTATION)
103///     }
104/// }
105/// ```
106#[proc_macro_attribute]
107pub fn user_doc(args: TokenStream, input: TokenStream) -> TokenStream {
108    let mut doc_section_lbl: Option<LitStr> = None;
109
110    let mut description: Option<LitStr> = None;
111    let mut syntax_example: Option<LitStr> = None;
112    let mut alt_syntax_example: Vec<Option<LitStr>> = vec![];
113    let mut sql_example: Option<LitStr> = None;
114    let mut standard_args: Vec<(Option<LitStr>, Option<LitStr>)> = vec![];
115    let mut udf_args: Vec<(Option<LitStr>, Option<LitStr>)> = vec![];
116    let mut related_udfs: Vec<Option<LitStr>> = vec![];
117
118    let parser = syn::meta::parser(|meta| {
119        if meta.path.is_ident("doc_section") {
120            meta.parse_nested_meta(|meta| {
121                if meta.path.is_ident("label") {
122                    doc_section_lbl = meta.value()?.parse()?;
123                    return Ok(());
124                }
125                Ok(())
126            })
127        } else if meta.path.is_ident("description") {
128            description = Some(meta.value()?.parse()?);
129            Ok(())
130        } else if meta.path.is_ident("syntax_example") {
131            syntax_example = Some(meta.value()?.parse()?);
132            Ok(())
133        } else if meta.path.is_ident("alternative_syntax") {
134            alt_syntax_example.push(Some(meta.value()?.parse()?));
135            Ok(())
136        } else if meta.path.is_ident("sql_example") {
137            sql_example = Some(meta.value()?.parse()?);
138            Ok(())
139        } else if meta.path.is_ident("standard_argument") {
140            let mut standard_arg: (Option<LitStr>, Option<LitStr>) = (None, None);
141            let m = meta.parse_nested_meta(|meta| {
142                if meta.path.is_ident("name") {
143                    standard_arg.0 = meta.value()?.parse()?;
144                    return Ok(());
145                } else if meta.path.is_ident("prefix") {
146                    standard_arg.1 = meta.value()?.parse()?;
147                    return Ok(());
148                }
149                Ok(())
150            });
151
152            standard_args.push(standard_arg.clone());
153
154            m
155        } else if meta.path.is_ident("argument") {
156            let mut arg: (Option<LitStr>, Option<LitStr>) = (None, None);
157            let m = meta.parse_nested_meta(|meta| {
158                if meta.path.is_ident("name") {
159                    arg.0 = meta.value()?.parse()?;
160                    return Ok(());
161                } else if meta.path.is_ident("description") {
162                    arg.1 = meta.value()?.parse()?;
163                    return Ok(());
164                }
165                Ok(())
166            });
167
168            udf_args.push(arg.clone());
169
170            m
171        } else if meta.path.is_ident("related_udf") {
172            let mut arg: Option<LitStr> = None;
173            let m = meta.parse_nested_meta(|meta| {
174                if meta.path.is_ident("name") {
175                    arg = meta.value()?.parse()?;
176                    return Ok(());
177                }
178                Ok(())
179            });
180
181            related_udfs.push(arg.clone());
182
183            m
184        } else {
185            Err(meta.error(format!("Unsupported property: {:?}", meta.path.get_ident())))
186        }
187    });
188
189    parse_macro_input!(args with parser);
190
191    // Parse the input struct
192    let input = parse_macro_input!(input as DeriveInput);
193    let name = input.clone().ident;
194
195    if doc_section_lbl.is_none() {
196        eprintln!("label for doc_section should exist");
197    }
198    let label = doc_section_lbl.as_ref().unwrap().value();
199    // Try to find a predefined const by label first.
200    // If there is no match but label exists, default value will be used for include and description
201    let doc_section_option = doc_sections_const().iter().find(|ds| ds.label == label);
202    let (doc_section_include, doc_section_label, doc_section_desc) =
203        match doc_section_option {
204            Some(section) => (section.include, section.label, section.description),
205            None => (true, label.as_str(), None),
206        };
207    let doc_section_description = doc_section_desc
208        .map(|desc| quote! { Some(#desc)})
209        .unwrap_or_else(|| quote! { None });
210
211    let sql_example = sql_example.map(|ex| {
212        quote! {
213            .with_sql_example(#ex)
214        }
215    });
216
217    let udf_args = udf_args
218        .iter()
219        .map(|(name, desc)| {
220            quote! {
221                .with_argument(#name, #desc)
222            }
223        })
224        .collect::<Vec<_>>();
225
226    let standard_args = standard_args
227        .iter()
228        .map(|(name, desc)| {
229            let desc = if let Some(d) = desc {
230                quote! { #d.into() }
231            } else {
232                quote! { None }
233            };
234
235            quote! {
236                .with_standard_argument(#name, #desc)
237            }
238        })
239        .collect::<Vec<_>>();
240
241    let related_udfs = related_udfs
242        .iter()
243        .map(|name| {
244            quote! {
245                .with_related_udf(#name)
246            }
247        })
248        .collect::<Vec<_>>();
249
250    let alt_syntax_example = alt_syntax_example.iter().map(|syn| {
251        quote! {
252            .with_alternative_syntax(#syn)
253        }
254    });
255
256    let generated = quote! {
257        #input
258
259        impl #name {
260            fn doc(&self) -> Option<&datafusion_doc::Documentation> {
261                static DOCUMENTATION: std::sync::LazyLock<datafusion_doc::Documentation> =
262                    std::sync::LazyLock::new(|| {
263                        datafusion_doc::Documentation::builder(datafusion_doc::DocSection { include: #doc_section_include, label: #doc_section_label, description: #doc_section_description },
264                    #description.to_string(), #syntax_example.to_string())
265                        #sql_example
266                        #(#alt_syntax_example)*
267                        #(#standard_args)*
268                        #(#udf_args)*
269                        #(#related_udfs)*
270                        .build()
271                    });
272                Some(&DOCUMENTATION)
273            }
274        }
275    };
276
277    // Debug the generated code if needed
278    // if name == "ArrayAgg" {
279    //     eprintln!("Generated code: {}", generated);
280    // }
281
282    // Return the generated code
283    TokenStream::from(generated)
284}