feat: newtype macro, user repo

This commit is contained in:
Orion Kindel 2023-07-07 21:38:27 -05:00
parent c788ee1be1
commit 568b248f3e
Signed by untrusted user who does not match committer: orion
GPG Key ID: 6D4165AE4C928719
7 changed files with 335 additions and 27 deletions

1
Cargo.lock generated
View File

@ -31,6 +31,7 @@ name = "api"
version = "0.1.0"
dependencies = [
"log",
"naan",
"nb",
"postgres",
"rand",

View File

@ -14,3 +14,4 @@ serde_json = "1"
log = "0.4"
postgres = {path = "./postgres/postgres"}
rand = "0.8"
naan = "0.1.32"

View File

@ -1,10 +1,13 @@
use postgres::{GenericClient, GenericRow};
use crate::newtype;
use crate::postgres::{DbError, Postgres};
use crate::repo::Ext;
newtype!(
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct HashedText(String);
);
pub trait HashedTextExt: Ext {
fn matches<S: AsRef<str>>(&self, this: &HashedText, other: S) -> Result<bool, Self::Error>;
@ -20,8 +23,7 @@ impl<Db> Ext for HashedTextExtImpl<Db> where Db: Postgres
impl<Db> HashedTextExt for HashedTextExtImpl<Db> where Db: Postgres
{
fn matches<S: AsRef<str>>(&self, this: &HashedText, other: S) -> Result<bool, DbError<Db>> {
static QUERY: &str =
"select public.hashed_text_matches($1, public.hashed_text_of_string($2))";
static QUERY: &str = "select public.hashed_text_matches($1, public.hashed_text_of_string($2))";
self.0
.with_client(|client| client.query_one(QUERY, &[&other.as_ref(), &this.0.as_str()]))
@ -31,11 +33,7 @@ impl<Db> HashedTextExt for HashedTextExtImpl<Db> where Db: Postgres
#[cfg(test)]
mod test {
use postgres::types::{Type};
use postgres::types::Type;
use super::{HashedText, HashedTextExt, HashedTextExtImpl};
use crate::postgres::test::{from_sql_owned, Client, Row};

View File

@ -1,21 +1,77 @@
use toad::config::Config;
use toad::net::Addrd;
use toad::platform::Platform as _;
use toad::req::Req;
use toad::resp::Resp;
mod app;
mod hashed_text;
mod postgres;
mod repo;
mod user;
#[macro_export]
macro_rules! newtype {
(
$(#[derive($($der:ident),+)])?
$(#[doc = $doc:literal])*
pub struct $name:ident(String);
) => {
$(#[derive($($der),+)])?
$(#[doc = $doc])*
pub struct $name(String);
impl AsRef<str> for $name {
fn as_ref(&self) -> &str {
self.0.as_str()
}
}
impl ToString for $name {
fn to_string(&self) -> String {
self.0.clone()
}
}
impl From<String> for $name {
fn from(s: String) -> $name {
$name(s)
}
}
impl From<&str> for $name {
fn from(s: &str) -> $name {
$name(s.to_string())
}
}
impl From<$name> for String {
fn from(s: $name) -> String {
s.0
}
}
};
(pub struct $name:ident($inner:ty);) => {
pub struct $name($inner);
impl From<$inner> for $name {
fn from(s: $inner) -> $name {
$name(s)
}
}
impl From<$name> for $inner {
fn from(s: $name) -> $inner {
s.0
}
}
};
}
newtype!(
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct Email(String);
);
type Dtls = toad::std::dtls::N;
type ToadT = toad::std::PlatformTypes<Dtls>;

View File

@ -1,11 +1,17 @@
use std::ops::{DerefMut};
use std::ops::DerefMut;
use std::pin::Pin;
use std::sync::{Mutex, MutexGuard};
use rand::{Rng};
use postgres::GenericRow;
use rand::Rng;
pub type DbError<Pg> = <<Pg as Postgres>::Client as postgres::GenericClient>::Error;
pub trait UnmarshalRow: Sized + Send + Sync {
fn unmarshal<R>(row: &R) -> Result<Self, R::Error>
where R: GenericRow;
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct ConnectionParams {
pub host: String,
@ -80,7 +86,9 @@ impl<C> Postgres for PostgresImpl<C>
#[cfg(test)]
pub mod test {
use std::any::Any;
use std::marker::PhantomData;
use std::sync::Mutex;
pub fn from_sql_owned<T>(to: &dyn ToSql) -> T
where T: 'static + for<'a> FromSql<'a> + postgres::types::Typed
@ -219,6 +227,7 @@ pub mod test {
#[non_exhaustive]
pub struct Client<E> {
pub __phantom: PhantomData<E>,
pub state: Box<dyn Any>,
pub execute: Box<dyn Execute<Self, E>>,
pub query: Box<dyn Query<Self, E>>,
pub query_one: Box<dyn QueryOne<Self, E>>,
@ -233,10 +242,19 @@ pub mod test {
unsafe impl<E> Send for Client<E> {}
unsafe impl<E> Sync for Client<E> {}
impl<E> Client<E> {
pub fn state_mut<T>(&mut self) -> &mut T
where T: 'static
{
self.state.downcast_mut::<T>().unwrap()
}
}
impl<E> Default for Client<E> where E: core::fmt::Debug
{
fn default() -> Self {
Client { __phantom: PhantomData,
state: Box::new(()),
execute: Box::new(|_, _, _| panic!("execute not implemented")),
query: Box::new(|_, _, _| panic!("query not implemented")),
query_one: Box::new(|_, _, _| panic!("query_one not implemented")),

View File

@ -1,13 +1,15 @@
pub trait Repo: Send + Sync {
type T;
type TPut;
type Patch;
type Insert;
type Error: core::fmt::Debug;
type Id: AsRef<str>;
fn get(&self, id: Self::Id) -> Result<Option<Self::T>, Self::Error>;
fn get_all(&self) -> Result<Vec<Self::T>, Self::Error>;
fn put(&self, id: Self::Id, state: Self::TPut) -> Result<(), Self::Error>;
fn del(&self, id: Self::Id) -> Result<(), Self::Error>;
fn patch(&self, id: Self::Id, state: Self::Patch) -> Result<bool, Self::Error>;
fn insert(&self, state: Self::Insert) -> Result<Self::Id, Self::Error>;
fn del(&self, id: Self::Id) -> Result<bool, Self::Error>;
}
/// An entity that has some operations which rely on

View File

@ -1,12 +1,20 @@
use crate::hashed_text::HashedText;
use crate::repo::Repo;
use crate::Email;
use naan::prelude::*;
use postgres::{GenericClient, GenericRow};
use crate::hashed_text::HashedText;
use crate::postgres::{DbError, Postgres, UnmarshalRow};
use crate::repo::Repo;
use crate::{newtype, Email};
newtype!(
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct UserId(String);
);
newtype!(
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct UserTag(String);
);
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct User {
@ -16,11 +24,235 @@ pub struct User {
email: Email,
}
impl UnmarshalRow for User {
fn unmarshal<R>(row: &R) -> Result<Self, R::Error>
where R: GenericRow
{
row.try_get::<_, String>("uid")
.zip(|_: &_| row.try_get::<_, String>("tag"))
.zip(|_: &_| row.try_get::<_, String>("password"))
.zip(|_: &_| row.try_get::<_, String>("email"))
.map(|(((uid, tag), password), email)| User { uid: UserId(uid),
tag: UserTag(tag),
password: HashedText::from(password),
email: Email(email) })
}
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct UserPut {
pub struct UserPatch {
tag: Option<UserTag>,
password: Option<HashedText>,
email: Option<Email>,
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct UserInsert {
tag: UserTag,
password: HashedText,
email: Email,
}
pub trait UserRepo: Repo<T = User, TPut = UserPut, Id = UserId> {}
pub trait UserRepo: Repo<T = User, Patch = UserPatch, Id = UserId> {}
pub struct UserRepoImpl<Db: 'static>(&'static Db);
impl<Db> Repo for UserRepoImpl<Db> where Db: Postgres + 'static
{
type T = User;
type Patch = UserPatch;
type Insert = UserInsert;
type Error = DbError<Db>;
type Id = UserId;
fn get(&self, id: Self::Id) -> Result<Option<Self::T>, Self::Error> {
static QUERY: &'static str =
"select uid, tag, password, email from public.usr where id = $1 :: uuid";
self.0
.with_client(|c| c.query_opt(QUERY, &[&id.as_ref()]))
.and_then(|opt| {
opt.as_ref()
.map(User::unmarshal)
.sequence::<hkt::ResultOk<_>>()
})
}
fn get_all(&self) -> Result<Vec<Self::T>, Self::Error> {
static QUERY: &'static str = "select uid, tag, password, email from public.usr";
self.0
.with_client(|c| c.query(QUERY, &[]))
.and_then(|vec| vec.iter().map(User::unmarshal).collect())
}
fn patch(&self, id: UserId, patch: UserPatch) -> Result<bool, Self::Error> {
static QUERY_LINES: [&'static str; 5] = ["update public.usr",
"set tag = coalesce($2, tag)",
" , password = coalesce($3, password)",
" , email = coalesce($4, email)",
"from public.usr where uid = $1"];
self.0
.with_client(|c| {
c.execute(&QUERY_LINES.iter()
.fold(String::new(), |b, a| format!("{b}{a}\n")),
&[&id.as_ref(),
&patch.tag.as_ref().map(|t| t.as_ref()),
&patch.password.as_ref().map(|h| h.as_ref()),
&patch.email.as_ref().map(|e| e.as_ref())])
})
.map(|n| n == 1)
}
fn insert(&self, insert: UserInsert) -> Result<UserId, Self::Error> {
static QUERY_LINES: [&'static str; 5] = ["insert into public.usr",
" (tag, password, email)",
"values",
" ($2, $3, $4)",
"returning uid;"];
self.0
.with_client(|c| {
c.query_one(&QUERY_LINES.iter()
.fold(String::new(), |b, a| format!("{b}{a}\n")),
&[&insert.tag.as_ref(),
&insert.password.as_ref(),
&insert.email.as_ref()])
})
.and_then(|r| r.try_get::<_, String>(0))
.map(UserId::from)
}
fn del(&self, id: UserId) -> Result<bool, Self::Error> {
static QUERY: &'static str = "delete from public.usr where uid = $1 :: uuid;";
self.0
.with_client(|c| c.execute(QUERY, &[&id.as_ref()]))
.map(|n| n == 1)
}
}
impl<Db> UserRepo for UserRepoImpl<Db> where Db: Postgres + 'static {}
#[cfg(test)]
mod tests {
use postgres::types::Type;
use super::{User, UserRepoImpl};
use crate::hashed_text::HashedText;
use crate::postgres::test::{from_sql_owned, Client, Row};
use crate::postgres::{Postgres, PostgresImpl};
use crate::repo::Repo;
use crate::user::{UserId, UserTag};
use crate::Email;
fn usr_row(usr: User) -> Row<()> {
Row::new(vec![("uid", Type::UUID),
("email", Type::TEXT),
("password", Type::TEXT),
("tag", Type::TEXT)]).value(Type::UUID, usr.uid.to_string())
.value(Type::TEXT, usr.email.to_string())
.value(Type::TEXT, usr.password.to_string())
.value(Type::TEXT, usr.tag.to_string())
}
#[test]
fn user_repo_get_one() {
let client = || Client::<()> { query_opt: Box::new(|_, q, ps| {
assert_eq!(q.unwrap_str(), "select uid, tag, password, email from public.usr where id = $1 :: uuid");
Ok(Some(usr_row(User {uid: UserId::from("1"), tag: UserTag::from("foo"), email: Email::from("foo@bar.baz"), password: HashedText::from("XXX")})).filter(|_| from_sql_owned::<String>(ps[0]) == String::from("1")))
}),
..Client::default() };
let db = PostgresImpl::try_new(|| Ok(client()), 1).unwrap();
let repo =
UserRepoImpl(unsafe { std::mem::transmute::<_, &'static PostgresImpl<Client<()>>>(&db) });
assert!(repo.get(UserId::from("1")).unwrap().is_some());
assert!(repo.get(UserId::from("0")).unwrap().is_none());
}
#[test]
fn user_repo_get_all() {
let client = || Client::<()> { query: Box::new(|_, q, _| {
assert_eq!(q.unwrap_str(),
"select uid, tag, password, email from public.usr");
Ok(vec![usr_row(User { uid: UserId::from("1"),
tag: UserTag::from("foo"),
email: Email::from("foo@bar.baz"),
password: HashedText::from("XXX") })])
}),
..Client::default() };
let db = PostgresImpl::try_new(|| Ok(client()), 1).unwrap();
let repo =
UserRepoImpl(unsafe { std::mem::transmute::<_, &'static PostgresImpl<Client<()>>>(&db) });
assert_eq!(repo.get_all().unwrap().len(), 1);
}
#[test]
fn user_repo_del() {
let client =
|| Client::<()> { state: Box::new(false), // already deleted?
execute: Box::new(|c, q, ps| {
assert_eq!(q.unwrap_str(),
"delete from public.usr where uid = $1 :: uuid;");
if from_sql_owned::<String>(ps[0]) == "1" && !*c.state_mut::<bool>() {
*c.state_mut::<bool>() = true;
Ok(1)
} else {
Ok(0)
}
}),
..Client::default() };
let db = PostgresImpl::try_new(|| Ok(client()), 1).unwrap();
let repo =
UserRepoImpl(unsafe { std::mem::transmute::<_, &'static PostgresImpl<Client<()>>>(&db) });
assert_eq!(repo.del(UserId::from("1")).unwrap(), true);
assert_eq!(repo.del(UserId::from("1")).unwrap(), false);
assert_eq!(repo.del(UserId::from("2")).unwrap(), false);
}
#[test]
fn user_repo_insert() {
let client =
|| Client::<()> { state: Box::new(vec![UserTag::from("foo")]),
query_one: Box::new(|c, q, ps| {
assert_eq!(q.unwrap_str(),
format!(
"insert into public.usr
(tag, password, email)
values
($2, $3, $4)
returning uid;"));
let tags = c.state_mut::<Vec<UserTag>>();
let tag = UserTag::from(from_sql_owned::<String>(ps[0]));
if tags.contains(&tag) {
Err(())
} else {
tags.push(tag);
Ok(Row::new(vec![("", Type::TEXT)]).value(Type::TEXT, tags.len().to_string()))
}
}),
..Client::default() };
let db = PostgresImpl::try_new(|| Ok(client()), 1).unwrap();
let repo =
UserRepoImpl(unsafe { std::mem::transmute::<_, &'static PostgresImpl<Client<()>>>(&db) });
assert!(repo.insert(UserId::from("1")).is_ok());
assert!(repo.insert(UserId::from("1")).is_err());
assert!(repo.insert(UserId::from("2")).is_ok());
}
}