#![feature(proc_macro_span)] extern crate proc_macro; use std::collections::HashMap; use std::fs; use proc_macro::TokenStream; use quote::quote; #[proc_macro_derive(Model, attributes(model))] pub fn model_derive(input: TokenStream) -> TokenStream { // Construct a representation of Rust code as a syntax tree // that we can manipulate let ast = syn::parse(input).unwrap(); // Build the trait implementation impl_model(&ast) } fn to_field_spec(field: &syn::Field) -> impl quote::ToTokens { fn parse_type(ty: &str) -> syn::Type { syn::parse_str(ty).unwrap() } let field_name = &field.ident; let field_type = &field.ty; let model_options: HashMap = field.attrs.iter() .filter_map(|attr| attr.parse_meta().ok()) .filter_map(|meta| if let syn::Meta::List(meta) = meta { Some(meta) } else { None }) .filter(|meta| meta.path.get_ident().map_or(false, |path| path == "model")) .flat_map(|model| { model.nested.into_iter().filter_map(|item| { if let syn::NestedMeta::Meta(syn::Meta::NameValue(data)) = item { Some((data.path, data.lit)) } else { None } }) }) .collect(); if field_type == &parse_type("Option") { quote! { ::tosin::db::models::Field::IntField { name: stringify!(#field_name) } } } else if field_type == &parse_type("Id") { // TODO foreign key constraint quote! { ::tosin::db::models::Field::IntField { name: stringify!(#field_name) } } } else if field_type == &parse_type("usize") { // TODO default quote! { ::tosin::db::models::Field::IntField { name: stringify!(#field_name) } } } else if field_type == &parse_type("String") { let max_length = model_options.iter() .find(|(name, _value)| name.get_ident().map_or(false, |path| path == "max_length")) .map(|(_name, value)| value); if let Some(max_length) = max_length { quote! { ::tosin::db::models::Field::CharField { name: stringify!(#field_name), max_length: Some(#max_length) } } } else { quote! { ::tosin::db::models::Field::CharField { name: stringify!(#field_name), max_length: None } } } } else if field_type == &parse_type("time::PrimitiveDateTime") { quote! { ::tosin::db::models::Field::DateTimeField { name: stringify!(#field_name) } } } else { use quote::ToTokens; panic!("can't handle {}", field.to_token_stream()) } } fn impl_model(ast: &syn::DeriveInput) -> TokenStream { let name = &ast.ident; let fields = if let syn::Data::Struct(ast) = &ast.data { ast.fields.iter() .map(to_field_spec) } else { panic!("not on a struct"); }; let gen = quote! { impl #name { pub const META: ::tosin::db::models::ModelMeta = ::tosin::db::models::ModelMeta { name: stringify!(#name), fields: &[ #(#fields),* ], }; } }; gen.into() } #[proc_macro] pub fn gather_migrations(_input: TokenStream) -> TokenStream { let call_site = proc_macro::Span::call_site(); let call_site_file = call_site.source_file(); let call_site_path = call_site_file.path(); if !call_site_file.is_real() { panic!("call site does not have a real path"); } let migrations_dir = call_site_path.parent().unwrap(); let migrations: Vec = migrations_dir.read_dir() .unwrap() .map(Result::unwrap) .map(|x| x.path().file_stem().unwrap().to_string_lossy().into_owned()) .filter(|x| x != "mod") .map(|x| syn::parse_str(&x).unwrap()) .collect(); let gen = quote! { #( mod #migrations; )* pub const ALL: &[Migration] = &[ #(#migrations::MIGRATION),* ]; }; gen.into() } #[proc_macro] pub fn gather_models(_input: TokenStream) -> TokenStream { let call_site = proc_macro::Span::call_site(); let call_site_file = call_site.source_file(); let call_site_path = call_site_file.path(); if !call_site_file.is_real() { panic!("call site does not have a real path"); } let call_site_ast = syn::parse_file(&fs::read_to_string(call_site_path).unwrap()).unwrap(); let models = call_site_ast.items.iter() .filter_map(|item| if let syn::Item::Struct(item) = item { Some(item) } else { None }) .filter(|item| item.attrs.iter().any(|attr| { let attr = if let Ok(syn::Meta::List(attr)) = attr.parse_meta() { attr } else { return false; }; if attr.path.get_ident().map_or(false, |hopefully_derive| hopefully_derive == "derive") { let mut derived = attr.nested.iter() .filter_map(|derived| if let syn::NestedMeta::Meta(derived) = derived { Some(derived) } else { None }); derived.any(|derived| derived.path().get_ident().map_or(false, |hopefully_model| hopefully_model == "Model")) } else { false } })) .map(|item| &item.ident); let gen = quote! { pub const ALL: &[tosin::db::models::ModelMeta] = &[ #(#models::META),* ]; }; gen.into() }