feat: newtype macro, user repo
This commit is contained in:
parent
c788ee1be1
commit
568b248f3e
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -31,6 +31,7 @@ name = "api"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"naan",
|
||||
"nb",
|
||||
"postgres",
|
||||
"rand",
|
||||
|
@ -14,3 +14,4 @@ serde_json = "1"
|
||||
log = "0.4"
|
||||
postgres = {path = "./postgres/postgres"}
|
||||
rand = "0.8"
|
||||
naan = "0.1.32"
|
||||
|
@ -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};
|
||||
|
60
src/main.rs
60
src/main.rs
@ -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>;
|
||||
|
@ -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")),
|
||||
|
@ -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
|
||||
|
242
src/user.rs
242
src/user.rs
@ -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());
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user