#![allow(proc_macro_derive_resolution_fallback)]
#![deny(missing_docs)]
use std::marker::PhantomData;
use std::error::Error;
use std::fmt::{Formatter, Display, self};
#[macro_use]
extern crate diesel;
extern crate chrono;
use chrono::NaiveDateTime;
use diesel::Connection;
use diesel::pg::Pg;
use diesel::prelude::*;
use diesel::dsl;
mod schema;
#[derive(Queryable, Debug, Clone)]
struct PleaseId {
id: i32,
creation: NaiveDateTime,
expiry: NaiveDateTime,
title: String,
refresh_count: i32,
}
#[derive(Debug, Clone)]
pub struct ExpiredId(PleaseId);
#[derive(Debug, PartialEq)]
pub enum PleaseError<P> {
Provider(P),
Query(diesel::result::Error),
Expired,
#[doc(hidden)]
__Nonexhaustive,
}
impl<P: Error> Display for PleaseError<P> {
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
match self {
&PleaseError::Provider(ref e) => Display::fmt(e, f),
&PleaseError::Query(ref e) => Display::fmt(e, f),
&PleaseError::Expired => Display::fmt(self.description(), f),
&PleaseError::__Nonexhaustive => unreachable!(),
}
}
}
impl<P: Error> Error for PleaseError<P> {
fn description(&self) -> &str {
match self {
&PleaseError::Provider(ref e) => e.description(),
&PleaseError::Query(ref e) => e.description(),
&PleaseError::Expired => "The `please` handle has expired and can no longer be used",
&PleaseError::__Nonexhaustive => unreachable!(),
}
}
fn cause(&self) -> Option<&Error> {
match self {
&PleaseError::Provider(ref e) => Some(e),
&PleaseError::Query(ref e) => Some(e),
&PleaseError::Expired => None,
&PleaseError::__Nonexhaustive => unreachable!(),
}
}
}
impl<P> From<diesel::result::Error> for PleaseError<P> {
fn from(other: diesel::result::Error) -> Self {
PleaseError::Query(other)
}
}
struct ErrorWrapper<E, P>(E, PhantomData<fn(P) -> P>);
impl<P, E: From<PleaseError<P>>> From<diesel::result::Error> for ErrorWrapper<E, P> {
fn from(other: diesel::result::Error) -> Self {
ErrorWrapper(PleaseError::Query(other).into(), PhantomData)
}
}
impl<P, E: From<PleaseError<P>>> From<PleaseError<P>> for ErrorWrapper<E, P> {
fn from(other: PleaseError<P>) -> Self {
ErrorWrapper(other.into(), PhantomData)
}
}
pub type PleaseResult<T, P> = Result<T, PleaseError<P>>;
pub trait ConnectionProvider {
type Error;
type Connection: Connection<Backend=Pg>;
fn get(&self) -> Result<Self::Connection, Self::Error>;
}
#[derive(Debug)]
pub struct PleaseHandle<P: ConnectionProvider> {
provider: P,
id: i32
}
impl<P: ConnectionProvider> PleaseHandle<P> {
fn transaction_internal<R, E: From<PleaseError<P::Error>>, F: FnOnce(&P::Connection) -> Result<R, E>>(provider: &P, f: F) -> Result<R, E> {
let conn = provider.get()
.map_err(PleaseError::Provider)?;
conn.transaction(|| f(&conn).map_err(|e| ErrorWrapper(e, PhantomData)))
.map_err(|ErrorWrapper(e, _)| e)
}
pub fn new(provider: P, title: &str) -> PleaseResult<Self, P::Error> {
use self::schema::*;
let id: i32 = Self::transaction_internal(&provider, |conn| -> PleaseResult<i32, P::Error> {
Ok(diesel::insert_into(please_ids::table)
.values(&please_ids::title.eq(title))
.returning(please_ids::id)
.get_result(conn)?)
})?;
Ok(PleaseHandle { provider, id })
}
pub fn new_with_cleanup(provider: P, title: &str) -> PleaseResult<Self, P::Error> {
let _ = Self::perform_cleanup(&provider);
Self::new(provider, title)
}
pub fn new_with_connection<C>(provider: P, title: &str, conn: &C) -> QueryResult<Self>
where
C: Connection<Backend=Pg> + ?Sized
{
use self::schema::*;
let id = diesel::insert_into(please_ids::table)
.values(&please_ids::title.eq(title))
.returning(please_ids::id)
.get_result(conn)?;
Ok(PleaseHandle {
provider,
id,
})
}
pub fn perform_cleanup(provider: &P) -> PleaseResult<Vec<ExpiredId>, P::Error> {
use self::schema::*;
Self::transaction_internal(provider, |conn| {
diesel::delete(please_ids::table.filter(please_ids::expiry.lt(dsl::now)))
.get_results(conn)
.map_err(PleaseError::Query)
.map(|v| {
v.into_iter().map(ExpiredId).collect()
})
})
}
pub fn transaction<R, E, F>(&mut self, f: F) -> Result<R, E>
where
E: From<PleaseError<P::Error>>,
F: FnOnce(&P::Connection, i32) -> Result<R, E>
{
use self::schema::*;
Self::transaction_internal(&self.provider, |conn| {
let num_rows = diesel::update(
please_ids::table.filter(please_ids::id.eq(self.id))
).set(
please_ids::refresh_count.eq(please_ids::refresh_count + 1)
).execute(conn).map_err(PleaseError::Query)?;
if num_rows == 1 {
f(conn, self.id)
} else {
Err(PleaseError::Expired.into())
}
})
}
pub fn refresh(&mut self) -> PleaseResult<(), P::Error> {
self.transaction(|_conn, _id| Ok(()))
}
pub fn expire(&mut self) -> PleaseResult<ExpiredId, P::Error> {
use self::schema::*;
Self::transaction_internal(&self.provider, |conn| {
diesel::delete(
please_ids::table.filter(please_ids::id.eq(self.id))
)
.get_result(conn)
.optional()?
.ok_or(PleaseError::Expired)
}).map(ExpiredId)
}
pub fn close(mut self) -> PleaseResult<(), P::Error> {
self.expire()?;
self.id = -1;
Ok(())
}
pub fn id(&self) -> i32 { self.id }
}
impl<P: ConnectionProvider> Drop for PleaseHandle<P> {
fn drop(&mut self) {
if self.id != -1 {
let _ = self.expire();
}
}
}
#[cfg(test)]
mod tests {
extern crate dotenv;
use super::*;
use std::env;
#[derive(Copy, Clone, Debug)]
struct TestConnectionProvider;
impl ConnectionProvider for TestConnectionProvider {
type Connection = PgConnection;
type Error = diesel::ConnectionError;
fn get(&self) -> Result<PgConnection, Self::Error> {
dotenv::dotenv().ok();
PgConnection::establish(&env::var("DATABASE_URL").unwrap())
}
}
fn new_handle(name: &str) -> PleaseHandle<TestConnectionProvider> {
PleaseHandle::new(TestConnectionProvider, name)
.expect("Failed to create handle")
}
fn new_handle_with_connection(name: &str) -> PleaseHandle<TestConnectionProvider> {
let conn = TestConnectionProvider.get()
.expect("Failed to get connection");
conn.transaction(|| {
PleaseHandle::new_with_connection(TestConnectionProvider, name, &conn)
}).expect("Failed to run transaction")
}
#[test]
fn smoke() {
let mut handle = new_handle("smoke");
handle.transaction(|_conn, _id| Ok::<(), PleaseError<_>>(()))
.expect("Failed to run no-op transaction");
handle.close()
.expect("Failed to close handle");
}
#[test]
fn smoke_with_connection() {
let mut handle = new_handle_with_connection("smoke");
handle.transaction(|_conn, _id| Ok::<(), PleaseError<_>>(()))
.expect("Failed to run no-op transaction");
handle.close()
.expect("Failed to close handle");
}
#[test]
fn two_handles() {
let mut handle1 = new_handle("two_handles::1");
let mut handle2 = new_handle("two_handles::2");
handle1.transaction(|_conn1, id1| {
handle2.transaction(|_conn2, id2| {
assert_ne!(id1, id2);
Ok::<(), PleaseError<_>>(())
})
}).expect("Failed to run transactions");
handle1.close()
.expect("Failed to close handle 1");
handle2.close()
.expect("Failed to close handle 2");
}
#[test]
fn expiry() {
let mut handle = new_handle("smoke");
handle.expire()
.expect("Failed to expire handle");
let err = handle.transaction(|_conn, _id| Ok::<(), PleaseError<_>>(()))
.expect_err("Transaction should fail on expired handle");
assert_eq!(err, PleaseError::Expired);
}
}