mirror of https://github.com/rwf2/Rocket.git
parent
01a5011b61
commit
60b9f06407
|
@ -8,6 +8,7 @@ members = [
|
|||
"core/codegen_next/",
|
||||
"core/http/",
|
||||
"contrib/lib",
|
||||
"contrib/codegen",
|
||||
"examples/cookies",
|
||||
"examples/errors",
|
||||
"examples/form_validation",
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
[package]
|
||||
name = "rocket_contrib_codegen"
|
||||
version = "0.4.0-dev"
|
||||
authors = ["Sergio Benitez <sb@sergio.bz>"]
|
||||
description = "Procedural macros for the Rocket contrib libraries."
|
||||
documentation = "https://api.rocket.rs/rocket_contrib_codegen/"
|
||||
homepage = "https://rocket.rs"
|
||||
repository = "https://github.com/SergioBenitez/Rocket"
|
||||
readme = "../../../README.md"
|
||||
keywords = ["rocket", "contrib", "code", "generation", "proc-macro"]
|
||||
license = "MIT/Apache-2.0"
|
||||
|
||||
# if publishing, add to config scripts
|
||||
publish = false
|
||||
|
||||
[features]
|
||||
database_attribute = []
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
quote = "0.6"
|
||||
proc-macro2 = { version = "0.4", features = ["nightly"] }
|
||||
syn = { version = "0.14", features = ["full", "extra-traits"] }
|
|
@ -0,0 +1,133 @@
|
|||
use proc_macro::{TokenStream, Diagnostic};
|
||||
use syn::{DataStruct, Fields, Data, Type, LitStr, DeriveInput, Ident, Visibility};
|
||||
use spanned::Spanned;
|
||||
|
||||
type Result<T> = ::std::result::Result<T, Diagnostic>;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct DatabaseInvocation {
|
||||
/// The name of the structure on which `#[database(..)] struct This(..)` was invoked.
|
||||
type_name: Ident,
|
||||
/// The visibility of the structure on which `#[database(..)] struct This(..)` was invoked.
|
||||
visibility: Visibility,
|
||||
/// The database name as passed in via #[database('database name')].
|
||||
db_name: String,
|
||||
/// The entire structure that the `database` attribute was called on.
|
||||
structure: DataStruct,
|
||||
/// The type inside the structure: struct MyDb(ThisType).
|
||||
connection_type: Type,
|
||||
}
|
||||
|
||||
const EXAMPLE: &str = "example: `struct MyDatabase(diesel::SqliteConnection);`";
|
||||
const ONLY_ON_STRUCTS_MSG: &str = "`database` attribute can only be used on structs";
|
||||
const ONLY_UNNAMED_FIELDS: &str = "`database` attribute can only be applied to structs with \
|
||||
exactly one unnamed field";
|
||||
const NO_GENERIC_STRUCTS: &str = "`database` attribute cannot be applied to a struct with a \
|
||||
generic type";
|
||||
|
||||
fn parse_invocation(attr: TokenStream, input: TokenStream) -> Result<DatabaseInvocation> {
|
||||
let attr_stream2 = ::proc_macro2::TokenStream::from(attr);
|
||||
let attr_span = attr_stream2.span();
|
||||
let string_lit = ::syn::parse2::<LitStr>(attr_stream2)
|
||||
.map_err(|_| attr_span.error("expected string literal"))?;
|
||||
|
||||
let input = ::syn::parse::<DeriveInput>(input).unwrap();
|
||||
if !input.generics.params.is_empty() {
|
||||
return Err(input.span().error(NO_GENERIC_STRUCTS));
|
||||
}
|
||||
|
||||
let structure = match input.data {
|
||||
Data::Struct(s) => s,
|
||||
_ => return Err(input.span().error(ONLY_ON_STRUCTS_MSG))
|
||||
};
|
||||
|
||||
let inner_type = match structure.fields {
|
||||
Fields::Unnamed(ref fields) if fields.unnamed.len() == 1 => {
|
||||
let first = fields.unnamed.first().expect("checked length");
|
||||
first.value().ty.clone()
|
||||
}
|
||||
_ => return Err(structure.fields.span().error(ONLY_UNNAMED_FIELDS).help(EXAMPLE))
|
||||
};
|
||||
|
||||
Ok(DatabaseInvocation {
|
||||
type_name: input.ident,
|
||||
visibility: input.vis,
|
||||
db_name: string_lit.value(),
|
||||
structure: structure,
|
||||
connection_type: inner_type,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn database_attr(attr: TokenStream, input: TokenStream) -> Result<TokenStream> {
|
||||
let invocation = parse_invocation(attr, input)?;
|
||||
|
||||
let connection_type = &invocation.connection_type;
|
||||
let database_name = &invocation.db_name;
|
||||
let request_guard_type = &invocation.type_name;
|
||||
let request_guard_vis = &invocation.visibility;
|
||||
let pool_type = Ident::new(&format!("{}Pool", request_guard_type), request_guard_type.span());
|
||||
|
||||
let tokens = quote! {
|
||||
#request_guard_vis struct #request_guard_type(
|
||||
pub ::rocket_contrib::databases::r2d2::PooledConnection<<#connection_type as ::rocket_contrib::databases::Poolable>::Manager>
|
||||
);
|
||||
#request_guard_vis struct #pool_type(
|
||||
::rocket_contrib::databases::r2d2::Pool<<#connection_type as ::rocket_contrib::databases::Poolable>::Manager>
|
||||
);
|
||||
|
||||
impl #request_guard_type {
|
||||
pub fn fairing() -> impl ::rocket::fairing::Fairing {
|
||||
use ::rocket_contrib::databases::Poolable;
|
||||
|
||||
::rocket::fairing::AdHoc::on_attach(|rocket| {
|
||||
let pool = ::rocket_contrib::databases::database_config(#database_name, rocket.config())
|
||||
.map(#connection_type::pool);
|
||||
|
||||
match pool {
|
||||
Ok(Ok(p)) => Ok(rocket.manage(#pool_type(p))),
|
||||
Err(config_error) => {
|
||||
::rocket::logger::log_err(&format!("Error while instantiating database: '{}': {}", #database_name, config_error));
|
||||
Err(rocket)
|
||||
},
|
||||
Ok(Err(pool_error)) => {
|
||||
::rocket::logger::log_err(&format!("Error initializing pool for '{}': {:?}", #database_name, pool_error));
|
||||
Err(rocket)
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Retrieves a connection of type `Self` from the `rocket` instance. Returns `Some` as long as
|
||||
/// `Self::fairing()` has been attached and there is at least one connection in the pool.
|
||||
pub fn get_one(rocket: &::rocket::Rocket) -> Option<Self> {
|
||||
rocket.state::<#pool_type>()
|
||||
.and_then(|pool| pool.0.get().ok())
|
||||
.map(#request_guard_type)
|
||||
}
|
||||
}
|
||||
|
||||
impl ::std::ops::Deref for #request_guard_type {
|
||||
type Target = #connection_type;
|
||||
|
||||
#[inline(always)]
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'r> ::rocket::request::FromRequest<'a, 'r> for #request_guard_type {
|
||||
type Error = ();
|
||||
|
||||
fn from_request(request: &'a ::rocket::request::Request<'r>) -> ::rocket::request::Outcome<Self, Self::Error> {
|
||||
let pool = request.guard::<::rocket::State<#pool_type>>()?;
|
||||
|
||||
match pool.0.get() {
|
||||
Ok(conn) => ::rocket::Outcome::Success(#request_guard_type(conn)),
|
||||
Err(_) => ::rocket::Outcome::Failure((::rocket::http::Status::ServiceUnavailable, ())),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(tokens.into())
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
#![feature(proc_macro_span, proc_macro_diagnostic)]
|
||||
#![recursion_limit="256"]
|
||||
|
||||
//! # Rocket Contrib - Code Generation
|
||||
//! This crate implements the code generation portion of the Rocket Contrib
|
||||
//! crate. This is for officially sanctioned contributor libraries that require
|
||||
//! code generation of some kind.
|
||||
//!
|
||||
//! This crate includes custom derives and procedural macros and will expand
|
||||
//! as-needed if future `rocket_contrib` features require code generation
|
||||
//! facilities.
|
||||
//!
|
||||
//! ## Procedural Macros
|
||||
//!
|
||||
//! This crate implements the following procedural macros:
|
||||
//!
|
||||
//! * **databases**
|
||||
//!
|
||||
//! The syntax for the `databases` macro is:
|
||||
//!
|
||||
//! <pre>
|
||||
//! macro := database(DATABASE_NAME)
|
||||
//! DATABASE_NAME := (string literal)
|
||||
//! </pre>
|
||||
|
||||
extern crate syn;
|
||||
extern crate proc_macro;
|
||||
extern crate proc_macro2;
|
||||
#[macro_use] extern crate quote;
|
||||
|
||||
mod spanned;
|
||||
|
||||
#[cfg(feature = "database_attribute")]
|
||||
mod database;
|
||||
|
||||
#[allow(dead_code)]
|
||||
use proc_macro::TokenStream;
|
||||
|
||||
#[cfg(feature = "database_attribute")]
|
||||
#[proc_macro_attribute]
|
||||
/// The procedural macro for the `databases` annotation.
|
||||
pub fn database(attr: TokenStream, input: TokenStream) -> TokenStream {
|
||||
::database::database_attr(attr, input).unwrap_or_else(|diag| {
|
||||
diag.emit();
|
||||
TokenStream::new()
|
||||
})
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
use proc_macro::Span;
|
||||
|
||||
use quote::ToTokens;
|
||||
|
||||
pub trait Spanned {
|
||||
fn span(&self) -> Span;
|
||||
}
|
||||
|
||||
// FIXME: Remove this once proc_macro's stabilize.
|
||||
impl<T: ToTokens> Spanned for T {
|
||||
fn span(&self) -> Span {
|
||||
let token_stream = self.into_token_stream();
|
||||
let mut iter = token_stream.into_iter();
|
||||
let mut span = match iter.next() {
|
||||
Some(tt) => tt.span().unstable(),
|
||||
None => {
|
||||
return Span::call_site();
|
||||
}
|
||||
};
|
||||
|
||||
for tt in iter {
|
||||
if let Some(joined) = span.join(tt.span().unstable()) {
|
||||
span = joined;
|
||||
}
|
||||
}
|
||||
|
||||
span
|
||||
}
|
||||
}
|
|
@ -17,6 +17,16 @@ msgpack = ["serde", "rmp-serde"]
|
|||
tera_templates = ["tera", "templates"]
|
||||
handlebars_templates = ["handlebars", "templates"]
|
||||
static_files = []
|
||||
database_pool_codegen = ["rocket_contrib_codegen", "rocket_contrib_codegen/database_attribute"]
|
||||
database_pool = ["r2d2", "database_pool_codegen"]
|
||||
diesel_pg_pool = ["database_pool", "diesel/postgres", "diesel/r2d2"]
|
||||
diesel_sqlite_pool = ["database_pool", "diesel/sqlite", "diesel/r2d2"]
|
||||
diesel_mysql_pool = ["database_pool", "diesel/mysql", "diesel/r2d2"]
|
||||
postgres_pool = ["database_pool", "postgres", "r2d2_postgres"]
|
||||
mysql_pool = ["database_pool", "mysql", "r2d2_mysql"]
|
||||
sqlite_pool = ["database_pool", "rusqlite", "r2d2_sqlite"]
|
||||
cypher_pool = ["database_pool", "rusted_cypher", "r2d2_cypher"]
|
||||
redis_pool = ["database_pool", "redis", "r2d2_redis"]
|
||||
|
||||
# Internal use only.
|
||||
templates = ["serde", "serde_json", "glob"]
|
||||
|
@ -38,6 +48,23 @@ handlebars = { version = "1.0", optional = true }
|
|||
glob = { version = "0.2", optional = true }
|
||||
tera = { version = "0.11", optional = true }
|
||||
|
||||
# Database dependencies
|
||||
diesel = { version = "1.0", default-features = false, optional = true }
|
||||
postgres = { version = "0.15", optional = true }
|
||||
r2d2 = { version = "0.8", optional = true }
|
||||
r2d2_postgres = { version = "0.14", optional = true }
|
||||
mysql = { version = "14", optional = true }
|
||||
r2d2_mysql = { version = "9", optional = true }
|
||||
rusqlite = { version = "0.13.0", optional = true }
|
||||
r2d2_sqlite = { version = "0.5", optional = true }
|
||||
rusted_cypher = { version = "1", optional = true }
|
||||
r2d2_cypher = { version = "0.4", optional = true }
|
||||
redis = { version = "0.8", optional = true }
|
||||
r2d2_redis = { version = "0.7", optional = true }
|
||||
|
||||
# Contrib codegen dependencies
|
||||
rocket_contrib_codegen = { path = "../codegen", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
rocket_codegen = { version = "0.4.0-dev", path = "../../core/codegen" }
|
||||
|
||||
|
|
|
@ -0,0 +1,917 @@
|
|||
//! # 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/<id>")]
|
||||
//! fn get_logs(conn: LogsDbConn, id: LogId) -> Result<Logs> {
|
||||
//! 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<String, Value>,
|
||||
}
|
||||
|
||||
/// 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<T> {
|
||||
/// 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<DatabaseConfig<'a>, 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<Self, foo::Error> {
|
||||
/// ...
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// struct FooConnectionManager { ... };
|
||||
///
|
||||
/// impl FooConnectionManager {
|
||||
/// pub fn new(...) -> Result<Self, foo::Error> {
|
||||
/// ...
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// 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<foo::Error>;
|
||||
///
|
||||
/// fn pool(config: DatabaseConfig) -> Result<r2d2::Pool<Self::Manager>, 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<Connection=Self>;
|
||||
/// 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<r2d2::Pool<Self::Manager>, Self::Error>;
|
||||
}
|
||||
|
||||
#[cfg(feature = "diesel_sqlite_pool")]
|
||||
impl Poolable for diesel::SqliteConnection {
|
||||
type Manager = diesel::r2d2::ConnectionManager<diesel::SqliteConnection>;
|
||||
type Error = r2d2::Error;
|
||||
|
||||
fn pool(config: DatabaseConfig) -> Result<r2d2::Pool<Self::Manager>, 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<diesel::PgConnection>;
|
||||
type Error = r2d2::Error;
|
||||
|
||||
fn pool(config: DatabaseConfig) -> Result<r2d2::Pool<Self::Manager>, 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<diesel::MysqlConnection>;
|
||||
type Error = r2d2::Error;
|
||||
|
||||
fn pool(config: DatabaseConfig) -> Result<r2d2::Pool<Self::Manager>, 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<postgres::Error>;
|
||||
|
||||
fn pool(config: DatabaseConfig) -> Result<r2d2::Pool<Self::Manager>, 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<r2d2::Pool<Self::Manager>, 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<r2d2::Pool<Self::Manager>, 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<r2d2::Pool<Self::Manager>, 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<redis::RedisError>;
|
||||
|
||||
fn pool(config: DatabaseConfig) -> Result<r2d2::Pool<Self::Manager>, 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<String, Value> = 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);
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
#![feature(use_extern_macros)]
|
||||
#![feature(crate_visibility_modifier)]
|
||||
#![feature(never_type)]
|
||||
|
||||
// TODO: Version URLs.
|
||||
#![doc(html_root_url = "https://api.rocket.rs")]
|
||||
|
@ -23,6 +24,7 @@
|
|||
//! * [handlebars_templates](struct.Template.html)
|
||||
//! * [tera_templates](struct.Template.html)
|
||||
//! * [uuid](struct.Uuid.html)
|
||||
//! * [database_pool](databases/index.html)
|
||||
//!
|
||||
//! The recommend way to include features from this crate via Cargo in your
|
||||
//! project is by adding a `[dependencies.rocket_contrib]` section to your
|
||||
|
@ -86,3 +88,15 @@ pub use uuid::{Uuid, UuidParseError};
|
|||
|
||||
#[cfg(feature = "static_files")]
|
||||
pub mod static_files;
|
||||
|
||||
#[cfg(feature = "database_pool")]
|
||||
pub mod databases;
|
||||
|
||||
#[cfg(feature = "database_pool_codegen")]
|
||||
#[allow(unused_imports)]
|
||||
#[macro_use]
|
||||
extern crate rocket_contrib_codegen;
|
||||
|
||||
#[cfg(feature = "database_pool_codegen")]
|
||||
#[doc(hidden)]
|
||||
pub use rocket_contrib_codegen::*;
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
#![feature(use_extern_macros)]
|
||||
|
||||
extern crate rocket;
|
||||
extern crate rocket_contrib;
|
||||
|
||||
#[cfg(feature = "databases")]
|
||||
mod databases_tests {
|
||||
use rocket_contrib::databases::{database, diesel};
|
||||
|
||||
#[database("foo")]
|
||||
struct TempStorage(diesel::SqliteConnection);
|
||||
|
||||
#[database("bar")]
|
||||
struct PrimaryDb(diesel::PgConnection);
|
||||
}
|
|
@ -208,3 +208,10 @@ crate fn pop_max_level() {
|
|||
pub fn init(level: LoggingLevel) -> bool {
|
||||
try_init(level, true)
|
||||
}
|
||||
|
||||
// This method exists as a shim for the log macros that need to be called from
|
||||
// an end user's code. It was added as part of the work to support database
|
||||
// connection pools via procedural macros.
|
||||
pub fn log_err(msg: &str) {
|
||||
error!("{}", msg);
|
||||
}
|
||||
|
|
|
@ -19,4 +19,4 @@ rand = "0.5"
|
|||
[dependencies.rocket_contrib]
|
||||
path = "../../contrib/lib"
|
||||
default_features = false
|
||||
features = [ "tera_templates" ]
|
||||
features = [ "tera_templates", "database_pool", "diesel_sqlite_pool" ]
|
||||
|
|
|
@ -1,2 +1,5 @@
|
|||
[global]
|
||||
template_dir = "static"
|
||||
|
||||
[global.databases.sqlite_database]
|
||||
url = "db/db.sqlite"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#! /usr/bin/env bash
|
||||
|
||||
SCRIPT_PATH=$(cd "$(dirname "$0")" ; pwd -P)
|
||||
DATABASE_URL="${SCRIPT_PATH}/db/db.sql"
|
||||
DATABASE_URL="${SCRIPT_PATH}/db/db.sqlite"
|
||||
|
||||
pushd "${SCRIPT_PATH}" > /dev/null
|
||||
# clear an existing database
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
use std::ops::Deref;
|
||||
|
||||
use diesel::sqlite::SqliteConnection;
|
||||
use diesel::r2d2::{ConnectionManager, Pool, PooledConnection};
|
||||
|
||||
use rocket::http::Status;
|
||||
use rocket::request::{self, FromRequest};
|
||||
use rocket::{Request, State, Outcome};
|
||||
|
||||
pub type SqlitePool = Pool<ConnectionManager<SqliteConnection>>;
|
||||
|
||||
pub const DATABASE_URL: &'static str = concat!(env!("CARGO_MANIFEST_DIR"), "/db/db.sql");
|
||||
|
||||
pub fn init_pool() -> SqlitePool {
|
||||
let manager = ConnectionManager::<SqliteConnection>::new(DATABASE_URL);
|
||||
Pool::new(manager).expect("db pool")
|
||||
}
|
||||
|
||||
pub struct Conn(pub PooledConnection<ConnectionManager<SqliteConnection>>);
|
||||
|
||||
impl Deref for Conn {
|
||||
type Target = SqliteConnection;
|
||||
|
||||
#[inline(always)]
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'r> FromRequest<'a, 'r> for Conn {
|
||||
type Error = ();
|
||||
|
||||
fn from_request(request: &'a Request<'r>) -> request::Outcome<Conn, ()> {
|
||||
let pool = request.guard::<State<SqlitePool>>()?;
|
||||
match pool.get() {
|
||||
Ok(conn) => Outcome::Success(Conn(conn)),
|
||||
Err(_) => Outcome::Failure((Status::ServiceUnavailable, ()))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
#![feature(plugin, decl_macro, const_fn)]
|
||||
#![feature(plugin, decl_macro, use_extern_macros, custom_derive, const_fn)]
|
||||
#![plugin(rocket_codegen)]
|
||||
|
||||
#[macro_use] extern crate rocket;
|
||||
|
@ -8,31 +8,35 @@ extern crate rocket_contrib;
|
|||
|
||||
mod static_files;
|
||||
mod task;
|
||||
mod db;
|
||||
#[cfg(test)] mod tests;
|
||||
|
||||
use rocket::Rocket;
|
||||
use rocket::request::{Form, FlashMessage};
|
||||
use rocket::response::{Flash, Redirect};
|
||||
use rocket_contrib::Template;
|
||||
use rocket_contrib::databases::database;
|
||||
use diesel::SqliteConnection;
|
||||
|
||||
use task::{Task, Todo};
|
||||
|
||||
#[database("sqlite_database")]
|
||||
pub struct DbConn(SqliteConnection);
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct Context<'a, 'b>{ msg: Option<(&'a str, &'b str)>, tasks: Vec<Task> }
|
||||
|
||||
impl<'a, 'b> Context<'a, 'b> {
|
||||
pub fn err(conn: &db::Conn, msg: &'a str) -> Context<'static, 'a> {
|
||||
pub fn err(conn: &DbConn, msg: &'a str) -> Context<'static, 'a> {
|
||||
Context{msg: Some(("error", msg)), tasks: Task::all(conn)}
|
||||
}
|
||||
|
||||
pub fn raw(conn: &db::Conn, msg: Option<(&'a str, &'b str)>) -> Context<'a, 'b> {
|
||||
pub fn raw(conn: &DbConn, msg: Option<(&'a str, &'b str)>) -> Context<'a, 'b> {
|
||||
Context{msg: msg, tasks: Task::all(conn)}
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/", data = "<todo_form>")]
|
||||
fn new(todo_form: Form<Todo>, conn: db::Conn) -> Flash<Redirect> {
|
||||
fn new(todo_form: Form<Todo>, conn: DbConn) -> Flash<Redirect> {
|
||||
let todo = todo_form.into_inner();
|
||||
if todo.description.is_empty() {
|
||||
Flash::error(Redirect::to("/"), "Description cannot be empty.")
|
||||
|
@ -44,7 +48,7 @@ fn new(todo_form: Form<Todo>, conn: db::Conn) -> Flash<Redirect> {
|
|||
}
|
||||
|
||||
#[put("/<id>")]
|
||||
fn toggle(id: i32, conn: db::Conn) -> Result<Redirect, Template> {
|
||||
fn toggle(id: i32, conn: DbConn) -> Result<Redirect, Template> {
|
||||
if Task::toggle_with_id(id, &conn) {
|
||||
Ok(Redirect::to("/"))
|
||||
} else {
|
||||
|
@ -53,7 +57,7 @@ fn toggle(id: i32, conn: db::Conn) -> Result<Redirect, Template> {
|
|||
}
|
||||
|
||||
#[delete("/<id>")]
|
||||
fn delete(id: i32, conn: db::Conn) -> Result<Flash<Redirect>, Template> {
|
||||
fn delete(id: i32, conn: DbConn) -> Result<Flash<Redirect>, Template> {
|
||||
if Task::delete_with_id(id, &conn) {
|
||||
Ok(Flash::success(Redirect::to("/"), "Todo was deleted."))
|
||||
} else {
|
||||
|
@ -62,27 +66,25 @@ fn delete(id: i32, conn: db::Conn) -> Result<Flash<Redirect>, Template> {
|
|||
}
|
||||
|
||||
#[get("/")]
|
||||
fn index(msg: Option<FlashMessage>, conn: db::Conn) -> Template {
|
||||
fn index(msg: Option<FlashMessage>, conn: DbConn) -> Template {
|
||||
Template::render("index", &match msg {
|
||||
Some(ref msg) => Context::raw(&conn, Some((msg.name(), msg.msg()))),
|
||||
None => Context::raw(&conn, None),
|
||||
})
|
||||
}
|
||||
|
||||
fn rocket() -> (Rocket, Option<db::Conn>) {
|
||||
let pool = db::init_pool();
|
||||
let conn = if cfg!(test) {
|
||||
Some(db::Conn(pool.get().expect("database connection for testing")))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
fn rocket() -> (Rocket, Option<DbConn>) {
|
||||
let rocket = rocket::ignite()
|
||||
.manage(pool)
|
||||
.attach(DbConn::fairing())
|
||||
.mount("/", routes![index, static_files::all])
|
||||
.mount("/todo", routes![new, toggle, delete])
|
||||
.attach(Template::fairing());
|
||||
|
||||
let conn = match cfg!(test) {
|
||||
true => DbConn::get_one(&rocket),
|
||||
false => None,
|
||||
};
|
||||
|
||||
(rocket, conn)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
use diesel;
|
||||
use diesel::prelude::*;
|
||||
use diesel::sqlite::SqliteConnection;
|
||||
use diesel::{self, prelude::*};
|
||||
|
||||
mod schema {
|
||||
table! {
|
||||
|
|
Loading…
Reference in New Issue