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_doc::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/// ```ignore
65/// pub struct ToDateFunc {
66///     signature: Signature,
67/// }
68/// impl ToDateFunc {
69///     fn doc(&self) -> Option<&datafusion_doc::Documentation> {
70///         static DOCUMENTATION: std::sync::LazyLock<
71///             datafusion_doc::Documentation,
72///         > = std::sync::LazyLock::new(|| {
73///             datafusion_doc::Documentation::builder(
74///                     datafusion_doc::DocSection {
75///                         include: true,
76///                         label: "Time and Date Functions",
77///                         description: None,
78///                     },
79///                     r"Converts a value to a date (`YYYY-MM-DD`).".to_string(),
80///                     "to_date('2017-05-31', '%Y-%m-%d')".to_string(),
81///                 )
82///                 .with_sql_example(
83///                     r#"```sql
84/// > select to_date('2023-01-31');
85/// +-----------------------------+
86/// | to_date(Utf8(\"2023-01-31\")) |
87/// +-----------------------------+
88/// | 2023-01-31                  |
89/// +-----------------------------+
90/// ```"#,
91///                 )
92///                 .with_standard_argument("expression", "String".into())
93///                 .with_argument(
94///                     "format_n",
95///                     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
96/// they appear with the first successful one being returned. If none of the formats successfully parse the expression
97/// an error will be returned.",
98///                 )
99///                 .build()
100///         });
101///         Some(&DOCUMENTATION)
102///     }
103/// }
104/// ```
105#[proc_macro_attribute]
106pub fn user_doc(args: TokenStream, input: TokenStream) -> TokenStream {
107    let mut doc_section_lbl: Option<LitStr> = None;
108
109    let mut description: Option<LitStr> = None;
110    let mut syntax_example: Option<LitStr> = None;
111    let mut alt_syntax_example: Vec<Option<LitStr>> = vec![];
112    let mut sql_example: Option<LitStr> = None;
113    let mut standard_args: Vec<(Option<LitStr>, Option<LitStr>)> = vec![];
114    let mut udf_args: Vec<(Option<LitStr>, Option<LitStr>)> = vec![];
115    let mut related_udfs: Vec<Option<LitStr>> = vec![];
116
117    let parser = syn::meta::parser(|meta| {
118        if meta.path.is_ident("doc_section") {
119            meta.parse_nested_meta(|meta| {
120                if meta.path.is_ident("label") {
121                    doc_section_lbl = meta.value()?.parse()?;
122                    return Ok(());
123                }
124                Ok(())
125            })
126        } else if meta.path.is_ident("description") {
127            description = Some(meta.value()?.parse()?);
128            Ok(())
129        } else if meta.path.is_ident("syntax_example") {
130            syntax_example = Some(meta.value()?.parse()?);
131            Ok(())
132        } else if meta.path.is_ident("alternative_syntax") {
133            alt_syntax_example.push(Some(meta.value()?.parse()?));
134            Ok(())
135        } else if meta.path.is_ident("sql_example") {
136            sql_example = Some(meta.value()?.parse()?);
137            Ok(())
138        } else if meta.path.is_ident("standard_argument") {
139            let mut standard_arg: (Option<LitStr>, Option<LitStr>) = (None, None);
140            let m = meta.parse_nested_meta(|meta| {
141                if meta.path.is_ident("name") {
142                    standard_arg.0 = meta.value()?.parse()?;
143                    return Ok(());
144                } else if meta.path.is_ident("prefix") {
145                    standard_arg.1 = meta.value()?.parse()?;
146                    return Ok(());
147                }
148                Ok(())
149            });
150
151            standard_args.push(standard_arg.clone());
152
153            m
154        } else if meta.path.is_ident("argument") {
155            let mut arg: (Option<LitStr>, Option<LitStr>) = (None, None);
156            let m = meta.parse_nested_meta(|meta| {
157                if meta.path.is_ident("name") {
158                    arg.0 = meta.value()?.parse()?;
159                    return Ok(());
160                } else if meta.path.is_ident("description") {
161                    arg.1 = meta.value()?.parse()?;
162                    return Ok(());
163                }
164                Ok(())
165            });
166
167            udf_args.push(arg.clone());
168
169            m
170        } else if meta.path.is_ident("related_udf") {
171            let mut arg: Option<LitStr> = None;
172            let m = meta.parse_nested_meta(|meta| {
173                if meta.path.is_ident("name") {
174                    arg = meta.value()?.parse()?;
175                    return Ok(());
176                }
177                Ok(())
178            });
179
180            related_udfs.push(arg.clone());
181
182            m
183        } else {
184            Err(meta.error(format!("Unsupported property: {:?}", meta.path.get_ident())))
185        }
186    });
187
188    parse_macro_input!(args with parser);
189
190    // Parse the input struct
191    let input = parse_macro_input!(input as DeriveInput);
192    let name = input.clone().ident;
193
194    if doc_section_lbl.is_none() {
195        eprintln!("label for doc_section should exist");
196    }
197    let label = doc_section_lbl.as_ref().unwrap().value();
198    // Try to find a predefined const by label first.
199    // If there is no match but label exists, default value will be used for include and description
200    let doc_section_option = doc_sections_const().iter().find(|ds| ds.label == label);
201    let (doc_section_include, doc_section_label, doc_section_desc) =
202        match doc_section_option {
203            Some(section) => (section.include, section.label, section.description),
204            None => (true, label.as_str(), None),
205        };
206    let doc_section_description = doc_section_desc
207        .map(|desc| quote! { Some(#desc)})
208        .unwrap_or_else(|| quote! { None });
209
210    let sql_example = sql_example.map(|ex| {
211        quote! {
212            .with_sql_example(#ex)
213        }
214    });
215
216    let udf_args = udf_args
217        .iter()
218        .map(|(name, desc)| {
219            quote! {
220                .with_argument(#name, #desc)
221            }
222        })
223        .collect::<Vec<_>>();
224
225    let standard_args = standard_args
226        .iter()
227        .map(|(name, desc)| {
228            let desc = if let Some(d) = desc {
229                quote! { #d.into() }
230            } else {
231                quote! { None }
232            };
233
234            quote! {
235                .with_standard_argument(#name, #desc)
236            }
237        })
238        .collect::<Vec<_>>();
239
240    let related_udfs = related_udfs
241        .iter()
242        .map(|name| {
243            quote! {
244                .with_related_udf(#name)
245            }
246        })
247        .collect::<Vec<_>>();
248
249    let alt_syntax_example = alt_syntax_example.iter().map(|syn| {
250        quote! {
251            .with_alternative_syntax(#syn)
252        }
253    });
254
255    let generated = quote! {
256        #input
257
258        impl #name {
259            fn doc(&self) -> Option<&datafusion_doc::Documentation> {
260                static DOCUMENTATION: std::sync::LazyLock<datafusion_doc::Documentation> =
261                    std::sync::LazyLock::new(|| {
262                        datafusion_doc::Documentation::builder(datafusion_doc::DocSection { include: #doc_section_include, label: #doc_section_label, description: #doc_section_description },
263                    #description.to_string(), #syntax_example.to_string())
264                        #sql_example
265                        #(#alt_syntax_example)*
266                        #(#standard_args)*
267                        #(#udf_args)*
268                        #(#related_udfs)*
269                        .build()
270                    });
271                Some(&DOCUMENTATION)
272            }
273        }
274    };
275
276    // Debug the generated code if needed
277    // if name == "ArrayAgg" {
278    //     eprintln!("Generated code: {}", generated);
279    // }
280
281    // Return the generated code
282    TokenStream::from(generated)
283}