supabase_wrappers_macros/
lib.rs

1extern crate proc_macro;
2use proc_macro::TokenStream;
3use proc_macro2::TokenStream as TokenStream2;
4use quote::{format_ident, quote, ToTokens, TokenStreamExt};
5use syn::{parse_macro_input, punctuated::Punctuated, ItemStruct, Lit, MetaNameValue, Token};
6
7/// Create necessary handler, validator and meta functions for foreign data wrapper
8///
9/// This macro will create three functions which can be used in Postgres.
10///
11/// 1. `<snake_case_fdw_name>_fdw_handler()` - foreign data wrapper handler function
12/// 2. `<snake_case_fdw_name>_fdw_validator()` - foreign data wrapper validator function
13/// 3. `<snake_case_fdw_name>_fdw_meta()` - function to return a table contains fdw metadata
14///
15/// # Example
16///
17/// ```rust,no_run
18/// use supabase_wrappers::prelude::*;
19///
20/// #[wrappers_fdw(
21///     version = "0.1.0",
22///     author = "Supabase",
23///     website = "https://github.com/supabase/wrappers/tree/main/wrappers/src/fdw/helloworld_fdw"
24/// )]
25/// pub struct HelloWorldFdw;
26/// ```
27///
28/// then you can use those functions in Postgres,
29///
30/// ```sql
31/// create extension wrappers;
32///
33/// create foreign data wrapper helloworld_wrapper
34///   handler hello_world_fdw_handler
35///   validator hello_world_fdw_validator;
36///
37/// select * from hello_world_fdw_meta();
38/// ```
39#[proc_macro_attribute]
40pub fn wrappers_fdw(attr: TokenStream, item: TokenStream) -> TokenStream {
41    let mut metas = TokenStream2::new();
42    let meta_attrs: Punctuated<MetaNameValue, Token![,]> =
43        parse_macro_input!(attr with Punctuated::parse_terminated);
44    let mut error_type: Option<String> = None;
45    for attr in meta_attrs {
46        let name = format!("{}", attr.path.segments.first().unwrap().ident);
47        if let Lit::Str(val) = attr.lit {
48            let value = val.value();
49            if name == "version" || name == "author" || name == "website" {
50                metas.append_all(quote! {
51                    meta.insert(#name.to_owned(), #value.to_owned());
52                });
53            } else if name == "error_type" {
54                error_type = Some(value);
55            }
56        }
57    }
58
59    let error_type_ident = if let Some(error_type) = error_type {
60        format_ident!("{}", error_type)
61    } else {
62        let quoted = quote! {
63            compile_error!("Missing `error_type` in the `wrappers_fdw` attribute");
64        };
65        return quoted.into();
66    };
67
68    let item: ItemStruct = parse_macro_input!(item as ItemStruct);
69    let item_tokens = item.to_token_stream();
70    let ident = item.ident;
71    let ident_str = ident.to_string();
72    let ident_snake = to_snake_case(ident_str.as_str());
73
74    let module_ident = format_ident!("__{}_pgrx", ident_snake);
75    let fn_ident = format_ident!("{}_handler", ident_snake);
76    let fn_validator_ident = format_ident!("{}_validator", ident_snake);
77    let fn_meta_ident = format_ident!("{}_meta", ident_snake);
78    let fn_get_meta_ident = format_ident!("{}_get_meta", ident_snake);
79
80    let quoted = quote! {
81        #item_tokens
82
83        mod #module_ident {
84            use super::#ident;
85            use std::collections::HashMap;
86            use pgrx::pg_sys::panic::{ErrorReport, ErrorReportable};
87            use pgrx::prelude::*;
88            use supabase_wrappers::prelude::*;
89
90            #[pg_extern(create_or_replace)]
91            fn #fn_ident() -> supabase_wrappers::FdwRoutine {
92                #ident::fdw_routine()
93            }
94
95            #[pg_extern(create_or_replace)]
96            fn #fn_validator_ident(options: Vec<Option<String>>, catalog: Option<pg_sys::Oid>) {
97                #ident::validator(options, catalog)
98                    .map_err(|e| <super::#error_type_ident as Into<ErrorReport>>::into(e))
99                    .unwrap_or_report();
100            }
101
102            pub(super) fn #fn_get_meta_ident() -> HashMap<String, String> {
103                let mut meta: HashMap<String, String> = HashMap::new();
104                #metas
105                meta
106            }
107
108            #[pg_extern(create_or_replace)]
109            fn #fn_meta_ident() -> TableIterator<'static, (
110                name!(name, Option<String>),
111                name!(version, Option<String>),
112                name!(author, Option<String>),
113                name!(website, Option<String>)
114            )> {
115                let meta = #fn_get_meta_ident();
116
117                TableIterator::new(vec![(
118                    Some(#ident_str.to_owned()),
119                    meta.get("version").map(|s| s.to_owned()),
120                    meta.get("author").map(|s| s.to_owned()),
121                    meta.get("website").map(|s| s.to_owned()),
122                )].into_iter())
123            }
124        }
125
126    };
127
128    quoted.into()
129}
130
131fn to_snake_case(s: &str) -> String {
132    let mut acc = String::new();
133    let mut prev = '_';
134    for ch in s.chars() {
135        if ch.is_uppercase() && prev != '_' {
136            acc.push('_');
137        }
138        acc.push(ch);
139        prev = ch;
140    }
141    acc.to_lowercase()
142}