//! # 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`](https://crates.io/crates/r2d2) and exposes connections through //! [request guards](../../rocket/request/trait.FromRequest.html). 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.Poolable.html) trait. See [Extending](#extending) //! for more. //! //! The next section provides a complete but un-detailed example of these steps //! in actions. The sections following provide more detail for each component. //! //! ## Example //! //! Before using this library, the `database_pool` feature in `rocket_contrib` //! must be enabled: //! //! ```toml //! [dependencies.rocket_contrib] //! version = "0.4.0-dev" //! default-features = false //! features = ["database_pool", "diesel_sqlite_pool"] //! ``` //! //! In `Rocket.toml` or the equivalent via environment variables: //! //! ```toml //! [global.databases] //! sqlite_logs = { url = "/path/to/database.sqlite" } //! ``` //! //! In your application's source code, one-time: //! //! ```rust,ignore //! #![feature(use_extern_macros)] //! extern crate rocket; //! extern crate rocket_contrib; //! //! use rocket_contrib::databases::{database, diesel}; //! //! #[database("sqlite_logs")] //! struct LogsDbConn(diesel::SqliteConnection); //! //! fn main() { //! rocket::ignite() //! .attach(LogsDbConn::fairing()) //! .launch(); //! } //! ``` //! //! Whenever a connection to the database is needed: //! //! ```rust,ignore //! #[get("/logs/")] //! fn get_logs(conn: LogsDbConn, id: LogId) -> Result { //! Logs::by_id(&conn, id) //! } //! ``` //! //! # Usage //! //! ## Configuration //! //! There are a few ways to configure your database connection. You can use the //! `Rocket.toml` file, you can build it yourself procedurally via the //! `rocket::custom()` method, or through environment variables. //! //! ### Configuring via `Rocket.toml` //! //! The following examples are all valid ways of configuring your database via //! the `Rocket.toml` file. //! //! The basic structure includes attaching a key to the `global.databases` table //! and including the __required__ keys `url` and `pool_size`. Additional //! options that can be added to the table vary by adapter and are referenced //! below in the [Supported Databases](#provided) section. //! //! ```toml //! [global.databases] //! my_database = { url = "database.sqlite", pool_size = 10 } //! //! [[global.databases.other_database]] //! url = "mysql://root:root@localhost/other_database //! pool_size = 25 //! ``` //! //! ### Configuring procedurally //! //! It's also possible to procedurally configure your database via the //! `rocket::custom()` method. Below is an example of doing this: //! //! ```rust,ignore //! extern crate rocket; //! //! use std::io::Error; //! use std::collections::HashMap; //! use rocket::config::{Config, Environment, Value}; //! //! fn main() { //! let mut database_config = HashMap::new(); //! let mut databases = HashMap::new(); //! //! database_config.insert("url", Value::from("database.sqlite")); //! databases.insert("my_db", Value::from(database_config)); //! //! let config = Config::build(Environment::Development) //! .extra("databases", databases) //! .finalize() //! .unwrap(); //! //! rocket::custom(config).launch(); //! } //! ``` //! //! ### Configuring via Environment Variable //! //! The final way to configure your databases is via an environment variable. //! Following the syntax laid out in the guide on [Environment Variables](https://rocket.rs/guide/configuration/#environment-variables), //! you can configure your database this way. Below is an example //! //! ```bash //! ROCKET_DATABASES={my_db={url="db.sqlite"}} //! ``` //! //! ## Guard Types //! //! The included database support generates request guard types that can be used //! with Rocket handlers. In order to associate a configured database with a //! type, you need to use the `database` procedural macro: //! //! ```rust //! # #![feature(use_extern_macros)] //! # extern crate rocket; //! # extern crate rocket_contrib; //! # use rocket_contrib::databases::{database, diesel}; //! //! #[database("my_db")] //! struct MyDatabase(diesel::SqliteConnection); //! ``` //! //! From there, the macro will generate code to turn your defined type into a //! valid request guard type. The interior type must have an implementation of //! the [`Poolable` trait](trait.Poolable.html). The trait implements methods //! on the interior type that are used by the generated code to spin up a //! connection pool. The trait can be used to extend other connection types that //! aren't supported in this library. See the section on [Extending](#extending) //! for more information. //! //! The generated code will give your defined type two methods, `get_one` and //! `fairing`, as well as implementations of the [`FromRequest`](../../rocket/request/trait.FromRequest.html) //! and [`Deref`](../../std/ops/trait.Deref.html) traits. //! //! The `fairing` method will allow you to attach your database type to the //! application state via the method call. You __will need__ to call the //! `fairing` method on your type in order to be able to retrieve connections //! in your request guards. //! //! Below is an example: //! //! ```rust,ignore //! # #![feature(use_extern_macros)] //! # //! # extern crate rocket; //! # extern crate rocket_contrib; //! # //! # use std::collections::HashMap; //! # use rocket::config::{Config, Environment, Value}; //! # use rocket_contrib::databases::{database, diesel}; //! # //! #[database("my_db")] //! struct MyDatabase(diesel::SqliteConnection); //! //! fn main() { //! # let mut db_config = HashMap::new(); //! # let mut databases = HashMap::new(); //! # //! # db_config.insert("url", Value::from("database.sqlite")); //! # db_config.insert("pool_size", Value::from(10)); //! # databases.insert("my_db", Value::from(db_config)); //! # //! # let config = Config::build(Environment::Development) //! # .extra("databases", databases) //! # .finalize() //! # .unwrap(); //! # //! rocket::custom(config) //! .attach(MyDatabase::fairing()); // Required! //! .launch(); //! } //! ``` //! //! ## Handlers //! //! For request handlers, you should use the database type you defined in your //! code as a request guard. Because of the `FromRequest` implementation that's //! generated at compile-time, you can use this type in such a way. For example: //! //! ```rust,ignore //! #[database("my_db") //! struct MyDatabase(diesel::MysqlConnection); //! ... //! #[get("/")] //! fn my_handler(conn: MyDatabase) { //! ... //! } //! ``` //! //! Additionally, because of the `Deref` implementation, you can dereference //! the database type in order to access the inner connection type. For example: //! //! ```rust,ignore //! #[get("/")] //! fn my_handler(conn: MyDatabase) { //! ... //! Thing::load(&conn); //! ... //! } //! ``` //! //! Under the hood, the dereferencing of your type is returning the interior //! type of your connection: //! //! ```rust,ignore //! &self.0 //! ``` //! //! This section should be simple. It should cover: //! //! * The fact that `MyType` is not a request guard, and you can use it. //! * The `Deref` impl and what it means for using `&my_conn`. //! //! # Database Support //! //! This library provides built-in support for many popular databases and their //! corresponding drivers. It also makes extending this support simple. //! //! ## Provided //! //! The list below includes all presently supported database adapters, their //! corresponding [`Poolable`] type, and any special considerations for //! configuration, if any. //! //! | Database Kind | Driver | `Poolable` Type | Feature | Notes | //! | -- ------------- | ----------------------- | ------------------------- | --------------------- | ----- | //! | MySQL | [Diesel](https://diesel.rs) | [`diesel::MysqlConnection`](http://docs.diesel.rs/diesel/mysql/struct.MysqlConnection.html) | `diesel_mysql_pool` | None | //! | MySQL | [`rust-mysql-simple`](https://github.com/blackbeam/rust-mysql-simple) | [`mysql::conn`](https://docs.rs/mysql/14.0.0/mysql/struct.Conn.html) | `mysql_pool` | None | //! | Postgres | [Diesel](https://diesel.rs) | [`diesel::PgConnection`](http://docs.diesel.rs/diesel/pg/struct.PgConnection.html) | `diesel_postgres_pool` | None | //! | Postgres | [Rust-Postgres](https://github.com/sfackler/rust-postgres) | [`postgres::Connection`](https://docs.rs/postgres/0.15.2/postgres/struct.Connection.html) | `postgres_pool` | None | //! | Sqlite | [Diesel](https://diesel.rs) | [`diesel::SqliteConnection`](http://docs.diesel.rs/diesel/prelude/struct.SqliteConnection.html) | `diesel_sqlite_pool` | None | //! | Sqlite | [`Rustqlite`](https://github.com/jgallagher/rusqlite) | [`rusqlite::Connection`](https://docs.rs/rusqlite/0.13.0/rusqlite/struct.Connection.html) | `sqlite_pool` | None | //! | Neo4j | [`rusted_cypher`](https://github.com/livioribeiro/rusted-cypher) | [`rusted_cypher::GraphClient`](https://docs.rs/rusted_cypher/1.1.0/rusted_cypher/graph/struct.GraphClient.html) | `cypher_pool` | None | //! | Redis | [`Redis-rs`](https://github.com/mitsuhiko/redis-rs) | [`redis::Connection`](https://docs.rs/redis/0.9.0/redis/struct.Connection.html) | `redis_pool` | None | //! //! ### How to use the table //! 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 inner //! type you should use for your database type should be what's listed in the //! corresponding `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 for your own type. See the documentation for the //! [`Poolable` trait](trait.Poolable.html) for more details on how to implement //! it and extend your type for use with Rocket's database pooling feature. pub extern crate r2d2; use std::collections::BTreeMap; use std::fmt::{self, Display, Formatter}; use std::marker::{Send, Sized}; use rocket::config::{self, Value}; pub use rocket_contrib_codegen::database; use self::r2d2::ManageConnection; #[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 = "cypher_pool")] pub extern crate rusted_cypher; #[cfg(feature = "cypher_pool")] pub extern crate r2d2_cypher; #[cfg(feature = "redis_pool")] pub extern crate redis; #[cfg(feature = "redis_pool")] pub extern crate r2d2_redis; /// A struct containing database configuration options from some configuration. /// /// For the following configuration: /// /// ```toml /// [[global.databases.my_database]] /// url = "postgres://root:root@localhost/my_database /// pool_size = 10 /// certs = "sample_cert.pem" /// key = "key.pem" /// ``` /// /// The following structure would be generated after calling /// `database_config("my_database", &some_config)`: /// /// ```ignore /// DatabaseConfig { /// url: "dummy_db.sqlite", /// pool_size: 10, /// extras: { /// "certs": String("certs.pem"), /// "key": String("key.pem") /// } /// } /// ``` #[derive(Debug, Clone, PartialEq)] pub struct DatabaseConfig<'a> { /// The connection URL specified in the Rocket configuration. pub url: &'a str, /// The size of the pool to be initialized. Defaults to the number of /// Rocket workers. pub pool_size: u32, /// Any extra options that are included in the configuration, **excluding** /// the url and pool_size. pub extras: BTreeMap, } /// A wrapper around `r2d2::Error`s or a custom database error type. This type /// is mostly relevant to implementors of the [Poolable](trait.Poolable.html) /// trait. /// /// Example usages of this type are in the `Poolable` implementations that ship /// with `rocket_contrib`. #[derive(Debug)] pub enum DbError { /// The custom error type to wrap alongside `r2d2::Error`. Custom(T), /// The error returned by an r2d2 pool. PoolError(r2d2::Error), } /// The error type for fetching the DatabaseConfig #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DatabaseConfigError { /// Returned when the `[[global.databases]]` key is missing or empty from /// the loaded configuration. MissingTable, /// Returned when the database configuration key is missing from the active /// configuration. MissingKey, /// Returned when the configuration associated with the key isn't in the /// expected [Table](../../rocket/config/type.Table.html) format. MalformedConfiguration, /// Returned when the `url` field is missing. MissingUrl, /// Returned when the `url` field is of the wrong type. MalformedUrl, /// Returned when the `pool_size` exceeds `u32::max_value()` or is negative. InvalidPoolSize(i64), } /// This method retrieves the database configuration from the loaded /// configuration and returns a [`DatabaseConfig`](struct.DatabaseConfig.html) /// struct. /// /// # Example: /// /// Given the following configuration: /// /// ```toml /// [[global.databases]] /// my_db = { url = "db/db.sqlite", pool_size = 25 } /// my_other_db = { url = "mysql://root:root@localhost/database" } /// ``` /// /// Calling the `database_config` method will return the /// [`DatabaseConfig`](struct.DatabaseConfig.html) structure for any valid /// configuration key. See the example code below. /// /// ```rust /// # extern crate rocket; /// # extern crate rocket_contrib; /// # /// # use std::{collections::BTreeMap, mem::drop}; /// # use rocket::{fairing::AdHoc, config::{Config, Environment, Value}}; /// use rocket_contrib::databases::{database_config, DatabaseConfigError}; /// /// # let mut databases = BTreeMap::new(); /// # /// # let mut my_db = BTreeMap::new(); /// # my_db.insert("url".to_string(), Value::from("db/db.sqlite")); /// # my_db.insert("pool_size".to_string(), Value::from(25)); /// # /// # let mut my_other_db = BTreeMap::new(); /// # my_other_db.insert("url".to_string(), Value::from("mysql://root:root@localhost/database")); /// # /// # databases.insert("my_db".to_string(), Value::from(my_db)); /// # databases.insert("my_other_db".to_string(), Value::from(my_other_db)); /// # /// # let config = Config::build(Environment::Development).extra("databases", databases).expect("custom config okay"); /// # /// # rocket::custom(config).attach(AdHoc::on_attach(|rocket| { /// # // HACK: This is a dirty hack required to be able to make this work /// # let thing = { /// # let rocket_config = rocket.config(); /// let config = database_config("my_db", rocket_config).expect("my_db config okay"); /// assert_eq!(config.url, "db/db.sqlite"); /// assert_eq!(config.pool_size, 25); /// /// let other_config = database_config("my_other_db", rocket_config).expect("my_other_db config okay"); /// assert_eq!(other_config.url, "mysql://root:root@localhost/database"); /// /// let error = database_config("invalid_db", rocket_config).unwrap_err(); /// assert_eq!(error, DatabaseConfigError::MissingKey); /// # /// # 10 /// # }; /// # /// # Ok(rocket) /// # })); /// ``` pub fn database_config<'a>( name: &str, from: &'a config::Config ) -> Result, DatabaseConfigError> { // Find the first `databases` config that's a table with a key of 'name' // equal to `name`. let connection_config = from.get_table("databases") .map_err(|_| DatabaseConfigError::MissingTable)? .get(name) .ok_or(DatabaseConfigError::MissingKey)? .as_table() .ok_or(DatabaseConfigError::MalformedConfiguration)?; let maybe_url = connection_config.get("url") .ok_or(DatabaseConfigError::MissingUrl)?; let url = maybe_url.as_str().ok_or(DatabaseConfigError::MalformedUrl)?; let pool_size = connection_config.get("pool_size") .and_then(Value::as_integer) .unwrap_or(from.workers as i64); if pool_size < 1 || pool_size > u32::max_value() as i64 { return Err(DatabaseConfigError::InvalidPoolSize(pool_size)); } let mut extras = connection_config.clone(); extras.remove("url"); extras.remove("pool_size"); Ok(DatabaseConfig { url, pool_size: pool_size as u32, extras: extras }) } impl<'a> Display for DatabaseConfigError { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { DatabaseConfigError::MissingTable => { write!(f, "A table named `databases` was not found for this configuration") }, DatabaseConfigError::MissingKey => { write!(f, "An entry in the `databases` table was not found for this key") }, DatabaseConfigError::MalformedConfiguration => { write!(f, "The configuration for this database is malformed") } DatabaseConfigError::MissingUrl => { write!(f, "The connection URL is missing for this database") }, DatabaseConfigError::MalformedUrl => { write!(f, "The specified connection URL is malformed") }, DatabaseConfigError::InvalidPoolSize(invalid_size) => { write!(f, "'{}' is not a valid value for `pool_size`", invalid_size) }, } } } /// Trait implemented by database adapters to allow for r2d2 connection pools to /// be easily created. /// /// # Provided Implementations /// /// Rocket Contrib implements `Poolable` on several common database adapters. /// The provided implementations are listed here. /// /// * **diesel::MysqlConnection** /// /// * **diesel::PgConnection** /// /// * **diesel::SqliteConnection** /// /// * **postgres::Connection** /// /// * **mysql::Conn** /// /// * **rusqlite::Connection** /// /// * **rusted_cypher::GraphClient** /// /// * **redis::Connection** /// /// # Implementation Guide /// /// As a 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 primitives. /// /// ## Example /// /// This example assumes a `FooConnectionManager` implementing the /// `ManageConnection`trait required by r2d2. This connection manager abstracts /// over a pool of `FooClient` connections. /// /// Given the following definition of the client and connection manager: /// /// ```rust,ignore /// struct FooClient { ... }; /// /// impl FooClient { /// pub fn new(...) -> Result { /// ... /// } /// } /// /// struct FooConnectionManager { ... }; /// /// impl FooConnectionManager { /// pub fn new(...) -> Result { /// ... /// } /// } /// ``` /// /// In order to allow for Rocket Contrib 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. /// /// Given the above definitions, the following would be a valid implementation /// of the `Poolable` trait: /// /// ```rust,ignore /// impl Poolable for FooClient { /// type Manager = FooConnectionManager; /// type Error = DbError; /// /// fn pool(config: DatabaseConfig) -> Result, Self::Error> { /// let manager = FooConnectionManager::new(config.url) /// .map_err(DbError::Custom)?; /// /// r2d2::Pool::builder().max_size(config.pool_size).build(manager) /// .map_err(DbError::PoolError) /// } /// } /// ``` /// /// In the above example, the connection manager is failable and returns the the /// `FooClient`'s error type. Since the error type can diverge from a simple /// r2d2 pool error, the [`DbError`](enum.DbError.html) wrapper is used. This /// error type is defined as part of the associated type in the `Poolable` trait /// definition. /// /// Additionally, you'll notice that the `pool` method of the trait is used to /// to create the connection manager and the pool. This method returns a /// `Result` containing an r2d2 pool monomorphized to the `Manager` associated /// type in the trait definition, or containing the `Error` associated type. /// /// In the event that the connection manager isn't failable (as is the case in /// Diesel's r2d2 connection manager, for example), the associated error type /// for the `Poolable` implementation can simply be `r2d2::Error` as this is the /// only error that can be returned by the `pool` method. You can refer to the /// included implementations of `Poolable` in the `rocket_contrib::databases` /// module for concrete examples. /// 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; /// Creates an r2d2 connection pool from the provided Manager associated /// type and returns the pool or the error associated with the trait /// implementation. fn pool(config: DatabaseConfig) -> Result, Self::Error>; } #[cfg(feature = "diesel_sqlite_pool")] impl Poolable for diesel::SqliteConnection { type Manager = diesel::r2d2::ConnectionManager; type Error = r2d2::Error; fn pool(config: DatabaseConfig) -> Result, Self::Error> { let manager = diesel::r2d2::ConnectionManager::new(config.url); r2d2::Pool::builder().max_size(config.pool_size).build(manager) } } #[cfg(feature = "diesel_pg_pool")] impl Poolable for diesel::PgConnection { type Manager = diesel::r2d2::ConnectionManager; type Error = r2d2::Error; fn pool(config: DatabaseConfig) -> Result, Self::Error> { let manager = diesel::r2d2::ConnectionManager::new(config.url); 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 = r2d2::Error; fn pool(config: DatabaseConfig) -> Result, Self::Error> { let manager = diesel::r2d2::ConnectionManager::new(config.url); r2d2::Pool::builder().max_size(config.pool_size).build(manager) } } // TODO: Come up with a way to handle TLS #[cfg(feature = "postgres_pool")] impl Poolable for postgres::Connection { type Manager = r2d2_postgres::PostgresConnectionManager; type Error = DbError; fn pool(config: DatabaseConfig) -> Result, Self::Error> { let manager = r2d2_postgres::PostgresConnectionManager::new(config.url, r2d2_postgres::TlsMode::None) .map_err(DbError::Custom)?; r2d2::Pool::builder().max_size(config.pool_size).build(manager) .map_err(DbError::PoolError) } } #[cfg(feature = "mysql_pool")] impl Poolable for mysql::Conn { type Manager = r2d2_mysql::MysqlConnectionManager; type Error = r2d2::Error; fn pool(config: DatabaseConfig) -> Result, Self::Error> { let opts = mysql::OptsBuilder::from_opts(config.url); let manager = r2d2_mysql::MysqlConnectionManager::new(opts); 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 = r2d2::Error; fn pool(config: DatabaseConfig) -> Result, Self::Error> { let manager = r2d2_sqlite::SqliteConnectionManager::file(config.url); r2d2::Pool::builder().max_size(config.pool_size).build(manager) } } #[cfg(feature = "cypher_pool")] impl Poolable for rusted_cypher::GraphClient { type Manager = r2d2_cypher::CypherConnectionManager; type Error = r2d2::Error; fn pool(config: DatabaseConfig) -> Result, Self::Error> { let manager = r2d2_cypher::CypherConnectionManager { url: config.url.to_string() }; r2d2::Pool::builder().max_size(config.pool_size).build(manager) } } #[cfg(feature = "redis_pool")] impl Poolable for redis::Connection { type Manager = r2d2_redis::RedisConnectionManager; type Error = DbError; fn pool(config: DatabaseConfig) -> Result, Self::Error> { let manager = r2d2_redis::RedisConnectionManager::new(config.url).map_err(DbError::Custom)?; r2d2::Pool::builder().max_size(config.pool_size).build(manager) .map_err(DbError::PoolError) } } #[cfg(test)] mod tests { use std::collections::BTreeMap; use rocket::{Config, config::{Environment, Value}}; use super::{DatabaseConfigError, database_config}; #[test] fn no_database_entry_in_config_returns_error() { let config = Config::build(Environment::Development) .finalize() .unwrap(); let database_config_result = database_config("dummy_db", &config); assert_eq!(Err(DatabaseConfigError::MissingTable), database_config_result); } #[test] fn no_matching_connection_returns_error() { // Laboriously setup the config extras let mut database_extra = BTreeMap::new(); let mut connection_config = BTreeMap::new(); connection_config.insert("url".to_string(), Value::from("dummy_db.sqlite")); connection_config.insert("pool_size".to_string(), Value::from(10)); database_extra.insert("dummy_db".to_string(), Value::from(connection_config)); let config = Config::build(Environment::Development) .extra("databases", database_extra) .finalize() .unwrap(); let database_config_result = database_config("real_db", &config); assert_eq!(Err(DatabaseConfigError::MissingKey), database_config_result); } #[test] fn incorrectly_structured_config_returns_error() { let mut database_extra = BTreeMap::new(); let connection_config = vec!["url", "dummy_db.slqite"]; database_extra.insert("dummy_db".to_string(), Value::from(connection_config)); let config = Config::build(Environment::Development) .extra("databases", database_extra) .finalize() .unwrap(); let database_config_result = database_config("dummy_db", &config); assert_eq!(Err(DatabaseConfigError::MalformedConfiguration), database_config_result); } #[test] fn missing_connection_string_returns_error() { let mut database_extra = BTreeMap::new(); let connection_config: BTreeMap = BTreeMap::new(); database_extra.insert("dummy_db", connection_config); let config = Config::build(Environment::Development) .extra("databases", database_extra) .finalize() .unwrap(); let database_config_result = database_config("dummy_db", &config); assert_eq!(Err(DatabaseConfigError::MissingUrl), database_config_result); } #[test] fn invalid_connection_string_returns_error() { let mut database_extra = BTreeMap::new(); let mut connection_config = BTreeMap::new(); connection_config.insert("url".to_string(), Value::from(42)); database_extra.insert("dummy_db", connection_config); let config = Config::build(Environment::Development) .extra("databases", database_extra) .finalize() .unwrap(); let database_config_result = database_config("dummy_db", &config); assert_eq!(Err(DatabaseConfigError::MalformedUrl), database_config_result); } #[test] fn negative_pool_size_returns_error() { let mut database_extra = BTreeMap::new(); let mut connection_config = BTreeMap::new(); connection_config.insert("url".to_string(), Value::from("dummy_db.sqlite")); connection_config.insert("pool_size".to_string(), Value::from(-1)); database_extra.insert("dummy_db", connection_config); let config = Config::build(Environment::Development) .extra("databases", database_extra) .finalize() .unwrap(); let database_config_result = database_config("dummy_db", &config); assert_eq!(Err(DatabaseConfigError::InvalidPoolSize(-1)), database_config_result); } #[test] fn pool_size_beyond_u32_max_returns_error() { let mut database_extra = BTreeMap::new(); let mut connection_config = BTreeMap::new(); connection_config.insert("url".to_string(), Value::from("dummy_db.sqlite")); connection_config.insert("pool_size".to_string(), Value::from(4294967296)); database_extra.insert("dummy_db", connection_config); let config = Config::build(Environment::Development) .extra("databases", database_extra) .finalize() .unwrap(); let database_config_result = database_config("dummy_db", &config); // The size of `0` is an overflow wrap-around assert_eq!(Err(DatabaseConfigError::InvalidPoolSize(0)), database_config_result); } #[test] fn happy_path_database_config() { let url = "dummy_db.sqlite"; let pool_size = 10; let mut database_extra = BTreeMap::new(); let mut connection_config = BTreeMap::new(); connection_config.insert("url".to_string(), Value::from(url)); connection_config.insert("pool_size".to_string(), Value::from(pool_size)); database_extra.insert("dummy_db", connection_config); let config = Config::build(Environment::Development) .extra("databases", database_extra) .finalize() .unwrap(); let database_config = database_config("dummy_db", &config).unwrap(); assert_eq!(url, database_config.url); assert_eq!(pool_size, database_config.pool_size); assert_eq!(0, database_config.extras.len()); } #[test] fn extras_do_not_contain_required_keys() { let url = "dummy_db.sqlite"; let pool_size = 10; let mut database_extra = BTreeMap::new(); let mut connection_config = BTreeMap::new(); connection_config.insert("url".to_string(), Value::from(url)); connection_config.insert("pool_size".to_string(), Value::from(pool_size)); database_extra.insert("dummy_db", connection_config); let config = Config::build(Environment::Development) .extra("databases", database_extra) .finalize() .unwrap(); let database_config = database_config("dummy_db", &config).unwrap(); assert_eq!(url, database_config.url); assert_eq!(pool_size, database_config.pool_size); assert_eq!(false, database_config.extras.contains_key("url")); assert_eq!(false, database_config.extras.contains_key("pool_size")); } #[test] fn extra_values_are_placed_in_extras_map() { let url = "dummy_db.sqlite"; let pool_size = 10; let tls_cert = "certs.pem"; let tls_key = "key.pem"; let mut database_extra = BTreeMap::new(); let mut connection_config = BTreeMap::new(); connection_config.insert("url".to_string(), Value::from(url)); connection_config.insert("pool_size".to_string(), Value::from(pool_size)); connection_config.insert("certs".to_string(), Value::from(tls_cert)); connection_config.insert("key".to_string(), Value::from(tls_key)); database_extra.insert("dummy_db", connection_config); let config = Config::build(Environment::Development) .extra("databases", database_extra) .finalize() .unwrap(); let database_config = database_config("dummy_db", &config).unwrap(); assert_eq!(url, database_config.url); assert_eq!(pool_size, database_config.pool_size); assert_eq!(true, database_config.extras.contains_key("certs")); assert_eq!(true, database_config.extras.contains_key("key")); println!("{:#?}", database_config); } }