From 18eaee9b169599a49d86da764299067d9cfce03b Mon Sep 17 00:00:00 2001 From: Dmitriy Pleshevskiy Date: Sun, 17 Oct 2021 15:08:46 +0300 Subject: [PATCH] feat: add transactions Closes #1 --- .gitignore | 3 +- .vscode/settings.json | 3 + Cargo.lock | 2 +- Cargo.toml | 4 +- README.md | 3 + examples/web/Cargo.toml | 2 +- examples/web/Makefile.toml | 5 ++ examples/web/README.md | 47 +++++++++++++ examples/web/src/app/list/controller.rs | 9 +++ examples/web/src/app/list/service.rs | 13 +++- examples/web/src/app/list/storage_type.rs | 2 + examples/web/src/db/list/storage.rs | 25 ++++++- examples/web/src/db/persistence.rs | 4 +- examples/web/src/lib.rs | 2 +- examples/web/src/rest/routes/api/list.rs | 11 ++- examples/web/src/rest/server_utils.rs | 3 +- examples/web/src/rest/types.rs | 4 +- rust-toolchain.toml | 2 + src/asyn.rs | 15 ++++ src/bb8_postgres.rs | 84 +++++++++++++++++++---- src/lib.rs | 3 + src/r2d2_postgres.rs | 80 ++++++++++++++++----- src/syn.rs | 13 ++++ 23 files changed, 293 insertions(+), 46 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 examples/web/Makefile.toml create mode 100644 examples/web/README.md create mode 100644 rust-toolchain.toml diff --git a/.gitignore b/.gitignore index 9f97022..5f32e70 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -target/ \ No newline at end of file +target/ +.env \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..0ac6bc2 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "rust.unstable_features": true +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 23a08cb..a770c08 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -493,7 +493,7 @@ checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" [[package]] name = "ood_persistence" -version = "0.1.1" +version = "0.2.0" dependencies = [ "async-trait", "bb8", diff --git a/Cargo.toml b/Cargo.toml index c7dcf43..f65d08f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ood_persistence" -version = "0.1.1" +version = "0.2.0" edition = "2018" authors = ["Dmitriy Pleshevskiy "] repository = "https://github.com/pleshevskiy/ood_persistence" @@ -11,6 +11,8 @@ license = "MIT OR Apache-2.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] +nightly = [] + async = ["async-trait"] sync = [] diff --git a/README.md b/README.md index 8fdf354..9fa2e14 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,9 @@ authors = ["Me "] ood_persistence = { version = "0", features = ["bb8_postgres"] } ``` +In stable rust channel you can use only connection interface, but if you use nightly channel, add an additional +"nightly" feature to your `Cargo.toml` and you can use transactions as well. + ## Usage See examples directory. diff --git a/examples/web/Cargo.toml b/examples/web/Cargo.toml index 4cb3558..56d93dc 100644 --- a/examples/web/Cargo.toml +++ b/examples/web/Cargo.toml @@ -15,7 +15,7 @@ dotenv = { version = "0.15", optional = true } async-trait = "0.1" # database -ood_persistence = { path = "../../", features = ["bb8_postgres"] } +ood_persistence = { path = "../../", features = ["nightly", "bb8_postgres"] } postgres-types = { version = "0.2", features = ["derive"] } # runtime diff --git a/examples/web/Makefile.toml b/examples/web/Makefile.toml new file mode 100644 index 0000000..ffba39b --- /dev/null +++ b/examples/web/Makefile.toml @@ -0,0 +1,5 @@ +[tasks.dev] +command = "cargo" +workspace = false +args = ["run", "--features", "dev"] +watch = { watch = ["src", "Cargo.toml", '.env'] } diff --git a/examples/web/README.md b/examples/web/README.md new file mode 100644 index 0000000..7551e19 --- /dev/null +++ b/examples/web/README.md @@ -0,0 +1,47 @@ +# Web example + +Simple rest api example with hyper, bb8, postgres + +## Deps + +For this example you need to install [docker] with [docker-compose], [nightly rust]. Follow the instructions on the official sites. + +[docker]: https://docs.docker.com/get-docker/ +[docker-compose]: https://docs.docker.com/compose/install/ +[nightly rust]: https://www.rust-lang.org/tools/install + +## Running + +Move to the example directory + +```sh +cd examples/web +``` + +Run configuration for docker-compose + +```sh +docker-compose -f docker-compose.dev.yml up +``` + +Or run postgres server manually. + +Then copy `.env.example` to `.env` and edit if you needed. + +```sh +cp .env.example .env +``` + +Now you can run server + +```sh +cargo run --features dev +``` + +Or if you have a [cargo make] + +```sh +cargo make dev +``` + +[cargo make]: https://github.com/sagiegurari/cargo-make diff --git a/examples/web/src/app/list/controller.rs b/examples/web/src/app/list/controller.rs index d7bb2d9..6c5b0bd 100644 --- a/examples/web/src/app/list/controller.rs +++ b/examples/web/src/app/list/controller.rs @@ -12,6 +12,11 @@ pub fn create_postgres_list_controller( } } +#[derive(Debug, Deserialize)] +pub struct AddListInput { + name: String, +} + pub struct ListController

where P: PersistencePool, @@ -29,4 +34,8 @@ where _ => Ok(None), } } + + pub async fn add_list(&self, input: AddListInput) -> ApiResult { + self.list_service.add_list(&input.name).await + } } diff --git a/examples/web/src/app/list/service.rs b/examples/web/src/app/list/service.rs index fa30ce0..288dc56 100644 --- a/examples/web/src/app/list/service.rs +++ b/examples/web/src/app/list/service.rs @@ -1,7 +1,9 @@ use super::storage_type::ListStorage; use super::{List, ListId}; use crate::db::list::storage::PostgresListStorage; -use crate::db::persistence::{PersistencePool, PostgresPersistence}; +use crate::db::persistence::{ + ConnectionClient, PersistencePool, PostgresPersistence, TransactionClient, +}; use crate::error::ApiResult; pub fn create_postgres_list_service( @@ -30,4 +32,13 @@ where let list = self.list_storage.get_list_opt(&mut conn, list_id).await?; Ok(list) } + + pub async fn add_list(&self, name: &str) -> ApiResult { + let mut conn = self.persistence.get_connection().await?; + + let mut trx = conn.start_transaction().await?; + let list = self.list_storage.add_list(&mut trx, name).await?; + trx.commit().await?; + Ok(list) + } } diff --git a/examples/web/src/app/list/storage_type.rs b/examples/web/src/app/list/storage_type.rs index 29d5a09..eedd235 100644 --- a/examples/web/src/app/list/storage_type.rs +++ b/examples/web/src/app/list/storage_type.rs @@ -7,4 +7,6 @@ where Conn: ConnectionClient, { async fn get_list_opt(&self, conn: &mut Conn, id: ListId) -> QueryResult>; + + async fn add_list(&self, conn: &mut Conn::Trx<'_>, name: &str) -> QueryResult; } diff --git a/examples/web/src/db/list/storage.rs b/examples/web/src/db/list/storage.rs index c7c7242..be495fa 100644 --- a/examples/web/src/db/list/storage.rs +++ b/examples/web/src/db/list/storage.rs @@ -1,16 +1,18 @@ 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 crate::db::persistence::{ + try_get_one, ConnectionClient, PostgresConnection, PostgresTransaction, QueryResult, +}; use postgres_types::Type; pub struct PostgresListStorage {} #[async_trait] -impl<'c> ListStorage> for PostgresListStorage { +impl<'p> ListStorage> for PostgresListStorage { async fn get_list_opt( &self, - conn: &mut PostgresConnection<'c>, + conn: &mut PostgresConnection<'p>, list_id: ListId, ) -> QueryResult> { let inner_conn = conn.inner(); @@ -26,4 +28,21 @@ impl<'c> ListStorage> for PostgresListStorage { .transpose() .map_err(From::from) } + + async fn add_list(&self, conn: &mut PostgresTransaction<'_>, name: &str) -> QueryResult { + let inner_conn = conn.inner(); + + let stmt = inner_conn + .prepare_typed( + "insert into lists as l (name) values ($1) returning l", + &[Type::TEXT], + ) + .await?; + + inner_conn + .query_one(&stmt, &[&name]) + .await + .and_then(try_get_one::) + .map_err(From::from) + } } diff --git a/examples/web/src/db/persistence.rs b/examples/web/src/db/persistence.rs index d3ae7ed..a7014fb 100644 --- a/examples/web/src/db/persistence.rs +++ b/examples/web/src/db/persistence.rs @@ -2,10 +2,10 @@ 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, + NoTlsPool as PostgresPool, Transaction as PostgresTransaction, }; pub use ood_persistence::{ - asyn::{ConnectionClient, PersistencePool}, + asyn::{ConnectionClient, PersistencePool, TransactionClient}, error::Result as QueryResult, }; diff --git a/examples/web/src/lib.rs b/examples/web/src/lib.rs index 6092d5a..9ed05f1 100644 --- a/examples/web/src/lib.rs +++ b/examples/web/src/lib.rs @@ -1,4 +1,4 @@ -#![allow(dead_code)] +#![deny(clippy::all)] #[macro_use] extern crate postgres_types; diff --git a/examples/web/src/rest/routes/api/list.rs b/examples/web/src/rest/routes/api/list.rs index 48d1dee..c231047 100644 --- a/examples/web/src/rest/routes/api/list.rs +++ b/examples/web/src/rest/routes/api/list.rs @@ -1,15 +1,19 @@ 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}; +use crate::rest::server_utils::{ + create_not_found_err_json_response, create_ok_json_response, deserialize_request_body, +}; pub enum Router { GetListById(String), + AddList, } impl MaybeFrom> for Router { fn maybe_from((method, uri_path_parts): RouteParts<'_>) -> Option { match (method, uri_path_parts) { (&Method::GET, [list_id]) => Some(Self::GetListById(list_id.to_string())), + (&Method::POST, []) => Some(Self::AddList), _ => None, } } @@ -27,6 +31,11 @@ impl Resolver for Router { None => create_not_found_err_json_response("List not found"), } } + Self::AddList => { + let input = deserialize_request_body(vars.body).await?; + let res = controller.add_list(input).await?; + create_ok_json_response(res) + } } } } diff --git a/examples/web/src/rest/server_utils.rs b/examples/web/src/rest/server_utils.rs index b21e27c..5504462 100644 --- a/examples/web/src/rest/server_utils.rs +++ b/examples/web/src/rest/server_utils.rs @@ -1,8 +1,7 @@ -use crate::error::StdResult; use crate::rest::prelude::*; use serde::{de, ser}; -pub async fn deserialize_request_body(req_body: Body) -> StdResult +pub async fn deserialize_request_body(req_body: Body) -> ApiResult where T: de::DeserializeOwned, { diff --git a/examples/web/src/rest/types.rs b/examples/web/src/rest/types.rs index d2aa9cf..1f39fb9 100644 --- a/examples/web/src/rest/types.rs +++ b/examples/web/src/rest/types.rs @@ -9,8 +9,8 @@ pub type QueryParams<'a> = HashMap<&'a str, &'a str>; #[derive(Debug)] pub struct ReqVariables<'params> { - body: Body, - query_params: QueryParams<'params>, + pub body: Body, + pub query_params: QueryParams<'params>, } impl<'params> ReqVariables<'params> { diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..271800c --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly" \ No newline at end of file diff --git a/src/asyn.rs b/src/asyn.rs index 52bf285..9495ab6 100644 --- a/src/asyn.rs +++ b/src/asyn.rs @@ -7,8 +7,23 @@ pub trait PersistencePool: Send + Sync { async fn get_connection(&self) -> error::Result; } +#[cfg_attr(feature = "nightly", async_trait)] pub trait ConnectionClient { type InnerConn; + #[cfg(feature = "nightly")] + type Trx<'t>: TransactionClient; + fn inner(&mut self) -> &mut Self::InnerConn; + + #[cfg(feature = "nightly")] + async fn start_transaction(&mut self) -> error::Result>; +} + +#[cfg(feature = "nightly")] +#[async_trait] +pub trait TransactionClient: ConnectionClient { + async fn commit(self) -> error::Result<()>; + + async fn rollback(self) -> error::Result<()>; } diff --git a/src/bb8_postgres.rs b/src/bb8_postgres.rs index 310a63e..0f2aa27 100644 --- a/src/bb8_postgres.rs +++ b/src/bb8_postgres.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "nightly")] +use crate::asyn::TransactionClient; use crate::asyn::{ConnectionClient, PersistencePool}; use crate::error; @@ -5,13 +7,15 @@ pub use bb8::{Pool, PooledConnection}; pub use bb8_postgres::tokio_postgres; pub use bb8_postgres::PostgresConnectionManager as Manager; +pub type InnerConn<'p, M> = PooledConnection<'p, M>; +pub type InnerTrx<'p> = tokio_postgres::Transaction<'p>; + pub type NoTlsManager = Manager; pub type NoTlsPersistence<'p> = Persistence<'p, NoTlsManager>; pub type NoTlsConnection<'p> = Connection<'p, NoTlsManager>; +pub type NoTlsInnerConn<'p> = InnerConn<'p, NoTlsManager>; pub type NoTlsPool = Pool; -pub type InnerConn<'p, M> = PooledConnection<'p, M>; - pub fn new(pool: &Pool) -> Persistence where M: bb8::ManageConnection, @@ -20,14 +24,13 @@ where } #[derive(Clone)] -pub struct Persistence<'p, M: bb8::ManageConnection>(&'p Pool); +pub struct Persistence<'p, M>(&'p Pool) +where + M: bb8::ManageConnection; #[async_trait] -impl<'p, M> PersistencePool for Persistence<'p, M> -where - M: bb8::ManageConnection + Send + Sync, -{ - type Conn = Connection<'p, M>; +impl<'p> PersistencePool for NoTlsPersistence<'p> { + type Conn = NoTlsConnection<'p>; async fn get_connection(&self) -> error::Result { self.0 @@ -38,15 +41,68 @@ where } } -pub struct Connection<'p, M: bb8::ManageConnection>(InnerConn<'p, M>); - -impl<'c, M> ConnectionClient for Connection<'c, M> +pub struct Connection<'p, M>(InnerConn<'p, M>) where - M: bb8::ManageConnection, -{ - type InnerConn = InnerConn<'c, M>; + M: bb8::ManageConnection; + +#[cfg_attr(feature = "nightly", async_trait)] +impl<'me> ConnectionClient for NoTlsConnection<'me> { + type InnerConn = NoTlsInnerConn<'me>; + + #[cfg(feature = "nightly")] + type Trx<'t> = Transaction<'t>; fn inner(&mut self) -> &mut Self::InnerConn { &mut self.0 } + + #[cfg(feature = "nightly")] + async fn start_transaction(&mut self) -> error::Result> { + self.0 + .transaction() + .await + .map_err(|_| error::PersistenceError::UpgradeToTransaction) + .map(Transaction) + } +} + +#[cfg(feature = "nightly")] +pub struct Transaction<'p>(InnerTrx<'p>); + +#[cfg(feature = "nightly")] +#[async_trait] +impl<'me> ConnectionClient for Transaction<'me> { + type InnerConn = InnerTrx<'me>; + + type Trx<'t> = Transaction<'t>; + + fn inner(&mut self) -> &mut Self::InnerConn { + &mut self.0 + } + + async fn start_transaction(&mut self) -> error::Result> { + self.0 + .transaction() + .await + .map_err(|_| error::PersistenceError::UpgradeToTransaction) + .map(Transaction) + } +} + +#[cfg(feature = "nightly")] +#[async_trait] +impl<'me> TransactionClient for Transaction<'me> { + async fn commit(self) -> error::Result<()> { + self.0 + .commit() + .await + .map_err(|_| error::PersistenceError::CommitTransaction) + } + + async fn rollback(self) -> error::Result<()> { + self.0 + .rollback() + .await + .map_err(|_| error::PersistenceError::RollbackTransaction) + } } diff --git a/src/lib.rs b/src/lib.rs index 59711f9..219ee62 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,6 @@ +#![deny(clippy::all)] +#![cfg_attr(feature = "nightly", feature(generic_associated_types))] + #[cfg(feature = "async")] #[macro_use] extern crate async_trait; diff --git a/src/r2d2_postgres.rs b/src/r2d2_postgres.rs index 13b98ef..a81c849 100644 --- a/src/r2d2_postgres.rs +++ b/src/r2d2_postgres.rs @@ -1,17 +1,21 @@ use crate::error; +#[cfg(feature = "nightly")] +use crate::syn::TransactionClient; use crate::syn::{ConnectionClient, PersistencePool}; pub use r2d2::{Pool, PooledConnection}; pub use r2d2_postgres::postgres; pub use r2d2_postgres::PostgresConnectionManager as Manager; +pub type InnerConn = PooledConnection; +pub type InnerTrx<'t> = postgres::Transaction<'t>; + pub type NoTlsManager = Manager; pub type NoTlsPersistence<'p> = Persistence<'p, NoTlsManager>; -pub type NoTlsConnection<'p> = Connection; +pub type NoTlsConnection = Connection; +pub type NoTlsInnerConn = InnerConn; pub type NoTlsPool = Pool; -pub type InnerConn = PooledConnection; - pub fn new(pool: &Pool) -> Persistence where M: r2d2::ManageConnection, @@ -20,14 +24,12 @@ where } #[derive(Clone)] -pub struct Persistence<'p, M: r2d2::ManageConnection>(&'p Pool); - -#[async_trait] -impl<'p, M> PersistencePool for Persistence<'p, M> +pub struct Persistence<'p, M>(&'p Pool) where - M: r2d2::ManageConnection + Send + Sync, -{ - type Conn = Connection; + M: r2d2::ManageConnection; + +impl<'p> PersistencePool for NoTlsPersistence<'p> { + type Conn = NoTlsConnection; fn get_connection(&self) -> error::Result { self.0 @@ -37,15 +39,61 @@ where } } -pub struct Connection(InnerConn); - -impl ConnectionClient for Connection +pub struct Connection(InnerConn) where - M: r2d2::ManageConnection, -{ - type InnerConn = InnerConn; + M: r2d2::ManageConnection; + +impl ConnectionClient for NoTlsConnection { + type InnerConn = NoTlsInnerConn; + + #[cfg(feature = "nightly")] + type Trx<'t> = Transaction<'t>; fn inner(&mut self) -> &mut Self::InnerConn { &mut self.0 } + + #[cfg(feature = "nightly")] + fn start_transaction(&mut self) -> error::Result> { + self.0 + .transaction() + .map_err(|_| error::PersistenceError::UpgradeToTransaction) + .map(Transaction) + } +} + +#[cfg(feature = "nightly")] +pub struct Transaction<'me>(InnerTrx<'me>); + +#[cfg(feature = "nightly")] +impl<'me> ConnectionClient for Transaction<'me> { + type InnerConn = InnerTrx<'me>; + + type Trx<'t> = Transaction<'t>; + + fn inner(&mut self) -> &mut Self::InnerConn { + &mut self.0 + } + + fn start_transaction(&mut self) -> error::Result> { + self.0 + .transaction() + .map_err(|_| error::PersistenceError::UpgradeToTransaction) + .map(Transaction) + } +} + +#[cfg(feature = "nightly")] +impl TransactionClient for Transaction<'_> { + fn commit(self) -> error::Result<()> { + self.0 + .commit() + .map_err(|_| error::PersistenceError::CommitTransaction) + } + + fn rollback(self) -> error::Result<()> { + self.0 + .rollback() + .map_err(|_| error::PersistenceError::RollbackTransaction) + } } diff --git a/src/syn.rs b/src/syn.rs index 461abf6..f0c006c 100644 --- a/src/syn.rs +++ b/src/syn.rs @@ -9,5 +9,18 @@ pub trait PersistencePool { pub trait ConnectionClient { type InnerConn; + #[cfg(feature = "nightly")] + type Trx<'t>: TransactionClient; + fn inner(&mut self) -> &mut Self::InnerConn; + + #[cfg(feature = "nightly")] + fn start_transaction(&mut self) -> error::Result>; +} + +#[cfg(feature = "nightly")] +pub trait TransactionClient: ConnectionClient { + fn commit(self) -> error::Result<()>; + + fn rollback(self) -> error::Result<()>; }