pgx_named_columns/
lib.rs

1#![doc = include_str!("../README.md")]
2
3#[macro_use]
4extern crate proc_macro_error;
5
6use proc_macro2::{Ident, Span};
7use quote::quote;
8use std::path::PathBuf;
9use syn::punctuated::Punctuated;
10use syn::spanned::Spanned;
11use syn::{
12    parse_macro_input, parse_quote, AttributeArgs, FnArg, GenericArgument, Item, ItemFn,
13    ItemStruct, Lit, NestedMeta, PatType, Path, PathArguments, ReturnType, Token, TraitBound, Type,
14    TypeParamBound, TypePath, TypeTuple,
15};
16
17fn get_return_iterator_item(function: &ItemFn) -> Option<&Ident> {
18    let ret = match &function.sig.output {
19        ReturnType::Type(_, typ) => typ.as_ref(),
20        ReturnType::Default => {
21            emit_error!(
22                &function.sig, "function has no return value";
23                note = "function must return an `impl Iterator`";
24                help = "add `-> impl Iterator<Item = YourRowStruct>`";
25            );
26            return None;
27        }
28    };
29
30    let bound = match ret {
31        Type::ImplTrait(impl_trait) => impl_trait.bounds.first().unwrap(),
32        ty => {
33            emit_error!(
34                ty, "return value must be an impl Iterator";
35                help = "change this type to `impl Iterator<Item = YourRowStruct>`";
36            );
37            return None;
38        }
39    };
40
41    let iterator_item = match bound {
42        TypeParamBound::Trait(TraitBound {
43            path: Path { segments, .. },
44            ..
45        }) if segments.len() == 1 => {
46            let first = segments.first().unwrap();
47            if first.ident != "Iterator" {
48                emit_error!(first.ident, "should be `Iterator`");
49                return None;
50            }
51
52            match &first.arguments {
53                PathArguments::AngleBracketed(ab) => ab
54                    .args
55                    .iter()
56                    .filter_map(|arg| match arg {
57                        GenericArgument::Binding(binding) if binding.ident == "Item" => {
58                            Some(&binding.ty)
59                        }
60                        _ => None,
61                    })
62                    .next()
63                    .expect("no associated type Item on Iterator"),
64                _ => abort!(&first.arguments, "the Iterator has no associated types"),
65            }
66        }
67        TypeParamBound::Lifetime(_) => unreachable!(),
68        TypeParamBound::Trait(TraitBound {
69            path: Path { segments, .. },
70            ..
71        }) => abort!(
72            segments,
73            "this trait should be exactly `Iterator` (with no path components before)"
74        ),
75    };
76
77    let iterator_item = match iterator_item {
78        Type::Path(TypePath { path, .. }) if path.segments.len() == 1 => &path.segments[0].ident,
79        Type::Path(_) => {
80            emit_error!(iterator_item, "expected an identifier (path given)");
81            return None;
82        }
83        _ => {
84            emit_error!(iterator_item, "expected an identifier");
85            return None;
86        }
87    };
88
89    Some(iterator_item)
90}
91
92#[cfg(not(doctest))]
93fn read_struct(source_path_span: Span, iterator_item: &Ident, source_path: &PathBuf) -> ItemStruct {
94    // That's gross. That's how I deal with doctests.
95    if iterator_item == "IndexedLetter" && source_path.ends_with("path/to/current/file.rs") {
96        return parse_quote! {
97            pub struct IndexedLetter {
98                idx: i8,
99                letter: char,
100            }
101        };
102    }
103
104    let source_contents = match std::fs::read_to_string(source_path) {
105        Ok(source_contents) => source_contents,
106        Err(io_err) => {
107            abort!(
108                source_path_span,
109                "io error opening {}: {}",
110                source_path.display(),
111                io_err,
112            )
113        }
114    };
115
116    let source = syn::parse_file(&source_contents).unwrap();
117    let struct_def = source
118        .items
119        .into_iter()
120        .filter_map(|item| match item {
121            Item::Struct(struct_item) if &struct_item.ident == iterator_item => Some(struct_item),
122            _ => None,
123        })
124        .next();
125
126    match struct_def {
127        Some(struct_def) => struct_def,
128        None => {
129            abort!(
130                iterator_item,
131                "no top-level structure with this name found in the file {}",
132                source_path.display();
133                info = source_path_span => "the file is specified here";
134            )
135        }
136    }
137}
138
139fn struct_to_tuple(s: ItemStruct) -> TypeTuple {
140    let fields = s.fields.into_iter().map(|field| {
141        let name = &field.ident;
142        let ty = &field.ty;
143
144        quote! { ::pgx::name!(#name, #ty) }
145    });
146
147    parse_quote! {
148        (
149            #(#fields,)*
150        )
151    }
152}
153
154/// Defines a [`#[pg_extern]`][::pgx::pg_extern] function, but with its columns specified as the
155/// fields of a structure
156///
157/// ## Conditions
158///
159/// This proc-macro _may only_ be applied to a top-level function item whose return type is
160/// `-> impl Iterator<Item = T>`, where `T` is a top-level structure in the same file as the
161/// function item. The trait [`Iterator`] above _must_ be written exactly as-is, with no leading
162/// module path (« `impl std::iter::Iterator<Item = T>` » will not work). The macro `#[pg_extern]`
163/// _may not_ be applied to an item where `#[pg_extern_columns]`, as the latter will automatically
164/// add the former where it should.
165///
166/// If the macro call doesn't respect one of these conditions, it might unexpectedly stop working at
167/// any point in time.
168///
169/// ## Usage
170///
171/// The macro must be given — as a single nameless parameter — a string literal corresponding to the
172/// current file in which the macro is used. This will be used in order to find the definition of
173/// the structure `T`. When [`proc_macro_span`][proc_macro_span] becomes stable, it is likely that
174/// this parameter will become useless, and even deprecated. Providing a parameter that isn't a path
175/// to the current file is undefined compile-time behavior.
176///
177/// Currently, passing arguments to the automatically-emitted `#[pg_extern]` _is not_ supported.
178///
179/// [proc_macro_span](https://doc.rust-lang.org/proc_macro/struct.Span.html#method.source_file)
180///
181/// ## Example
182///
183/// ```rust
184/// # use pgx::*;
185/// # use pgx_named_columns::*;
186/// #
187/// const ALPHABET: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
188///
189/// struct IndexedLetter {
190///     idx: i8,
191///     letter: char,
192/// }
193///
194/// #[pg_extern_columns("path/to/current/file.rs")]
195/// fn alphabet(length: i8) -> impl Iterator<Item = IndexedLetter> {
196///     panic!("{}", file!());
197///     ALPHABET
198///         .chars()
199///         .take(length.clamp(0, 25) as usize)
200///         .enumerate()
201///         .map(|(idx, letter)| IndexedLetter {
202///             idx: idx as _,
203///             letter,
204///         })
205/// }
206/// ```
207#[proc_macro_error]
208#[proc_macro_attribute]
209pub fn pg_extern_columns(
210    attr: proc_macro::TokenStream,
211    input: proc_macro::TokenStream,
212) -> proc_macro::TokenStream {
213    let source_path = match parse_macro_input!(attr as AttributeArgs) {
214        attr if attr.is_empty() => {
215            emit_error!(
216                Span::call_site(), "missing path";
217                help = r#"add the path in brackets: #[pg_extern_columns("path/to/this/file.rs")]"#;
218            );
219            None
220        }
221        attr if attr.len() == 1 => match &attr[0] {
222            nm @ NestedMeta::Lit(Lit::Str(str)) => Some((PathBuf::from(str.value()), nm.span())),
223            attr => {
224                emit_error!(attr, "only argument should be a string literal");
225                None
226            }
227        },
228        attr => {
229            emit_error!(attr[1], "too many arguments given to #[pg_extern_columns]");
230            None
231        }
232    };
233
234    let function = parse_macro_input!(input as ItemFn);
235    let iterator_item = get_return_iterator_item(&function);
236
237    if let Some(attr) = function
238        .attrs
239        .iter()
240        .find(|attr| attr.path.segments.last().unwrap().ident == "pg_extern")
241    {
242        emit_error!(attr, "#[pg_extern] shouldn't be applied to this function, #[pg_extern_columns] applies it automatically");
243    }
244
245    proc_macro_error::abort_if_dirty();
246
247    // FIXME: when source_file() is stable
248    //  let source_path = iterator_item.span().source_file().path();
249    let (source_path, source_path_span) = source_path.unwrap();
250    let iterator_item = iterator_item.unwrap();
251
252    let struct_def = read_struct(source_path_span, iterator_item, &source_path);
253
254    let struct_name = struct_def.ident.clone();
255
256    let function_name = &function.sig.ident;
257    let mut function_sig = function.sig.clone();
258
259    let field_names = struct_def
260        .fields
261        .iter()
262        .map(|field| field.ident.as_ref().unwrap());
263
264    let into_tuple = quote! {
265        (
266            #(self.#field_names,)*
267        )
268    };
269
270    let tuple = Type::Tuple(struct_to_tuple(struct_def));
271
272    let args = function
273        .sig
274        .inputs
275        .iter()
276        .map(|arg| {
277            if let FnArg::Typed(arg) = arg {
278                arg
279            } else {
280                unreachable!()
281            }
282        })
283        .enumerate()
284        .map(|(i, arg)| (Ident::new(&format!("arg{}", i), arg.span()), arg))
285        .collect::<Vec<_>>();
286
287    let calling_args = args
288        .iter()
289        .map(|(i, _)| i)
290        .collect::<Punctuated<&Ident, Token![,]>>();
291
292    function_sig.inputs = args
293        .iter()
294        .map::<FnArg, _>(|(name, PatType { ty, .. })| parse_quote! { #name: #ty })
295        .collect::<Punctuated<FnArg, Token![,]>>();
296
297    function_sig.output = parse_quote! {
298        -> impl std::iter::Iterator<Item = #tuple>
299    };
300
301    let wrapping_module_name = Ident::new(
302        &format!("__pgx_named_columns_wrapper_{}", function_name),
303        function_name.span(),
304    );
305
306    let q = quote! {
307        #function
308
309        mod #wrapping_module_name {
310            #![allow(deprecated)]
311
312            use super::*;
313            use pgx::*;
314
315            type Tuple = #tuple;
316
317            trait IntoTuple {
318                fn into_tuple(self) -> Tuple;
319            }
320
321            impl IntoTuple for #struct_name {
322                #[inline]
323                fn into_tuple(self) -> Tuple {
324                    #into_tuple
325                }
326            }
327
328            #[pg_extern]
329            #function_sig {
330                super::#function_name(#calling_args).map(IntoTuple::into_tuple)
331            }
332        }
333    };
334
335    q.into()
336}