Переглянути джерело

Add initial support in type system for foreign keys.

Kestrel 2 роки тому
батько
коміт
2029487b72
3 змінених файлів з 80 додано та 10 видалено
  1. 53 10
      microrm-macros/src/lib.rs
  2. 12 0
      microrm/src/lib.rs
  3. 15 0
      microrm/src/model.rs

+ 53 - 10
microrm-macros/src/lib.rs

@@ -19,6 +19,16 @@ fn parse_microrm_ref(attrs: &[syn::Attribute]) -> proc_macro2::TokenStream {
     quote! { ::microrm }
 }
 
+fn parse_fk(attrs: &[syn::Attribute]) -> bool {
+    for attr in attrs {
+        if attr.path.segments.len() == 1 && attr.path.segments.last().unwrap().ident == "microrm_foreign" {
+            return true
+        }
+    }
+
+    false
+}
+
 /// Turns a serializable/deserializable struct into a microrm entity model.
 ///
 /// There are two important visible effects:
@@ -34,7 +44,7 @@ fn parse_microrm_ref(attrs: &[syn::Attribute]) -> proc_macro2::TokenStream {
 /// - `#[microrm_internal]`: this is internal to the microrm crate (of extremely limited usefulness
 /// outside the microrm library)
 /// The following are understood on individual fields
-/// - `#[microrm_foreign]`: this is a foreign key (and must be an type implementing `EntityID`)
+/// - `#[microrm_foreign]`: this is a foreign key (and the field must be of a type implementing `EntityID`)
 #[proc_macro_derive(Entity, attributes(microrm_internal, microrm_foreign))]
 pub fn derive_entity(tokens: TokenStream) -> TokenStream {
     let input = parse_macro_input!(tokens as DeriveInput);
@@ -56,11 +66,13 @@ pub fn derive_entity(tokens: TokenStream) -> TokenStream {
         _ => panic!("Can only use derive(Entity) on non-unit structs with named fields!"),
     };
 
-    let mut variants = syn::punctuated::Punctuated::<syn::Ident, syn::token::Comma>::new();
-    let mut field_names =
-        syn::punctuated::Punctuated::<proc_macro2::TokenStream, syn::token::Comma>::new();
-    let mut value_references =
-        syn::punctuated::Punctuated::<proc_macro2::TokenStream, syn::token::Comma>::new();
+    let mut variants = Vec::new();
+    let mut field_names = Vec::new();
+    let mut value_references = Vec::new();
+
+    let mut foreign_keys = Vec::new();
+    let mut foreign_key_impls = Vec::new();
+
     for name in fields.named.iter() {
         let converted_case =
             format!("{}", name.ident.as_ref().unwrap().clone()).to_case(Case::UpperCamel);
@@ -71,9 +83,34 @@ pub fn derive_entity(tokens: TokenStream) -> TokenStream {
         let field_name_str = format!("{}", field_name);
         field_names.push(quote! { Self::Column::#converted_case => #field_name_str });
 
+        if parse_fk(&name.attrs) {
+            let fk_struct_name = format_ident!("{}{}ForeignKey", struct_name, converted_case);
+            let ty = &name.ty;
+            foreign_keys.push(quote!{
+                &#fk_struct_name { col: #enum_name::#converted_case }
+            });
+            foreign_key_impls.push(quote!{
+                struct #fk_struct_name {
+                    col: #enum_name
+                }
+                impl #microrm_ref::model::EntityForeignKey<#enum_name> for #fk_struct_name {
+                    fn local_column(&self) -> &#enum_name { &self.col }
+                    fn foreign_table_name(&self) -> &'static str {
+                        <<#ty as #microrm_ref::model::EntityID>::Entity as #microrm_ref::model::Entity>::table_name()
+                    }
+                    fn foreign_column_name(&self) -> &'static str {
+                        "id"
+                    }
+                }
+            });
+        }
+
         value_references.push(quote! { &self. #field_name });
     }
 
+    let fk_array_ident = format_ident!("FOREIGN_KEYS_{}", struct_name);
+    let fk_array_len = foreign_keys.len();
+
     let field_count = fields.named.iter().count();
 
     quote!{
@@ -82,7 +119,7 @@ pub fn derive_entity(tokens: TokenStream) -> TokenStream {
         #[allow(unused)]
         pub enum #enum_name {
             ID,
-            #variants
+            #(#variants),*
         }
 
         #[derive(Debug,PartialEq,Clone,Copy,#microrm_ref::re_export::serde::Serialize,#microrm_ref::re_export::serde::Deserialize)]
@@ -101,7 +138,7 @@ pub fn derive_entity(tokens: TokenStream) -> TokenStream {
         }
 
         impl #microrm_ref::re_export::rusqlite::ToSql for #id_name {
-            fn to_sql(&self) -> Result<#microrm_ref::re_export::rusqlite::types::ToSqlOutput<'_>, #microrm_ref::re_export::rusqlite::Error> {
+            fn to_sql(&self) -> #microrm_ref::re_export::rusqlite::Result<#microrm_ref::re_export::rusqlite::types::ToSqlOutput<'_>> {
                 self.0.to_sql()
             }
         }
@@ -122,13 +159,19 @@ pub fn derive_entity(tokens: TokenStream) -> TokenStream {
             fn name(c: Self::Column) -> &'static str {
                 match c {
                     Self::Column::ID => "ID",
-                    #field_names
+                    #(#field_names),*
                 }
             }
             fn values(&self) -> Vec<&dyn #microrm_ref::re_export::rusqlite::ToSql> {
-                vec![ #value_references ]
+                vec![ #(#value_references),* ]
+            }
+
+            fn foreign_keys() -> &'static [&'static dyn #microrm_ref::model::EntityForeignKey<Self::Column>] {
+                &[#(#foreign_keys),*]
             }
         }
+
+        #(#foreign_key_impls)*
     }.into()
 }
 

