chore: migrate source code from old repo to this repo

This commit is contained in:
Lucien Cartier-Tilet 2025-01-26 15:03:24 +01:00
parent 39f757991a
commit 96ac2aa979
Signed by: phundrak
GPG Key ID: 347803E8073EACE0
8 changed files with 2544 additions and 0 deletions

1886
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

17
georm-macros/Cargo.toml Normal file
View File

@ -0,0 +1,17 @@
[package]
name = "georm-macros"
description = "Macro support for Georm. Not intended to be used directly."
version.workspace = true
license.workspace = true
edition.workspace = true
authors.workspace = true
repository.workspace = true
[lib]
proc-macro = true
[dependencies]
deluxe = "0.5.0"
proc-macro2 = "1.0.93"
quote = "1.0.38"
syn = "2.0.96"

View File

@ -0,0 +1,233 @@
use quote::quote;
use std::fmt::{self, Display};
#[derive(deluxe::ExtractAttributes)]
#[deluxe(attributes(georm))]
pub struct GeormStructAttributes {
pub table: String,
#[deluxe(default = Vec::new())]
pub one_to_many: Vec<O2MRelationship>,
#[deluxe(default = Vec::new())]
pub many_to_many: Vec<M2MRelationship>,
}
#[derive(deluxe::ParseMetaItem)]
pub struct O2MRelationship {
pub name: String,
pub remote_id: String,
pub table: String,
pub entity: syn::Type,
}
impl From<&O2MRelationship> for proc_macro2::TokenStream {
fn from(value: &O2MRelationship) -> Self {
let query = format!(
"SELECT * FROM {} WHERE {} = $1",
value.table, value.remote_id
);
let entity = &value.entity;
let function = syn::Ident::new(
&format!("get_{}", value.name),
proc_macro2::Span::call_site(),
);
quote! {
pub async fn #function(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result<Vec<#entity>> {
query_as!(#entity, #query, self.get_id()).fetch_all(pool).await
}
}
}
}
#[derive(deluxe::ParseMetaItem, Clone)]
pub struct M2MLink {
pub table: String,
pub from: String,
pub to: String,
}
//#[georm(
// table = "users",
// many_to_many = [
// {
// name = friends,
// entity: User,
// link = { table = "user_friendships", from: "user1", to "user2" }
// }
// ]
//)]
#[derive(deluxe::ParseMetaItem)]
pub struct M2MRelationship {
pub name: String,
pub entity: syn::Type,
pub table: String,
#[deluxe(default = String::from("id"))]
pub remote_id: String,
pub link: M2MLink,
}
pub struct Identifier {
pub table: String,
pub id: String,
}
pub struct M2MRelationshipComplete {
pub name: String,
pub entity: syn::Type,
pub local: Identifier,
pub remote: Identifier,
pub link: M2MLink,
}
impl M2MRelationshipComplete {
pub fn new(other: &M2MRelationship, local_table: &String, local_id: String) -> Self {
Self {
name: other.name.clone(),
entity: other.entity.clone(),
link: other.link.clone(),
local: Identifier {
table: local_table.to_string(),
id: local_id,
},
remote: Identifier {
table: other.table.clone(),
id: other.remote_id.clone(),
},
}
}
}
impl From<&M2MRelationshipComplete> for proc_macro2::TokenStream {
fn from(value: &M2MRelationshipComplete) -> Self {
let function = syn::Ident::new(
&format!("get_{}", value.name),
proc_macro2::Span::call_site(),
);
let entity = &value.entity;
let query = format!(
"
SELECT remote.*
FROM {} local
JOIN {} link ON link.{} = local.{}
JOIN {} remote ON link.{} = remote.{}
WHERE local.{} = $1
",
value.local.table,
value.link.table,
value.link.from,
value.local.id,
value.remote.table,
value.link.to,
value.remote.id,
value.local.id
);
quote! {
pub async fn #function(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result<Vec<#entity>> {
query_as!(#entity, #query, self.get_id()).fetch_all(pool).await
}
}
}
}
#[derive(deluxe::ExtractAttributes, Clone)]
#[deluxe(attributes(georm))]
struct GeormFieldAttributes {
#[deluxe(default = false)]
pub id: bool,
#[deluxe(default = None)]
pub column: Option<String>,
#[deluxe(default = None)]
pub relation: Option<O2ORelationship>,
}
// #[georm(
// table = "profileId",
// one_to_one = { name = profile, id = "id", entity = Profile, nullable }
// )]
#[derive(deluxe::ParseMetaItem, Clone)]
pub struct O2ORelationship {
pub entity: syn::Type,
pub table: String,
#[deluxe(default = String::from("id"))]
pub remote_id: String,
#[deluxe(default = false)]
pub nullable: bool,
pub name: String,
}
#[derive(Clone)]
pub struct GeormField {
pub ident: syn::Ident,
pub field: syn::Field,
pub ty: syn::Type,
pub column: Option<String>,
pub id: bool,
pub relation: Option<O2ORelationship>,
}
impl GeormField {
pub fn new(field: &mut syn::Field) -> Self {
let ident = field.clone().ident.unwrap();
let ty = field.clone().ty;
let attrs: GeormFieldAttributes =
deluxe::extract_attributes(field).expect("Could not extract attributes from field");
let GeormFieldAttributes {
id,
column,
relation,
} = attrs;
Self {
ident,
field: field.to_owned(),
id,
ty,
relation,
column,
}
}
}
impl Display for GeormField {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}",
self.column
.clone()
.unwrap_or_else(|| self.ident.to_string())
)
}
}
impl From<&GeormField> for proc_macro2::TokenStream {
fn from(value: &GeormField) -> Self {
let Some(relation) = value.relation.clone() else {
return quote! {};
};
let function = syn::Ident::new(
&format!("get_{}", relation.name),
proc_macro2::Span::call_site(),
);
let entity = &relation.entity;
let return_type = if relation.nullable {
quote! { Option<#entity> }
} else {
quote! { #entity }
};
let query = format!(
"SELECT * FROM {} WHERE {} = $1",
relation.table, relation.remote_id
);
let local_ident = &value.field.ident;
let fetch = if relation.nullable {
quote! { fetch_optional }
} else {
quote! { fetch_one }
};
quote! {
pub async fn #function(&value, pool: &::sqlx::PgPool) -> ::sqlx::Result<#return_type> {
query_as!(#entity, #query, value.#local_ident).#fetch(pool).await
}
}
}
}

View File

@ -0,0 +1,60 @@
use ir::GeormField;
use quote::quote;
mod ir;
mod relationships;
mod trait_implementation;
fn extract_georm_field_attrs(
ast: &mut syn::DeriveInput,
) -> deluxe::Result<(Vec<GeormField>, GeormField)> {
let syn::Data::Struct(s) = &mut ast.data else {
return Err(syn::Error::new_spanned(
ast,
"Cannot apply to something other than a struct",
));
};
let fields = s
.fields
.clone()
.into_iter()
.map(|mut field| GeormField::new(&mut field))
.collect::<Vec<GeormField>>();
let identifiers: Vec<GeormField> = fields
.clone()
.into_iter()
.filter(|field| field.id)
.collect();
match identifiers.len() {
0 => Err(syn::Error::new_spanned(
ast,
"Struct {name} must have one identifier",
)),
1 => Ok((fields, identifiers.first().unwrap().clone())),
_ => {
let id1 = identifiers.first().unwrap();
let id2 = identifiers.get(1).unwrap();
Err(syn::Error::new_spanned(id2.field.clone(), format!(
"Field {} cannot be an identifier, {} already is one.\nOnly one identifier is supported.",
id1.ident, id2.ident
)))
}
}
}
pub fn georm_derive_macro2(
item: proc_macro2::TokenStream,
) -> deluxe::Result<proc_macro2::TokenStream> {
let mut ast: syn::DeriveInput = syn::parse2(item).expect("Failed to parse input");
let struct_attrs: ir::GeormStructAttributes =
deluxe::extract_attributes(&mut ast).expect("Could not extract attributes from struct");
let (fields, id) = extract_georm_field_attrs(&mut ast)?;
let trait_impl = trait_implementation::derive_trait(&ast, &struct_attrs.table, &fields, &id);
let relationships = relationships::derive_relationships(&ast, &struct_attrs, &fields, &id);
let code = quote! {
#trait_impl
#relationships
};
println!("{code}");
Ok(code)
}

View File

@ -0,0 +1,54 @@
use std::str::FromStr;
use crate::georm::ir::M2MRelationshipComplete;
use super::ir::GeormField;
use proc_macro2::TokenStream;
use quote::quote;
fn join_token_streams(token_streams: &[TokenStream]) -> TokenStream {
let newline = TokenStream::from_str("\n").unwrap();
token_streams
.iter()
.cloned()
.flat_map(|ts| std::iter::once(ts).chain(std::iter::once(newline.clone())))
.collect()
}
fn derive<T, P>(relationships: &[T], condition: P) -> TokenStream
where
for<'a> &'a T: Into<TokenStream>,
P: FnMut(&&T) -> bool,
{
let implementations: Vec<TokenStream> = relationships
.iter()
.filter(condition)
.map(std::convert::Into::into)
.collect();
join_token_streams(&implementations)
}
pub fn derive_relationships(
ast: &syn::DeriveInput,
struct_attrs: &super::ir::GeormStructAttributes,
fields: &[GeormField],
id: &GeormField,
) -> TokenStream {
let struct_name = &ast.ident;
let one_to_one = derive(fields, |field| field.relation.is_none());
let one_to_many = derive(&struct_attrs.one_to_many, |_| true);
let many_to_many: Vec<M2MRelationshipComplete> = struct_attrs
.many_to_many
.iter()
.map(|v| M2MRelationshipComplete::new(v, &struct_attrs.table, id.to_string()))
.collect();
let many_to_many = derive(&many_to_many, |_| true);
quote! {
impl #struct_name {
#one_to_one
#one_to_many
#many_to_many
}
}
}

View File

@ -0,0 +1,137 @@
use super::ir::GeormField;
use quote::quote;
fn generate_find_query(table: &str, id: &GeormField) -> proc_macro2::TokenStream {
let find_string = format!("SELECT * FROM {table} WHERE {id} = $1",);
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: &[GeormField]) -> proc_macro2::TokenStream {
let inputs: Vec<String> = (1..=fields.len()).map(|num| format!("${num}")).collect();
let create_string = format!(
"INSERT INTO {table} ({}) VALUES ({}) RETURNING *",
fields
.iter()
.map(std::string::ToString::to_string)
.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: &[GeormField],
id: &GeormField,
) -> proc_macro2::TokenStream {
let mut fields: Vec<&GeormField> = fields.iter().filter(|f| !f.id).collect();
let update_columns = fields
.iter()
.enumerate()
.map(|(i, &field)| format!("{field} = ${}", i + 1))
.collect::<Vec<String>>()
.join(", ");
let update_string = format!(
"UPDATE {table} SET {update_columns} WHERE {id} = ${} RETURNING *",
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: &GeormField) -> proc_macro2::TokenStream {
let delete_string = format!("DELETE FROM {table} WHERE {id} = $1");
let ty = &id.ty;
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> {
Self::delete_by_id(pool, self.get_id()).await
}
}
}
fn generate_get_id(id: &GeormField) -> proc_macro2::TokenStream {
let ident = &id.ident;
let ty = &id.ty;
quote! {
fn get_id(&self) -> &#ty {
&self.#ident
}
}
}
pub fn derive_trait(
ast: &syn::DeriveInput,
table: &str,
fields: &[GeormField],
id: &GeormField,
) -> proc_macro2::TokenStream {
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 get_id = generate_get_id(id);
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);
quote! {
impl #impl_generics Georm<#ty> for #ident #type_generics #where_clause {
#get_id
#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
}
}
}

