Skip to main content

i_love_jesus_macros/
lib.rs

1// SPDX-FileCopyrightText: 2024, 2025 Joseph Silva
2//
3// SPDX-License-Identifier: MIT OR Apache-2.0
4
5use proc_macro::TokenStream;
6use quote::quote;
7use quote::quote_spanned;
8use syn::parse::Parse;
9use syn::parse::ParseStream;
10use syn::parse_macro_input;
11use syn::spanned::Spanned;
12use syn::Data;
13use syn::DeriveInput;
14use syn::Fields;
15use syn::Meta;
16use syn::Path;
17use syn::Token;
18
19/// Generates a public module with `CursorKey` implementations for each field of the struct.
20///
21/// The generated module is somewhat similar to one generated by the `diesel::table` macro. For each field, there's a struct in the form of `pub struct field_name;` which implements `CursorKey`.
22///
23/// # Required type attributes
24/// - `#[diesel(table_name = /* name */)]` specifies the path to the module generated by the `diesel::table` macro.
25/// - `#[cursor_keys_module(name = /* name */)]` specifies the name of the module that will be generated.
26///
27/// # Optional field attributes
28/// - If you use the `diesel::expression::Selectable` macro, then the `CursorKeysModule` macro will adapt to the `#[diesel(select_expression = /* expression */)]` and `#[diesel(select_expression_type = /* type */)]` attributes when generating implementations of CursorKey::get_sql_value. These attributes "just work" as long as you always use the `as_select` method for queries that return this struct. Type inference is not supported yet. For best performance, make sure that the selected expressions match the indexed expressions.
29#[proc_macro_derive(CursorKeysModule, attributes(diesel, cursor_keys_module))]
30pub fn cursor_keys_module_derive(input: TokenStream) -> TokenStream {
31    let input = parse_macro_input!(input as DeriveInput);
32    let struct_name = input.ident;
33    let mut table_name = None;
34    let mut module_name = None;
35    for attr in input.attrs {
36        if let Meta::List(meta) = attr.meta {
37            if table_name.is_none() && meta.path.is_ident("diesel") {
38                if let Ok(a) = meta.parse_args::<MetaNameValue>() {
39                    if a.path.is_ident("table_name") {
40                        table_name = Some(a.value);
41                    }
42                }
43            } else if module_name.is_none() && meta.path.is_ident("cursor_keys_module") {
44                if let Ok(a) = meta.parse_args::<MetaNameValue>() {
45                    if a.path.is_ident("name") {
46                        module_name = Some(a.value);
47                    }
48                }
49            }
50        }
51    }
52    let Some(table_name) = table_name else {
53        return quote! {
54            compile_error!("missing `#[diesel(table_name = /* name */)]`");
55        }
56        .into();
57    };
58    let Some(module_name) = module_name else {
59        return quote! {
60            compile_error!("missing `#[cursor_keys_module(name = /* name */)]`");
61        }
62        .into();
63    };
64    let Data::Struct(data) = input.data else {
65        return quote! {
66            compile_error!("input must be struct");
67        }
68        .into();
69    };
70    let Fields::Named(fields) = data.fields else {
71        return quote! {
72            compile_error!("tuple struct not supported")
73        }
74        .into();
75    };
76    let struct_defs = fields.named.iter().map(|field| {
77        let span = field.span();
78        let ident = field
79            .ident
80            .as_ref()
81            .expect("should have already returned early in the tuple struct case");
82        quote_spanned! {span =>
83            #[allow(non_camel_case_types)]
84            #[derive(Clone, Copy, Debug, Default)]
85            pub struct #ident;
86        }
87    });
88    let impls = fields.named.iter().map(|field| {
89        let span = field.span();
90        let ident = field.ident.as_ref().expect("should have already returned early in the tuple struct case");
91        let ty = &field.ty;
92        let column = quote! {
93            #table_name::#ident
94        };
95        let mut select_expression = None;
96        let mut select_expression_type = None;
97        for attr in &field.attrs {
98            if let Meta::List(meta) = &attr.meta {
99                if meta.path.is_ident("diesel") {
100                    if let Ok(a) = meta.parse_args::<MetaNameValue>() {
101                        if a.path.is_ident("select_expression") {
102                            select_expression = Some(a.value);
103                        } else if a.path.is_ident("select_expression_type") {
104                            select_expression_type = Some(a.value);
105                        }
106                    }
107                }
108            }
109        }
110        let (select_expression, select_expression_type) = match (select_expression, select_expression_type) {
111            (Some(_), None) => return quote_spanned! {span =>
112                compile_error!("missing `#[diesel(select_expression_type = /* type */)]`");
113            },
114            (None, Some(_)) => return quote_spanned! {span =>
115                compile_error!("missing `#[diesel(select_expression = /* expression */)]`");
116            },
117            (None, None) => (column.clone(), column.clone()),
118            (Some(select_expression), Some(select_expression_type)) => (select_expression, select_expression_type),
119        };
120        quote_spanned! {span =>
121            impl ::i_love_jesus::CursorKey<#struct_name> for #module_name::#ident {
122                type SqlType = <#select_expression_type as ::diesel::Expression>::SqlType;
123                type CursorValue = ::diesel::dsl::AsExprOf<#ty, Self::SqlType>;
124                type SqlValue = #select_expression_type;
125                fn get_cursor_value(cursor: &#struct_name) -> Self::CursorValue {
126                    ::diesel::expression::AsExpression::<Self::SqlType>::as_expression(::std::clone::Clone::clone(&cursor.#ident))
127                }
128                fn get_sql_value() -> Self::SqlValue {
129                    #select_expression
130                }
131            }
132        }
133    });
134
135    // impls are outside of the module, otherwise the names defined by `struct_defs` might conflict with names referenced by `impls`
136    quote! {
137        pub mod #module_name {
138            use super::*;
139            #(#struct_defs)*
140        }
141        #(#impls)*
142    }
143    .into()
144}
145
146/// Similar to [`syn::MetaNameValue`], except the value can be a type.
147struct MetaNameValue {
148    pub path: Path,
149    pub value: proc_macro2::TokenStream,
150}
151
152impl Parse for MetaNameValue {
153    fn parse(input: ParseStream) -> syn::Result<Self> {
154        let path = input.parse()?;
155        let _: Token![=] = input.parse()?;
156        let value = input.parse()?;
157        Ok(MetaNameValue { path, value })
158    }
159}