1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
extern crate proc_macro;
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote, ToTokens, TokenStreamExt};
use syn::{parse_macro_input, punctuated::Punctuated, ItemStruct, Lit, MetaNameValue, Token};

/// Create necessary handler, validator and meta functions for foreign data wrapper
///
/// This macro will create three functions which can be used in Postgres.
///
/// 1. `<snake_case_fdw_name>_fdw_handler()` - foreign data wrapper handler function
/// 2. `<snake_case_fdw_name>_fdw_validator()` - foreign data wrapper validator function
/// 3. `<snake_case_fdw_name>_fdw_meta()` - function to return a table contains fdw metadata
///
/// # Example
///
/// ```rust,no_run
/// use supabase_wrappers::prelude::*;
///
/// #[wrappers_fdw(
///     version = "0.1.0",
///     author = "Supabase",
///     website = "https://github.com/supabase/wrappers/tree/main/wrappers/src/fdw/helloworld_fdw"
/// )]
/// pub struct HelloWorldFdw;
/// ```
///
/// then you can use those functions in Postgres,
///
/// ```sql
/// create extension wrappers;
///
/// create foreign data wrapper helloworld_wrapper
///   handler hello_world_fdw_handler
///   validator hello_world_fdw_validator;
///
/// select * from hello_world_fdw_meta();
/// ```
#[proc_macro_attribute]
pub fn wrappers_fdw(attr: TokenStream, item: TokenStream) -> TokenStream {
    let mut metas = TokenStream2::new();
    let meta_attrs: Punctuated<MetaNameValue, Token![,]> =
        parse_macro_input!(attr with Punctuated::parse_terminated);
    for attr in meta_attrs {
        let name = format!("{}", attr.path.segments.first().unwrap().ident);
        if let Lit::Str(val) = attr.lit {
            let value = val.value();
            metas.append_all(quote! {
                meta.insert(#name.to_owned(), #value.to_owned());
            });
        }
    }

    let item: ItemStruct = parse_macro_input!(item as ItemStruct);
    let item_tokens = item.to_token_stream();
    let ident = item.ident;
    let ident_str = ident.to_string();
    let ident_snake = to_snake_case(ident_str.as_str());

    let module_ident = format_ident!("__{}_pgx", ident_snake);
    let fn_ident = format_ident!("{}_handler", ident_snake);
    let fn_validator_ident = format_ident!("{}_validator", ident_snake);
    let fn_meta_ident = format_ident!("{}_meta", ident_snake);

    let quoted = quote! {
        #item_tokens

        mod #module_ident {
            use super::#ident;
            use std::collections::HashMap;
            use pgx::prelude::*;
            use supabase_wrappers::prelude::*;

            #[pg_extern(create_or_replace)]
            fn #fn_ident() -> supabase_wrappers::FdwRoutine {
                #ident::fdw_routine()
            }

            #[pg_extern(create_or_replace)]
            fn #fn_validator_ident(options: Vec<Option<String>>, catalog: Option<pg_sys::Oid>) {
                #ident::validator(options, catalog)
            }

            #[pg_extern(create_or_replace)]
            fn #fn_meta_ident() -> TableIterator<'static, (
                name!(name, Option<String>),
                name!(version, Option<String>),
                name!(author, Option<String>),
                name!(website, Option<String>)
            )> {
                let mut meta: HashMap<String, String> = HashMap::new();

                #metas

                TableIterator::new(vec![(
                    Some(#ident_str.to_owned()),
                    meta.get("version").map(|s| s.to_owned()),
                    meta.get("author").map(|s| s.to_owned()),
                    meta.get("website").map(|s| s.to_owned()),
                )].into_iter())
            }
        }

    };

    quoted.into()
}

fn to_snake_case(s: &str) -> String {
    let mut acc = String::new();
    let mut prev = '_';
    for ch in s.chars() {
        if ch.is_uppercase() && prev != '_' {
            acc.push('_');
        }
        acc.push(ch);
        prev = ch;
    }
    acc.to_lowercase()
}