+ 12 - 0
microrm/src/lib.rs

@@ -207,4 +207,16 @@ mod test {
     fn in_memory_schema() {
         let _db = DB::new_in_memory(simple_schema());
     }
+
+    #[derive(serde::Serialize, serde::Deserialize, crate::Entity)]
+    #[microrm_internal]
+    pub struct S2 {
+        #[microrm_foreign]
+        parent_id: S1ID
+    }
+
+    #[test]
+    fn simple_foreign_key() {
+        let _db = DB::new_in_memory(super::model::SchemaModel::new().add::<S1>().add::<S2>());
+    }
 }

+ 15 - 0
microrm/src/model.rs

@@ -60,6 +60,8 @@ pub trait Entity: for<'de> serde::Deserialize<'de> + serde::Serialize {
     where
         Self: Sized;
     fn values(&self) -> Vec<&dyn rusqlite::ToSql>;
+
+    fn foreign_keys() -> &'static [&'static dyn EntityForeignKey<Self::Column>];
 }
 
 /// Trait representing the columns of a database entity
@@ -74,6 +76,19 @@ pub trait EntityID: std::fmt::Debug + Copy + crate::re_export::rusqlite::ToSql {
     fn raw_id(&self) -> i64;
 }
 
+/// Trait for a foreign key relationship
+pub trait EntityForeignKey<T: EntityColumns> {
+    fn local_column(&self) -> &T;
+    fn foreign_table_name(&self) -> &'static str;
+    fn foreign_column_name(&self) -> &'static str;
+}
+
+impl<E: Entity<Column = T>, T: EntityColumns<Entity = E>> EntityForeignKey<T> for (T, &'static str, &'static str) {
+    fn local_column(&self) -> &T { &self.0 }
+    fn foreign_table_name(&self) -> &'static str { self.1 }
+    fn foreign_column_name(&self) -> &'static str { self.2 }
+}
+
 /// How we describe an entire schema
 #[derive(Debug)]
 pub struct SchemaModel {