microrm-macros 0.2.5

Procedural macro implementations for the microrm crate.
Documentation
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};

pub fn derive(tokens: TokenStream) -> TokenStream {
    let input = parse_macro_input!(tokens as DeriveInput);

    if let syn::Data::Enum(e) = &input.data {
        return derive_modelable_enum(&input, e);
    }

    if let syn::Data::Struct(s) = &input.data {
        if s.fields.len() == 1 {
            return derive_transparent_struct(&input, s);
        }
    }

    derive_modelable_general(&input)
}

fn derive_transparent_struct(input: &syn::DeriveInput, ds: &syn::DataStruct) -> TokenStream {
    // for single-element structs, we can simply store these transparently however the element
    // would be stored
    let microrm_ref = crate::parse_microrm_ref(&input.attrs);
    let struct_name = &input.ident;

    let field = ds.fields.iter().next().unwrap();

    let (bind_to, build_from) = if let Some(i) = &field.ident {
        (
            quote! { self.#i.bind_to(stmt, col) },
            quote! { Self { #i: field.0 } },
        )
    } else {
        (
            quote! { self.0.bind_to(stmt, col) },
            quote! { Self(field.0) },
        )
    };

    let field_ty = &field.ty;

    quote! {
        impl #microrm_ref::model::Modelable for #struct_name {
            fn bind_to(&self, stmt: &mut #microrm_ref::re_export::sqlite::Statement, col: usize) -> #microrm_ref::re_export::sqlite::Result<()> {
                #bind_to
            }

            fn build_from(stmt: &#microrm_ref::re_export::sqlite::Statement, col_offset: usize) -> #microrm_ref::re_export::sqlite::Result<(Self,usize)> {
                let field = #field_ty::build_from(stmt, col_offset)?;
                Ok((#build_from, field.1))
            }

            fn column_type() -> &'static str where Self: Sized {
                <#field_ty as #microrm_ref::model::Modelable>::column_type()
            }
        }
    }.into()
}

fn derive_modelable_enum(input: &syn::DeriveInput, de: &syn::DataEnum) -> TokenStream {
    for variant in &de.variants {
        if !variant.fields.is_empty() {
            return derive_modelable_general(input);
        }
    }

    // only unit variants! we can store as a string
    let microrm_ref = crate::parse_microrm_ref(&input.attrs);
    let enum_name = &input.ident;

    let mut variant_names = Vec::new();
    let mut variant_name_strs = Vec::new();

    for variant in &de.variants {
        variant_names.push(variant.ident.clone());
        variant_name_strs.push(variant.ident.to_string());
    }

    quote! {
        impl #microrm_ref::model::Modelable for #enum_name {
            fn bind_to(&self, stmt: &mut #microrm_ref::re_export::sqlite::Statement, col: usize) -> #microrm_ref::re_export::sqlite::Result<()> {
                match self {
                    #(
                        Self::#variant_names => #variant_name_strs.bind_to(stmt, col)
                    ),*
                }
            }

            fn build_from(stmt: &#microrm_ref::re_export::sqlite::Statement, col_offset: usize) -> #microrm_ref::re_export::sqlite::Result<(Self,usize)> {
                let str_form = String::build_from(stmt, col_offset)?.0;
                #(
                    if str_form == #variant_name_strs { return Ok((Self::#variant_names, 1)) }
                )*

                return Err(#microrm_ref::re_export::sqlite::Error { code: None, message: None })
            }

            fn column_type() -> &'static str where Self: Sized {
                "text"
            }
        }
    }.into()
}

fn derive_modelable_general(input: &syn::DeriveInput) -> TokenStream {
    let microrm_ref = crate::parse_microrm_ref(&input.attrs);

    let ident = &input.ident;

    quote!{
        impl #microrm_ref::model::Modelable for #ident {
            fn bind_to(&self, stmt: &mut #microrm_ref::re_export::sqlite::Statement, col: usize) -> #microrm_ref::re_export::sqlite::Result<()> {
                use #microrm_ref::re_export::sqlite;
                use #microrm_ref::model::Modelable;
                serde_json::to_string(self).expect("can be serialized").bind_to(stmt, col)
            }
            fn build_from(stmt: &#microrm_ref::re_export::sqlite::Statement, col_offset: usize) -> #microrm_ref::re_export::sqlite::Result<(Self,usize)> {
                use #microrm_ref::re_export::sqlite;
                use #microrm_ref::model::Modelable;
                let str_data = stmt.read::<String>(col_offset).map_err(|e| sqlite::Error { code: None, message: Some(e.to_string()) })?;
                let data = serde_json::from_str(str_data.as_str()).map_err(|e| sqlite::Error { code: None, message: Some(e.to_string()) })?;
                Ok((data,1))
            }
            fn column_type() -> &'static str where Self: Sized {
                "blob"
            }
        }
    }.into()
}