inital commit

This commit is contained in:
Dmitriy Pleshevskiy 2021-10-12 18:14:02 +03:00
commit 944715d610
42 changed files with 3176 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
target/

1105
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

30
Cargo.toml Normal file
View file

@ -0,0 +1,30 @@
[package]
name = "ood_persistence"
version = "0.1.0"
edition = "2018"
authors = ["Dmitriy Pleshevskiy <dmitriy@ideascup.me>"]
repository = "https://github.com/pleshevskiy/ood_persistence"
description = "Asynchronous and synchronous interfaces and persistence implementations for your OOD architecture"
keywords = ["objected", "design", "architecture", "interface", "implementation"]
categories = ["rust-patterns", "database", "database-implementations"]
license = "MIT OR Apache-2.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
async = ["async-trait"]
sync = []
bb8_postgres = ["async", "bb8", "bb8-postgres"]
r2d2_postgres = ["sync", "r2d2", "r2d2-postgres"]
[dependencies]
async-trait = { version = "0.1", optional = true }
bb8 = { version = "0.7", optional = true }
bb8-postgres = { version = "0.7", optional = true }
r2d2 = { version = "0.8", optional = true }
r2d2-postgres = { package = "r2d2_postgres", version = "0.18", optional = true }
[workspace]
members = ["examples/*"]

25
README.md Normal file
View file

@ -0,0 +1,25 @@
# OOD Persistence
Asynchronous and synchronous interfaces and persistence implementations for your OOD architecture
## Installation
Add `ood_persistence = { version = "0", features = ["<IMPLEMENTATION_NAME>"] }` as a dependency in `Cargo.toml`.
NOTE: change `<IMPLEMENTATION_NOTE>` to feature name from available list. See `Cargo.toml` for more information.
`Cargo.toml` example:
```toml
[package]
name = "my-crate"
version = "0.1.0"
authors = ["Me <user@rust-lang.org>"]
[dependencies]
ood_persistence = { version = "0", features = ["bb8_postgres"] }
```
## Usage
See examples directory.

11
examples/web/.env.example Normal file
View file

@ -0,0 +1,11 @@
RUST_BACKTRACE="1"
RUST_LOG="debug"
POSTGRES_PASSWORD="test"
POSTGRES_USER="postgres"
POSTGRES_DB="x"
DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5577/${POSTGRES_DB}"
DATABASE_POOL_MAX_SIZE=15
SERVER_PORT=32444
FEATURE_CORS=true

1059
examples/web/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

30
examples/web/Cargo.toml Normal file
View file

