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 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#[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 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}