From c15e69a6c4abb2e47196065c73c7798e8737d915 Mon Sep 17 00:00:00 2001
From: Matthieu Bessat <mbess@mbess.net>
Date: Sat, 28 Dec 2024 19:56:04 +0100
Subject: [PATCH] feat: add repository code generator

---
 README.md                                 |   2 +
 lib/generator_attr/src/lib.rs             |   4 +-
 lib/generator_cli/src/gen_migrations.rs   |  61 ++++
 lib/generator_cli/src/gen_repositories.rs | 118 ++++++++
 lib/generator_cli/src/main.rs             | 330 +---------------------
 lib/generator_cli/src/models.rs           |  20 ++
 lib/generator_cli/src/parse_models.rs     | 216 ++++++++++++++
 7 files changed, 432 insertions(+), 319 deletions(-)
 create mode 100644 lib/generator_cli/src/gen_migrations.rs
 create mode 100644 lib/generator_cli/src/gen_repositories.rs
 create mode 100644 lib/generator_cli/src/models.rs
 create mode 100644 lib/generator_cli/src/parse_models.rs

diff --git a/README.md b/README.md
index ec15f40..2689b07 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,7 @@
 # [WIP] sqlxgentools
 
+better name: sqlitexgentools, sqlitexgen
+
 Tools to generate SQL migrations and Rust SQLx repositories code from models structs in a SQLite context.
 
 Will be used in [minauthator](https://forge.lefuturiste.fr/mbess/minauthator).
diff --git a/lib/generator_attr/src/lib.rs b/lib/generator_attr/src/lib.rs
index d231b6e..e4a6c25 100644
--- a/lib/generator_attr/src/lib.rs
+++ b/lib/generator_attr/src/lib.rs
@@ -1,12 +1,12 @@
 use proc_macro::TokenStream;
 
 #[proc_macro_attribute]
-pub fn sql_generator_model(attr: TokenStream, item: TokenStream) -> TokenStream {
+pub fn sql_generator_model(_attr: TokenStream, item: TokenStream) -> TokenStream {
     item
 }
 
 #[proc_macro_derive(SqlGeneratorDerive, attributes(sql_generator_field))]
-pub fn sql_generator_field(item: TokenStream) -> TokenStream {
+pub fn sql_generator_field(_item: TokenStream) -> TokenStream {
     TokenStream::new()
 }
 
diff --git a/lib/generator_cli/src/gen_migrations.rs b/lib/generator_cli/src/gen_migrations.rs
new file mode 100644
index 0000000..5c9bd7a
--- /dev/null
+++ b/lib/generator_cli/src/gen_migrations.rs
@@ -0,0 +1,61 @@
+use anyhow::{Result, anyhow};
+
+use crate::models::{Field, Model};
+
+
+// Implementations
+impl Field {
+    /// return sqlite type
+    fn sql_type(&self) -> Option<String> {
+        // for now, we just match against the rust type string representation
+        match self.rust_type.as_str() {
+            "u64" => Some("INTEGER".into()),
+            "u32" => Some("INTEGER".into()),
+            "i32" => Some("INTEGER".into()),
+            "i64" => Some("INTEGER".into()),
+            "f64" => Some("REAL".into()),
+            "f32" => Some("REAL".into()),
+            "String" => Some("TEXT".into()),
+            "DateTime" => Some("DATETIME".into()),
+            "Json" => Some("TEXT".into()),
+            "Vec<u8>" => Some("BLOB".into()),
+            _ => Some("TEXT".into())
+        }
+    }
+}
+
+/// Generate CREATE TABLE statement from parsed model
+pub fn generate_create_table_sql(models: &Vec<Model>) -> Result<String> {
+    let mut sql_code: String = "".into();
+    for model in models.iter() {
+        let mut fields_sql: Vec<String> = vec![];
+        for field in model.fields.iter() {
+            let mut additions: String = "".into();
+            let sql_type = field.sql_type()
+                .ok_or(anyhow!(format!("Could not find SQL type for field {}", field.name)))?;
+            if !field.is_nullable {
+                additions.push_str(" NOT NULL");
+            }
+            if field.is_unique {
+                additions.push_str(" UNIQUE");
+            }
+            if field.is_primary {
+                additions.push_str(" PRIMARY KEY");
+            }
+            fields_sql.push(
+                format!("\t{: <#18}\t{}{}", field.name, sql_type, additions)
+            );
+        }
+
+        sql_code.push_str(
+            &format!(
+                "CREATE TABLE {} (\n{}\n);",
+                model.table_name,
+                fields_sql.join(",\n")
+            )
+        );
+    }
+    
+    Ok(sql_code)
+}
+
diff --git a/lib/generator_cli/src/gen_repositories.rs b/lib/generator_cli/src/gen_repositories.rs
new file mode 100644
index 0000000..309f081
--- /dev/null
+++ b/lib/generator_cli/src/gen_repositories.rs
@@ -0,0 +1,118 @@
+use anyhow::{Result, anyhow};
+use proc_macro2::TokenStream;
+use quote::{format_ident, quote};
+use syn::File;
+
+use crate::models::Model;
+
+
+fn gen_get_all_method(model: &Model) -> TokenStream {
+    let resource_ident = format_ident!("{}", &model.name);
+    let error_msg = format!("Failed to fetch resource {:?}", model.name.clone());
+    let select_query = format!("SELECT * FROM {}", model.table_name);
+
+    quote! {
+        pub fn get_all(&self) -> Result<Vec<#resource_ident>> {
+            sqlx::query_as::<_, #resource_ident>(#select_query)
+                .fetch_all(&self.storage.0)
+                .await
+                .context(#error_msg)
+        }
+    }
+}
+
+fn gen_get_by_id_method(model: &Model) -> TokenStream {
+    let resource_ident = format_ident!("{}", &model.name);
+    let error_msg = format!("Failed to fetch resource {:?}", model.name.clone());
+    let select_query = format!("SELECT * FROM {} WHERE id = $1", model.table_name);
+
+    quote! {
+        pub fn get_by_id(&self, id: &str) -> Result<#resource_ident> {
+            sqlx::query_as::<_, #resource_ident>(#select_query)
+                .bind(user_id)
+                .fetch_one(&self.storage.0)
+                .await
+                .context(#error_msg)
+        }
+    }
+}
+
+fn gen_insert_method(model: &Model) -> TokenStream {
+    let resource_ident = format_ident!("{}", &model.name);
+    let error_msg = format!("Failed to insert resource {:?}", model.name.clone());
+    let sql_columns = model.fields.iter()
+        .map(|f| f.name.clone())
+        .collect::<Vec<String>>()
+        .join(", ");
+    let value_templates = (1..(model.fields.len()+1))
+        .map(|i| format!("${}", i))
+        .collect::<Vec<String>>()
+        .join(", ");
+    let insert_query = format!(
+        "INSERT INTO {} ({}) VALUES ({})",
+        model.table_name,
+        sql_columns,
+        value_templates
+    );
+    let field_names: Vec<proc_macro2::Ident> = model.fields.iter()
+        .map(|f| format_ident!("{}", &f.name))
+        .collect();
+
+    quote! {
+        pub fn insert(&self, entity: &#resource_ident) -> Result<()> {
+            sqlx::query(#insert_query)
+                #( .bind( &entity.#field_names ) )*
+                .execute(&self.storage.0)
+                .await
+                .context(#error_msg)?;
+
+            Ok(())
+        }
+    }
+}
+
+fn generate_repository_file(model: &Model) -> Result<()> {
+    let resource_name = model.name.clone();
+
+    let resource_ident = format_ident!("{}", &resource_name);
+    let repository_ident = format_ident!("{}Repository", resource_ident);
+
+    let get_all_method_code = gen_get_all_method(&model);
+    let get_by_id_method_code = gen_get_by_id_method(&model);
+    let insert_method_code = gen_insert_method(&model);
+
+    let base_repository_code: TokenStream = quote! {
+        struct #repository_ident {
+            storage: &Storage
+        }
+
+        impl #repository_ident {
+            fn new(storage: &Storage) -> Self {
+                #repository_ident {
+                    storage
+                }
+            }
+
+            #get_all_method_code
+
+            #get_by_id_method_code
+
+            #insert_method_code
+        }
+    };
+    // convert TokenStream into rust code as string
+    let parse_res: syn::Result<File> = syn::parse2(base_repository_code);
+    let pretty = prettyplease::unparse(&parse_res?);
+    println!("{}", pretty);
+
+    Ok(())
+}
+
+/// Generate base repositories for all models
+pub fn generate_repositories_source_files(models: &Vec<Model>) -> Result<()> {
+    for model in models.iter() {
+        let _ = generate_repository_file(model)?;
+    }
+    Ok(())
+}
+
diff --git a/lib/generator_cli/src/main.rs b/lib/generator_cli/src/main.rs
index e4c3d75..1ced6da 100644
--- a/lib/generator_cli/src/main.rs
+++ b/lib/generator_cli/src/main.rs
@@ -1,31 +1,15 @@
-use std::{fs, path::Path};
+use std::path::Path;
 use attribute_derive::FromAttr;
 
 use argh::FromArgs;
 use anyhow::{Result, anyhow};
-use convert_case::{Case, Casing};
-use proc_macro2::TokenStream;
-use quote::{format_ident, quote};
-use syn::{File, Type};
+use gen_migrations::generate_create_table_sql;
+use gen_repositories::generate_repositories_source_files;
 
-// BASE MODELS
-
-#[derive(Debug)]
-struct Model {
-    name: String,
-    table_name: String,
-    fields: Vec<Field>
-}
-
-#[derive(Debug)]
-struct Field {
-    name: String,
-    rust_type: String,
-    is_nullable: bool,
-    is_unique: bool,
-    is_primary: bool,
-    default: Option<String>
-}
+pub mod models;
+pub mod parse_models;
+pub mod gen_migrations;
+pub mod gen_repositories;
 
 #[derive(FromAttr, PartialEq, Debug, Default)]
 #[attribute(ident = sql_generator_model)]
@@ -40,297 +24,6 @@ pub struct SqlGeneratorFieldAttr {
     is_unique: Option<bool>
 }
 
-// Implementations
-
-fn generate_repository_code() -> Result<()> {
-    let resource_name = "User";
-    let resource_ident = format_ident!("{}", &resource_name);
-    let repository_ident = format_ident!("{}Repository", resource_ident);
-    let error_msg = format!("Failed to fetch resource {:?} by id", resource_name);
-    let token_stream: TokenStream = quote! {
-        struct #repository_ident {}
-
-        impl #repository_ident {
-            pub fn get_by_id(storage: &Storage, id: &str) -> Result<#resource_ident> {
-                sqlx::query_as::<_, #resource_ident>("SELECT * FROM users WHERE id = $1")
-                    .bind(user_id)
-                    .fetch_one(&storage.0)
-                    .await
-                    .context(#error_msg)
-            }
-        }
-    };
-    // convert TokenStream into rust code as string
-    dbg!(&token_stream);
-    let parse_res: syn::Result<File> = syn::parse2(token_stream);
-    let pretty = prettyplease::unparse(&parse_res?);
-    println!("{}", pretty);
-    Ok(())
-}
-
-
-impl Field {
-    /// return sqlite type
-    fn sql_type(&self) -> Option<String> {
-        // for now, we just match against the rust type string representation
-        match self.rust_type.as_str() {
-            "u64" => Some("INTEGER".into()),
-            "u32" => Some("INTEGER".into()),
-            "i32" => Some("INTEGER".into()),
-            "i64" => Some("INTEGER".into()),
-            "f64" => Some("REAL".into()),
-            "f32" => Some("REAL".into()),
-            "String" => Some("TEXT".into()),
-            "DateTime" => Some("DATETIME".into()),
-            "Json" => Some("TEXT".into()),
-            "Vec<u8>" => Some("BLOB".into()),
-            _ => Some("TEXT".into())
-        }
-    }
-}
-
-/// Take struct name as source, apply snake case and pluralize with a s
-fn generate_table_name_from_struct_name(struct_name: &str) -> String {
-    return format!("{}s", struct_name.clone().to_case(Case::Snake));
-}
-
-fn extract_generic_type(base_segments: Vec<String>, ty: &syn::Type) -> Option<&syn::Type> {
-    // If it is not `TypePath`, it is not possible to be `Option<T>`, return `None`
-    if let syn::Type::Path(syn::TypePath { qself: None, path }) = ty {
-        // We have limited the 5 ways to write `Option`, and we can see that after `Option`,
-        // there will be no `PathSegment` of the same level
-        // Therefore, we only need to take out the highest level `PathSegment` and splice it into a string
-        // for comparison with the analysis result
-        let segments_str = &path
-            .segments
-            .iter()
-            .map(|segment| segment.ident.to_string())
-            .collect::<Vec<_>>()
-            .join(":");
-        // Concatenate `PathSegment` into a string, compare and take out the `PathSegment` where `Option` is located
-
-        let option_segment = base_segments
-            .iter()
-            .find(|s| segments_str == *s)
-            .and_then(|_| path.segments.last());
-        let inner_type = option_segment
-            // Take out the generic parameters of the `PathSegment` where `Option` is located
-            // If it is not generic, it is not possible to be `Option<T>`, return `None`
-            // But this situation may not occur
-            .and_then(|path_seg| match &path_seg.arguments {
-                syn::PathArguments::AngleBracketed(syn::AngleBracketedGenericArguments {
-                    args,
-                    ..
-                }) => args.first(),
-                _ => None,
-            })
-            // Take out the type information in the generic parameter
-            // If it is not a type, it is not possible to be `Option<T>`, return `None`
-            // But this situation may not occur
-            .and_then(|generic_arg| match generic_arg {
-                syn::GenericArgument::Type(ty) => Some(ty),
-                _ => None,
-            });
-        // Return `T` in `Option<T>`
-        return inner_type;
-    }
-    None
-}
-
-fn get_type_first_ident(inp: &Type) -> Option<String> {
-    match inp {
-        syn::Type::Path(field_type_path) => {
-            Some(field_type_path.path.segments.get(0).unwrap().ident.to_string())
-        },
-        _ => {
-            None
-        }
-    }
-}
-
-fn parse_model_attribute(item: &syn::ItemStruct) -> Result<Option<SqlGeneratorModelAttr>> {
-    for attr in item.attrs.iter() {
-        let attr_ident = match attr.path().get_ident() {
-            Some(v) => v,
-            None => {
-                continue;
-            }
-        };
-        if attr_ident.to_string() != "sql_generator_model" {
-            continue;
-        }
-
-        match SqlGeneratorModelAttr::from_attribute(attr) {
-            Ok(v) => {
-                return Ok(Some(v));
-            },
-            Err(err) => {
-                return Err(anyhow!("Failed to parse sql_generator_model attribute macro: {}", err));
-            }
-        };
-    }
-    Ok(None)
-}
-
-fn parse_field_attribute(field: &syn::Field) -> Result<Option<SqlGeneratorFieldAttr>> {
-    for attr in field.attrs.iter() {
-        let attr_ident = match attr.path().get_ident() {
-            Some(v) => v,
-            None => {
-                continue;
-            }
-        };
-        if attr_ident.to_string() != "sql_generator_field" {
-            continue;
-        }
-
-        match SqlGeneratorFieldAttr::from_attribute(attr) {
-            Ok(v) => {
-                return Ok(Some(v));
-            },
-            Err(err) => {
-                return Err(anyhow!("Failed to parse sql_generator_field attribute macro: {}", err));
-            }
-        };
-    }
-    Ok(None)
-}
-
-/// Scan for models struct in a rust module and return a struct representing the model
-fn parse_models(models_mod_path: &Path) -> Result<Vec<Model>> {
-    let models_code = fs::read_to_string(models_mod_path)?;
-    let parsed_file = syn::parse_file(&models_code)?;
-
-    let mut models: Vec<Model> = vec![];
-
-    for item in parsed_file.items {
-        match item {
-            syn::Item::Struct(itemval) => {
-                let model_name = itemval.ident.to_string();
-                let model_attrs = match parse_model_attribute(&itemval)? {
-                    Some(v) => v,
-                    None => {
-                        // we require model struct to have the `sql_generator_model` attribute
-                        continue;
-                    }
-                };
-
-                let mut fields: Vec<Field> = vec![];
-                for field in itemval.fields.iter() {
-                    let field_name = field.ident.clone().unwrap().to_string();
-                    let field_type = field.ty.clone();
-                    // println!("field {}", field_name);
-                    
-                    let mut output_field = Field {
-                        name: field_name,
-                        rust_type: "Unknown".into(),
-                        default: Some("".into()),
-                        is_nullable: false,
-                        is_primary: false,
-                        is_unique: false
-                    };
-
-                    let first_type = match get_type_first_ident(&field_type) {
-                        Some(v) => v,
-                        None => {
-                            return Err(anyhow!("Could not extract ident from Option inner type"));
-                        }
-                    };
-                    let mut final_type = first_type.clone();
-                    if first_type == "Option" {
-                        output_field.is_nullable = true;
-                        let inner_type = match extract_generic_type(
-                            vec!["Option".into(), "std:option:Option".into(), "core:option:Option".into()],
-                            &field_type
-                        ) {
-                            Some(v) => v,
-                            None => {
-                                return Err(anyhow!("Could not extract type from Option"));
-                            }
-                        };
-                        final_type = match get_type_first_ident(inner_type) {
-                            Some(v) => v,
-                            None => {
-                                return Err(anyhow!("Could not extract ident from Option inner type"));
-                            }
-                        }
-                    }
-                    if first_type == "Vec" {
-                        let inner_type = match extract_generic_type(
-                            vec!["Vec".into()],
-                            &field_type
-                        ) {
-                            Some(v) => v,
-                            None => {
-                                return Err(anyhow!("Could not extract type from Vec"));
-                            }
-                        };
-                        final_type = match get_type_first_ident(inner_type) {
-                            Some(v) => format!("Vec<{}>", v),
-                            None => {
-                                return Err(anyhow!("Could not extract ident from Vec inner type"));
-                            }
-                        }
-                    }
-                    output_field.rust_type = final_type;
-
-                    // parse attribute
-                    if let Some(field_attr) = parse_field_attribute(field)? {
-                        output_field.is_primary = field_attr.is_primary.unwrap_or_default();
-                        output_field.is_unique = field_attr.is_unique.unwrap_or_default();
-                    }
-
-                    fields.push(output_field);
-                }
-                models.push(Model {
-                    name: model_name.clone(),
-                    table_name: model_attrs.table_name
-                        .unwrap_or(generate_table_name_from_struct_name(&model_name)),
-                    fields
-                })
-            },
-            _ => {}
-        }
-    }
-    Ok(models)
-}
-
-/// Generate CREATE TABLE statement from parsed model
-fn generate_create_table_sql(models: &Vec<Model>) -> Result<String> {
-
-    let mut sql_code: String = "".into();
-    for model in models.iter() {
-        let mut fields_sql: Vec<String> = vec![];
-        for field in model.fields.iter() {
-            let mut additions: String = "".into();
-            let sql_type = field.sql_type()
-                .ok_or(anyhow!(format!("Could not find SQL type for field {}", field.name)))?;
-            if !field.is_nullable {
-                additions.push_str(" NOT NULL");
-            }
-            if field.is_unique {
-                additions.push_str(" UNIQUE");
-            }
-            if field.is_primary {
-                additions.push_str(" PRIMARY KEY");
-            }
-            fields_sql.push(
-                format!("\t{: <#18}\t{}{}", field.name, sql_type, additions)
-            );
-        }
-
-        sql_code.push_str(
-            &format!(
-                "CREATE TABLE {} (\n{}\n);",
-                model.table_name,
-                fields_sql.join(",\n")
-            )
-        );
-    }
-    
-    Ok(sql_code)
-}
-
 
 
 #[derive(FromArgs, PartialEq, Debug)]
@@ -374,20 +67,23 @@ pub fn main() -> Result<()> {
     if !project_root_path.exists() {
         return Err(anyhow!("Could not resolve project root path."));
     }
+
     // search for a models modules
     let models_mod_location = "src/models.rs";
     let models_mod_path = project_root_path.join(models_mod_location);
     if !project_root_path.exists() {
         return Err(anyhow!("Could not resolve models modules."));
     }
-    let models = parse_models(&models_mod_path)?;
+    eprintln!("Parsing models…");
+    let models = parse_models::parse_models(&models_mod_path)?;
 
     match args.nested {
         GeneratorArgsSubCommands::GenerateRepositories(opts) => {
-            println!("Generate repositories");
-            todo!();
+            eprintln!("Generating repositories…");
+            let _ = generate_repositories_source_files(&models)?;
         },
         GeneratorArgsSubCommands::GenerateCreateMigration(opts) => {
+            eprintln!("Generating migrations…");
             let sql_code = generate_create_table_sql(&models)?;
             println!("{}", sql_code);
         }
diff --git a/lib/generator_cli/src/models.rs b/lib/generator_cli/src/models.rs
new file mode 100644
index 0000000..89efd79
--- /dev/null
+++ b/lib/generator_cli/src/models.rs
@@ -0,0 +1,20 @@
+// BASE MODELS
+use fully_pub::fully_pub;
+
+#[derive(Debug)]
+#[fully_pub]
+struct Model {
+    name: String,
+    table_name: String,
+    fields: Vec<Field>
+}
+
+#[derive(Debug)]
+#[fully_pub]
+struct Field {
+    name: String,
+    rust_type: String,
+    is_nullable: bool,
+    is_unique: bool,
+    is_primary: bool
+}
diff --git a/lib/generator_cli/src/parse_models.rs b/lib/generator_cli/src/parse_models.rs
new file mode 100644
index 0000000..6eb5656
--- /dev/null
+++ b/lib/generator_cli/src/parse_models.rs
@@ -0,0 +1,216 @@
+use std::{fs, path::Path};
+use attribute_derive::FromAttr;
+
+use anyhow::{Result, anyhow};
+use convert_case::{Case, Casing};
+use syn::Type;
+
+use crate::{models::{Field, Model}, SqlGeneratorFieldAttr, SqlGeneratorModelAttr};
+
+fn extract_generic_type(base_segments: Vec<String>, ty: &syn::Type) -> Option<&syn::Type> {
+    // If it is not `TypePath`, it is not possible to be `Option<T>`, return `None`
+    if let syn::Type::Path(syn::TypePath { qself: None, path }) = ty {
+        // We have limited the 5 ways to write `Option`, and we can see that after `Option`,
+        // there will be no `PathSegment` of the same level
+        // Therefore, we only need to take out the highest level `PathSegment` and splice it into a string
+        // for comparison with the analysis result
+        let segments_str = &path
+            .segments
+            .iter()
+            .map(|segment| segment.ident.to_string())
+            .collect::<Vec<_>>()
+            .join(":");
+        // Concatenate `PathSegment` into a string, compare and take out the `PathSegment` where `Option` is located
+
+        let option_segment = base_segments
+            .iter()
+            .find(|s| segments_str == *s)
+            .and_then(|_| path.segments.last());
+        let inner_type = option_segment
+            // Take out the generic parameters of the `PathSegment` where `Option` is located
+            // If it is not generic, it is not possible to be `Option<T>`, return `None`
+            // But this situation may not occur
+            .and_then(|path_seg| match &path_seg.arguments {
+                syn::PathArguments::AngleBracketed(syn::AngleBracketedGenericArguments {
+                    args,
+                    ..
+                }) => args.first(),
+                _ => None,
+            })
+            // Take out the type information in the generic parameter
+            // If it is not a type, it is not possible to be `Option<T>`, return `None`
+            // But this situation may not occur
+            .and_then(|generic_arg| match generic_arg {
+                syn::GenericArgument::Type(ty) => Some(ty),
+                _ => None,
+            });
+        // Return `T` in `Option<T>`
+        return inner_type;
+    }
+    None
+}
+
+fn get_type_first_ident(inp: &Type) -> Option<String> {
+    match inp {
+        syn::Type::Path(field_type_path) => {
+            Some(field_type_path.path.segments.get(0).unwrap().ident.to_string())
+        },
+        _ => {
+            None
+        }
+    }
+}
+
+fn parse_model_attribute(item: &syn::ItemStruct) -> Result<Option<SqlGeneratorModelAttr>> {
+    for attr in item.attrs.iter() {
+        let attr_ident = match attr.path().get_ident() {
+            Some(v) => v,
+            None => {
+                continue;
+            }
+        };
+        if attr_ident.to_string() != "sql_generator_model" {
+            continue;
+        }
+
+        match SqlGeneratorModelAttr::from_attribute(attr) {
+            Ok(v) => {
+                return Ok(Some(v));
+            },
+            Err(err) => {
+                return Err(anyhow!("Failed to parse sql_generator_model attribute macro: {}", err));
+            }
+        };
+    }
+    Ok(None)
+}
+
+fn parse_field_attribute(field: &syn::Field) -> Result<Option<SqlGeneratorFieldAttr>> {
+    for attr in field.attrs.iter() {
+        let attr_ident = match attr.path().get_ident() {
+            Some(v) => v,
+            None => {
+                continue;
+            }
+        };
+        if attr_ident.to_string() != "sql_generator_field" {
+            continue;
+        }
+
+        match SqlGeneratorFieldAttr::from_attribute(attr) {
+            Ok(v) => {
+                return Ok(Some(v));
+            },
+            Err(err) => {
+                return Err(anyhow!("Failed to parse sql_generator_field attribute macro: {}", err));
+            }
+        };
+    }
+    Ok(None)
+}
+
+/// Take struct name as source, apply snake case and pluralize with a s
+fn generate_table_name_from_struct_name(struct_name: &str) -> String {
+    return format!(
+        "{}s",
+        struct_name.to_case(Case::Snake)
+    );
+}
+
+/// Scan for models struct in a rust module and return a struct representing the model
+pub fn parse_models(models_mod_path: &Path) -> Result<Vec<Model>> {
+    let models_code = fs::read_to_string(models_mod_path)?;
+    let parsed_file = syn::parse_file(&models_code)?;
+
+    let mut models: Vec<Model> = vec![];
+
+    for item in parsed_file.items {
+        match item {
+            syn::Item::Struct(itemval) => {
+                let model_name = itemval.ident.to_string();
+                let model_attrs = match parse_model_attribute(&itemval)? {
+                    Some(v) => v,
+                    None => {
+                        // we require model struct to have the `sql_generator_model` attribute
+                        continue;
+                    }
+                };
+
+                let mut fields: Vec<Field> = vec![];
+                for field in itemval.fields.iter() {
+                    let field_name = field.ident.clone().unwrap().to_string();
+                    let field_type = field.ty.clone();
+                    // println!("field {}", field_name);
+                    
+                    let mut output_field = Field {
+                        name: field_name,
+                        rust_type: "Unknown".into(),
+                        is_nullable: false,
+                        is_primary: false,
+                        is_unique: false
+                    };
+
+                    let first_type: String = match get_type_first_ident(&field_type) {
+                        Some(v) => v,
+                        None => {
+                            return Err(anyhow!("Could not extract ident from Option inner type"));
+                        }
+                    };
+                    let mut final_type = first_type.clone();
+                    if first_type == "Option" {
+                        output_field.is_nullable = true;
+                        let inner_type = match extract_generic_type(
+                            vec!["Option".into(), "std:option:Option".into(), "core:option:Option".into()],
+                            &field_type
+                        ) {
+                            Some(v) => v,
+                            None => {
+                                return Err(anyhow!("Could not extract type from Option"));
+                            }
+                        };
+                        final_type = match get_type_first_ident(inner_type) {
+                            Some(v) => v,
+                            None => {
+                                return Err(anyhow!("Could not extract ident from Option inner type"));
+                            }
+                        }
+                    }
+                    if first_type == "Vec" {
+                        let inner_type = match extract_generic_type(
+                            vec!["Vec".into()],
+                            &field_type
+                        ) {
+                            Some(v) => v,
+                            None => {
+                                return Err(anyhow!("Could not extract type from Vec"));
+                            }
+                        };
+                        final_type = match get_type_first_ident(inner_type) {
+                            Some(v) => format!("Vec<{}>", v),
+                            None => {
+                                return Err(anyhow!("Could not extract ident from Vec inner type"));
+                            }
+                        }
+                    }
+                    output_field.rust_type = final_type;
+
+                    // parse attribute
+                    if let Some(field_attr) = parse_field_attribute(field)? {
+                        output_field.is_primary = field_attr.is_primary.unwrap_or_default();
+                        output_field.is_unique = field_attr.is_unique.unwrap_or_default();
+                    }
+
+                    fields.push(output_field);
+                }
+                models.push(Model {
+                    name: model_name.clone(),
+                    table_name: model_attrs.table_name
+                        .unwrap_or(generate_table_name_from_struct_name(&model_name)),
+                    fields
+                })
+            },
+            _ => {}
+        }
+    }
+    Ok(models)
+}