@ -0,0 +1,30 @@
[package]
name = "web_example"
version = "0.1.0"
edition = "2018"
publish = false
[dependencies]
# configuration
log = "0.4"
env_logger = "0.7"
itconfig = { version = "1.1", features = ["macro"] }
lazy_static = "1.4"
# for local development
dotenv = { version = "0.15", optional = true }
async-trait = "0.1"
# database
ood_persistence = { path = "../../", features = ["bb8_postgres"] }
postgres-types = { version = "0.2", features = ["derive"] }
# runtime
tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread", "signal"] }
# server
hyper = { version = "0.14", features = ["server", "http1", "runtime"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
[features]
dev = ["dotenv"]

8
examples/web/Migra.toml Normal file
View file

@ -0,0 +1,8 @@
root = "database"
[database]
connection = "$DATABASE_URL"
[migrations]
directory = "migrations"
table_name = "migrations"

View file

View file

@ -0,0 +1,5 @@
create table lists (
id serial primary key,
name text not null
);

View file

@ -0,0 +1,5 @@
create table lists (
id serial primary key,
name text not null
);

View file

@ -0,0 +1,13 @@
version: '3'
services:
postgresql:
image: postgres:12.3-alpine
ports:
- 5577:5432
volumes:
- ./database/initdb.d:/docker-entrypoint-initdb.d
environment:
POSTGRES_PASSWORD: test
POSTGRES_USER: postgres
POSTGRES_DB: x

View file

@ -0,0 +1,8 @@
use super::List;
pub fn create_list_1_mock() -> List {
List {
id: 1,
name: String::from("My first list"),
}
}

View file

@ -0,0 +1,32 @@
use super::service::{create_postgres_list_service, ListService};
use super::{List, ListId};
use crate::db::persistence::PersistencePool;
use crate::db::persistence::PostgresPersistence;
use crate::error::ApiResult;
pub fn create_postgres_list_controller(
persistence: PostgresPersistence,
) -> ListController<PostgresPersistence> {
ListController {
list_service: create_postgres_list_service(persistence),
}
}
pub struct ListController<P>
where
P: PersistencePool,
{
list_service: ListService<P>,
}
impl<P> ListController<P>
where
P: PersistencePool,
{
pub async fn get_list_opt(&self, list_id: Option<ListId>) -> ApiResult<Option<List>> {
match list_id {
Some(list_id) => self.list_service.get_list_opt(list_id).await,
_ => Ok(None),
}
}
}

View file

@ -0,0 +1,14 @@
#[cfg(test)]
pub mod _mocks;
pub mod controller;
pub mod service;
pub mod storage_type;
pub type ListId = i32;
#[derive(Debug, PartialEq, Eq, Serialize)]
pub struct List {
pub id: ListId,
pub name: String,
}

View file

@ -0,0 +1,33 @@
use super::storage_type::ListStorage;
use super::{List, ListId};
use crate::db::list::storage::PostgresListStorage;
use crate::db::persistence::{PersistencePool, PostgresPersistence};
use crate::error::ApiResult;
pub fn create_postgres_list_service(
persistence: PostgresPersistence,
) -> ListService<PostgresPersistence> {
ListService {
persistence,
list_storage: Box::new(PostgresListStorage {}),
}
}
pub struct ListService<P>
where
P: PersistencePool,
{
persistence: P,
list_storage: Box<dyn ListStorage<P::Conn>>,
}
impl<P> ListService<P>
where
P: PersistencePool,
{
pub async fn get_list_opt(&self, list_id: ListId) -> ApiResult<Option<List>> {
let mut conn = self.persistence.get_connection().await?;
let list = self.list_storage.get_list_opt(&mut conn, list_id).await?;
Ok(list)
}
}

View file

@ -0,0 +1,10 @@
use super::{List, ListId};
use crate::db::persistence::{ConnectionClient, QueryResult};
#[async_trait]
pub trait ListStorage<Conn>: Send + Sync
where
Conn: ConnectionClient,
{
async fn get_list_opt(&self, conn: &mut Conn, id: ListId) -> QueryResult<Option<List>>;
}

View file

@ -0,0 +1 @@
pub mod list;

View file

@ -0,0 +1,47 @@
#![allow(non_snake_case)]
itconfig::config! {
#![config(unwrap)]
// RUST_BACKTRACE => "1",
RUST_LOG => "error",
database {
URL,
pool {
MAX_SIZE: u32 => 15,
}
}
server {
PORT: u16 => 8000,
},
feature {
static CORS: bool => false,
}
}
/// Util for configure application via env variables.
///
/// * Reads .env file for configure application (only for `dev` feature)
/// * Enables log macros via `env_logger` (See: https://docs.rs/env_logger)
/// * Initializes env config (See: https://docs.rs/itconfig)
///
/// Note: When enabled `dev` feature, this function try to load .env file
/// for configure application. If .env file cannot read will panic.
pub fn load_env_config() {
#[cfg(feature = "dev")]
dotenv::dotenv().expect("Cannot load .env file");
init();
env_logger::init();
#[cfg(feature = "dev")]
debug!("Env variables from .env file loaded successfully");
debug!("Env configuration loaded successfully");
}

View file

@ -0,0 +1,21 @@
use crate::app::list::List;
pub mod storage;
pub type DbListId = i32;
#[derive(Debug, FromSql)]
#[postgres(name = "lists")]
struct DbList {
pub id: DbListId,
pub name: String,
}
impl From<DbList> for List {
fn from(db: DbList) -> Self {
Self {
id: db.id,
name: db.name,
}
}
}

View file

@ -0,0 +1,29 @@
use super::DbList;
use crate::app::list::storage_type::ListStorage;
use crate::app::list::{List, ListId};
use crate::db::persistence::{try_get_one, ConnectionClient, PostgresConnection, QueryResult};
use postgres_types::Type;
pub struct PostgresListStorage {}
#[async_trait]
impl<'c> ListStorage<PostgresConnection<'c>> for PostgresListStorage {
async fn get_list_opt(
&self,
conn: &mut PostgresConnection<'c>,
list_id: ListId,
) -> QueryResult<Option<List>> {
let inner_conn = conn.inner();
let stmt = inner_conn
.prepare_typed("select l from lists as l where l.id = $1", &[Type::INT4])
.await?;
inner_conn
.query_opt(&stmt, &[&list_id])
.await?
.map(try_get_one::<DbList, _>)
.transpose()
.map_err(From::from)
}
}

View file

@ -0,0 +1,2 @@
pub mod list;
pub mod persistence;

View file

@ -0,0 +1,35 @@
use crate::config;
use ood_persistence::bb8_postgres::{tokio_postgres, NoTlsManager};
pub use ood_persistence::bb8_postgres::{
NoTlsConnection as PostgresConnection, NoTlsPersistence as PostgresPersistence,
NoTlsPool as PostgresPool,
};
pub use ood_persistence::{
asyn::{ConnectionClient, PersistencePool},
error::Result as QueryResult,
};
pub async fn create_postgres_pool() -> PostgresPool {
let db_conn_config = config::database::URL()
.parse()
.expect("Failed to convert database url to database config");
let manager = NoTlsManager::new(db_conn_config, tokio_postgres::NoTls);
let pool = PostgresPool::builder()
.max_size(config::database::pool::MAX_SIZE())
.build(manager)
.await
.expect("Failed to create database pool");
debug!("Created database DatabaseConnection pool successfully");
pool
}
pub fn try_get_one<Db, App>(row: tokio_postgres::Row) -> Result<App, tokio_postgres::Error>
where
Db: postgres_types::FromSqlOwned,
App: From<Db>,
{
row.try_get(0).map(From::<Db>::from)
}

65
examples/web/src/error.rs Normal file
View file

@ -0,0 +1,65 @@
use ood_persistence::error::PersistenceError;
use std::error;
use std::fmt;
pub type SyncStdError = Box<dyn error::Error + Send + Sync + 'static>;
pub type StdResult<T> = Result<T, SyncStdError>;
pub type ApiResult<T> = Result<T, Error>;
#[derive(Debug)]
pub enum Error {
PersistenceError(PersistenceError),
Rest(RestKind),
Serde(serde_json::Error),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::PersistenceError(err) => write!(f, "{}", err),
Self::Rest(err) => write!(f, "{}", err),
Self::Serde(err) => write!(f, "{}", err),
}
}
}
impl std::error::Error for Error {}
impl From<PersistenceError> for Error {
fn from(err: PersistenceError) -> Self {
Self::PersistenceError(err)
}
}
impl From<hyper::Error> for Error {
fn from(err: hyper::Error) -> Self {
Self::Rest(RestKind::Hyper(err))
}
}
impl From<hyper::http::Error> for Error {
fn from(err: hyper::http::Error) -> Self {
Self::Rest(RestKind::HyperHttp(err))
}
}
impl From<serde_json::Error> for Error {
fn from(err: serde_json::Error) -> Self {
Self::Serde(err)
}
}
#[derive(Debug)]
pub enum RestKind {
Hyper(hyper::Error),
HyperHttp(hyper::http::Error),
}
impl fmt::Display for RestKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Hyper(err) => write!(f, "{}", err),
Self::HyperHttp(err) => write!(f, "{}", err),
}
}
}

