feat: add foreign one_to_one relationships

This commit is contained in:
2025-03-02 16:07:30 +01:00
parent 4ff2df1a4e
commit aafbfb7964
13 changed files with 1667 additions and 291 deletions

View File

@@ -1,195 +0,0 @@
use quote::quote;
#[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>> {
::sqlx::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,
}
#[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>> {
::sqlx::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 relation: Option<O2ORelationship>,
}
#[derive(deluxe::ParseMetaItem, Clone, Debug)]
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, Debug)]
pub struct GeormField {
pub ident: syn::Ident,
pub field: syn::Field,
pub ty: syn::Type,
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, relation } = attrs;
Self {
ident,
field: field.to_owned(),
id,
ty,
relation,
}
}
}
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(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result<#return_type> {
::sqlx::query_as!(#entity, #query, self.#local_ident).#fetch(pool).await
}
}
}
}

View File

@@ -0,0 +1,79 @@
use quote::quote;
#[derive(deluxe::ParseMetaItem, Clone)]
pub struct M2MLink {
pub table: String,
pub from: String,
pub to: String,
}
#[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>> {
::sqlx::query_as!(#entity, #query, self.get_id()).fetch_all(pool).await
}
}
}
}

View File

@@ -0,0 +1,98 @@
use quote::quote;
pub mod simple_relationship;
use simple_relationship::{OneToMany, OneToOne, SimpleRelationship};
pub mod m2m_relationship;
use m2m_relationship::M2MRelationship;
#[derive(deluxe::ExtractAttributes)]
#[deluxe(attributes(georm))]
pub struct GeormStructAttributes {
pub table: String,
#[deluxe(default = Vec::new())]
pub one_to_one: Vec<SimpleRelationship<OneToOne>>,
#[deluxe(default = Vec::new())]
pub one_to_many: Vec<SimpleRelationship<OneToMany>>,
#[deluxe(default = Vec::new())]
pub many_to_many: Vec<M2MRelationship>,
}
#[derive(deluxe::ExtractAttributes, Clone)]
#[deluxe(attributes(georm))]
struct GeormFieldAttributes {
#[deluxe(default = false)]
pub id: bool,
#[deluxe(default = None)]
pub relation: Option<O2ORelationship>,
}
#[derive(deluxe::ParseMetaItem, Clone, Debug)]
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, Debug)]
pub struct GeormField {
pub ident: syn::Ident,
pub field: syn::Field,
pub ty: syn::Type,
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, relation } = attrs;
Self {
ident,
field: field.to_owned(),
id,
ty,
relation,
}
}
}
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(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result<#return_type> {
::sqlx::query_as!(#entity, #query, self.#local_ident).#fetch(pool).await
}
}
}
}

View File

@@ -0,0 +1,66 @@
use quote::quote;
pub trait SimpleRelationshipType {}
#[derive(deluxe::ParseMetaItem, Default)]
pub struct OneToOne;
impl SimpleRelationshipType for OneToOne {}
#[derive(deluxe::ParseMetaItem, Default)]
pub struct OneToMany;
impl SimpleRelationshipType for OneToMany {}
#[derive(deluxe::ParseMetaItem)]
pub struct SimpleRelationship<T>
where
T: SimpleRelationshipType + deluxe::ParseMetaItem + Default,
{
pub name: String,
pub remote_id: String,
pub table: String,
pub entity: syn::Type,
#[deluxe(default = T::default())]
_phantom: T,
}
impl<T> SimpleRelationship<T>
where
T: SimpleRelationshipType + deluxe::ParseMetaItem + Default,
{
pub fn make_query(&self) -> String {
format!("SELECT * FROM {} WHERE {} = $1", self.table, self.remote_id)
}
pub fn make_function_name(&self) -> syn::Ident {
syn::Ident::new(
&format!("get_{}", self.name),
proc_macro2::Span::call_site(),
)
}
}
impl From<&SimpleRelationship<OneToOne>> for proc_macro2::TokenStream {
fn from(value: &SimpleRelationship<OneToOne>) -> Self {
let query = value.make_query();
let entity = &value.entity;
let function = value.make_function_name();
quote! {
pub async fn #function(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result<Option<#entity>> {
::sqlx::query_as!(#entity, #query, self.get_id()).fetch_optional(pool).await
}
}
}
}
impl From<&SimpleRelationship<OneToMany>> for proc_macro2::TokenStream {
fn from(value: &SimpleRelationship<OneToMany>) -> Self {
let query = value.make_query();
let entity = &value.entity;
let function = value.make_function_name();
quote! {
pub async fn #function(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result<Vec<#entity>> {
::sqlx::query_as!(#entity, #query, self.get_id()).fetch_all(pool).await
}
}
}
}

View File

@@ -1,6 +1,6 @@
use std::str::FromStr;
use crate::georm::ir::M2MRelationshipComplete;
use crate::georm::ir::m2m_relationship::M2MRelationshipComplete;
use super::ir::GeormField;
use proc_macro2::TokenStream;
@@ -15,16 +15,12 @@ fn join_token_streams(token_streams: &[TokenStream]) -> TokenStream {
.collect()
}
fn derive<T, P>(relationships: &[T], condition: P) -> TokenStream
fn derive<T>(relationships: &[T]) -> 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();
let implementations: Vec<TokenStream> =
relationships.iter().map(std::convert::Into::into).collect();
join_token_streams(&implementations)
}
@@ -35,18 +31,20 @@ pub fn derive_relationships(
id: &GeormField,
) -> TokenStream {
let struct_name = &ast.ident;
let one_to_one = derive(fields, |field| field.relation.is_some());
let one_to_many = derive(&struct_attrs.one_to_many, |_| true);
let one_to_one_local = derive(fields);
let one_to_one_remote = derive(&struct_attrs.one_to_one);
let one_to_many = derive(&struct_attrs.one_to_many);
let many_to_many: Vec<M2MRelationshipComplete> = struct_attrs
.many_to_many
.iter()
.map(|v| M2MRelationshipComplete::new(v, &struct_attrs.table, id.ident.to_string()))
.collect();
let many_to_many = derive(&many_to_many, |_| true);
let many_to_many = derive(&many_to_many);
quote! {
impl #struct_name {
#one_to_one
#one_to_one_local
#one_to_one_remote
#one_to_many
#many_to_many
}