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) +}