17
examples/web/src/lib.rs Normal file
View file

@ -0,0 +1,17 @@
#![allow(dead_code)]
#[macro_use]
extern crate postgres_types;
#[macro_use]
extern crate log;
#[macro_use]
extern crate async_trait;
#[macro_use]
extern crate serde;
pub mod config;
pub mod error;
mod app;
mod db;
pub mod rest;

10
examples/web/src/main.rs Normal file
View file

@ -0,0 +1,10 @@
use web_example::config::load_env_config;
use web_example::error::StdResult;
use web_example::rest::server::start_server;
#[tokio::main]
async fn main() -> StdResult<()> {
load_env_config();
start_server().await?;
Ok(())
}

View file

@ -0,0 +1,10 @@
use crate::db::persistence::{PostgresPersistence, PostgresPool};
pub struct RestGlobalContext {
pub pool: PostgresPool,
}
#[derive(Clone)]
pub struct RestReqContext<'p> {
pub persistence: PostgresPersistence<'p>,
}

View file

@ -0,0 +1,6 @@
pub mod context;
pub mod prelude;
pub mod routes;
pub mod server;
pub mod server_utils;
pub mod types;

View file

@ -0,0 +1,6 @@
pub use super::types::{QueryParams, ReqVariables, RestResponseData, RestResult};
pub use crate::error::{ApiResult, StdResult};
pub use hyper::{
header::{self, HeaderValue},
Body, Method, Request, Response, StatusCode,
};

