Skip to content
Open
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[workspace]
members = ["crates/taom-database", "crates/taom-database-macro"]

[dependencies]
actix-governor = "0.5"
actix-web = "4.9"
Expand All @@ -26,6 +29,7 @@ semver = "1.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_with = { version = "3.9", features = ["base64", "time_0_3"] }
taom-database = { path = "./crates/taom-database" }
tokio = "1.39"
tokio-postgres = { version = "0.7", features = ["with-serde_json-1", "with-uuid-1"] }
url = "2.5"
Expand Down
12 changes: 12 additions & 0 deletions crates/taom-database-macro/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "taom-database-macro"
version = "0.1.0"
edition = "2021"

[lib]
proc-macro = true

[dependencies]
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "2", features = ["full", "parsing"] }
58 changes: 58 additions & 0 deletions crates/taom-database-macro/src/attribute.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
use syn::{Attribute, LitStr, Result, Token};

macro_rules! fail {
($t:expr, $m:expr) => {
return Err(syn::Error::new_spanned($t, $m))
};
}

macro_rules! try_set {
(option : $i:expr, $v:expr, $t:expr) => {
match $i {
None => {
$i = Some($v);
Ok(())
}
Some(_) => fail!($t, "duplicate attribute"),
}
};
(bool : $i:expr, $t:expr) => {
match $i {
false => {
$i = true;
Ok(())
}
true => fail!($t, "duplicate attribute"),
}
};
}

#[derive(Default)]
pub(crate) struct DbRowAttribute {
pub rename: Option<String>,
pub default: bool,
}

pub fn parse_db_row_attr(attrs: &[Attribute]) -> Result<DbRowAttribute> {
let mut result_attr = DbRowAttribute::default();

for attr in attrs.iter().filter(|attr| attr.path().is_ident("db_row")) {
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("rename") {
meta.input.parse::<Token![=]>()?;
let val: LitStr = meta.input.parse()?;
try_set!(option : result_attr.rename, val.value(), val)
} else if meta.path.is_ident("default") {
try_set!(bool : result_attr.default, meta.path)
} else {
let msg = match meta.path.get_ident() {
Some(ident) => format!("Unexpected attribute `{}` in db_row", ident),
None => "Unexpected attribute".to_string(),
};
fail!(meta.path, msg)
}
})?
}

Ok(result_attr)
}
64 changes: 64 additions & 0 deletions crates/taom-database-macro/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
use attribute::parse_db_row_attr;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput, Fields, Result};

mod attribute;

#[proc_macro_derive(FromRow, attributes(db_row))]
pub fn derive_from_row(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);

let syn::Data::Struct(data) = input.data else {
return TokenStream::from(
syn::Error::new(input.ident.span(), "Only structs can derive `FromRow`")
.to_compile_error(),
);
};

let struct_name = input.ident;
let fields = data
.fields
.iter()
.enumerate()
.map(|(i, field)| {
let attr = parse_db_row_attr(field.attrs.as_slice())?;
Ok(match field.ident.as_ref() {
Some(name) => {
let db_name = attr.rename.unwrap_or(name.to_string());
match attr.default {
true => quote! { #name: row.try_get(#db_name).unwrap_or_default() },
false => quote! { #name: row.try_get(#db_name)? },
}
}
None => match (attr.rename, attr.default) {
(Some(db_name), true) => quote! { row.try_get(#db_name).unwrap_or_default() },
(Some(db_name), false) => quote! { row.try_get(#db_name)? },
(None, true) => quote! { row.try_get(#i).unwrap_or_default() },
(None, false) => quote! { row.try_get(#i)? },
}
})
})
.collect::<Result<Vec<_>>>();

let fields = match fields {
Ok(ts) => ts,
Err(e) => return e.to_compile_error().into(),
};

let struct_self = match data.fields {
Fields::Named(_) => quote! { Self {#(#fields),*} },
Fields::Unnamed(_) => quote! { Self(#(#fields),*) },
Fields::Unit => quote! { Self },
};

quote! {
#[automatically_derived]
impl ::taom_database::FromRow for #struct_name {
fn from_row(row: ::tokio_postgres::Row) -> Result<Self, ::tokio_postgres::Error> {
Ok(#struct_self)
}
}
}
.into()
}
10 changes: 10 additions & 0 deletions crates/taom-database/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[package]
name = "taom-database"
version = "0.1.0"
edition = "2021"

[dependencies]
deadpool-postgres = "0.14"
futures = "0.3"
taom-database-macro = { path = "../taom-database-macro"}
tokio-postgres = { version = "0.7", features = ["with-serde_json-1", "with-uuid-1"] }
61 changes: 61 additions & 0 deletions crates/taom-database/src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
use deadpool_postgres::{Config, ManagerConfig, Pool, RecyclingMethod, Runtime};
use tokio_postgres::NoTls;

use crate::error::PoolError;

#[derive(Default)]
pub struct ConfigBuilder<'b> {
host: Option<&'b str>,
password: Option<&'b str>,
user: Option<&'b str>,
database: Option<&'b str>,
recycling_method: RecyclingMethod,
runtime: Option<Runtime>,
}

impl<'b> ConfigBuilder<'b> {
pub fn host(mut self, host: &'b str) -> Self {
self.host = Some(host);
self
}
pub fn password(mut self, password: &'b str) -> Self {
self.password = Some(password);
self
}
pub fn user(mut self, user: &'b str) -> Self {
self.user = Some(user);
self
}
pub fn database(mut self, database: &'b str) -> Self {
self.database = Some(database);
self
}
pub fn recycling_method(mut self, recycling_method: RecyclingMethod) -> Self {
self.recycling_method = recycling_method;
self
}
pub fn runtime(mut self, runtime: Runtime) -> Self {
self.runtime = Some(runtime);
self
}

pub async fn build(self) -> Result<Pool, PoolError> {
let mut pg_config = Config::new();
pg_config.host = self.host.map(str::to_string);
pg_config.password = self.password.map(str::to_string);
pg_config.user = self.user.map(str::to_string);
pg_config.dbname = self.database.map(str::to_string);
pg_config.manager = Some(ManagerConfig {
recycling_method: self.recycling_method,
});

let pool = pg_config
.create_pool(self.runtime, NoTls)
.map_err(|_| PoolError::Creation)?;

// Try to connect to database to test if the database exist
let _ = pool.get().await.map_err(|_| PoolError::Connection)?;

Ok(pool)
}
}
29 changes: 29 additions & 0 deletions crates/taom-database/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
use core::fmt::Display;

use tokio_postgres::Error;

use crate::Query;

pub enum PoolError {
Creation,
Connection,
}

#[derive(Debug)]
pub enum QueryError<R> {
PreparationFailed(Query<'static>),
HasMoreThanOneRow(/*first_row:*/ R),
ExecuteFailed(Error),
HasNoRow,
}

impl<R> Display for QueryError<R> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::PreparationFailed(_) => write!(f, "failure during the preparation of the query"),
Self::HasMoreThanOneRow(_) => write!(f, "collect more than one result"),
Self::ExecuteFailed(err) => write!(f, "{err}"),
Self::HasNoRow => write!(f, "no row found"),
}
}
}
Loading