feat: add repository code generator
This commit is contained in:
parent
fa48c7a2b4
commit
c15e69a6c4
7 changed files with 432 additions and 319 deletions
|
@ -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).
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
61
lib/generator_cli/src/gen_migrations.rs
Normal file
61
lib/generator_cli/src/gen_migrations.rs
Normal file
|
@ -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)
|
||||
}
|
||||
|
118
lib/generator_cli/src/gen_repositories.rs
Normal file
118
lib/generator_cli/src/gen_repositories.rs
Normal file
|
@ -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(())
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
20
lib/generator_cli/src/models.rs
Normal file
20
lib/generator_cli/src/models.rs
Normal file
|
@ -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
|
||||
}
|
216
lib/generator_cli/src/parse_models.rs
Normal file
216
lib/generator_cli/src/parse_models.rs
Normal file
|
@ -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)
|
||||
}
|
Loading…
Reference in a new issue