View file

@ -0,0 +1,32 @@
use crate::app::list::controller::create_postgres_list_controller;
use crate::rest::routes::*;
use crate::rest::server_utils::{create_not_found_err_json_response, create_ok_json_response};
pub enum Router {
GetListById(String),
}
impl MaybeFrom<RouteParts<'_>> for Router {
fn maybe_from((method, uri_path_parts): RouteParts<'_>) -> Option<Self> {
match (method, uri_path_parts) {
(&Method::GET, [list_id]) => Some(Self::GetListById(list_id.to_string())),
_ => None,
}
}
}
#[async_trait]
impl Resolver for Router {
async fn resolve(&self, ctx: RestReqContext<'_>, vars: ReqVariables<'_>) -> RestResult {
let controller = create_postgres_list_controller(ctx.persistence);
match self {
Self::GetListById(list_id) => {
let res = controller.get_list_opt(list_id.parse().ok()).await?;
match res {
Some(list) => create_ok_json_response(list),
None => create_not_found_err_json_response("List not found"),
}
}
}
}
}

View file

@ -0,0 +1,33 @@
use crate::rest::routes::*;
mod list;
pub enum Router {
List(list::Router),
}
impl MaybeFrom<RouteParts<'_>> for Router {
fn maybe_from((method, uri_path_parts): RouteParts<'_>) -> Option<Self> {
let rest_parts = &uri_path_parts[1..];
uri_path_parts.get(0).copied().and_then(|part| match part {
"lists" => list::Router::maybe_from((method, rest_parts)).map(Self::List),
_ => None,
})
}
}
#[async_trait]
impl Resolver for Router {
async fn resolve(&self, ctx: RestReqContext<'_>, vars: ReqVariables<'_>) -> RestResult {
let mut res = match self {
Self::List(router) => router.resolve(ctx, vars).await?,
};
res.headers_mut().append(
header::CONTENT_TYPE,
HeaderValue::from_static("application/json; charset=utf-8"),
);
Ok(res)
}
}

View file

@ -0,0 +1,16 @@
use crate::rest::context::RestReqContext;
use crate::rest::prelude::*;
mod api;
pub mod root;
#[async_trait]
pub trait Resolver {
async fn resolve(&self, ctx: RestReqContext<'_>, vars: ReqVariables<'_>) -> RestResult;
}
type RouteParts<'a> = (&'a Method, &'a [&'a str]);
trait MaybeFrom<T>: Sized {
fn maybe_from(_: T) -> Option<Self>;
}

View file

@ -0,0 +1,44 @@
use crate::config;
use crate::rest::routes::*;
#[non_exhaustive]
pub enum Router {
#[allow(clippy::upper_case_acronyms)]
CORS,
HealthCheck,
NotFound,
Api(api::Router),
}
impl From<RouteParts<'_>> for Router {
fn from((method, uri_path_parts): RouteParts<'_>) -> Self {
match (method, uri_path_parts) {
(&Method::OPTIONS, _) if config::feature::CORS() => Self::CORS,
(_, &["api", ..]) => api::Router::maybe_from((method, &uri_path_parts[1..]))
.map(Self::Api)
.unwrap_or(Self::NotFound),
(&Method::GET, &["health"]) => Self::HealthCheck,
_ => Self::NotFound,
}
}
}
#[async_trait]
impl Resolver for Router {
async fn resolve(&self, ctx: RestReqContext<'_>, vars: ReqVariables<'_>) -> RestResult {
let res = match self {
Self::CORS => Response::builder()
.status(StatusCode::OK)
.body(Body::empty())?,
Self::Api(route) => route.resolve(ctx, vars).await?,
Self::HealthCheck => Response::builder()
.status(StatusCode::OK)
.body(Body::from("Ok"))?,
Self::NotFound => Response::builder()
.status(StatusCode::NOT_FOUND)
.body(Body::from("Not Found"))?,
};
Ok(res)
}
}

