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}