82
georm-macros/src/lib.rs Normal file
View File

@ -0,0 +1,82 @@
//! Creates ORM functionality for ``SQLx`` with `PostgreSQL`.
//!
//! This crate provides the trait implementation `Georm` 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 or an id
//! provided by the interfaces user
//!
//! 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 `Georm` found in the `georm`
//! 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 `#[georm(table = "my_table_name")]` atop of the structure,
//! after the `Georm` derive.
//!
//! ## Entity Identifier
//! You will also need to add `#[georm(id)]` atop of the field of your
//! struct that will be used as the identifier of your entity.
//!
//! ## Column Name
//! If the name of a field does not match the name of its related
//! column, you can use `#[georm(column = "...")]` to specify the
//! correct value.
//!
//! ```ignore
//! #[derive(Georm)]
//! #[georm(table = "users")]
//! pub struct User {
//! #[georm(id)]
//! id: String,
//! #[georm(column = "name")]
//! 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 Georm 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, youre in luck!
//! Otherwise, pull requests to add additional syntaxes are most
//! welcome.
mod georm;
use georm::georm_derive_macro2;
/// Generates GEORM code for Sqlx for a struct.
///
/// # Panics
///
/// May panic if errors arise while parsing and generating code.
#[proc_macro_derive(Georm, attributes(georm))]
pub fn georm_derive_macro(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
georm_derive_macro2(item.into()).unwrap().into()
}