View file

@ -0,0 +1,95 @@
use super::server_utils;
use crate::config;
use crate::db::persistence::create_postgres_pool;
use crate::error::SyncStdError;
use crate::rest::context::{RestGlobalContext, RestReqContext};
use crate::rest::prelude::*;
use crate::rest::routes::{self, Resolver};
use crate::rest::types::REST_INTERNAL_SERVER_ERROR;
use hyper::service::{make_service_fn, service_fn};
use hyper::Server;
use std::sync::Arc;
/// Waits for the Ctrl+C signal for graceful shutdown backend
async fn shutdown_signal() {
tokio::signal::ctrl_c()
.await
.expect("failed to install CTRL+C signal handler");
}
pub async fn start_server() -> StdResult<()> {
let pool = create_postgres_pool().await;
// let persistence = ood_persistence::bb8_postgres::new(&pool);
let context = Arc::new(RestGlobalContext { pool });
let new_service = make_service_fn(move |_| {
let context = context.clone();
async {
Ok::<_, SyncStdError>(service_fn(move |req| process_request(req, context.clone())))
}
});
let port = config::server::PORT();
let addr = ([0, 0, 0, 0], port).into();
let server = Server::bind(&addr)
.serve(new_service)
.with_graceful_shutdown(shutdown_signal());
info!("🚀 Server listening on http://localhost:{}", port);
server.await?;
Ok(())
}
fn split_request_uri_path(uri_path: &str) -> Vec<&str> {
uri_path
.split('/')
.filter(|part| !part.is_empty())
.collect()
}
async fn process_request(
req: Request<Body>,
context: Arc<RestGlobalContext>,
) -> StdResult<Response<Body>> {
let (req_parts, req_body) = req.into_parts();
let query_params = server_utils::get_query_params(req_parts.uri.query());
let req_variables = ReqVariables::new(req_body, query_params);
let method = &req_parts.method;
let uri_path_parts = &split_request_uri_path(req_parts.uri.path())[..];
let req_context = RestReqContext {
persistence: ood_persistence::bb8_postgres::new(&context.pool),
};
let route = routes::root::Router::from((method, uri_path_parts));
let mut res = match route.resolve(req_context, req_variables).await {
Err(_) => server_utils::create_json_response(
StatusCode::INTERNAL_SERVER_ERROR,
RestResponseData::<serde_json::Value>::error(REST_INTERNAL_SERVER_ERROR),
)
// TODO(pleshevskiy): investigate why `Send` is not implemented
.unwrap(),
Ok(res) => res,
};
if config::feature::CORS() {
let headers = res.headers_mut();
headers.insert(
header::ACCESS_CONTROL_ALLOW_ORIGIN,
HeaderValue::from_static("*"),
);
headers.insert(
header::ACCESS_CONTROL_ALLOW_METHODS,
HeaderValue::from_static("HEAD, GET, POST, PUT, PATCH"),
);
headers.insert(
header::ACCESS_CONTROL_ALLOW_HEADERS,
HeaderValue::from_static("Authorization, Content-Type"),
);
}
Ok(res)
}

View file

