## Coverage Report

#### Created: 2025-11-22 06:00

next uncovered line (L), next uncovered region (R), next uncovered branch (B)

    /Users/andrewlamb/Software/datafusion/datafusion/macros/src/user_doc.rs

    Line
    Count
    Source
    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
    24
    extern crate proc_macro;
    25
    use datafusion_doc::scalar_doc_sections::doc_sections_const;
    26
    use proc_macro::TokenStream;
    27
    use quote::quote;
    28
    use 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]
    106
    86
    pub fn user_doc(args: TokenStream, input: TokenStream) -> TokenStream {
    107
    86
        let mut doc_section_lbl: Option<LitStr> = None;
    108
    109
    86
        let mut description: Option<LitStr> = None;
    110
    86
        let mut syntax_example: Option<LitStr> = None;
    111
    86
        let mut alt_syntax_example: Vec<Option<LitStr>> = vec![];
    112
    86
        let mut sql_example: Option<LitStr> = None;
    113
    86
        let mut standard_args: Vec<(Option<LitStr>, Option<LitStr>)> = vec![];
    114
    86
        let mut udf_args: Vec<(Option<LitStr>, Option<LitStr>)> = vec![];
    115
    86
        let mut related_udfs: Vec<Option<LitStr>> = vec![];
    116
    117
    530
        let parser86 = syn::meta::parser86(|meta| {
    118
    530
            if meta.path.is_ident("doc_section") {
    119
    86
                meta.parse_nested_meta(|meta| {
    120
    86
                    if meta.path.is_ident("label") {
    121
    86
                        doc_section_lbl = meta.value()?0.parse()?0;
    122
    86
                        return Ok(());
    123
    0
                    }
    124
    0
                    Ok(())
    125
    86
                })
    126
    444
            } else if meta.path.is_ident("description") {
    127
    86
                description = Some(meta.value()?0.parse()?0);
    128
    86
                Ok(())
    129
    358
            } else if meta.path.is_ident("syntax_example") {
    130
    86
                syntax_example = Some(meta.value()?0.parse()?0);
    131
    86
                Ok(())
    132
    272
            } else if meta.path.is_ident("alternative_syntax") {
    133
    7
                alt_syntax_example.push(Some(meta.value()?0.parse()?0));
    134
    7
                Ok(())
    135
    265
            } else if meta.path.is_ident("sql_example") {
    136
    78
                sql_example = Some(meta.value()?0.parse()?0);
    137
    78
                Ok(())
    138
    187
            } else if meta.path.is_ident("standard_argument") {
    139
    59
                let mut standard_arg: (Option<LitStr>, Option<LitStr>) = (None, None);
    140
    117
                let m59 = meta59.parse_nested_meta59(|meta| {
    141
    117
                    if meta.path.is_ident("name") {
    142
    59
                        standard_arg.0 = meta.value()?0.parse()?0;
    143
    59
                        return Ok(());
    144
    58
                    } else if meta.path.is_ident("prefix") {
    145
    58
                        standard_arg.1 = meta.value()?0.parse()?0;
    146
    58
                        return Ok(());
    147
    0
                    }
    148
    0
                    Ok(())
    149
    117
                });
    150
    151
    59
                standard_args.push(standard_arg.clone());
    152
    153
    59
                m
    154
    128
            } else if meta.path.is_ident("argument") {
    155
    100
                let mut arg: (Option<LitStr>, Option<LitStr>) = (None, None);
    156
    200
                let m100 = meta100.parse_nested_meta100(|meta| {
    157
    200
                    if meta.path.is_ident("name") {
    158
    100
                        arg.0 = meta.value()?0.parse()?0;
    159
    100
                        return Ok(());
    160
    100
                    } else if meta.path.is_ident("description") {
    161
    100
                        arg.1 = meta.value()?0.parse()?0;
    162
    100
                        return Ok(());
    163
    0
                    }
    164
    0
                    Ok(())
    165
    200
                });
    166
    167
    100
                udf_args.push(arg.clone());
    168
    169
    100
                m
    170
    28
            } else if meta.path.is_ident("related_udf") {
    171
    28
                let mut arg: Option<LitStr> = None;
    172
    28
                let m = meta.parse_nested_meta(|meta| {
    173
    28
                    if meta.path.is_ident("name") {
    174
    28
                        arg = meta.value()?0.parse()?0;
    175
    28
                        return Ok(());
    176
    0
                    }
    177
    0
                    Ok(())
    178
    28
                });
    179
    180
    28
                related_udfs.push(arg.clone());
    181
    182
    28
                m
    183
            } else {
    184
    0
                Err(meta.error(format!("Unsupported property: {:?}", meta.path.get_ident())))
    185
            }
    186
    530
        });
    187
    188
    86
        parse_macro_input!(args with parser);
    189
    190
        // Parse the input struct
    191
    86
        let input = parse_macro_input!(input as DeriveInput);
    192
    86
        let name = input.clone().ident;
    193
    194
    86
        if doc_section_lbl.is_none() {
    195
    0
            eprintln!("label for doc_section should exist");
    196
    86
        }
    197
    86
        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
    347
        let doc_section_option86 = doc_sections_const().iter()86.find86(|ds| ds.label == label);
    201
    86
        let (doc_section_include, doc_section_label, doc_section_desc) =
    202
    86
            match doc_section_option {
    203
    86
                Some(section) => (section.include, section.label, section.description),
    204
    0
                None => (true, label.as_str(), None),
    205
            };
    206
    86
        let doc_section_description = doc_section_desc
    207
    86
            .map(|desc| quote!7 { Some(#desc)}7)
    208
    86
            .unwrap_or_else(|| quote!79 { None }79);
    209
    210
    86
        let sql_example = sql_example.map(|ex| {78
    211
    78
            quote! {
    212
                .with_sql_example(#ex)
    213
            }
    214
    78
        });
    215
    216
    86
        let udf_args = udf_args
    217
    86
            .iter()
    218
    100
            .map86(|(name, desc)| {
    219
    100
                quote! {
    220
                    .with_argument(#name, #desc)
    221
                }
    222
    100
            })
    223
    86
            .collect::<Vec<_>>();
    224
    225
    86
        let standard_args = standard_args
    226
    86
            .iter()
    227
    86
            .map(|(name, desc)| {59
    228
    59
                let desc = if let Some(d58) = desc {
    229
    58
                    quote! { #d.into() }
    230
                } else {
    231
    1
                    quote! { None }
    232
                };
    233
    234
    59
                quote! {
    235
                    .with_standard_argument(#name, #desc)
    236
                }
    237
    59
            })
    238
    86
            .collect::<Vec<_>>();
    239
    240
    86
        let related_udfs = related_udfs
    241
    86
            .iter()
    242
    86
            .map(|name| {28
    243
    28
                quote! {
    244
                    .with_related_udf(#name)
    245
                }
    246
    28
            })
    247
    86
            .collect::<Vec<_>>();
    248
    249
    86
        let alt_syntax_example = alt_syntax_example.iter().map(|syn| {7
    250
    7
            quote! {
    251
                .with_alternative_syntax(#syn)
    252
            }
    253
    7
        });
    254
    255
    86
        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
    86
        TokenStream::from(generated)
    283
    86
    }