75
src/lib.rs Normal file
View File

@ -0,0 +1,75 @@
pub use georm_macros::Georm;
pub trait Georm<Id> {
/// Find the entiy in the database based on its identifier.
///
/// # Errors
/// Returns any error Postgres may have encountered
fn find(
pool: &sqlx::PgPool,
id: &Id,
) -> impl std::future::Future<Output = sqlx::Result<Option<Self>>> + Send
where
Self: Sized;
/// Create the entity in the database.
///
/// # Errors
/// Returns any error Postgres may have encountered
fn create(
&self,
pool: &sqlx::PgPool,
) -> impl std::future::Future<Output = sqlx::Result<Self>> + Send
where
Self: Sized;
/// Update an entity with a matching identifier in the database.
///
/// # Errors
/// Returns any error Postgres may have encountered
fn update(
&self,
pool: &sqlx::PgPool,
) -> impl std::future::Future<Output = sqlx::Result<Self>> + Send
where
Self: Sized;
/// Update an entity with a matching identifier in the database if
/// it exists, create it otherwise.
///
/// # Errors
/// Returns any error Postgres may have encountered
fn create_or_update(
&self,
pool: &sqlx::PgPool,
) -> impl std::future::Future<Output = sqlx::Result<Self>> + Send
where
Self: Sized;
/// Delete the entity from the database if it exists.
///
/// # Returns
/// Returns the amount of rows affected by the deletion.
///
/// # Errors
/// Returns any error Postgres may have encountered
fn delete(
&self,
pool: &sqlx::PgPool,
) -> impl std::future::Future<Output = sqlx::Result<u64>> + Send;
/// Delete any entity with the identifier `id`.
///
/// # Returns
/// Returns the amount of rows affected by the deletion.
///
/// # Errors
/// Returns any error Postgres may have encountered
fn delete_by_id(
pool: &sqlx::PgPool,
id: &Id,
) -> impl std::future::Future<Output = sqlx::Result<u64>> + Send;
/// Returns the identifier of the entity.
fn get_id(&self) -> &Id;
}