@ -0,0 +1,65 @@
use crate::error::StdResult;
use crate::rest::prelude::*;
use serde::{de, ser};
pub async fn deserialize_request_body<T>(req_body: Body) -> StdResult<T>
where
T: de::DeserializeOwned,
{
let body_bytes = hyper::body::to_bytes(req_body).await?;
serde_json::from_slice(&body_bytes).map_err(From::from)
}
pub fn serialize_response<T>(res: Response<T>) -> RestResult
where
T: ser::Serialize,
{
let (parts, body) = res.into_parts();
let body = serde_json::to_vec(&body)?;
Ok(Response::from_parts(parts, Body::from(body)))
}
pub fn get_query_params(req_query: Option<&str>) -> QueryParams<'_> {
req_query
.map(|query| {
query
.split('&')
.into_iter()
.filter_map(|param| {
let mut parts = param.split('=');
if let (Some(key), Some(value)) = (parts.next(), parts.next()) {
Some((key, value))
} else {
None
}
})
.collect::<QueryParams<'_>>()
})
.unwrap_or_default()
}
pub fn create_not_found_err_json_response(message: &'static str) -> RestResult {
create_err_json_response(StatusCode::NOT_FOUND, message)
}
pub fn create_err_json_response(status: StatusCode, message: &'static str) -> RestResult {
create_json_response::<serde_json::Value>(status, RestResponseData::simple_error(message))
}
pub fn create_ok_json_response<Data: ser::Serialize>(body: Data) -> RestResult {
create_json_response(StatusCode::OK, RestResponseData::new(body))
}
pub fn create_json_response<Data: ser::Serialize>(
status: StatusCode,
body: RestResponseData<Data>,
) -> RestResult {
let res = Response::builder()
.status(status)
.header(
header::CONTENT_TYPE,
HeaderValue::from_static("application/json; charset=utf-8"),
)
.body(body)?;
serialize_response(res)
}

View file

@ -0,0 +1,64 @@
use crate::rest::prelude::ApiResult;
use hyper::{Body, Response};
use serde::Serialize;
use std::collections::HashMap;
pub type RestResult = ApiResult<Response<Body>>;
pub type QueryParams<'a> = HashMap<&'a str, &'a str>;
#[derive(Debug)]
pub struct ReqVariables<'params> {
body: Body,
query_params: QueryParams<'params>,
}
impl<'params> ReqVariables<'params> {
pub fn new(body: Body, query_params: QueryParams<'params>) -> Self {
ReqVariables { body, query_params }
}
}
#[derive(Debug, Serialize)]
pub struct RestResponseData<Data: Serialize> {
data: Option<Data>,
error: Option<RestResponseError>,
}
impl<S: Serialize> Default for RestResponseData<S> {
fn default() -> Self {
Self {
data: None,
error: None,
}
}
}
impl<Data: Serialize> RestResponseData<Data> {
pub fn new(data: Data) -> Self {
Self {
data: Some(data),
..Default::default()
}
}
pub fn error(err: RestResponseError) -> Self {
Self {
error: Some(err),
..Default::default()
}
}
pub fn simple_error(message: &'static str) -> Self {
Self::error(RestResponseError { message })
}
}
#[derive(Debug, Serialize)]
pub struct RestResponseError {
message: &'static str,
}
pub const REST_INTERNAL_SERVER_ERROR: RestResponseError = RestResponseError {
message: "internal server error",
};

14
src/asyn.rs Normal file
View file

@ -0,0 +1,14 @@
use crate::error;
#[async_trait]
pub trait PersistencePool: Send + Sync {
type Conn: ConnectionClient;
async fn get_connection(&self) -> error::Result<Self::Conn>;
}
pub trait ConnectionClient {
type InnerConn;
fn inner(&mut self) -> &mut Self::InnerConn;
}

52
src/bb8_postgres.rs Normal file
View file

@ -0,0 +1,52 @@
use crate::asyn::{ConnectionClient, PersistencePool};
use crate::error;
pub use bb8::{Pool, PooledConnection};
pub use bb8_postgres::tokio_postgres;
pub use bb8_postgres::PostgresConnectionManager as Manager;
pub type NoTlsManager = Manager<tokio_postgres::NoTls>;
pub type NoTlsPersistence<'p> = Persistence<'p, NoTlsManager>;
pub type NoTlsConnection<'p> = Connection<'p, NoTlsManager>;
pub type NoTlsPool = Pool<NoTlsManager>;
pub type InnerConn<'p, M> = PooledConnection<'p, M>;
pub fn new<M>(pool: &Pool<M>) -> Persistence<M>
where
M: bb8::ManageConnection,
{
Persistence(pool)
}
#[derive(Clone)]
pub struct Persistence<'p, M: bb8::ManageConnection>(&'p Pool<M>);
#[async_trait]
impl<'p, M> PersistencePool for Persistence<'p, M>
where
M: bb8::ManageConnection + Send + Sync,
{
type Conn = Connection<'p, M>;
async fn get_connection(&self) -> error::Result<Self::Conn> {
self.0
.get()
.await
.map_err(|_| error::PersistenceError::GetConnection)
.map(Connection)
}
}
pub struct Connection<'p, M: bb8::ManageConnection>(InnerConn<'p, M>);
impl<'c, M> ConnectionClient for Connection<'c, M>
where
M: bb8::ManageConnection,
{
type InnerConn = InnerConn<'c, M>;
fn inner(&mut self) -> &mut Self::InnerConn {
&mut self.0
}
}

