From 2366bff05fe435977106106b918bb7e5a1d057df Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 12 Mar 2021 15:44:19 -0800 Subject: [PATCH] Rearrange contrib database support modules. --- contrib/lib/src/databases.rs | 877 ------------------------ contrib/lib/src/databases/config.rs | 89 +++ contrib/lib/src/databases/connection.rs | 188 +++++ contrib/lib/src/databases/error.rs | 27 + contrib/lib/src/databases/mod.rs | 395 +++++++++++ contrib/lib/src/databases/poolable.rs | 195 ++++++ 6 files changed, 894 insertions(+), 877 deletions(-) delete mode 100644 contrib/lib/src/databases.rs create mode 100644 contrib/lib/src/databases/config.rs create mode 100644 contrib/lib/src/databases/connection.rs create mode 100644 contrib/lib/src/databases/error.rs create mode 100644 contrib/lib/src/databases/mod.rs create mode 100644 contrib/lib/src/databases/poolable.rs diff --git a/contrib/lib/src/databases.rs b/contrib/lib/src/databases.rs deleted file mode 100644 index eddbdfc5..00000000 --- a/contrib/lib/src/databases.rs +++ /dev/null @@ -1,877 +0,0 @@ -//! Traits, utilities, and a macro for easy database connection pooling. -//! -//! # Overview -//! -//! This module provides traits, utilities, and a procedural macro that allows -//! you to easily connect your Rocket application to databases through -//! connection pools. A _database connection pool_ is a data structure that -//! maintains active database connections for later use in the application. -//! This implementation of connection pooling support is based on -//! [`r2d2`] and exposes connections through [request guards]. Databases are -//! individually configured through Rocket's regular configuration mechanisms: a -//! `Rocket.toml` file, environment variables, or procedurally. -//! -//! Connecting your Rocket application to a database using this library occurs -//! in three simple steps: -//! -//! 1. Configure your databases in `Rocket.toml`. -//! (see [Configuration](#configuration)) -//! 2. Associate a request guard type and fairing with each database. -//! (see [Guard Types](#guard-types)) -//! 3. Use the request guard to retrieve a connection in a handler. -//! (see [Handlers](#handlers)) -//! -//! For a list of supported databases, see [Provided Databases](#provided). This -//! support can be easily extended by implementing the [`Poolable`] trait. See -//! [Extending](#extending) for more. -//! -//! ## Example -//! -//! Before using this library, the feature corresponding to your database type -//! in `rocket_contrib` must be enabled: -//! -//! ```toml -//! [dependencies.rocket_contrib] -//! version = "0.5.0-dev" -//! default-features = false -//! features = ["diesel_sqlite_pool"] -//! ``` -//! -//! See [Provided](#provided) for a list of supported database and their -//! associated feature name. -//! -//! In whichever configuration source you choose, configure a `databases` -//! dictionary with an internal dictionary for each database, here `sqlite_logs` -//! in a TOML source: -//! -//! ```toml -//! [global.databases] -//! sqlite_logs = { url = "/path/to/database.sqlite" } -//! ``` -//! -//! In your application's source code, one-time: -//! -//! ```rust -//! #[macro_use] extern crate rocket; -//! #[macro_use] extern crate rocket_contrib; -//! -//! # #[cfg(feature = "diesel_sqlite_pool")] -//! # mod test { -//! use rocket_contrib::databases::diesel; -//! -//! #[database("sqlite_logs")] -//! struct LogsDbConn(diesel::SqliteConnection); -//! -//! #[launch] -//! fn rocket() -> rocket::Rocket { -//! rocket::ignite().attach(LogsDbConn::fairing()) -//! } -//! # } fn main() {} -//! ``` -//! -//! Whenever a connection to the database is needed: -//! -//! ```rust -//! # #[macro_use] extern crate rocket; -//! # #[macro_use] extern crate rocket_contrib; -//! # -//! # #[cfg(feature = "diesel_sqlite_pool")] -//! # mod test { -//! # use rocket_contrib::databases::diesel; -//! # -//! # #[database("sqlite_logs")] -//! # struct LogsDbConn(diesel::SqliteConnection); -//! # -//! # type Logs = (); -//! # type Result = std::result::Result; -//! # -//! #[get("/logs/")] -//! async fn get_logs(conn: LogsDbConn, id: usize) -> Result { -//! # /* -//! conn.run(|c| Logs::by_id(c, id)).await -//! # */ -//! # Ok(()) -//! } -//! # } fn main() {} -//! ``` -//! -//! # Usage -//! -//! ## Configuration -//! -//! Databases can be configured as any other values. Using the default -//! configuration provider, either via `Rocket.toml` or environment variables. -//! You can also use a custom provider. -//! -//! ### `Rocket.toml` -//! -//! To configure a database via `Rocket.toml`, add a table for each database -//! to the `databases` table where the key is a name of your choice. The table -//! should have a `url` key and, optionally, a `pool_size` key. This looks as -//! follows: -//! -//! ```toml -//! # Option 1: -//! [global.databases] -//! sqlite_db = { url = "db.sqlite" } -//! -//! # Option 2: -//! [global.databases.my_db] -//! url = "mysql://root:root@localhost/my_db" -//! -//! # With a `pool_size` key: -//! [global.databases] -//! sqlite_db = { url = "db.sqlite", pool_size = 20 } -//! ``` -//! -//! The table _requires_ one key: -//! -//! * `url` - the URl to the database -//! -//! Additionally, all configurations accept the following _optional_ keys: -//! -//! * `pool_size` - the size of the pool, i.e., the number of connections to -//! pool (defaults to the configured number of workers * 2) -//! -//! Additional options may be required or supported by other adapters. -//! -//! ### Procedurally -//! -//! Databases can also be configured procedurally via `rocket::custom()`. -//! The example below does just this: -//! -//! ```rust -//! # #[cfg(feature = "diesel_sqlite_pool")] { -//! use rocket::figment::{value::{Map, Value}, util::map}; -//! -//! #[rocket::launch] -//! fn rocket() -> _ { -//! let db: Map<_, Value> = map! { -//! "url" => "db.sqlite".into(), -//! "pool_size" => 10.into() -//! }; -//! -//! let figment = rocket::Config::figment() -//! .merge(("databases", map!["my_db" => db])); -//! -//! rocket::custom(figment) -//! } -//! # rocket(); -//! # } -//! ``` -//! -//! ### Environment Variables -//! -//! Lastly, databases can be configured via environment variables by specifying -//! the `databases` table as detailed in the [Environment Variables -//! configuration -//! guide](https://rocket.rs/master/guide/configuration/#environment-variables): -//! -//! ```bash -//! ROCKET_DATABASES='{my_db={url="db.sqlite"}}' -//! ``` -//! -//! Multiple databases can be specified in the `ROCKET_DATABASES` environment variable -//! as well by comma separating them: -//! -//! ```bash -//! ROCKET_DATABASES='{my_db={url="db.sqlite"},my_pg_db={url="postgres://root:root@localhost/my_pg_db"}}' -//! ``` -//! -//! ## Guard Types -//! -//! Once a database has been configured, the `#[database]` attribute can be used -//! to tie a type in your application to a configured database. The database -//! attributes accepts a single string parameter that indicates the name of the -//! database. This corresponds to the database name set as the database's -//! configuration key. -//! -//! The macro generates a [`FromRequest`] implementation for the decorated type, -//! allowing the type to be used as a request guard. This implementation -//! retrieves a connection from the database pool or fails with a -//! `Status::ServiceUnavailable` if connecting to the database times out. -//! -//! The macro will also generate two inherent methods on the decorated type: -//! -//! * `fn fairing() -> impl Fairing` -//! -//! Returns a fairing that initializes the associated database connection -//! pool. -//! -//! * `async fn get_one(&Rocket) -> Option` -//! -//! Retrieves a connection wrapper from the configured pool. Returns `Some` -//! as long as `Self::fairing()` has been attached. -//! -//! The attribute can only be applied to unit-like structs with one type. The -//! internal type of the structure must implement [`Poolable`]. -//! -//! ```rust -//! # extern crate rocket; -//! # #[macro_use] extern crate rocket_contrib; -//! # #[cfg(feature = "diesel_sqlite_pool")] -//! # mod test { -//! use rocket_contrib::databases::diesel; -//! -//! #[database("my_db")] -//! struct MyDatabase(diesel::SqliteConnection); -//! # } -//! ``` -//! -//! Other databases can be used by specifying their respective [`Poolable`] -//! type: -//! -//! ```rust -//! # extern crate rocket; -//! # #[macro_use] extern crate rocket_contrib; -//! # #[cfg(feature = "postgres_pool")] -//! # mod test { -//! use rocket_contrib::databases::postgres; -//! -//! #[database("my_pg_db")] -//! struct MyPgDatabase(postgres::Client); -//! # } -//! ``` -//! -//! The fairing returned from the generated `fairing()` method _must_ be -//! attached for the request guard implementation to succeed. Putting the pieces -//! together, a use of the `#[database]` attribute looks as follows: -//! -//! ```rust -//! # #[macro_use] extern crate rocket; -//! # #[macro_use] extern crate rocket_contrib; -//! # -//! # #[cfg(feature = "diesel_sqlite_pool")] { -//! # use rocket::figment::{value::{Map, Value}, util::map}; -//! use rocket_contrib::databases::diesel; -//! -//! #[database("my_db")] -//! struct MyDatabase(diesel::SqliteConnection); -//! -//! #[launch] -//! fn rocket() -> _ { -//! # let db: Map<_, Value> = map![ -//! # "url" => "db.sqlite".into(), "pool_size" => 10.into() -//! # ]; -//! # let figment = rocket::Config::figment().merge(("databases", map!["my_db" => db])); -//! rocket::custom(figment).attach(MyDatabase::fairing()) -//! } -//! # } -//! ``` -//! -//! ## Handlers -//! -//! Finally, use your type as a request guard in a handler to retrieve a -//! connection wrapper for the database: -//! -//! ```rust -//! # #[macro_use] extern crate rocket; -//! # #[macro_use] extern crate rocket_contrib; -//! # -//! # #[cfg(feature = "diesel_sqlite_pool")] -//! # mod test { -//! # use rocket_contrib::databases::diesel; -//! #[database("my_db")] -//! struct MyDatabase(diesel::SqliteConnection); -//! -//! #[get("/")] -//! fn my_handler(conn: MyDatabase) { -//! // ... -//! } -//! # } -//! ``` -//! -//! A connection can be retrieved and used with the `run()` method: -//! -//! ```rust -//! # #[macro_use] extern crate rocket; -//! # #[macro_use] extern crate rocket_contrib; -//! # -//! # #[cfg(feature = "diesel_sqlite_pool")] -//! # mod test { -//! # use rocket_contrib::databases::diesel; -//! # type Data = (); -//! #[database("my_db")] -//! struct MyDatabase(diesel::SqliteConnection); -//! -//! fn load_from_db(conn: &diesel::SqliteConnection) -> Data { -//! // Do something with connection, return some data. -//! # () -//! } -//! -//! #[get("/")] -//! async fn my_handler(mut conn: MyDatabase) -> Data { -//! conn.run(|c| load_from_db(c)).await -//! } -//! # } -//! ``` -//! -//! # Database Support -//! -//! Built-in support is provided for many popular databases and drivers. Support -//! can be easily extended by [`Poolable`] implementations. -//! -//! ## Provided -//! -//! The list below includes all presently supported database adapters and their -//! corresponding [`Poolable`] type. -//! -// Note: Keep this table in sync with site/guite/6-state.md -//! | Kind | Driver | Version | `Poolable` Type | Feature | -//! |----------|-----------------------|-----------|--------------------------------|------------------------| -//! | MySQL | [Diesel] | `1` | [`diesel::MysqlConnection`] | `diesel_mysql_pool` | -//! | MySQL | [`rust-mysql-simple`] | `18` | [`mysql::Conn`] | `mysql_pool` | -//! | Postgres | [Diesel] | `1` | [`diesel::PgConnection`] | `diesel_postgres_pool` | -//! | Postgres | [Rust-Postgres] | `0.19` | [`postgres::Client`] | `postgres_pool` | -//! | Sqlite | [Diesel] | `1` | [`diesel::SqliteConnection`] | `diesel_sqlite_pool` | -//! | Sqlite | [`Rusqlite`] | `0.23` | [`rusqlite::Connection`] | `sqlite_pool` | -//! | Memcache | [`memcache`] | `0.15` | [`memcache::Client`] | `memcache_pool` | -//! -//! [Diesel]: https://diesel.rs -//! [`rusqlite::Connection`]: https://docs.rs/rusqlite/0.23.0/rusqlite/struct.Connection.html -//! [`diesel::SqliteConnection`]: http://docs.diesel.rs/diesel/prelude/struct.SqliteConnection.html -//! [`postgres::Client`]: https://docs.rs/postgres/0.19/postgres/struct.Client.html -//! [`diesel::PgConnection`]: http://docs.diesel.rs/diesel/pg/struct.PgConnection.html -//! [`mysql::Conn`]: https://docs.rs/mysql/18/mysql/struct.Conn.html -//! [`diesel::MysqlConnection`]: http://docs.diesel.rs/diesel/mysql/struct.MysqlConnection.html -//! [`Rusqlite`]: https://github.com/jgallagher/rusqlite -//! [Rust-Postgres]: https://github.com/sfackler/rust-postgres -//! [`rust-mysql-simple`]: https://github.com/blackbeam/rust-mysql-simple -//! [`diesel::PgConnection`]: http://docs.diesel.rs/diesel/pg/struct.PgConnection.html -//! [`memcache`]: https://github.com/aisk/rust-memcache -//! [`memcache::Client`]: https://docs.rs/memcache/0.15/memcache/struct.Client.html -//! -//! The above table lists all the supported database adapters in this library. -//! In order to use particular `Poolable` type that's included in this library, -//! you must first enable the feature listed in the "Feature" column. The -//! interior type of your decorated database type should match the type in the -//! "`Poolable` Type" column. -//! -//! ## Extending -//! -//! Extending Rocket's support to your own custom database adapter (or other -//! database-like struct that can be pooled by `r2d2`) is as easy as -//! implementing the [`Poolable`] trait. See the documentation for [`Poolable`] -//! for more details on how to implement it. -//! -//! [`FromRequest`]: rocket::request::FromRequest -//! [request guards]: rocket::request::FromRequest -//! [`Poolable`]: crate::databases::Poolable - -pub extern crate r2d2; - -#[cfg(any( - feature = "diesel_sqlite_pool", - feature = "diesel_postgres_pool", - feature = "diesel_mysql_pool" -))] -pub extern crate diesel; - -use std::marker::PhantomData; -use std::sync::Arc; - -use rocket::fairing::{AdHoc, Fairing}; -use rocket::request::{Request, Outcome, FromRequest}; -use rocket::outcome::IntoOutcome; -use rocket::http::Status; - -use rocket::tokio::sync::{OwnedSemaphorePermit, Semaphore, Mutex}; -use rocket::tokio::time::timeout; - -use self::r2d2::ManageConnection; - -#[doc(hidden)] pub use rocket_contrib_codegen::*; - -#[cfg(feature = "postgres_pool")] pub extern crate postgres; -#[cfg(feature = "postgres_pool")] pub extern crate r2d2_postgres; - -#[cfg(feature = "mysql_pool")] pub extern crate mysql; -#[cfg(feature = "mysql_pool")] pub extern crate r2d2_mysql; - -#[cfg(feature = "sqlite_pool")] pub extern crate rusqlite; -#[cfg(feature = "sqlite_pool")] pub extern crate r2d2_sqlite; - -#[cfg(feature = "memcache_pool")] pub extern crate memcache; -#[cfg(feature = "memcache_pool")] pub extern crate r2d2_memcache; - -/// A base `Config` for any `Poolable` type. -/// -/// For the following configuration: -/// -/// ```toml -/// [global.databases.my_database] -/// url = "postgres://root:root@localhost/my_database" -/// pool_size = 10 -/// timeout = 5 -/// ``` -/// -/// ...`Config::from("my_database", rocket)` would return the following struct: -/// -/// ```rust -/// # use rocket_contrib::databases::Config; -/// Config { -/// url: "postgres://root:root@localhost/my_database".into(), -/// pool_size: 10, -/// timeout: 5 -/// }; -/// ``` -/// -/// If you want to implement your own custom database adapter (or other -/// database-like struct that can be pooled by `r2d2`) and need some more -/// configurations options, you may need to define a custom `Config` struct. -/// Note, however, that the configuration values in `Config` are required. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -pub struct Config { - /// Connection URL specified in the Rocket configuration. - pub url: String, - /// Initial pool size. Defaults to the number of Rocket workers * 2. - pub pool_size: u32, - /// How long to wait, in seconds, for a new connection before timing out. - /// Defaults to `5`. - // FIXME: Use `time`. - pub timeout: u8, -} - -use serde::{Serialize, Deserialize}; -use rocket::figment::{self, Figment, providers::Serialized}; - -impl Config { - /// Retrieves the database configuration for the database named `name`. - /// - /// This function is primarily used by the generated code from the - /// `#[database]` attribute. - /// - /// # Example - /// - /// ```rust - /// # #[cfg(feature = "diesel_sqlite_pool")] { - /// # use rocket::figment::{Figment, providers::{Format, Toml}}; - /// // Assume that these are the contents of `Rocket.toml`: - /// # let toml = Toml::string(r#" - /// [global.databases] - /// my_db = { url = "db/db.sqlite", pool_size = 25 } - /// my_other_db = { url = "mysql://root:root@localhost/database" } - /// # "#).nested(); - /// - /// use rocket_contrib::databases::Config; - /// - /// fn pool(rocket: &rocket::Rocket) { - /// let config = Config::from("my_db", rocket).unwrap(); - /// assert_eq!(config.url, "db/db.sqlite"); - /// assert_eq!(config.pool_size, 25); - /// - /// let config = Config::from("my_other_db", rocket).unwrap(); - /// assert_eq!(config.url, "mysql://root:root@localhost/database"); - /// assert_eq!(config.pool_size, (rocket.config().workers * 2) as u32); - /// - /// let config = Config::from("unknown_db", rocket); - /// assert!(config.is_err()) - /// } - /// # - /// # let config = Figment::from(rocket::Config::default()).merge(toml); - /// # let rocket = rocket::custom(config); - /// # pool(&rocket); - /// # } - /// ``` - pub fn from(db_name: &str, rocket: &rocket::Rocket) -> Result { - let db_key = format!("databases.{}", db_name); - let key = |name: &str| format!("{}.{}", db_key, name); - Figment::from(rocket.figment()) - .merge(Serialized::default(&key("pool_size"), rocket.config().workers * 2)) - .merge(Serialized::default(&key("timeout"), 5)) - .extract_inner::(&db_key) - } -} - -/// A wrapper around `r2d2::Error`s or a custom database error type. -/// -/// This type is only relevant to implementors of the [`Poolable`] trait. See -/// the [`Poolable`] documentation for more information on how to use this type. -#[derive(Debug)] -pub enum Error { - /// A custom error of type `T`. - Custom(T), - /// An error occurred while initializing an `r2d2` pool. - Pool(r2d2::Error), - /// An error occurred while extracting a `figment` configuration. - Config(figment::Error), -} - -impl From for Error { - fn from(error: figment::Error) -> Self { - Error::Config(error) - } -} - -impl From for Error { - fn from(error: r2d2::Error) -> Self { - Error::Pool(error) - } -} - -/// Trait implemented by `r2d2`-based database adapters. -/// -/// # Provided Implementations -/// -/// Implementations of `Poolable` are provided for the following types: -/// -/// * `diesel::MysqlConnection` -/// * `diesel::PgConnection` -/// * `diesel::SqliteConnection` -/// * `postgres::Connection` -/// * `mysql::Conn` -/// * `rusqlite::Connection` -/// -/// # Implementation Guide -/// -/// As an r2d2-compatible database (or other resource) adapter provider, -/// implementing `Poolable` in your own library will enable Rocket users to -/// consume your adapter with its built-in connection pooling support. -/// -/// ## Example -/// -/// Consider a library `foo` with the following types: -/// -/// * `foo::ConnectionManager`, which implements [`r2d2::ManageConnection`] -/// * `foo::Connection`, the `Connection` associated type of -/// `foo::ConnectionManager` -/// * `foo::Error`, errors resulting from manager instantiation -/// -/// In order for Rocket to generate the required code to automatically provision -/// a r2d2 connection pool into application state, the `Poolable` trait needs to -/// be implemented for the connection type. The following example implements -/// `Poolable` for `foo::Connection`: -/// -/// ```rust -/// # mod foo { -/// # use std::fmt; -/// # use rocket_contrib::databases::r2d2; -/// # #[derive(Debug)] pub struct Error; -/// # impl std::error::Error for Error { } -/// # impl fmt::Display for Error { -/// # fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { Ok(()) } -/// # } -/// # -/// # pub struct Connection; -/// # pub struct ConnectionManager; -/// # -/// # type Result = std::result::Result; -/// # -/// # impl ConnectionManager { -/// # pub fn new(url: &str) -> Result { Err(Error) } -/// # } -/// # -/// # impl self::r2d2::ManageConnection for ConnectionManager { -/// # type Connection = Connection; -/// # type Error = Error; -/// # fn connect(&self) -> Result { panic!(()) } -/// # fn is_valid(&self, _: &mut Connection) -> Result<()> { panic!() } -/// # fn has_broken(&self, _: &mut Connection) -> bool { panic!() } -/// # } -/// # } -/// use rocket_contrib::databases::{r2d2, Error, Config, Poolable, PoolResult}; -/// -/// impl Poolable for foo::Connection { -/// type Manager = foo::ConnectionManager; -/// type Error = foo::Error; -/// -/// fn pool(db_name: &str, rocket: &rocket::Rocket) -> PoolResult { -/// let config = Config::from(db_name, rocket)?; -/// let manager = foo::ConnectionManager::new(&config.url).map_err(Error::Custom)?; -/// Ok(r2d2::Pool::builder().max_size(config.pool_size).build(manager)?) -/// } -/// } -/// ``` -/// -/// In this example, `ConnectionManager::new()` method returns a `foo::Error` on -/// failure. The [`Error`] enum consolidates this type, the `r2d2::Error` type -/// that can result from `r2d2::Pool::builder()`, and the -/// [`figment::Error`](rocket::figment::Error) type from -/// `database::Config::from()`. -/// -/// In the event that a connection manager isn't fallible (as is the case with -/// Diesel's r2d2 connection manager, for instance), the associated error type -/// for the `Poolable` implementation should be `std::convert::Infallible`. -/// -/// For more concrete example, consult Rocket's existing implementations of -/// [`Poolable`]. -pub trait Poolable: Send + Sized + 'static { - /// The associated connection manager for the given connection type. - type Manager: ManageConnection; - - /// The associated error type in the event that constructing the connection - /// manager and/or the connection pool fails. - type Error: std::fmt::Debug; - - /// Creates an `r2d2` connection pool for `Manager::Connection`, returning - /// the pool on success. - fn pool(db_name: &str, rocket: &rocket::Rocket) -> PoolResult; -} - -/// A type alias for the return type of [`Poolable::pool()`]. -#[allow(type_alias_bounds)] -pub type PoolResult = Result, Error>; - -#[cfg(feature = "diesel_sqlite_pool")] -impl Poolable for diesel::SqliteConnection { - type Manager = diesel::r2d2::ConnectionManager; - type Error = std::convert::Infallible; - - fn pool(db_name: &str, rocket: &rocket::Rocket) -> PoolResult { - let config = Config::from(db_name, rocket)?; - let manager = diesel::r2d2::ConnectionManager::new(&config.url); - Ok(r2d2::Pool::builder().max_size(config.pool_size).build(manager)?) - } -} - -#[cfg(feature = "diesel_postgres_pool")] -impl Poolable for diesel::PgConnection { - type Manager = diesel::r2d2::ConnectionManager; - type Error = std::convert::Infallible; - - fn pool(db_name: &str, rocket: &rocket::Rocket) -> PoolResult { - let config = Config::from(db_name, rocket)?; - let manager = diesel::r2d2::ConnectionManager::new(&config.url); - Ok(r2d2::Pool::builder().max_size(config.pool_size).build(manager)?) - } -} - -#[cfg(feature = "diesel_mysql_pool")] -impl Poolable for diesel::MysqlConnection { - type Manager = diesel::r2d2::ConnectionManager; - type Error = std::convert::Infallible; - - fn pool(db_name: &str, rocket: &rocket::Rocket) -> PoolResult { - let config = Config::from(db_name, rocket)?; - let manager = diesel::r2d2::ConnectionManager::new(&config.url); - Ok(r2d2::Pool::builder().max_size(config.pool_size).build(manager)?) - } -} - -// TODO: Add a feature to enable TLS in `postgres`; parse a suitable `config`. -#[cfg(feature = "postgres_pool")] -impl Poolable for postgres::Client { - type Manager = r2d2_postgres::PostgresConnectionManager; - type Error = postgres::Error; - - fn pool(db_name: &str, rocket: &rocket::Rocket) -> PoolResult { - let config = Config::from(db_name, rocket)?; - let url = config.url.parse().map_err(Error::Custom)?; - let manager = r2d2_postgres::PostgresConnectionManager::new(url, postgres::tls::NoTls); - Ok(r2d2::Pool::builder().max_size(config.pool_size).build(manager)?) - } -} - -#[cfg(feature = "mysql_pool")] -impl Poolable for mysql::Conn { - type Manager = r2d2_mysql::MysqlConnectionManager; - type Error = std::convert::Infallible; - - fn pool(db_name: &str, rocket: &rocket::Rocket) -> PoolResult { - let config = Config::from(db_name, rocket)?; - let opts = mysql::OptsBuilder::from_opts(&config.url); - let manager = r2d2_mysql::MysqlConnectionManager::new(opts); - Ok(r2d2::Pool::builder().max_size(config.pool_size).build(manager)?) - } -} - -#[cfg(feature = "sqlite_pool")] -impl Poolable for rusqlite::Connection { - type Manager = r2d2_sqlite::SqliteConnectionManager; - type Error = std::convert::Infallible; - - fn pool(db_name: &str, rocket: &rocket::Rocket) -> PoolResult { - let config = Config::from(db_name, rocket)?; - let manager = r2d2_sqlite::SqliteConnectionManager::file(&*config.url); - Ok(r2d2::Pool::builder().max_size(config.pool_size).build(manager)?) - } -} - -#[cfg(feature = "memcache_pool")] -impl Poolable for memcache::Client { - type Manager = r2d2_memcache::MemcacheConnectionManager; - // Unused, but we might want it in the future without a breaking change. - type Error = memcache::MemcacheError; - - fn pool(db_name: &str, rocket: &rocket::Rocket) -> PoolResult { - let config = Config::from(db_name, rocket)?; - let manager = r2d2_memcache::MemcacheConnectionManager::new(&*config.url); - Ok(r2d2::Pool::builder().max_size(config.pool_size).build(manager)?) - } -} - -/// Unstable internal details of generated code for the #[database] attribute. -/// -/// This type is implemented here instead of in generated code to ensure all -/// types are properly checked. -#[doc(hidden)] -pub struct ConnectionPool { - config: Config, - // This is an 'Option' so that we can drop the pool in a 'spawn_blocking'. - pool: Option>, - semaphore: Arc, - _marker: PhantomData K>, -} - -impl Clone for ConnectionPool { - fn clone(&self) -> Self { - ConnectionPool { - config: self.config.clone(), - pool: self.pool.clone(), - semaphore: self.semaphore.clone(), - _marker: PhantomData - } - } -} - -/// Unstable internal details of generated code for the #[database] attribute. -/// -/// This type is implemented here instead of in generated code to ensure all -/// types are properly checked. -#[doc(hidden)] -pub struct Connection { - connection: Arc>>>, - permit: Option, - _marker: PhantomData K>, -} - -// A wrapper around spawn_blocking that propagates panics to the calling code. -async fn run_blocking(job: F) -> R - where F: FnOnce() -> R + Send + 'static, R: Send + 'static, -{ - match tokio::task::spawn_blocking(job).await { - Ok(ret) => ret, - Err(e) => match e.try_into_panic() { - Ok(panic) => std::panic::resume_unwind(panic), - Err(_) => unreachable!("spawn_blocking tasks are never cancelled"), - } - } -} - -macro_rules! dberr { - ($msg:literal, $db_name:expr, $efmt:literal, $error:expr, $rocket:expr) => ({ - rocket::error!(concat!("database ", $msg, " error for pool named `{}`"), $db_name); - error_!($efmt, $error); - return Err($rocket); - }); -} - -impl ConnectionPool { - pub fn fairing(fairing_name: &'static str, db: &'static str) -> impl Fairing { - AdHoc::on_attach(fairing_name, move |rocket| async move { - let config = match Config::from(db, &rocket) { - Ok(config) => config, - Err(e) => dberr!("config", db, "{}", e, rocket), - }; - - let pool_size = config.pool_size; - match C::pool(db, &rocket) { - Ok(pool) => Ok(rocket.manage(ConnectionPool:: { - config, - pool: Some(pool), - semaphore: Arc::new(Semaphore::new(pool_size as usize)), - _marker: PhantomData, - })), - Err(Error::Config(e)) => dberr!("config", db, "{}", e, rocket), - Err(Error::Pool(e)) => dberr!("pool init", db, "{}", e, rocket), - Err(Error::Custom(e)) => dberr!("pool manager", db, "{:?}", e, rocket), - } - }) - } - - async fn get(&self) -> Result, ()> { - let duration = std::time::Duration::from_secs(self.config.timeout as u64); - let permit = match timeout(duration, self.semaphore.clone().acquire_owned()).await { - Ok(p) => p.expect("internal invariant broken: semaphore should not be closed"), - Err(_) => { - error_!("database connection retrieval timed out"); - return Err(()); - } - }; - - let pool = self.pool.as_ref().cloned() - .expect("internal invariant broken: self.pool is Some"); - - match run_blocking(move || pool.get_timeout(duration)).await { - Ok(c) => Ok(Connection { - connection: Arc::new(Mutex::new(Some(c))), - permit: Some(permit), - _marker: PhantomData, - }), - Err(e) => { - error_!("failed to get a database connection: {}", e); - Err(()) - } - } - } - - #[inline] - pub async fn get_one(rocket: &rocket::Rocket) -> Option> { - match rocket.state::() { - Some(pool) => pool.get().await.ok(), - None => None - } - } - - #[inline] - pub async fn get_pool(rocket: &rocket::Rocket) -> Option { - rocket.state::().map(|pool| pool.clone()) - } -} - -impl Connection { - #[inline] - pub async fn run(&self, f: F) -> R - where F: FnOnce(&mut C) -> R + Send + 'static, - R: Send + 'static, - { - let mut connection = self.connection.clone().lock_owned().await; - run_blocking(move || { - let conn = connection.as_mut() - .expect("internal invariant broken: self.connection is Some"); - f(conn) - }).await - } -} - -impl Drop for Connection { - fn drop(&mut self) { - let connection = self.connection.clone(); - let permit = self.permit.take(); - tokio::spawn(async move { - let mut connection = connection.lock_owned().await; - tokio::task::spawn_blocking(move || { - if let Some(conn) = connection.take() { - drop(conn); - } - - // Explicitly dropping the permit here so that it's only - // released after the connection is. - drop(permit); - }) - }); - } -} - -impl Drop for ConnectionPool { - fn drop(&mut self) { - let pool = self.pool.take(); - tokio::task::spawn_blocking(move || drop(pool)); - } -} - -#[rocket::async_trait] -impl<'a, 'r, K: 'static, C: Poolable> FromRequest<'a, 'r> for Connection { - type Error = (); - - #[inline] - async fn from_request(request: &'a Request<'r>) -> Outcome { - match request.managed_state::>() { - Some(c) => c.get().await.into_outcome(Status::ServiceUnavailable), - None => { - error_!("Missing database fairing for `{}`", std::any::type_name::()); - Outcome::Failure((Status::InternalServerError, ())) - } - } - } -} diff --git a/contrib/lib/src/databases/config.rs b/contrib/lib/src/databases/config.rs new file mode 100644 index 00000000..e86699e9 --- /dev/null +++ b/contrib/lib/src/databases/config.rs @@ -0,0 +1,89 @@ +/// A base `Config` for any `Poolable` type. +/// +/// For the following configuration: +/// +/// ```toml +/// [global.databases.my_database] +/// url = "postgres://root:root@localhost/my_database" +/// pool_size = 10 +/// timeout = 5 +/// ``` +/// +/// ...`Config::from("my_database", rocket)` would return the following struct: +/// +/// ```rust +/// # use rocket_contrib::databases::Config; +/// Config { +/// url: "postgres://root:root@localhost/my_database".into(), +/// pool_size: 10, +/// timeout: 5 +/// }; +/// ``` +/// +/// If you want to implement your own custom database adapter (or other +/// database-like struct that can be pooled by `r2d2`) and need some more +/// configurations options, you may need to define a custom `Config` struct. +/// Note, however, that the configuration values in `Config` are required. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct Config { + /// Connection URL specified in the Rocket configuration. + pub url: String, + /// Initial pool size. Defaults to the number of Rocket workers * 2. + pub pool_size: u32, + /// How long to wait, in seconds, for a new connection before timing out. + /// Defaults to `5`. + // FIXME: Use `time`. + pub timeout: u8, +} + +use serde::{Serialize, Deserialize}; +use rocket::figment::{self, Figment, providers::Serialized}; + +impl Config { + /// Retrieves the database configuration for the database named `name`. + /// + /// This function is primarily used by the generated code from the + /// `#[database]` attribute. + /// + /// # Example + /// + /// ```rust + /// # #[cfg(feature = "diesel_sqlite_pool")] { + /// # use rocket::figment::{Figment, providers::{Format, Toml}}; + /// // Assume that these are the contents of `Rocket.toml`: + /// # let toml = Toml::string(r#" + /// [global.databases] + /// my_db = { url = "db/db.sqlite", pool_size = 25 } + /// my_other_db = { url = "mysql://root:root@localhost/database" } + /// # "#).nested(); + /// + /// use rocket_contrib::databases::Config; + /// + /// fn pool(rocket: &rocket::Rocket) { + /// let config = Config::from("my_db", rocket).unwrap(); + /// assert_eq!(config.url, "db/db.sqlite"); + /// assert_eq!(config.pool_size, 25); + /// + /// let config = Config::from("my_other_db", rocket).unwrap(); + /// assert_eq!(config.url, "mysql://root:root@localhost/database"); + /// assert_eq!(config.pool_size, (rocket.config().workers * 2) as u32); + /// + /// let config = Config::from("unknown_db", rocket); + /// assert!(config.is_err()) + /// } + /// # + /// # let config = Figment::from(rocket::Config::default()).merge(toml); + /// # let rocket = rocket::custom(config); + /// # pool(&rocket); + /// # } + /// ``` + pub fn from(db_name: &str, rocket: &rocket::Rocket) -> Result { + let db_key = format!("databases.{}", db_name); + let key = |name: &str| format!("{}.{}", db_key, name); + Figment::from(rocket.figment()) + .merge(Serialized::default(&key("pool_size"), rocket.config().workers * 2)) + .merge(Serialized::default(&key("timeout"), 5)) + .extract_inner::(&db_key) + } +} + diff --git a/contrib/lib/src/databases/connection.rs b/contrib/lib/src/databases/connection.rs new file mode 100644 index 00000000..0985fd70 --- /dev/null +++ b/contrib/lib/src/databases/connection.rs @@ -0,0 +1,188 @@ +use std::marker::PhantomData; +use std::sync::Arc; + +use rocket::fairing::{AdHoc, Fairing}; +use rocket::request::{Request, Outcome, FromRequest}; +use rocket::outcome::IntoOutcome; +use rocket::http::Status; + +use rocket::tokio::sync::{OwnedSemaphorePermit, Semaphore, Mutex}; +use rocket::tokio::time::timeout; + +use crate::databases::{Config, Poolable, Error}; + +/// Unstable internal details of generated code for the #[database] attribute. +/// +/// This type is implemented here instead of in generated code to ensure all +/// types are properly checked. +#[doc(hidden)] +pub struct ConnectionPool { + config: Config, + // This is an 'Option' so that we can drop the pool in a 'spawn_blocking'. + pool: Option>, + semaphore: Arc, + _marker: PhantomData K>, +} + +impl Clone for ConnectionPool { + fn clone(&self) -> Self { + ConnectionPool { + config: self.config.clone(), + pool: self.pool.clone(), + semaphore: self.semaphore.clone(), + _marker: PhantomData + } + } +} + +/// Unstable internal details of generated code for the #[database] attribute. +/// +/// This type is implemented here instead of in generated code to ensure all +/// types are properly checked. +#[doc(hidden)] +pub struct Connection { + connection: Arc>>>, + permit: Option, + _marker: PhantomData K>, +} + +// A wrapper around spawn_blocking that propagates panics to the calling code. +async fn run_blocking(job: F) -> R + where F: FnOnce() -> R + Send + 'static, R: Send + 'static, +{ + match tokio::task::spawn_blocking(job).await { + Ok(ret) => ret, + Err(e) => match e.try_into_panic() { + Ok(panic) => std::panic::resume_unwind(panic), + Err(_) => unreachable!("spawn_blocking tasks are never cancelled"), + } + } +} + +macro_rules! dberr { + ($msg:literal, $db_name:expr, $efmt:literal, $error:expr, $rocket:expr) => ({ + rocket::error!(concat!("database ", $msg, " error for pool named `{}`"), $db_name); + error_!($efmt, $error); + return Err($rocket); + }); +} + +impl ConnectionPool { + pub fn fairing(fairing_name: &'static str, db: &'static str) -> impl Fairing { + AdHoc::on_attach(fairing_name, move |rocket| async move { + let config = match Config::from(db, &rocket) { + Ok(config) => config, + Err(e) => dberr!("config", db, "{}", e, rocket), + }; + + let pool_size = config.pool_size; + match C::pool(db, &rocket) { + Ok(pool) => Ok(rocket.manage(ConnectionPool:: { + config, + pool: Some(pool), + semaphore: Arc::new(Semaphore::new(pool_size as usize)), + _marker: PhantomData, + })), + Err(Error::Config(e)) => dberr!("config", db, "{}", e, rocket), + Err(Error::Pool(e)) => dberr!("pool init", db, "{}", e, rocket), + Err(Error::Custom(e)) => dberr!("pool manager", db, "{:?}", e, rocket), + } + }) + } + + async fn get(&self) -> Result, ()> { + let duration = std::time::Duration::from_secs(self.config.timeout as u64); + let permit = match timeout(duration, self.semaphore.clone().acquire_owned()).await { + Ok(p) => p.expect("internal invariant broken: semaphore should not be closed"), + Err(_) => { + error_!("database connection retrieval timed out"); + return Err(()); + } + }; + + let pool = self.pool.as_ref().cloned() + .expect("internal invariant broken: self.pool is Some"); + + match run_blocking(move || pool.get_timeout(duration)).await { + Ok(c) => Ok(Connection { + connection: Arc::new(Mutex::new(Some(c))), + permit: Some(permit), + _marker: PhantomData, + }), + Err(e) => { + error_!("failed to get a database connection: {}", e); + Err(()) + } + } + } + + #[inline] + pub async fn get_one(rocket: &rocket::Rocket) -> Option> { + match rocket.state::() { + Some(pool) => pool.get().await.ok(), + None => None + } + } + + #[inline] + pub async fn get_pool(rocket: &rocket::Rocket) -> Option { + rocket.state::().map(|pool| pool.clone()) + } +} + +impl Connection { + #[inline] + pub async fn run(&self, f: F) -> R + where F: FnOnce(&mut C) -> R + Send + 'static, + R: Send + 'static, + { + let mut connection = self.connection.clone().lock_owned().await; + run_blocking(move || { + let conn = connection.as_mut() + .expect("internal invariant broken: self.connection is Some"); + f(conn) + }).await + } +} + +impl Drop for Connection { + fn drop(&mut self) { + let connection = self.connection.clone(); + let permit = self.permit.take(); + tokio::spawn(async move { + let mut connection = connection.lock_owned().await; + tokio::task::spawn_blocking(move || { + if let Some(conn) = connection.take() { + drop(conn); + } + + // Explicitly dropping the permit here so that it's only + // released after the connection is. + drop(permit); + }) + }); + } +} + +impl Drop for ConnectionPool { + fn drop(&mut self) { + let pool = self.pool.take(); + tokio::task::spawn_blocking(move || drop(pool)); + } +} + +#[rocket::async_trait] +impl<'a, 'r, K: 'static, C: Poolable> FromRequest<'a, 'r> for Connection { + type Error = (); + + #[inline] + async fn from_request(request: &'a Request<'r>) -> Outcome { + match request.managed_state::>() { + Some(c) => c.get().await.into_outcome(Status::ServiceUnavailable), + None => { + error_!("Missing database fairing for `{}`", std::any::type_name::()); + Outcome::Failure((Status::InternalServerError, ())) + } + } + } +} diff --git a/contrib/lib/src/databases/error.rs b/contrib/lib/src/databases/error.rs new file mode 100644 index 00000000..50882d74 --- /dev/null +++ b/contrib/lib/src/databases/error.rs @@ -0,0 +1,27 @@ +use rocket::figment; + +/// A wrapper around `r2d2::Error`s or a custom database error type. +/// +/// This type is only relevant to implementors of the [`Poolable`] trait. See +/// the [`Poolable`] documentation for more information on how to use this type. +#[derive(Debug)] +pub enum Error { + /// A custom error of type `T`. + Custom(T), + /// An error occurred while initializing an `r2d2` pool. + Pool(r2d2::Error), + /// An error occurred while extracting a `figment` configuration. + Config(figment::Error), +} + +impl From for Error { + fn from(error: figment::Error) -> Self { + Error::Config(error) + } +} + +impl From for Error { + fn from(error: r2d2::Error) -> Self { + Error::Pool(error) + } +} diff --git a/contrib/lib/src/databases/mod.rs b/contrib/lib/src/databases/mod.rs new file mode 100644 index 00000000..294066af --- /dev/null +++ b/contrib/lib/src/databases/mod.rs @@ -0,0 +1,395 @@ +//! Traits, utilities, and a macro for easy database connection pooling. +//! +//! # Overview +//! +//! This module provides traits, utilities, and a procedural macro that allows +//! you to easily connect your Rocket application to databases through +//! connection pools. A _database connection pool_ is a data structure that +//! maintains active database connections for later use in the application. +//! This implementation of connection pooling support is based on +//! [`r2d2`] and exposes connections through [request guards]. Databases are +//! individually configured through Rocket's regular configuration mechanisms: a +//! `Rocket.toml` file, environment variables, or procedurally. +//! +//! Connecting your Rocket application to a database using this library occurs +//! in three simple steps: +//! +//! 1. Configure your databases in `Rocket.toml`. +//! (see [Configuration](#configuration)) +//! 2. Associate a request guard type and fairing with each database. +//! (see [Guard Types](#guard-types)) +//! 3. Use the request guard to retrieve a connection in a handler. +//! (see [Handlers](#handlers)) +//! +//! For a list of supported databases, see [Provided Databases](#provided). This +//! support can be easily extended by implementing the [`Poolable`] trait. See +//! [Extending](#extending) for more. +//! +//! ## Example +//! +//! Before using this library, the feature corresponding to your database type +//! in `rocket_contrib` must be enabled: +//! +//! ```toml +//! [dependencies.rocket_contrib] +//! version = "0.5.0-dev" +//! default-features = false +//! features = ["diesel_sqlite_pool"] +//! ``` +//! +//! See [Provided](#provided) for a list of supported database and their +//! associated feature name. +//! +//! In whichever configuration source you choose, configure a `databases` +//! dictionary with an internal dictionary for each database, here `sqlite_logs` +//! in a TOML source: +//! +//! ```toml +//! [global.databases] +//! sqlite_logs = { url = "/path/to/database.sqlite" } +//! ``` +//! +//! In your application's source code, one-time: +//! +//! ```rust +//! #[macro_use] extern crate rocket; +//! #[macro_use] extern crate rocket_contrib; +//! +//! # #[cfg(feature = "diesel_sqlite_pool")] +//! # mod test { +//! use rocket_contrib::databases::diesel; +//! +//! #[database("sqlite_logs")] +//! struct LogsDbConn(diesel::SqliteConnection); +//! +//! #[launch] +//! fn rocket() -> rocket::Rocket { +//! rocket::ignite().attach(LogsDbConn::fairing()) +//! } +//! # } fn main() {} +//! ``` +//! +//! Whenever a connection to the database is needed: +//! +//! ```rust +//! # #[macro_use] extern crate rocket; +//! # #[macro_use] extern crate rocket_contrib; +//! # +//! # #[cfg(feature = "diesel_sqlite_pool")] +//! # mod test { +//! # use rocket_contrib::databases::diesel; +//! # +//! # #[database("sqlite_logs")] +//! # struct LogsDbConn(diesel::SqliteConnection); +//! # +//! # type Logs = (); +//! # type Result = std::result::Result; +//! # +//! #[get("/logs/")] +//! async fn get_logs(conn: LogsDbConn, id: usize) -> Result { +//! # /* +//! conn.run(|c| Logs::by_id(c, id)).await +//! # */ +//! # Ok(()) +//! } +//! # } fn main() {} +//! ``` +//! +//! # Usage +//! +//! ## Configuration +//! +//! Databases can be configured as any other values. Using the default +//! configuration provider, either via `Rocket.toml` or environment variables. +//! You can also use a custom provider. +//! +//! ### `Rocket.toml` +//! +//! To configure a database via `Rocket.toml`, add a table for each database +//! to the `databases` table where the key is a name of your choice. The table +//! should have a `url` key and, optionally, a `pool_size` key. This looks as +//! follows: +//! +//! ```toml +//! # Option 1: +//! [global.databases] +//! sqlite_db = { url = "db.sqlite" } +//! +//! # Option 2: +//! [global.databases.my_db] +//! url = "mysql://root:root@localhost/my_db" +//! +//! # With a `pool_size` key: +//! [global.databases] +//! sqlite_db = { url = "db.sqlite", pool_size = 20 } +//! ``` +//! +//! The table _requires_ one key: +//! +//! * `url` - the URl to the database +//! +//! Additionally, all configurations accept the following _optional_ keys: +//! +//! * `pool_size` - the size of the pool, i.e., the number of connections to +//! pool (defaults to the configured number of workers * 2) +//! +//! Additional options may be required or supported by other adapters. +//! +//! ### Procedurally +//! +//! Databases can also be configured procedurally via `rocket::custom()`. +//! The example below does just this: +//! +//! ```rust +//! # #[cfg(feature = "diesel_sqlite_pool")] { +//! use rocket::figment::{value::{Map, Value}, util::map}; +//! +//! #[rocket::launch] +//! fn rocket() -> _ { +//! let db: Map<_, Value> = map! { +//! "url" => "db.sqlite".into(), +//! "pool_size" => 10.into() +//! }; +//! +//! let figment = rocket::Config::figment() +//! .merge(("databases", map!["my_db" => db])); +//! +//! rocket::custom(figment) +//! } +//! # rocket(); +//! # } +//! ``` +//! +//! ### Environment Variables +//! +//! Lastly, databases can be configured via environment variables by specifying +//! the `databases` table as detailed in the [Environment Variables +//! configuration +//! guide](https://rocket.rs/master/guide/configuration/#environment-variables): +//! +//! ```bash +//! ROCKET_DATABASES='{my_db={url="db.sqlite"}}' +//! ``` +//! +//! Multiple databases can be specified in the `ROCKET_DATABASES` environment variable +//! as well by comma separating them: +//! +//! ```bash +//! ROCKET_DATABASES='{my_db={url="db.sqlite"},my_pg_db={url="postgres://root:root@localhost/my_pg_db"}}' +//! ``` +//! +//! ## Guard Types +//! +//! Once a database has been configured, the `#[database]` attribute can be used +//! to tie a type in your application to a configured database. The database +//! attributes accepts a single string parameter that indicates the name of the +//! database. This corresponds to the database name set as the database's +//! configuration key. +//! +//! The macro generates a [`FromRequest`] implementation for the decorated type, +//! allowing the type to be used as a request guard. This implementation +//! retrieves a connection from the database pool or fails with a +//! `Status::ServiceUnavailable` if connecting to the database times out. +//! +//! The macro will also generate two inherent methods on the decorated type: +//! +//! * `fn fairing() -> impl Fairing` +//! +//! Returns a fairing that initializes the associated database connection +//! pool. +//! +//! * `async fn get_one(&Rocket) -> Option` +//! +//! Retrieves a connection wrapper from the configured pool. Returns `Some` +//! as long as `Self::fairing()` has been attached. +//! +//! The attribute can only be applied to unit-like structs with one type. The +//! internal type of the structure must implement [`Poolable`]. +//! +//! ```rust +//! # extern crate rocket; +//! # #[macro_use] extern crate rocket_contrib; +//! # #[cfg(feature = "diesel_sqlite_pool")] +//! # mod test { +//! use rocket_contrib::databases::diesel; +//! +//! #[database("my_db")] +//! struct MyDatabase(diesel::SqliteConnection); +//! # } +//! ``` +//! +//! Other databases can be used by specifying their respective [`Poolable`] +//! type: +//! +//! ```rust +//! # extern crate rocket; +//! # #[macro_use] extern crate rocket_contrib; +//! # #[cfg(feature = "postgres_pool")] +//! # mod test { +//! use rocket_contrib::databases::postgres; +//! +//! #[database("my_pg_db")] +//! struct MyPgDatabase(postgres::Client); +//! # } +//! ``` +//! +//! The fairing returned from the generated `fairing()` method _must_ be +//! attached for the request guard implementation to succeed. Putting the pieces +//! together, a use of the `#[database]` attribute looks as follows: +//! +//! ```rust +//! # #[macro_use] extern crate rocket; +//! # #[macro_use] extern crate rocket_contrib; +//! # +//! # #[cfg(feature = "diesel_sqlite_pool")] { +//! # use rocket::figment::{value::{Map, Value}, util::map}; +//! use rocket_contrib::databases::diesel; +//! +//! #[database("my_db")] +//! struct MyDatabase(diesel::SqliteConnection); +//! +//! #[launch] +//! fn rocket() -> _ { +//! # let db: Map<_, Value> = map![ +//! # "url" => "db.sqlite".into(), "pool_size" => 10.into() +//! # ]; +//! # let figment = rocket::Config::figment().merge(("databases", map!["my_db" => db])); +//! rocket::custom(figment).attach(MyDatabase::fairing()) +//! } +//! # } +//! ``` +//! +//! ## Handlers +//! +//! Finally, use your type as a request guard in a handler to retrieve a +//! connection wrapper for the database: +//! +//! ```rust +//! # #[macro_use] extern crate rocket; +//! # #[macro_use] extern crate rocket_contrib; +//! # +//! # #[cfg(feature = "diesel_sqlite_pool")] +//! # mod test { +//! # use rocket_contrib::databases::diesel; +//! #[database("my_db")] +//! struct MyDatabase(diesel::SqliteConnection); +//! +//! #[get("/")] +//! fn my_handler(conn: MyDatabase) { +//! // ... +//! } +//! # } +//! ``` +//! +//! A connection can be retrieved and used with the `run()` method: +//! +//! ```rust +//! # #[macro_use] extern crate rocket; +//! # #[macro_use] extern crate rocket_contrib; +//! # +//! # #[cfg(feature = "diesel_sqlite_pool")] +//! # mod test { +//! # use rocket_contrib::databases::diesel; +//! # type Data = (); +//! #[database("my_db")] +//! struct MyDatabase(diesel::SqliteConnection); +//! +//! fn load_from_db(conn: &diesel::SqliteConnection) -> Data { +//! // Do something with connection, return some data. +//! # () +//! } +//! +//! #[get("/")] +//! async fn my_handler(mut conn: MyDatabase) -> Data { +//! conn.run(|c| load_from_db(c)).await +//! } +//! # } +//! ``` +//! +//! # Database Support +//! +//! Built-in support is provided for many popular databases and drivers. Support +//! can be easily extended by [`Poolable`] implementations. +//! +//! ## Provided +//! +//! The list below includes all presently supported database adapters and their +//! corresponding [`Poolable`] type. +//! +// Note: Keep this table in sync with site/guite/6-state.md +//! | Kind | Driver | Version | `Poolable` Type | Feature | +//! |----------|-----------------------|-----------|--------------------------------|------------------------| +//! | MySQL | [Diesel] | `1` | [`diesel::MysqlConnection`] | `diesel_mysql_pool` | +//! | MySQL | [`rust-mysql-simple`] | `18` | [`mysql::Conn`] | `mysql_pool` | +//! | Postgres | [Diesel] | `1` | [`diesel::PgConnection`] | `diesel_postgres_pool` | +//! | Postgres | [Rust-Postgres] | `0.19` | [`postgres::Client`] | `postgres_pool` | +//! | Sqlite | [Diesel] | `1` | [`diesel::SqliteConnection`] | `diesel_sqlite_pool` | +//! | Sqlite | [`Rusqlite`] | `0.23` | [`rusqlite::Connection`] | `sqlite_pool` | +//! | Memcache | [`memcache`] | `0.15` | [`memcache::Client`] | `memcache_pool` | +//! +//! [Diesel]: https://diesel.rs +//! [`rusqlite::Connection`]: https://docs.rs/rusqlite/0.23.0/rusqlite/struct.Connection.html +//! [`diesel::SqliteConnection`]: http://docs.diesel.rs/diesel/prelude/struct.SqliteConnection.html +//! [`postgres::Client`]: https://docs.rs/postgres/0.19/postgres/struct.Client.html +//! [`diesel::PgConnection`]: http://docs.diesel.rs/diesel/pg/struct.PgConnection.html +//! [`mysql::Conn`]: https://docs.rs/mysql/18/mysql/struct.Conn.html +//! [`diesel::MysqlConnection`]: http://docs.diesel.rs/diesel/mysql/struct.MysqlConnection.html +//! [`Rusqlite`]: https://github.com/jgallagher/rusqlite +//! [Rust-Postgres]: https://github.com/sfackler/rust-postgres +//! [`rust-mysql-simple`]: https://github.com/blackbeam/rust-mysql-simple +//! [`diesel::PgConnection`]: http://docs.diesel.rs/diesel/pg/struct.PgConnection.html +//! [`memcache`]: https://github.com/aisk/rust-memcache +//! [`memcache::Client`]: https://docs.rs/memcache/0.15/memcache/struct.Client.html +//! +//! The above table lists all the supported database adapters in this library. +//! In order to use particular `Poolable` type that's included in this library, +//! you must first enable the feature listed in the "Feature" column. The +//! interior type of your decorated database type should match the type in the +//! "`Poolable` Type" column. +//! +//! ## Extending +//! +//! Extending Rocket's support to your own custom database adapter (or other +//! database-like struct that can be pooled by `r2d2`) is as easy as +//! implementing the [`Poolable`] trait. See the documentation for [`Poolable`] +//! for more details on how to implement it. +//! +//! [`FromRequest`]: rocket::request::FromRequest +//! [request guards]: rocket::request::FromRequest +//! [`Poolable`]: crate::databases::Poolable + +pub extern crate r2d2; + +#[cfg(any( + feature = "diesel_sqlite_pool", + feature = "diesel_postgres_pool", + feature = "diesel_mysql_pool" +))] +pub extern crate diesel; + +#[cfg(feature = "postgres_pool")] pub extern crate postgres; +#[cfg(feature = "postgres_pool")] pub extern crate r2d2_postgres; + +#[cfg(feature = "mysql_pool")] pub extern crate mysql; +#[cfg(feature = "mysql_pool")] pub extern crate r2d2_mysql; + +#[cfg(feature = "sqlite_pool")] pub extern crate rusqlite; +#[cfg(feature = "sqlite_pool")] pub extern crate r2d2_sqlite; + +#[cfg(feature = "memcache_pool")] pub extern crate memcache; +#[cfg(feature = "memcache_pool")] pub extern crate r2d2_memcache; + +mod poolable; +mod config; +mod error; +mod connection; + +pub use self::poolable::{Poolable, PoolResult}; +pub use self::config::Config; +pub use self::error::Error; + +#[doc(hidden)] +pub use rocket_contrib_codegen::*; + +#[doc(hidden)] +pub use self::connection::*; diff --git a/contrib/lib/src/databases/poolable.rs b/contrib/lib/src/databases/poolable.rs new file mode 100644 index 00000000..6def733e --- /dev/null +++ b/contrib/lib/src/databases/poolable.rs @@ -0,0 +1,195 @@ +use r2d2::ManageConnection; + +use crate::databases::{Config, Error}; + +/// Trait implemented by `r2d2`-based database adapters. +/// +/// # Provided Implementations +/// +/// Implementations of `Poolable` are provided for the following types: +/// +/// * `diesel::MysqlConnection` +/// * `diesel::PgConnection` +/// * `diesel::SqliteConnection` +/// * `postgres::Connection` +/// * `mysql::Conn` +/// * `rusqlite::Connection` +/// +/// # Implementation Guide +/// +/// As an r2d2-compatible database (or other resource) adapter provider, +/// implementing `Poolable` in your own library will enable Rocket users to +/// consume your adapter with its built-in connection pooling support. +/// +/// ## Example +/// +/// Consider a library `foo` with the following types: +/// +/// * `foo::ConnectionManager`, which implements [`r2d2::ManageConnection`] +/// * `foo::Connection`, the `Connection` associated type of +/// `foo::ConnectionManager` +/// * `foo::Error`, errors resulting from manager instantiation +/// +/// In order for Rocket to generate the required code to automatically provision +/// a r2d2 connection pool into application state, the `Poolable` trait needs to +/// be implemented for the connection type. The following example implements +/// `Poolable` for `foo::Connection`: +/// +/// ```rust +/// # mod foo { +/// # use std::fmt; +/// # use rocket_contrib::databases::r2d2; +/// # #[derive(Debug)] pub struct Error; +/// # impl std::error::Error for Error { } +/// # impl fmt::Display for Error { +/// # fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { Ok(()) } +/// # } +/// # +/// # pub struct Connection; +/// # pub struct ConnectionManager; +/// # +/// # type Result = std::result::Result; +/// # +/// # impl ConnectionManager { +/// # pub fn new(url: &str) -> Result { Err(Error) } +/// # } +/// # +/// # impl self::r2d2::ManageConnection for ConnectionManager { +/// # type Connection = Connection; +/// # type Error = Error; +/// # fn connect(&self) -> Result { panic!(()) } +/// # fn is_valid(&self, _: &mut Connection) -> Result<()> { panic!() } +/// # fn has_broken(&self, _: &mut Connection) -> bool { panic!() } +/// # } +/// # } +/// use rocket_contrib::databases::{r2d2, Error, Config, Poolable, PoolResult}; +/// +/// impl Poolable for foo::Connection { +/// type Manager = foo::ConnectionManager; +/// type Error = foo::Error; +/// +/// fn pool(db_name: &str, rocket: &rocket::Rocket) -> PoolResult { +/// let config = Config::from(db_name, rocket)?; +/// let manager = foo::ConnectionManager::new(&config.url).map_err(Error::Custom)?; +/// Ok(r2d2::Pool::builder().max_size(config.pool_size).build(manager)?) +/// } +/// } +/// ``` +/// +/// In this example, `ConnectionManager::new()` method returns a `foo::Error` on +/// failure. The [`Error`] enum consolidates this type, the `r2d2::Error` type +/// that can result from `r2d2::Pool::builder()`, and the +/// [`figment::Error`](rocket::figment::Error) type from +/// `database::Config::from()`. +/// +/// In the event that a connection manager isn't fallible (as is the case with +/// Diesel's r2d2 connection manager, for instance), the associated error type +/// for the `Poolable` implementation should be `std::convert::Infallible`. +/// +/// For more concrete example, consult Rocket's existing implementations of +/// [`Poolable`]. +pub trait Poolable: Send + Sized + 'static { + /// The associated connection manager for the given connection type. + type Manager: ManageConnection; + + /// The associated error type in the event that constructing the connection + /// manager and/or the connection pool fails. + type Error: std::fmt::Debug; + + /// Creates an `r2d2` connection pool for `Manager::Connection`, returning + /// the pool on success. + fn pool(db_name: &str, rocket: &rocket::Rocket) -> PoolResult; +} + +/// A type alias for the return type of [`Poolable::pool()`]. +#[allow(type_alias_bounds)] +pub type PoolResult = Result, Error>; + +#[cfg(feature = "diesel_sqlite_pool")] +impl Poolable for diesel::SqliteConnection { + type Manager = diesel::r2d2::ConnectionManager; + type Error = std::convert::Infallible; + + fn pool(db_name: &str, rocket: &rocket::Rocket) -> PoolResult { + let config = Config::from(db_name, rocket)?; + let manager = diesel::r2d2::ConnectionManager::new(&config.url); + Ok(r2d2::Pool::builder().max_size(config.pool_size).build(manager)?) + } +} + +#[cfg(feature = "diesel_postgres_pool")] +impl Poolable for diesel::PgConnection { + type Manager = diesel::r2d2::ConnectionManager; + type Error = std::convert::Infallible; + + fn pool(db_name: &str, rocket: &rocket::Rocket) -> PoolResult { + let config = Config::from(db_name, rocket)?; + let manager = diesel::r2d2::ConnectionManager::new(&config.url); + Ok(r2d2::Pool::builder().max_size(config.pool_size).build(manager)?) + } +} + +#[cfg(feature = "diesel_mysql_pool")] +impl Poolable for diesel::MysqlConnection { + type Manager = diesel::r2d2::ConnectionManager; + type Error = std::convert::Infallible; + + fn pool(db_name: &str, rocket: &rocket::Rocket) -> PoolResult { + let config = Config::from(db_name, rocket)?; + let manager = diesel::r2d2::ConnectionManager::new(&config.url); + Ok(r2d2::Pool::builder().max_size(config.pool_size).build(manager)?) + } +} + +// TODO: Add a feature to enable TLS in `postgres`; parse a suitable `config`. +#[cfg(feature = "postgres_pool")] +impl Poolable for postgres::Client { + type Manager = r2d2_postgres::PostgresConnectionManager; + type Error = postgres::Error; + + fn pool(db_name: &str, rocket: &rocket::Rocket) -> PoolResult { + let config = Config::from(db_name, rocket)?; + let url = config.url.parse().map_err(Error::Custom)?; + let manager = r2d2_postgres::PostgresConnectionManager::new(url, postgres::tls::NoTls); + Ok(r2d2::Pool::builder().max_size(config.pool_size).build(manager)?) + } +} + +#[cfg(feature = "mysql_pool")] +impl Poolable for mysql::Conn { + type Manager = r2d2_mysql::MysqlConnectionManager; + type Error = std::convert::Infallible; + + fn pool(db_name: &str, rocket: &rocket::Rocket) -> PoolResult { + let config = Config::from(db_name, rocket)?; + let opts = mysql::OptsBuilder::from_opts(&config.url); + let manager = r2d2_mysql::MysqlConnectionManager::new(opts); + Ok(r2d2::Pool::builder().max_size(config.pool_size).build(manager)?) + } +} + +#[cfg(feature = "sqlite_pool")] +impl Poolable for rusqlite::Connection { + type Manager = r2d2_sqlite::SqliteConnectionManager; + type Error = std::convert::Infallible; + + fn pool(db_name: &str, rocket: &rocket::Rocket) -> PoolResult { + let config = Config::from(db_name, rocket)?; + let manager = r2d2_sqlite::SqliteConnectionManager::file(&*config.url); + Ok(r2d2::Pool::builder().max_size(config.pool_size).build(manager)?) + } +} + +#[cfg(feature = "memcache_pool")] +impl Poolable for memcache::Client { + type Manager = r2d2_memcache::MemcacheConnectionManager; + // Unused, but we might want it in the future without a breaking change. + type Error = memcache::MemcacheError; + + fn pool(db_name: &str, rocket: &rocket::Rocket) -> PoolResult { + let config = Config::from(db_name, rocket)?; + let manager = r2d2_memcache::MemcacheConnectionManager::new(&*config.url); + Ok(r2d2::Pool::builder().max_size(config.pool_size).build(manager)?) + } +} +