blob: 58c2cc2b1b2ac998c3bbf7c2de1225e0eab26912 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
#![doc(
html_logo_url = "https://raw.githubusercontent.com/apache/datafusion/19fe44cf2f30cbdd63d4a4f52c74055163c6cc38/docs/logos/standalone_logo/logo_original.svg",
html_favicon_url = "https://raw.githubusercontent.com/apache/datafusion/19fe44cf2f30cbdd63d4a4f52c74055163c6cc38/docs/logos/standalone_logo/logo_original.svg"
)]
#![cfg_attr(docsrs, feature(doc_cfg))]
extern crate proc_macro;
use datafusion_doc::scalar_doc_sections::doc_sections_const;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput, LitStr};
/// This procedural macro is intended to parse a rust custom attribute and create user documentation
/// from it by constructing a `DocumentBuilder()` automatically. The `Documentation` can be
/// retrieved from the `documentation()` method
/// declared on `AggregateUDF`, `WindowUDFImpl`, `ScalarUDFImpl` traits.
/// For `doc_section`, this macro will try to find corresponding predefined `DocSection` by label field
/// Predefined `DocSection` can be found in datafusion/expr/src/udf.rs
/// Example:
/// ```ignore
/// #[user_doc(
/// doc_section(label = "Time and Date Functions"),
/// description = r"Converts a value to a date (`YYYY-MM-DD`).",
/// syntax_example = "to_date('2017-05-31', '%Y-%m-%d')",
/// sql_example = r#"```sql
/// > select to_date('2023-01-31');
/// +-----------------------------+
/// | to_date(Utf8(\"2023-01-31\")) |
/// +-----------------------------+
/// | 2023-01-31 |
/// +-----------------------------+
/// ```"#,
/// standard_argument(name = "expression", prefix = "String"),
/// argument(
/// name = "format_n",
/// 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
/// they appear with the first successful one being returned. If none of the formats successfully parse the expression
/// an error will be returned."
/// )
/// )]
/// #[derive(Debug)]
/// pub struct ToDateFunc {
/// signature: Signature,
/// }
/// ```
/// will generate the following code
/// ```ignore
/// pub struct ToDateFunc {
/// signature: Signature,
/// }
/// impl ToDateFunc {
/// fn doc(&self) -> Option<&datafusion_doc::Documentation> {
/// static DOCUMENTATION: std::sync::LazyLock<
/// datafusion_doc::Documentation,
/// > = std::sync::LazyLock::new(|| {
/// datafusion_doc::Documentation::builder(
/// datafusion_doc::DocSection {
/// include: true,
/// label: "Time and Date Functions",
/// description: None,
/// },
/// r"Converts a value to a date (`YYYY-MM-DD`).".to_string(),
/// "to_date('2017-05-31', '%Y-%m-%d')".to_string(),
/// )
/// .with_sql_example(
/// r#"```sql
/// > select to_date('2023-01-31');
/// +-----------------------------+
/// | to_date(Utf8(\"2023-01-31\")) |
/// +-----------------------------+
/// | 2023-01-31 |
/// +-----------------------------+
/// ```"#,
/// )
/// .with_standard_argument("expression", "String".into())
/// .with_argument(
/// "format_n",
/// 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
/// they appear with the first successful one being returned. If none of the formats successfully parse the expression
/// an error will be returned.",
/// )
/// .build()
/// });
/// Some(&DOCUMENTATION)
/// }
/// }
/// ```
#[proc_macro_attribute]
pub fn user_doc(args: TokenStream, input: TokenStream) -> TokenStream {
let mut doc_section_lbl: Option<LitStr> = None;
let mut description: Option<LitStr> = None;
let mut syntax_example: Option<LitStr> = None;
let mut alt_syntax_example: Vec<Option<LitStr>> = vec![];
let mut sql_example: Option<LitStr> = None;
let mut standard_args: Vec<(Option<LitStr>, Option<LitStr>)> = vec![];
let mut udf_args: Vec<(Option<LitStr>, Option<LitStr>)> = vec![];
let mut related_udfs: Vec<Option<LitStr>> = vec![];
let parser = syn::meta::parser(|meta| {
if meta.path.is_ident("doc_section") {
meta.parse_nested_meta(|meta| {
if meta.path.is_ident("label") {
doc_section_lbl = meta.value()?.parse()?;
return Ok(());
}
Ok(())
})
} else if meta.path.is_ident("description") {
description = Some(meta.value()?.parse()?);
Ok(())
} else if meta.path.is_ident("syntax_example") {
syntax_example = Some(meta.value()?.parse()?);
Ok(())
} else if meta.path.is_ident("alternative_syntax") {
alt_syntax_example.push(Some(meta.value()?.parse()?));
Ok(())
} else if meta.path.is_ident("sql_example") {
sql_example = Some(meta.value()?.parse()?);
Ok(())
} else if meta.path.is_ident("standard_argument") {
let mut standard_arg: (Option<LitStr>, Option<LitStr>) = (None, None);
let m = meta.parse_nested_meta(|meta| {
if meta.path.is_ident("name") {
standard_arg.0 = meta.value()?.parse()?;
return Ok(());
} else if meta.path.is_ident("prefix") {
standard_arg.1 = meta.value()?.parse()?;
return Ok(());
}
Ok(())
});
standard_args.push(standard_arg.clone());
m
} else if meta.path.is_ident("argument") {
let mut arg: (Option<LitStr>, Option<LitStr>) = (None, None);
let m = meta.parse_nested_meta(|meta| {
if meta.path.is_ident("name") {
arg.0 = meta.value()?.parse()?;
return Ok(());
} else if meta.path.is_ident("description") {
arg.1 = meta.value()?.parse()?;
return Ok(());
}
Ok(())
});
udf_args.push(arg.clone());
m
} else if meta.path.is_ident("related_udf") {
let mut arg: Option<LitStr> = None;
let m = meta.parse_nested_meta(|meta| {
if meta.path.is_ident("name") {
arg = meta.value()?.parse()?;
return Ok(());
}
Ok(())
});
related_udfs.push(arg.clone());
m
} else {
Err(meta.error(format!("Unsupported property: {:?}", meta.path.get_ident())))
}
});
parse_macro_input!(args with parser);
// Parse the input struct
let input = parse_macro_input!(input as DeriveInput);
let name = input.clone().ident;
if doc_section_lbl.is_none() {
eprintln!("label for doc_section should exist");
}
let label = doc_section_lbl.as_ref().unwrap().value();
// Try to find a predefined const by label first.
// If there is no match but label exists, default value will be used for include and description
let doc_section_option = doc_sections_const().iter().find(|ds| ds.label == label);
let (doc_section_include, doc_section_label, doc_section_desc) =
match doc_section_option {
Some(section) => (section.include, section.label, section.description),
None => (true, label.as_str(), None),
};
let doc_section_description = doc_section_desc
.map(|desc| quote! { Some(#desc)})
.unwrap_or_else(|| quote! { None });
let sql_example = sql_example.map(|ex| {
quote! {
.with_sql_example(#ex)
}
});
let udf_args = udf_args
.iter()
.map(|(name, desc)| {
quote! {
.with_argument(#name, #desc)
}
})
.collect::<Vec<_>>();
let standard_args = standard_args
.iter()
.map(|(name, desc)| {
let desc = if let Some(d) = desc {
quote! { #d.into() }
} else {
quote! { None }
};
quote! {
.with_standard_argument(#name, #desc)
}
})
.collect::<Vec<_>>();
let related_udfs = related_udfs
.iter()
.map(|name| {
quote! {
.with_related_udf(#name)
}
})
.collect::<Vec<_>>();
let alt_syntax_example = alt_syntax_example.iter().map(|syn| {
quote! {
.with_alternative_syntax(#syn)
}
});
let generated = quote! {
#input
impl #name {
fn doc(&self) -> Option<&datafusion_doc::Documentation> {
static DOCUMENTATION: std::sync::LazyLock<datafusion_doc::Documentation> =
std::sync::LazyLock::new(|| {
datafusion_doc::Documentation::builder(datafusion_doc::DocSection { include: #doc_section_include, label: #doc_section_label, description: #doc_section_description },
#description.to_string(), #syntax_example.to_string())
#sql_example
#(#alt_syntax_example)*
#(#standard_args)*
#(#udf_args)*
#(#related_udfs)*
.build()
});
Some(&DOCUMENTATION)
}
}
};
// Debug the generated code if needed
// if name == "ArrayAgg" {
// eprintln!("Generated code: {}", generated);
// }
// Return the generated code
TokenStream::from(generated)
}