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}