generated from phundrak/rust-poem-openapi-template
feat: add new crud macro for easier entity manipulation in DB
All checks were successful
CI / tests (push) Successful in 7m52s
All checks were successful
CI / tests (push) Successful in 7m52s
This commit is contained in:
23
gejdr-macros/src/crud/ir.rs
Normal file
23
gejdr-macros/src/crud/ir.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
#[derive(deluxe::ExtractAttributes)]
|
||||
#[deluxe(attributes(crud))]
|
||||
pub struct CrudStructAttributes {
|
||||
pub table: String,
|
||||
}
|
||||
|
||||
#[derive(deluxe::ExtractAttributes, Clone)]
|
||||
#[deluxe(attributes(crud))]
|
||||
pub struct CrudFieldAttributes {
|
||||
#[deluxe(default = false)]
|
||||
pub id: bool,
|
||||
#[deluxe(default = None)]
|
||||
pub column: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CrudField {
|
||||
pub ident: syn::Ident,
|
||||
pub field: syn::Field,
|
||||
pub column: String,
|
||||
pub id: bool,
|
||||
pub ty: syn::Type,
|
||||
}
|
||||
188
gejdr-macros/src/crud/mod.rs
Normal file
188
gejdr-macros/src/crud/mod.rs
Normal file
@@ -0,0 +1,188 @@
|
||||
use ir::{CrudField, CrudFieldAttributes, CrudStructAttributes};
|
||||
use quote::quote;
|
||||
use syn::DeriveInput;
|
||||
|
||||
mod ir;
|
||||
|
||||
fn extract_crud_field_attrs(ast: &mut DeriveInput) -> deluxe::Result<(Vec<CrudField>, CrudField)> {
|
||||
let mut field_attrs: Vec<CrudField> = Vec::new();
|
||||
// let mut identifier: Option<CrudIdentifier> = None;
|
||||
let mut identifier: Option<CrudField> = None;
|
||||
let mut identifier_counter = 0;
|
||||
if let syn::Data::Struct(s) = &mut ast.data {
|
||||
for field in &mut s.fields {
|
||||
let ident = field.clone().ident.unwrap();
|
||||
let ty = field.clone().ty;
|
||||
let attrs: CrudFieldAttributes =
|
||||
deluxe::extract_attributes(field).expect("Could not extract attributes from field");
|
||||
let field = CrudField {
|
||||
ident: ident.clone(),
|
||||
field: field.to_owned(),
|
||||
column: attrs.column.unwrap_or_else(|| ident.to_string()),
|
||||
id: attrs.id,
|
||||
ty,
|
||||
};
|
||||
if attrs.id {
|
||||
identifier_counter += 1;
|
||||
identifier = Some(field.clone());
|
||||
}
|
||||
if identifier_counter > 1 {
|
||||
return Err(syn::Error::new_spanned(
|
||||
field.field,
|
||||
"Struct {name} can only have one identifier",
|
||||
));
|
||||
}
|
||||
field_attrs.push(field);
|
||||
}
|
||||
}
|
||||
if identifier_counter < 1 {
|
||||
Err(syn::Error::new_spanned(
|
||||
ast,
|
||||
"Struct {name} must have one identifier",
|
||||
))
|
||||
} else {
|
||||
Ok((field_attrs, identifier.unwrap()))
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_find_query(table: &str, id: &CrudField) -> proc_macro2::TokenStream {
|
||||
let find_string = format!("SELECT * FROM {} WHERE {} = $1", table, id.column);
|
||||
let ty = &id.ty;
|
||||
quote! {
|
||||
async fn find(pool: &::sqlx::PgPool, id: &#ty) -> ::sqlx::Result<Option<Self>> {
|
||||
::sqlx::query_as!(Self, #find_string, id)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_create_query(table: &str, fields: &[CrudField]) -> proc_macro2::TokenStream {
|
||||
let inputs: Vec<String> = (1..=fields.len()).map(|num| format!("${num}")).collect();
|
||||
let create_string = format!(
|
||||
"INSERT INTO {} ({}) VALUES ({}) RETURNING *",
|
||||
table,
|
||||
fields
|
||||
.iter()
|
||||
.map(|v| v.column.clone())
|
||||
.collect::<Vec<String>>()
|
||||
.join(", "),
|
||||
inputs.join(", ")
|
||||
);
|
||||
let field_idents: Vec<syn::Ident> = fields.iter().map(|f| f.ident.clone()).collect();
|
||||
quote! {
|
||||
async fn create(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result<Self> {
|
||||
::sqlx::query_as!(
|
||||
Self,
|
||||
#create_string,
|
||||
#(self.#field_idents),*
|
||||
)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_update_query(
|
||||
table: &str,
|
||||
fields: &[CrudField],
|
||||
id: &CrudField,
|
||||
) -> proc_macro2::TokenStream {
|
||||
let mut fields: Vec<&CrudField> = fields.iter().filter(|f| !f.id).collect();
|
||||
let update_columns = fields
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, &field)| format!("{} = ${}", field.column, i + 1))
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ");
|
||||
let update_string = format!(
|
||||
"UPDATE {} SET {} WHERE {} = ${} RETURNING *",
|
||||
table,
|
||||
update_columns,
|
||||
id.column,
|
||||
fields.len() + 1
|
||||
);
|
||||
fields.push(id);
|
||||
let field_idents: Vec<_> = fields.iter().map(|f| f.ident.clone()).collect();
|
||||
quote! {
|
||||
async fn update(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result<Self> {
|
||||
::sqlx::query_as!(
|
||||
Self,
|
||||
#update_string,
|
||||
#(self.#field_idents),*
|
||||
)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_delete_query(table: &str, id: &CrudField) -> proc_macro2::TokenStream {
|
||||
let delete_string = format!("DELETE FROM {} WHERE {} = $1", table, id.column);
|
||||
let ty = &id.ty;
|
||||
let ident = &id.ident;
|
||||
|
||||
quote! {
|
||||
async fn delete_by_id(pool: &::sqlx::PgPool, id: &#ty) -> ::sqlx::Result<u64> {
|
||||
let rows_affected = ::sqlx::query!(#delete_string, id)
|
||||
.execute(pool)
|
||||
.await?
|
||||
.rows_affected();
|
||||
Ok(rows_affected)
|
||||
}
|
||||
|
||||
async fn delete(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result<u64> {
|
||||
let rows_affected = ::sqlx::query!(#delete_string, self.#ident)
|
||||
.execute(pool)
|
||||
.await?
|
||||
.rows_affected();
|
||||
Ok(rows_affected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn crud_derive_macro2(
|
||||
item: proc_macro2::TokenStream,
|
||||
) -> deluxe::Result<proc_macro2::TokenStream> {
|
||||
// parse
|
||||
let mut ast: DeriveInput = syn::parse2(item).expect("Failed to parse input");
|
||||
|
||||
// extract struct attributes
|
||||
let CrudStructAttributes { table } =
|
||||
deluxe::extract_attributes(&mut ast).expect("Could not extract attributes from struct");
|
||||
|
||||
// extract field attributes
|
||||
let (fields, id) = extract_crud_field_attrs(&mut ast)?;
|
||||
let ty = &id.ty;
|
||||
let id_ident = &id.ident;
|
||||
|
||||
// define impl variables
|
||||
let ident = &ast.ident;
|
||||
let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl();
|
||||
|
||||
// generate
|
||||
let find_query = generate_find_query(&table, &id);
|
||||
let create_query = generate_create_query(&table, &fields);
|
||||
let update_query = generate_update_query(&table, &fields, &id);
|
||||
let delete_query = generate_delete_query(&table, &id);
|
||||
let code = quote! {
|
||||
impl #impl_generics Crud<#ty> for #ident #type_generics #where_clause {
|
||||
#find_query
|
||||
|
||||
#create_query
|
||||
|
||||
#update_query
|
||||
|
||||
async fn create_or_update(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result<Self> {
|
||||
if Self::find(pool, &self.#id_ident).await?.is_some() {
|
||||
self.update(pool).await
|
||||
} else {
|
||||
self.create(pool).await
|
||||
}
|
||||
}
|
||||
|
||||
#delete_query
|
||||
}
|
||||
};
|
||||
Ok(code)
|
||||
}
|
||||
81
gejdr-macros/src/lib.rs
Normal file
81
gejdr-macros/src/lib.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
#![deny(clippy::all)]
|
||||
#![deny(clippy::pedantic)]
|
||||
#![deny(clippy::nursery)]
|
||||
#![allow(clippy::module_name_repetitions)]
|
||||
#![allow(clippy::unused_async)]
|
||||
#![allow(clippy::useless_let_if_seq)] // Reason: prevents some OpenApi structs from compiling
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
//! Create ``SQLx`` CRUD code for a struct in Postgres.
|
||||
//!
|
||||
//! This crate provides the trait implementation `Crud` which
|
||||
//! generates the following ``SQLx`` queries:
|
||||
//! - find an entity by id
|
||||
//!
|
||||
//! SQL query: `SELECT * FROM ... WHERE <id> = ...`
|
||||
//! - insert an entity into the database
|
||||
//!
|
||||
//! SQL query: `INSERT INTO ... (...) VALUES (...) RETURNING *`
|
||||
//! - update an entity in the database
|
||||
//!
|
||||
//! SQL query: `UPDATE ... SET ... WHERE <id> = ... RETURNING *`
|
||||
//! - delete an entity from the database using its id
|
||||
//!
|
||||
//! SQL query: `DELETE FROM ... WHERE <id> = ...`
|
||||
//! - update an entity or create it if it does not already exist in
|
||||
//! - the database
|
||||
//!
|
||||
//! This macro relies on the trait `Crud` found in the `gejdr-core`
|
||||
//! crate.
|
||||
//!
|
||||
//! To use this macro, you need to add it to the derives of the
|
||||
//! struct. You will also need to define its identifier
|
||||
//!
|
||||
//! # Usage
|
||||
//!
|
||||
//! Add `#[crud(table = "my_table_name")]` atop of the structure,
|
||||
//! after the `Crud` derive. You will also need to add `#[crud(id)]`
|
||||
//! atop of the field of your struct that will be used as the
|
||||
//! identifier of your entity.
|
||||
//!
|
||||
//! ```ignore
|
||||
//! #[derive(Crud)]
|
||||
//! #[crud(table = "users")]
|
||||
//! pub struct User {
|
||||
//! #[crud(id)]
|
||||
//! id: String,
|
||||
//! username: String,
|
||||
//! created_at: Timestampz,
|
||||
//! last_updated: Timestampz,
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! With the example of the `User` struct, this links it to the
|
||||
//! `users` table of the connected database. It will use `Users.id` to
|
||||
//! uniquely identify a user entity.
|
||||
//!
|
||||
//! # Limitations
|
||||
//! ## ID
|
||||
//! For now, only one identifier is supported. It does not have to be
|
||||
//! a primary key, but it is strongly encouraged to use GeJDR’s Crud
|
||||
//! ID on a unique and non-null column of your database schema.
|
||||
//!
|
||||
//! ## Database type
|
||||
//!
|
||||
//! For now, only the ``PostgreSQL`` syntax is supported. If you use
|
||||
//! another database that uses the same syntax, you’re in luck!
|
||||
//! Otherwise, pull requests to add additional syntaxes are most
|
||||
//! welcome.
|
||||
|
||||
mod crud;
|
||||
use crud::crud_derive_macro2;
|
||||
|
||||
/// Generates CRUD code for Sqlx for a struct.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// May panic if errors arise while parsing and generating code.
|
||||
#[proc_macro_derive(Crud, attributes(crud))]
|
||||
pub fn crud_derive_macro(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
crud_derive_macro2(item.into()).unwrap().into()
|
||||
}
|
||||
Reference in New Issue
Block a user