WIP
This commit is contained in:
parent
c15e69a6c4
commit
09791951d9
13 changed files with 250 additions and 128 deletions
20
Cargo.lock
generated
20
Cargo.lock
generated
|
|
@ -486,14 +486,6 @@ dependencies = [
|
|||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generator_attr"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"attribute-derive",
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generator_cli"
|
||||
version = "0.0.0"
|
||||
|
|
@ -503,10 +495,12 @@ dependencies = [
|
|||
"attribute-derive",
|
||||
"convert_case",
|
||||
"fully_pub",
|
||||
"heck",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"structmeta",
|
||||
"syn",
|
||||
]
|
||||
|
|
@ -1207,9 +1201,9 @@ version = "0.0.0"
|
|||
dependencies = [
|
||||
"chrono",
|
||||
"fully_pub",
|
||||
"generator_attr",
|
||||
"serde",
|
||||
"sqlx",
|
||||
"sqlx_tools_attributes",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1546,6 +1540,14 @@ dependencies = [
|
|||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlx_tools_attributes"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"attribute-derive",
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.0"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"lib/generator_cli",
|
||||
"lib/sqlx_tools_attributes",
|
||||
"lib/sqlx_tools_generator_cli",
|
||||
"lib/sandbox"
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,92 +0,0 @@
|
|||
use std::path::Path;
|
||||
use attribute_derive::FromAttr;
|
||||
|
||||
use argh::FromArgs;
|
||||
use anyhow::{Result, anyhow};
|
||||
use gen_migrations::generate_create_table_sql;
|
||||
use gen_repositories::generate_repositories_source_files;
|
||||
|
||||
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)]
|
||||
pub struct SqlGeneratorModelAttr {
|
||||
table_name: Option<String>
|
||||
}
|
||||
|
||||
#[derive(FromAttr, PartialEq, Debug, Default)]
|
||||
#[attribute(ident = sql_generator_field)]
|
||||
pub struct SqlGeneratorFieldAttr {
|
||||
is_primary: Option<bool>,
|
||||
is_unique: Option<bool>
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[derive(FromArgs, PartialEq, Debug)]
|
||||
/// Generate SQL CREATE TABLE migrations
|
||||
#[argh(subcommand, name = "generate-create-migrations")]
|
||||
struct GenerateCreateMigration {
|
||||
}
|
||||
|
||||
#[derive(FromArgs, PartialEq, Debug)]
|
||||
/// Generate Rust SQLx repositories code
|
||||
#[argh(subcommand, name = "generate-repositories")]
|
||||
struct GenerateRepositories {
|
||||
}
|
||||
|
||||
#[derive(FromArgs, PartialEq, Debug)]
|
||||
#[argh(subcommand)]
|
||||
enum GeneratorArgsSubCommands {
|
||||
GenerateCreateMigration(GenerateCreateMigration),
|
||||
GenerateRepositories(GenerateRepositories),
|
||||
}
|
||||
|
||||
#[derive(FromArgs)]
|
||||
/// SQLX Generator args
|
||||
struct GeneratorArgs {
|
||||
/// whether or not to debug
|
||||
#[argh(switch, short = 'd')]
|
||||
debug: bool,
|
||||
|
||||
#[argh(positional)]
|
||||
project_root: Option<String>,
|
||||
|
||||
#[argh(subcommand)]
|
||||
nested: GeneratorArgsSubCommands
|
||||
}
|
||||
|
||||
pub fn main() -> Result<()> {
|
||||
let args: GeneratorArgs = argh::from_env();
|
||||
let project_root = &args.project_root.unwrap_or(".".to_string());
|
||||
let project_root_path = Path::new(&project_root);
|
||||
eprintln!("Using project root at: {:?}", &project_root_path.canonicalize()?);
|
||||
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."));
|
||||
}
|
||||
eprintln!("Parsing models…");
|
||||
let models = parse_models::parse_models(&models_mod_path)?;
|
||||
|
||||
match args.nested {
|
||||
GeneratorArgsSubCommands::GenerateRepositories(opts) => {
|
||||
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);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -11,4 +11,4 @@ chrono = "0.4.39"
|
|||
fully_pub = "0.1.4"
|
||||
serde = "1.0.216"
|
||||
sqlx = { version = "0.8.2", features = ["chrono", "uuid", "sqlite"] }
|
||||
generator_attr = { path = "../generator_attr" }
|
||||
sqlx_tools_attributes = { path = "../sqlx_tools_attributes" }
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use chrono::{DateTime, Utc};
|
|||
use sqlx::types::Json;
|
||||
use fully_pub::fully_pub;
|
||||
|
||||
use generator_attr::{sql_generator_model, SqlGeneratorDerive};
|
||||
use sqlx_tools_attributes::{sql_generator_model, SqlGeneratorDerive};
|
||||
|
||||
#[derive(sqlx::Type, Clone, Debug, PartialEq)]
|
||||
enum UserStatus {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
[package]
|
||||
name = "generator_attr"
|
||||
name = "sqlx_tools_attributes"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
|
@ -12,9 +12,11 @@ argh = "0.1.13"
|
|||
attribute-derive = "0.10.3"
|
||||
convert_case = "0.6.0"
|
||||
fully_pub = "0.1.4"
|
||||
heck = "0.5.0"
|
||||
prettyplease = "0.2.25"
|
||||
proc-macro2 = "1.0.92"
|
||||
quote = "1.0.38"
|
||||
serde = "1.0.216"
|
||||
serde_json = "1.0.134"
|
||||
structmeta = "0.3.0"
|
||||
syn = { version = "2.0.92", features = ["extra-traits", "full", "parsing"] }
|
||||
|
|
@ -49,7 +49,7 @@ pub fn generate_create_table_sql(models: &Vec<Model>) -> Result<String> {
|
|||
|
||||
sql_code.push_str(
|
||||
&format!(
|
||||
"CREATE TABLE {} (\n{}\n);",
|
||||
"CREATE TABLE {} (\n{}\n);\n",
|
||||
model.table_name,
|
||||
fields_sql.join(",\n")
|
||||
)
|
||||
|
|
@ -1,7 +1,10 @@
|
|||
use anyhow::{Result, anyhow};
|
||||
use anyhow::Result;
|
||||
use fully_pub::fully_pub;
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{format_ident, quote};
|
||||
use serde::Serialize;
|
||||
use syn::File;
|
||||
use heck::ToSnakeCase;
|
||||
|
||||
use crate::models::Model;
|
||||
|
||||
|
|
@ -12,9 +15,9 @@ fn gen_get_all_method(model: &Model) -> TokenStream {
|
|||
let select_query = format!("SELECT * FROM {}", model.table_name);
|
||||
|
||||
quote! {
|
||||
pub fn get_all(&self) -> Result<Vec<#resource_ident>> {
|
||||
pub async fn get_all(&self) -> Result<Vec<#resource_ident>> {
|
||||
sqlx::query_as::<_, #resource_ident>(#select_query)
|
||||
.fetch_all(&self.storage.0)
|
||||
.fetch_all(&self.db.0)
|
||||
.await
|
||||
.context(#error_msg)
|
||||
}
|
||||
|
|
@ -24,13 +27,19 @@ fn gen_get_all_method(model: &Model) -> TokenStream {
|
|||
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);
|
||||
let primary_key = &model.fields.iter()
|
||||
.find(|f| f.is_primary)
|
||||
.expect("A model must have at least one primary key")
|
||||
.name;
|
||||
let select_query = format!("SELECT * FROM {} WHERE {} = $1", model.table_name, primary_key);
|
||||
|
||||
let func_name_ident = format_ident!("get_by_{}", primary_key);
|
||||
|
||||
quote! {
|
||||
pub fn get_by_id(&self, id: &str) -> Result<#resource_ident> {
|
||||
pub async fn #func_name_ident(&self, item_id: &str) -> Result<#resource_ident> {
|
||||
sqlx::query_as::<_, #resource_ident>(#select_query)
|
||||
.bind(user_id)
|
||||
.fetch_one(&self.storage.0)
|
||||
.bind(item_id)
|
||||
.fetch_one(&self.db.0)
|
||||
.await
|
||||
.context(#error_msg)
|
||||
}
|
||||
|
|
@ -59,10 +68,10 @@ fn gen_insert_method(model: &Model) -> TokenStream {
|
|||
.collect();
|
||||
|
||||
quote! {
|
||||
pub fn insert(&self, entity: &#resource_ident) -> Result<()> {
|
||||
pub async fn insert(&self, entity: &#resource_ident) -> Result<()> {
|
||||
sqlx::query(#insert_query)
|
||||
#( .bind( &entity.#field_names ) )*
|
||||
.execute(&self.storage.0)
|
||||
.execute(&self.db.0)
|
||||
.await
|
||||
.context(#error_msg)?;
|
||||
|
||||
|
|
@ -71,9 +80,11 @@ fn gen_insert_method(model: &Model) -> TokenStream {
|
|||
}
|
||||
}
|
||||
|
||||
fn generate_repository_file(model: &Model) -> Result<()> {
|
||||
fn generate_repository_file(model: &Model) -> Result<SourceNodeContainer> {
|
||||
let resource_name = model.name.clone();
|
||||
|
||||
let resource_module_ident = format_ident!("{}", &model.module_path.get(0).unwrap());
|
||||
|
||||
let resource_ident = format_ident!("{}", &resource_name);
|
||||
let repository_ident = format_ident!("{}Repository", resource_ident);
|
||||
|
||||
|
|
@ -81,15 +92,21 @@ fn generate_repository_file(model: &Model) -> Result<()> {
|
|||
let get_by_id_method_code = gen_get_by_id_method(&model);
|
||||
let insert_method_code = gen_insert_method(&model);
|
||||
|
||||
// TODO: add import line
|
||||
|
||||
let base_repository_code: TokenStream = quote! {
|
||||
struct #repository_ident {
|
||||
storage: &Storage
|
||||
use crate::models::#resource_module_ident::#resource_ident;
|
||||
use crate::services::database::Database;
|
||||
use anyhow::{Result, Context};
|
||||
|
||||
pub struct #repository_ident {
|
||||
db: Database
|
||||
}
|
||||
|
||||
impl #repository_ident {
|
||||
fn new(storage: &Storage) -> Self {
|
||||
pub fn new(db: Database) -> Self {
|
||||
#repository_ident {
|
||||
storage
|
||||
db
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -103,16 +120,47 @@ fn generate_repository_file(model: &Model) -> Result<()> {
|
|||
// 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(())
|
||||
Ok(SourceNodeContainer {
|
||||
name: format!("{}_repository.rs", model.name.to_snake_case()),
|
||||
inner: SourceNode::File(pretty)
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
#[fully_pub]
|
||||
enum SourceNode {
|
||||
File(String),
|
||||
Directory(Vec<SourceNodeContainer>)
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
#[fully_pub]
|
||||
struct SourceNodeContainer {
|
||||
name: String,
|
||||
inner: SourceNode
|
||||
}
|
||||
|
||||
/// Generate base repositories for all models
|
||||
pub fn generate_repositories_source_files(models: &Vec<Model>) -> Result<()> {
|
||||
pub fn generate_repositories_source_files(models: &Vec<Model>) -> Result<SourceNodeContainer> {
|
||||
let mut nodes: Vec<SourceNodeContainer> = vec![];
|
||||
for model in models.iter() {
|
||||
let _ = generate_repository_file(model)?;
|
||||
let snc = generate_repository_file(model)?;
|
||||
nodes.push(snc)
|
||||
}
|
||||
Ok(())
|
||||
|
||||
let mut mod_index_code: String = String::new();
|
||||
for node in &nodes {
|
||||
let module_name = node.name.replace(".rs", "");
|
||||
mod_index_code.push_str(&format!("pub mod {module_name};\n"));
|
||||
}
|
||||
nodes.push(SourceNodeContainer {
|
||||
name: "mod.rs".into(),
|
||||
inner: SourceNode::File(mod_index_code.to_string())
|
||||
});
|
||||
Ok(SourceNodeContainer {
|
||||
name: "".into(),
|
||||
inner: SourceNode::Directory(nodes)
|
||||
})
|
||||
}
|
||||
|
||||
140
lib/sqlx_tools_generator_cli/src/main.rs
Normal file
140
lib/sqlx_tools_generator_cli/src/main.rs
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
use std::{ffi::OsStr, path::Path};
|
||||
use attribute_derive::FromAttr;
|
||||
|
||||
use argh::FromArgs;
|
||||
use anyhow::{Result, anyhow};
|
||||
use gen_migrations::generate_create_table_sql;
|
||||
use gen_repositories::{generate_repositories_source_files, SourceNodeContainer};
|
||||
|
||||
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)]
|
||||
pub struct SqlGeneratorModelAttr {
|
||||
table_name: Option<String>
|
||||
}
|
||||
|
||||
#[derive(FromAttr, PartialEq, Debug, Default)]
|
||||
#[attribute(ident = sql_generator_field)]
|
||||
pub struct SqlGeneratorFieldAttr {
|
||||
is_primary: Option<bool>,
|
||||
is_unique: Option<bool>
|
||||
}
|
||||
|
||||
|
||||
#[derive(FromArgs, PartialEq, Debug)]
|
||||
/// Generate SQL CREATE TABLE migrations
|
||||
#[argh(subcommand, name = "gen-migrations")]
|
||||
struct GenerateMigration {
|
||||
/// path of file where to write all in one generated SQL migration
|
||||
#[argh(option, short = 'o')]
|
||||
output: Option<String>
|
||||
}
|
||||
|
||||
#[derive(FromArgs, PartialEq, Debug)]
|
||||
/// Generate Rust SQLx repositories code
|
||||
#[argh(subcommand, name = "gen-repositories")]
|
||||
struct GenerateRepositories {
|
||||
}
|
||||
|
||||
#[derive(FromArgs, PartialEq, Debug)]
|
||||
#[argh(subcommand)]
|
||||
enum GeneratorArgsSubCommands {
|
||||
GenerateMigration(GenerateMigration),
|
||||
GenerateRepositories(GenerateRepositories),
|
||||
}
|
||||
|
||||
#[derive(FromArgs)]
|
||||
/// SQLX Generator args
|
||||
struct GeneratorArgs {
|
||||
/// whether or not to debug
|
||||
#[argh(switch, short = 'd')]
|
||||
debug: bool,
|
||||
|
||||
/// path where to find Cargo.toml
|
||||
#[argh(option)]
|
||||
project_root: Option<String>,
|
||||
|
||||
#[argh(subcommand)]
|
||||
nested: GeneratorArgsSubCommands
|
||||
}
|
||||
|
||||
fn write_source_code(base_path: &Path, snc: SourceNodeContainer) -> Result<()> {
|
||||
let path = base_path.join(snc.name);
|
||||
match snc.inner {
|
||||
gen_repositories::SourceNode::File(code) => {
|
||||
println!("writing file {:?}", path);
|
||||
std::fs::write(path, code)?;
|
||||
},
|
||||
gen_repositories::SourceNode::Directory(dir) => {
|
||||
for node in dir {
|
||||
write_source_code(&path, node)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn main() -> Result<()> {
|
||||
let args: GeneratorArgs = argh::from_env();
|
||||
let project_root = &args.project_root.unwrap_or(".".to_string());
|
||||
let project_root_path = Path::new(&project_root);
|
||||
eprintln!("Using project root at: {:?}", &project_root_path.canonicalize()?);
|
||||
if !project_root_path.exists() {
|
||||
return Err(anyhow!("Could not resolve project root path."));
|
||||
}
|
||||
|
||||
// check Cargo.toml
|
||||
let main_manifest_location = "Cargo.toml";
|
||||
let main_manifest_path = project_root_path.join(main_manifest_location);
|
||||
if !main_manifest_path.exists() {
|
||||
return Err(anyhow!("Could not find Cargo.toml in project root."));
|
||||
}
|
||||
|
||||
// search for a models modules
|
||||
let models_mod_location = "src/models.rs";
|
||||
let mut models_mod_path = project_root_path.join(models_mod_location);
|
||||
if !models_mod_path.exists() {
|
||||
let models_mod_location = "src/models/mod.rs";
|
||||
models_mod_path = project_root_path.join(models_mod_location);
|
||||
}
|
||||
if !models_mod_path.exists() {
|
||||
return Err(anyhow!("Could not resolve models modules."));
|
||||
}
|
||||
if models_mod_path.file_name().map(|x| x == OsStr::new("mod.rs")).unwrap_or(false) {
|
||||
models_mod_path.pop();
|
||||
}
|
||||
eprintln!("Found models in project, parsing models");
|
||||
let models = parse_models::parse_models_from_module(&models_mod_path)?;
|
||||
dbg!(&models);
|
||||
|
||||
match args.nested {
|
||||
GeneratorArgsSubCommands::GenerateRepositories(opts) => {
|
||||
eprintln!("Generating repositories…");
|
||||
// search for a repository module
|
||||
let repositories_mod_location = "src/repositories";
|
||||
let repositories_mod_path = project_root_path.join(repositories_mod_location);
|
||||
if !repositories_mod_path.exists() {
|
||||
return Err(anyhow!("Could not resolve repositories modules."));
|
||||
}
|
||||
let snc = generate_repositories_source_files(&models)?;
|
||||
dbg!(&snc);
|
||||
write_source_code(&repositories_mod_path, snc)?;
|
||||
},
|
||||
GeneratorArgsSubCommands::GenerateMigration(opts) => {
|
||||
eprintln!("Generating migrations…");
|
||||
let sql_code = generate_create_table_sql(&models)?;
|
||||
if let Some(out_location) = opts.output {
|
||||
let output_path = Path::new(&out_location);
|
||||
let write_res = std::fs::write(output_path, sql_code);
|
||||
eprintln!("{:?}", write_res);
|
||||
} else {
|
||||
println!("{}", sql_code);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ use fully_pub::fully_pub;
|
|||
#[derive(Debug)]
|
||||
#[fully_pub]
|
||||
struct Model {
|
||||
module_path: Vec<String>,
|
||||
name: String,
|
||||
table_name: String,
|
||||
fields: Vec<Field>
|
||||
|
|
@ -117,9 +117,9 @@ fn generate_table_name_from_struct_name(struct_name: &str) -> String {
|
|||
);
|
||||
}
|
||||
|
||||
/// 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)?;
|
||||
/// Scan for models struct in a rust file and return a struct representing the model
|
||||
pub fn parse_models(source_code_path: &Path) -> Result<Vec<Model>> {
|
||||
let models_code = fs::read_to_string(source_code_path)?;
|
||||
let parsed_file = syn::parse_file(&models_code)?;
|
||||
|
||||
let mut models: Vec<Model> = vec![];
|
||||
|
|
@ -203,6 +203,7 @@ pub fn parse_models(models_mod_path: &Path) -> Result<Vec<Model>> {
|
|||
fields.push(output_field);
|
||||
}
|
||||
models.push(Model {
|
||||
module_path: vec![source_code_path.file_stem().unwrap().to_str().unwrap().to_string()],
|
||||
name: model_name.clone(),
|
||||
table_name: model_attrs.table_name
|
||||
.unwrap_or(generate_table_name_from_struct_name(&model_name)),
|
||||
|
|
@ -214,3 +215,22 @@ pub fn parse_models(models_mod_path: &Path) -> Result<Vec<Model>> {
|
|||
}
|
||||
Ok(models)
|
||||
}
|
||||
|
||||
/// Scan for models struct in a rust file and return a struct representing the model
|
||||
pub fn parse_models_from_module(module_path: &Path) -> Result<Vec<Model>> {
|
||||
let mut models: Vec<Model> = vec![];
|
||||
|
||||
if module_path.is_file() {
|
||||
models.extend(parse_models(&module_path)?);
|
||||
return Ok(models);
|
||||
}
|
||||
|
||||
let entries = fs::read_dir(module_path)
|
||||
.map_err(|err| anyhow!("Could not scan models directory. {:?}", err))?;
|
||||
for dir_entry_res in entries {
|
||||
let file_path = dir_entry_res?.path();
|
||||
models.extend(parse_models_from_module(&file_path)?)
|
||||
}
|
||||
|
||||
Ok(models)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue