inital commit
This commit is contained in:
commit
944715d610
42 changed files with 3176 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
target/
|
1105
Cargo.lock
generated
Normal file
1105
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
30
Cargo.toml
Normal file
30
Cargo.toml
Normal 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
25
README.md
Normal 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
11
examples/web/.env.example
Normal 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
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
30
examples/web/Cargo.toml
Normal 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
8
examples/web/Migra.toml
Normal file
|
@ -0,0 +1,8 @@
|
|||
root = "database"
|
||||
|
||||
[database]
|
||||
connection = "$DATABASE_URL"
|
||||
|
||||
[migrations]
|
||||
directory = "migrations"
|
||||
table_name = "migrations"
|
0
examples/web/database/initdb.d/.gitkeep
Normal file
0
examples/web/database/initdb.d/.gitkeep
Normal file
5
examples/web/database/initdb.d/schema.sql
Normal file
5
examples/web/database/initdb.d/schema.sql
Normal file
|
@ -0,0 +1,5 @@
|
|||
|
||||
create table lists (
|
||||
id serial primary key,
|
||||
name text not null
|
||||
);
|
5
examples/web/database/schema.sql
Normal file
5
examples/web/database/schema.sql
Normal file
|
@ -0,0 +1,5 @@
|
|||
|
||||
create table lists (
|
||||
id serial primary key,
|
||||
name text not null
|
||||
);
|
13
examples/web/docker-compose.dev.yml
Normal file
13
examples/web/docker-compose.dev.yml
Normal 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
|
8
examples/web/src/app/list/_mocks.rs
Normal file
8
examples/web/src/app/list/_mocks.rs
Normal file
|
@ -0,0 +1,8 @@
|
|||
use super::List;
|
||||
|
||||
pub fn create_list_1_mock() -> List {
|
||||
List {
|
||||
id: 1,
|
||||
name: String::from("My first list"),
|
||||
}
|
||||
}
|
32
examples/web/src/app/list/controller.rs
Normal file
32
examples/web/src/app/list/controller.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
}
|
14
examples/web/src/app/list/mod.rs
Normal file
14
examples/web/src/app/list/mod.rs
Normal 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,
|
||||
}
|
33
examples/web/src/app/list/service.rs
Normal file
33
examples/web/src/app/list/service.rs
Normal 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)
|
||||
}
|
||||
}
|
10
examples/web/src/app/list/storage_type.rs
Normal file
10
examples/web/src/app/list/storage_type.rs
Normal 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>>;
|
||||
}
|
1
examples/web/src/app/mod.rs
Normal file
1
examples/web/src/app/mod.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub mod list;
|
47
examples/web/src/config.rs
Normal file
47
examples/web/src/config.rs
Normal 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");
|
||||
}
|
21
examples/web/src/db/list/mod.rs
Normal file
21
examples/web/src/db/list/mod.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
29
examples/web/src/db/list/storage.rs
Normal file
29
examples/web/src/db/list/storage.rs
Normal 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)
|
||||
}
|
||||
}
|
2
examples/web/src/db/mod.rs
Normal file
2
examples/web/src/db/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub mod list;
|
||||
pub mod persistence;
|
35
examples/web/src/db/persistence.rs
Normal file
35
examples/web/src/db/persistence.rs
Normal 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
65
examples/web/src/error.rs
Normal 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
17
examples/web/src/lib.rs
Normal 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
10
examples/web/src/main.rs
Normal 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(())
|
||||
}
|
10
examples/web/src/rest/context.rs
Normal file
10
examples/web/src/rest/context.rs
Normal 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>,
|
||||
}
|
6
examples/web/src/rest/mod.rs
Normal file
6
examples/web/src/rest/mod.rs
Normal file
|
@ -0,0 +1,6 @@
|
|||
pub mod context;
|
||||
pub mod prelude;
|
||||
pub mod routes;
|
||||
pub mod server;
|
||||
pub mod server_utils;
|
||||
pub mod types;
|
6
examples/web/src/rest/prelude.rs
Normal file
6
examples/web/src/rest/prelude.rs
Normal 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,
|
||||
};
|
32
examples/web/src/rest/routes/api/list.rs
Normal file
32
examples/web/src/rest/routes/api/list.rs
Normal 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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
33
examples/web/src/rest/routes/api/mod.rs
Normal file
33
examples/web/src/rest/routes/api/mod.rs
Normal 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)
|
||||
}
|
||||
}
|
16
examples/web/src/rest/routes/mod.rs
Normal file
16
examples/web/src/rest/routes/mod.rs
Normal 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>;
|
||||
}
|
44
examples/web/src/rest/routes/root.rs
Normal file
44
examples/web/src/rest/routes/root.rs
Normal 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)
|
||||
}
|
||||
}
|
95
examples/web/src/rest/server.rs
Normal file
95
examples/web/src/rest/server.rs
Normal 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)
|
||||
}
|
65
examples/web/src/rest/server_utils.rs
Normal file
65
examples/web/src/rest/server_utils.rs
Normal 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)
|
||||
}
|
64
examples/web/src/rest/types.rs
Normal file
64
examples/web/src/rest/types.rs
Normal 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
14
src/asyn.rs
Normal 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
52
src/bb8_postgres.rs
Normal 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
36
src/error.rs
Normal 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
23
src/lib.rs
Normal 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
51
src/r2d2_postgres.rs
Normal 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
13
src/syn.rs
Normal 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;
|
||||
}
|
Reference in a new issue