dbstruct-derive 0.4.0

Build a typed database by defining a struct
Documentation
use proc_macro2::TokenStream;
use quote::quote;

use crate::ir::{Accessor, Ir, NewMethod, Struct};

pub fn codegen(ir: Ir) -> TokenStream {
    #![allow(unused_variables)]
    let struct_ident = ir.definition.ident.clone();
    let definition = definition(ir.definition, &ir.bounds);
    let accessors = accessor_impl(ir.accessors);
    let new_impl = new_impl(ir.new);
    let bounds = ir.bounds;

    match bounds {
        Some(bounds) => quote!(
            #definition

            impl<DS> #struct_ident<DS> #bounds {
                #new_impl
                #accessors
            }
        ),
        None => quote!(
            #definition

            impl #struct_ident {
                #new_impl
                #accessors
            }
        ),
    }
}

fn new_impl(new: NewMethod) -> TokenStream {
    let NewMethod {
        locals,
        members,
        vis,
        arg,
        error_ty,
    } = new;
    quote!(
        #vis fn new(#arg) -> Result<Self, ::dbstruct::Error<#error_ty>> {
            #(#locals)*
            Ok(Self {
                ds,
                #(#members),*
            })
        }
    )
}

fn accessor_fn(
    Accessor {
        vis,
        ident,
        returns,
        body,
    }: Accessor,
) -> TokenStream {
    quote!(#vis fn #ident(&self) -> #returns #body)
}

fn accessor_impl(accessors: Vec<Accessor>) -> TokenStream {
    let functions: Vec<_> = accessors.into_iter().map(accessor_fn).collect();
    quote!(
        #(#functions)*
    )
}

fn definition(definition: Struct, bounds: &Option<syn::WhereClause>) -> TokenStream {
    let Struct {
        ident,
        vis,
        member_vars,
        db,
    } = definition;
    match bounds {
        Some(bounds) => {
            let predicates = &bounds.predicates;
            quote!(
                #vis struct #ident<#predicates> {
                    ds: DS,
                    #(#member_vars),*
                }
            )
        }
        None => quote!(
        #vis struct #ident {
            #db,
            #(#member_vars),*
        }),
    }
    .into()
}

#[cfg(test)]
mod tests {
    use std::str::FromStr;

    use syn::parse::Parser;
    use syn::parse_quote;

    use super::*;

    fn test_struct(fields: &[&'static str]) -> Struct {
        let parser = syn::Field::parse_named;
        Struct {
            ident: parse_quote!(Test),
            vis: parse_quote!(pub),
            member_vars: fields
                .into_iter()
                .map(|s| parser.parse_str(s))
                .map(Result::unwrap)
                .collect(),
            db: parser.parse_str("ds: DS").unwrap(),
        }
    }

    fn test_bounds() -> syn::WhereClause {
        parse_quote!(where DS: dbstruct::DataStore + Clone)
    }

    #[test]
    fn output_is_struct_item() {
        let fields = [
            "u8_field: u8",
            "vec_field: Vec<u32>",
            "map_field: HashMap<f32, f64>",
        ];
        let rust = definition(test_struct(&fields), &Some(test_bounds()));
        println!("{}", rust);
        assert!(syn::parse2::<syn::ItemStruct>(rust).is_ok())
    }

    fn test_accessor() -> Accessor {
        Accessor {
            vis: parse_quote!(pub),
            ident: parse_quote!(queue),
            returns: parse_quote!(dbstruct::wrapper::Vec<u32>),
            body: parse_quote!({
                dbstruct::wrapper::Vec::new(self.ds.clone(), 2, self.queue_len.clone())
            }),
        }
    }

    #[test]
    fn output_is_function_item() {
        let rust = accessor_fn(test_accessor());
        println!("{rust}");
        assert!(syn::parse2::<syn::ItemFn>(rust).is_ok())
    }

    fn test_new_impl() -> NewMethod {
        NewMethod {
            members: vec![parse_quote!(u8field: 0)],
            vis: parse_quote!(pub),
            locals: Vec::new(),
            arg: Some(parse_quote!(ds: DS)),
            error_ty: parse_quote!(DS),
        }
    }

    #[test]
    fn new_impl_is_function_item() {
        let rust = new_impl(test_new_impl());
        println!("{rust}");
        assert!(syn::parse2::<syn::ItemFn>(rust).is_ok())
    }

    #[test]
    fn code_is_parsable() {
        let ir = Ir {
            definition: test_struct(&["u8field: u8"]),
            new: test_new_impl(),
            accessors: vec![test_accessor()],
            bounds: parse_quote!(where DS: dbstruct::DataStore + std::clone::Clone),
        };
        let rust = codegen(ir);
        println!("{rust}");
        assert!(syn::parse2::<syn::File>(rust).is_ok())
    }

    #[test]
    fn end_to_end() {
        use crate::model::Model;
        use syn::parse_str;

        let input_attr = proc_macro2::TokenStream::from_str("db=sled").unwrap();
        let input_struct: syn::ItemStruct = parse_str(
            "        
pub struct Test {
    // #[dbstruct(Default)]
    primes: Vec<u32>,
}",
        )
        .unwrap();

        let model = Model::try_from(input_struct, input_attr).unwrap();
        let ir = Ir::from(model);
        let rust = codegen(ir);

        println!("{rust}");
        assert!(syn::parse2::<syn::File>(rust).is_ok())
    }

    #[test]
    fn default_vec_and_map () {
        use crate::model::Model;
        use syn::parse_str;

        let input_attr = proc_macro2::TokenStream::from_str("db=sled").unwrap();
        let input_struct: syn::ItemStruct = parse_str(
            r##"        
pub struct Test {
    /// a small list that we dont want structdb to wrap for us
    #[dbstruct(Default)]
    small_list: Vec<u8>,
    #[dbstruct(Default)]
    small_map: HashMap<usize, u32>,
}
"##,
        )
        .unwrap();

        let model = Model::try_from(input_struct, input_attr).unwrap();
        let ir = Ir::from(model);
        let rust = codegen(ir);

        println!("{rust}");
        assert!(syn::parse2::<syn::File>(rust).is_ok())
    }
}