36
src/error.rs Normal file
View file

@ -0,0 +1,36 @@
use std::error;
use std::fmt;
pub type Result<T> = std::result::Result<T, PersistenceError>;
#[derive(Debug)]
pub enum PersistenceError {
GetConnection,
UpgradeToTransaction,
CommitTransaction,
RollbackTransaction,
DbError(Box<dyn std::error::Error>),
}
impl fmt::Display for PersistenceError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PersistenceError::GetConnection => f.write_str("Cannot get connection"),
PersistenceError::UpgradeToTransaction => {
f.write_str("Cannot upgrade connection to transaction")
}
PersistenceError::CommitTransaction => f.write_str("Cannot commit transaction"),
PersistenceError::RollbackTransaction => f.write_str("Cannot rollback transaction"),
PersistenceError::DbError(err) => write!(f, "DbError: {}", err),
}
}
}
impl error::Error for PersistenceError {}
#[cfg(feature = "bb8_postgres")]
impl From<bb8_postgres::tokio_postgres::Error> for PersistenceError {
fn from(err: bb8_postgres::tokio_postgres::Error) -> Self {
Self::DbError(Box::new(err))
}
}

23
src/lib.rs Normal file
View file

@ -0,0 +1,23 @@
#[cfg(feature = "async")]
#[macro_use]
extern crate async_trait;
#[cfg(feature = "async")]
pub mod asyn;
#[cfg(feature = "sync")]
pub mod syn;
#[cfg(feature = "bb8")]
pub use bb8;
#[cfg(feature = "bb8_postgres")]
pub mod bb8_postgres;
#[cfg(feature = "r2d2")]
pub use r2d2;
#[cfg(feature = "r2d2_postgres")]
pub mod r2d2_postgres;
pub mod error;

51
src/r2d2_postgres.rs Normal file
View file

@ -0,0 +1,51 @@
use crate::error;
use crate::syn::{ConnectionClient, PersistencePool};
pub use r2d2::{Pool, PooledConnection};
pub use r2d2_postgres::postgres;
pub use r2d2_postgres::PostgresConnectionManager as Manager;
pub type NoTlsManager = Manager<postgres::NoTls>;
pub type NoTlsPersistence<'p> = Persistence<'p, NoTlsManager>;
pub type NoTlsConnection<'p> = Connection<NoTlsManager>;
pub type NoTlsPool = Pool<NoTlsManager>;
pub type InnerConn<M> = PooledConnection<M>;
pub fn new<M>(pool: &Pool<M>) -> Persistence<M>
where
M: r2d2::ManageConnection,
{
Persistence(pool)
}
#[derive(Clone)]
pub struct Persistence<'p, M: r2d2::ManageConnection>(&'p Pool<M>);
#[async_trait]
impl<'p, M> PersistencePool for Persistence<'p, M>
where
M: r2d2::ManageConnection + Send + Sync,
{
type Conn = Connection<M>;
fn get_connection(&self) -> error::Result<Self::Conn> {
self.0
.get()
.map_err(|_| error::PersistenceError::GetConnection)
.map(Connection)
}
}
pub struct Connection<M: r2d2::ManageConnection>(InnerConn<M>);
impl<M> ConnectionClient for Connection<M>
where
M: r2d2::ManageConnection,
{
type InnerConn = InnerConn<M>;
fn inner(&mut self) -> &mut Self::InnerConn {
&mut self.0
}
}

13
src/syn.rs Normal file
View file

@ -0,0 +1,13 @@
use crate::error;
pub trait PersistencePool {
type Conn: ConnectionClient;
fn get_connection(&self) -> error::Result<Self::Conn>;
}
pub trait ConnectionClient {
type InnerConn;
fn inner(&mut self) -> &mut Self::InnerConn;
}