Page Menu
Home
DevCentral
Search
Configure Global Search
Log In
Files
F11709026
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
25 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/Cargo.toml b/Cargo.toml
index 8c2e78f..7224a0f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,37 +1,37 @@
[package]
name = "limiting-factor"
-version = "0.7.0"
+version = "0.7.1"
authors = [
"Sébastien Santoro <dereckson@espace-win.org>",
]
description = "Library to create a REST API with Diesel and Rocket"
readme = "README.md"
keywords = [
"Diesel",
"API",
"Rocket",
"REST",
]
categories = [
"web-programming",
]
license = "BSD-2-Clause"
repository = "https://devcentral.nasqueron.org/source/limiting-factor/"
[dependencies]
diesel = { version = "^1.0.0", features = ["postgres", "r2d2", "chrono"], optional = true }
dotenv = "0.9.0"
log = "^0.4.4"
r2d2 = { version = "^0.8.2", optional = true }
rocket = "^0.4.0"
rocket_contrib = { version = "^0.4.0", features = [ "json" ] }
serde = { version = "1.0", optional = true }
[features]
default = ["minimal"]
minimal = ["serialization"]
full = ["pgsql", "serialization"]
pgsql = ["diesel", "r2d2"]
serialization = ["serde"]
diff --git a/src/api/replies.rs b/src/api/replies.rs
index 73effa3..78fb5fa 100644
--- a/src/api/replies.rs
+++ b/src/api/replies.rs
@@ -1,221 +1,218 @@
//! # API standard and JSON responses.
//!
//! This module provides useful traits and methods to craft API replies from an existing type.
#[cfg(feature = "pgsql")]
use diesel::result::{DatabaseErrorInformation, DatabaseErrorKind, QueryResult};
#[cfg(feature = "pgsql")]
use diesel::result::Error as ResultError;
use rocket::http::Status;
use rocket_contrib::json::Json;
#[cfg(feature = "serialization")]
use serde::Serialize;
-#[cfg(feature = "pgsql")]
-use std::error::Error;
-
/* -------------------------------------------------------------
Custom types
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
pub type ApiJsonResponse<T> = Result<Json<T>, Status>;
/* -------------------------------------------------------------
API Response
:: Implementation for QueryResult (Diesel ORM)
:: Implementation for Json (Rocket contrib)
:: Implementation for Serialize (Serde)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/// This trait allows to consume an object into an HTTP response.
pub trait ApiResponse<T> {
/// Consumes the value and creates a JSON or a Status result response.
fn into_json_response(self) -> ApiJsonResponse<T>;
}
#[cfg(feature = "pgsql")]
impl<T> ApiResponse<T> for QueryResult<T> {
/// Prepares an API response from a query result.
///
/// The result is the data structure prepared by the Diesel ORM after a SELECT query
/// with one result, for example using `first` method. You can also you use it to
/// parse the returning result (... RETURNING *), which is a default for Diesel after
/// an INSERT query.
///
/// So result can be:
/// - Ok(T)
/// - Err(E) where E is a Status containing an HTTP error code according the situation
///
/// # Examples
///
/// To offer a /player/foo route to serve player information from the players table:
///
/// ```
/// use limiting_factor::api::ApiResponse;
/// use limiting_factor::api::ApiJsonResponse;
///
/// #[get("/player/<name>")]
/// pub fn get_player(connection: DatabaseConnection, name: String) -> ApiJsonResponse<Player> {
/// players
/// .filter(username.eq(&name))
/// .first::<Player>(&*connection)
/// .into_json_response()
/// }
/// ```
///
/// This will produce a JSON representation when the result is found,
/// a 404 error when no result is found, a 500 error if there is a database issue.
///
/// To insert a new player in the same table:
///
/// ```
/// use limiting_factor::api::ApiResponse;
/// use limiting_factor::api::ApiJsonResponse;
///
/// #[post("/register", format="application/json", data="<user>")]
/// pub fn register(connection: DatabaseConnection, user: Json<UserToRegister>) -> ApiJsonResponse<Player> {
/// let user: UserToRegister = user.into_inner();
/// let player_to_create = user.to_new_player();
///
/// diesel::insert_into(players)
/// .values(&player_to_create)
/// .get_result::<Player>(&*connection)
/// .into_json_response()
/// }
/// ```
///
/// This will produce a JSON representation of the newly inserted player if successful.
/// If the insert fails because of an unique constraint violation (e.g. an username already
/// taken), it returns a 409 Conflict.
/// If the failure is from a foreign key integrity constraint, it returns a 400.
/// If there is any other database issue, it returns a 500.
fn into_json_response(self) -> ApiJsonResponse<T> {
self
// CASE I - The query returns one value, we return a JSON representation fo the item
.map(|item| Json(item))
.map_err(|error| match error {
// Case II - The query returns no result, we return a 404 Not found response
ResultError::NotFound => Status::NotFound,
// Case III - We need to handle a database error, which could be a 400/409/500
ResultError::DatabaseError(kind, details) => {
build_database_error_response(kind, details)
}
// Case IV - The error is probably server responsibility, log it and throw a 500
_ => error.into_failure_response(),
})
}
}
/// Prepares an API response from a JSON.
impl<T> ApiResponse<T> for Json<T> {
fn into_json_response(self) -> ApiJsonResponse<T> {
Ok(self)
}
}
/// Prepares an API response from a Serde-serializable result.
///
/// This is probably the easiest way to convert most struct
/// into API responders.
///
/// # Examples
///
#[cfg(feature = "serialization")]
impl<T> ApiResponse<T> for T
where T: Serialize
{
fn into_json_response(self) -> ApiJsonResponse<T> {
Ok(Json(self))
}
}
/* -------------------------------------------------------------
API Delete Response
:: Implementation for QueryResult (Diesel ORM)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
#[cfg(feature = "pgsql")]
/// This trait allows to consume an object into an HTTP response.
///
/// This response is a odd case for DELETE queries, which return
/// a scalar with the rows deleted count value, or an error.
pub trait ApiDeleteResponse<T> {
/// Consumes the value and creates a JSON or a Status result response.
fn into_delete_json_response(self) -> ApiJsonResponse<()>;
}
#[cfg(feature = "pgsql")]
impl ApiDeleteResponse<usize> for QueryResult<usize> {
fn into_delete_json_response(self) -> ApiJsonResponse<()> {
match self {
Ok(0) => Err(Status::NotFound),
Ok(1) => Ok(Json(())),
_ => Err(Status::BadRequest),
}
}
}
/* -------------------------------------------------------------
Failure response
:: Implementation for diesel::result::Error
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/// This trait allows to consume an object into an HTTP failure response.
pub trait FailureResponse {
/// Consumes the variable and creates a Failure response .
fn into_failure_response(self) -> Status;
}
#[cfg(feature = "pgsql")]
impl FailureResponse for ResultError {
/// Consumes the error and creates a 500 Internal server error Status response.
fn into_failure_response(self) -> Status {
- build_internal_server_error_response(self.description())
+ build_internal_server_error_response(&self.to_string())
}
}
/* -------------------------------------------------------------
Helper methods to prepare API responses
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
#[deprecated(since="0.6.0", note="Use directly Status::NotFound instead.")]
pub fn build_not_found_response() -> Status {
Status::NotFound
}
#[deprecated(since="0.6.0", note="Use directly Status::BadRequest instead.")]
pub fn build_bad_request_response() -> Status {
Status::BadRequest
}
pub fn build_internal_server_error_response(message: &str) -> Status {
warn!(target:"api", "{}", message);
Status::InternalServerError
}
#[cfg(feature = "pgsql")]
fn build_database_error_response(error_kind: DatabaseErrorKind, info: Box<dyn DatabaseErrorInformation>) -> Status {
match error_kind {
// Case IIIa - The query tries to do an INSERT violating an unique constraint
// e.g. two INSERT with the same unique value
// We return a 409 Conflict
DatabaseErrorKind::UniqueViolation => Status::Conflict,
// Case IIIb - The query violated a foreign key constraint
// e.g. an INSERT referring to a non existing user 1004
// when there is no id 1004 in users table
// We return a 400 Bad request
DatabaseErrorKind::ForeignKeyViolation => Status::BadRequest,
// Case IIIc - For other databases errors, the client responsibility isn't involved.
_ => build_internal_server_error_response(info.message()),
}
}
diff --git a/src/config.rs b/src/config.rs
index 44a084c..e74667e 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -1,187 +1,186 @@
//! # Service configuration.
//!
//! This module allows to configure the service.
//!
//! It provides a Config trait to build custom configuration implementation.
//!
//! It also provides a `DefaultConfig` implementation of this `Config` trait to
//! extract variables from an .env file or environment.
use dotenv::dotenv;
#[cfg(feature = "pgsql")]
use kernel::DefaultService;
use kernel::{MinimalService, Service};
use rocket::Route;
use std::env;
-use std::error::Error;
use ErrorResult;
/* -------------------------------------------------------------
Config trait
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/// This trait allows to provide a configuration for the resources needed by the API.
pub trait Config {
fn get_database_url(&self) -> &str;
fn get_entry_point(&self) -> &str;
fn get_database_pool_size(&self) -> u32;
fn with_database(&self) -> bool;
fn into_service(self, routes: Vec<Route>) -> Box<dyn Service>;
}
/* -------------------------------------------------------------
EnvironmentConfigurable
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/// This trait allows to configure the object from the environment
pub trait EnvironmentConfigurable {
fn parse_environment() -> ErrorResult<Self> where Self: Sized;
}
/* -------------------------------------------------------------
DefaultConfig
:: Config
:: sui generis implementation
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/// This is a default implementation of the `Config` trait, which extracts the following variables
/// from an .env file or environment:
///
/// - `API_ENTRY_POINT` (facultative, by default `/`): the mouting point of the API methods
/// - `DATABASE_URL` (mandatory): the URL to connect to your database
/// - `DATABASE_POOL_SIZE` (facultative, by default 4): the number of connections to open
#[cfg(feature = "pgsql")]
pub struct DefaultConfig {
database_url: String,
entry_point: String,
database_pool_size: u32,
with_database: bool,
}
#[cfg(feature = "pgsql")]
impl DefaultConfig {
const DEFAULT_DATABASE_POOL_SIZE: u32 = 4;
}
#[cfg(feature = "pgsql")]
impl Config for DefaultConfig {
fn get_database_url(&self) -> &str { &self.database_url }
fn get_entry_point(&self) -> &str { &self.entry_point }
fn get_database_pool_size(&self) -> u32 { self.database_pool_size }
fn with_database(&self) -> bool { self.with_database }
fn into_service(self, routes: Vec<Route>) -> Box<dyn Service> {
let service = DefaultService {
config: self,
routes: Box::new(routes),
};
Box::new(service)
}
}
#[cfg(feature = "pgsql")]
impl EnvironmentConfigurable for DefaultConfig {
fn parse_environment() -> ErrorResult<Self> {
if let Err(error) = dotenv() {
- warn!(target: "config", "Can't parse .env: {}", error.description());
+ warn!(target: "config", "Can't parse .env: {}", error);
};
let with_database = env::var("LF_DISABLE_DATABASE").is_err();
let database_url = match env::var("DATABASE_URL") {
Ok(url) => url,
Err(e) => {
if with_database {
error!(target: "config", "You need to specify a DATABASE_URL variable in the environment (or .env file).");
return Err(Box::new(e));
}
String::new()
}
};
let entry_point = env::var("API_ENTRY_POINT").unwrap_or(String::from("/"));
let database_pool_size = match env::var("DATABASE_POOL_SIZE") {
Ok(variable) => {
match variable.parse::<u32>() {
Ok(size) => size,
Err(_) => {
warn!(target: "config", "The DATABASE_POOL_SIZE variable must be an unsigned integer.");
DefaultConfig::DEFAULT_DATABASE_POOL_SIZE
},
}
},
Err(_) => DefaultConfig::DEFAULT_DATABASE_POOL_SIZE,
};
Ok(DefaultConfig {
database_url,
entry_point,
database_pool_size,
with_database,
})
}
}
/* -------------------------------------------------------------
MinimalConfig
:: Config
:: sui generis implementation
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/// This is a minimal implementation of the `Config` trait, which extracts the following variables
/// from an .env file or environment:
///
/// - `API_ENTRY_POINT` (facultative, by default `/`): the mouting point of the API methods
///
/// It sets the server not to use a database.
pub struct MinimalConfig {
entry_point: String,
}
impl Config for MinimalConfig {
fn get_database_url(&self) -> &str {
""
}
fn get_entry_point(&self) -> &str {
&self.entry_point
}
fn get_database_pool_size(&self) -> u32 {
0
}
fn with_database(&self) -> bool { false }
fn into_service(self, routes: Vec<Route>) -> Box<dyn Service> {
let service = MinimalService {
config: self,
routes: Box::new(routes),
};
Box::new(service)
}
}
impl EnvironmentConfigurable for MinimalConfig {
fn parse_environment() -> ErrorResult<Self> {
if let Err(error) = dotenv() {
- warn!(target: "config", "Can't parse .env: {}", error.description());
+ warn!(target: "config", "Can't parse .env: {}", error);
};
let entry_point = env::var("API_ENTRY_POINT").unwrap_or(String::from("/"));
Ok(MinimalConfig {
entry_point,
})
}
}
diff --git a/src/database.rs b/src/database.rs
index e5f05f1..d1a232c 100644
--- a/src/database.rs
+++ b/src/database.rs
@@ -1,105 +1,104 @@
//! This module handles a database layer, mainly intended to be used
//! with a web server or framework like Rocket or Iron.
//!
//! It leverages diesel and r2d2.
//!
//! Most code comes from the Rocket manual:
//! https://rocket.rs/guide/state/#databases
use diesel::Connection;
use diesel::pg::PgConnection;
use diesel::r2d2::ConnectionManager;
use diesel::r2d2::Pool;
use diesel::r2d2::PooledConnection;
use ErrorResult;
use r2d2::Error as PoolError;
use rocket::http::Status;
use rocket::Outcome;
use rocket::request::FromRequest;
use rocket::request::Outcome as RequestOutcome;
use rocket::Request;
use rocket::State;
-use std::error::Error;
use std::ops::Deref;
/* -------------------------------------------------------------
Custom types
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
pub type PostgreSQLPool = Pool<ConnectionManager<PgConnection>>;
/* -------------------------------------------------------------
DatabaseConnection
:: FromRequest
:: Deref
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/// Represents an established working database connection from the pool
pub struct DatabaseConnection(pub PooledConnection<ConnectionManager<PgConnection>>);
impl<'a, 'r> FromRequest<'a, 'r> for DatabaseConnection {
type Error = ();
fn from_request(request: &'a Request<'r>) -> RequestOutcome<Self, Self::Error> {
let pool = request.guard::<State<PostgreSQLPool>>()?;
match pool.get() {
Ok(connection) => Outcome::Success(DatabaseConnection(connection)),
Err(error) => {
- warn!(target:"request", "Can't get a connection from the pool: {}", error.description());
+ warn!(target:"request", "Can't get a connection from the pool: {}", error);
Outcome::Failure((Status::ServiceUnavailable, ()))
},
}
}
}
impl Deref for DatabaseConnection {
type Target = PgConnection;
fn deref(&self) -> &Self::Target {
&self.0
}
}
/* -------------------------------------------------------------
Helper methods to get a database connection
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/// Builds a r2d2 database pool, to be used in a request guard or a managed state.
///
/// # Examples
///
/// ```
/// rocket::ignite()
/// .manage(initialize_database_pool(String::from("postgres://::1/test"), 4)?)
/// .mount("/", routes)
/// .launch();
/// ```
pub fn initialize_database_pool(url: &str, max_size: u32) -> Result<PostgreSQLPool, PoolError> {
let manager = ConnectionManager::<PgConnection>::new(url);
Pool::builder()
.max_size(max_size)
.build(manager)
}
/// Allows to test if it's possible to establish a connection to the database.
///
/// The goal is to test early any issue with the connection, and loudly warn or fail
/// if the database can't be reached.
///
/// # Examples
///
/// ```
/// // Initial connection to test if the database configuration works
/// {
/// test_database_connection(&config.database_url)?;
/// info!(target: "runner", "Connection to database established.");
/// }
/// ```
pub fn test_database_connection(database_url: &str) -> ErrorResult<()> {
PgConnection::establish(database_url)?;
Ok(())
}
diff --git a/src/kernel.rs b/src/kernel.rs
index b45fcb8..3ed1545 100644
--- a/src/kernel.rs
+++ b/src/kernel.rs
@@ -1,240 +1,240 @@
//! # Service execution utilities.
//!
//! Provides methods to start the server and handle the application
use config::{Config, MinimalConfig};
#[cfg(feature = "pgsql")]
use config::DefaultConfig;
#[cfg(feature = "pgsql")]
use database::{initialize_database_pool, test_database_connection};
use ErrorResult;
use rocket::Route;
use rocket::ignite;
use std::process;
use std::marker::PhantomData;
use config::EnvironmentConfigurable;
/* -------------------------------------------------------------
Service
Allow to define config and routes. Launch a server.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
pub trait Service {
fn get_config(&self) -> &dyn Config;
fn get_routes(&self) -> &[Route];
fn launch_server(&mut self) -> ErrorResult<()>;
fn check_service_configuration(&self) -> ErrorResult<()>;
fn run (&mut self) -> ErrorResult<()> {
info!(target: "runner", "Server started.");
{
self.check_service_configuration()?
}
self.launch_server()?;
Ok(())
}
}
/* -------------------------------------------------------------
Default service
Allow to define config and routes. Launch a server.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/// The default service offers a pgsql database connection with Diesel and r2d2.
#[cfg(feature = "pgsql")]
pub struct DefaultService {
pub config: DefaultConfig,
pub routes: Box<Vec<Route>>,
}
#[cfg(feature = "pgsql")]
impl Service for DefaultService {
fn get_config(&self) -> &dyn Config { &self.config }
fn get_routes(&self) -> &[Route] { self.routes.as_slice() }
fn launch_server(&mut self) -> ErrorResult<()> {
let config = self.get_config();
let routes = self.get_routes();
let mut server = ignite();
if config.with_database() {
server = server.manage(
initialize_database_pool(config.get_database_url(), config.get_database_pool_size())?
);
}
server
.mount(config.get_entry_point(), routes.to_vec())
.launch();
Ok(())
}
fn check_service_configuration(&self) -> ErrorResult<()> {
let config = self.get_config();
if config.with_database() {
test_database_connection(config.get_database_url())?;
info!(target: "runner", "Connection to database established.");
}
Ok(())
}
}
/* -------------------------------------------------------------
Minimal service
Allow to define config and routes. Launch a server.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/// The minimal service allows to spawn a server without any extra feature.
pub struct MinimalService {
pub config: MinimalConfig,
pub routes: Box<Vec<Route>>,
}
impl Service for MinimalService {
fn get_config(&self) -> &dyn Config { &self.config }
fn get_routes(&self) -> &[Route] { self.routes.as_slice() }
fn launch_server(&mut self) -> ErrorResult<()> {
let config = self.get_config();
let routes = self.get_routes();
ignite()
.mount(config.get_entry_point(), routes.to_vec())
.launch();
Ok(())
}
fn check_service_configuration(&self) -> ErrorResult<()> { Ok(()) }
}
/* -------------------------------------------------------------
Base application as concrete implementation
:: Application
:: sui generis implementation
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/// The application structure allows to encapsulate the service into a CLI application.
///
/// The application takes care to run the service and quits with a correct exit code.
///
/// It also takes care of initialisation logic like parse the environment to extract
/// the configuration.
pub struct Application<U>
where U: Config
{
service: Box<dyn Service>,
config_type: PhantomData<U>,
}
impl<U> Application<U>
where U: Config + EnvironmentConfigurable
{
pub fn new (config: U, routes: Vec<Route>) -> Self {
Application {
service: config.into_service(routes),
config_type: PhantomData,
}
}
/// Starts the application
///
/// # Exit codes
///
/// The software will exit with the following error codes:
///
/// - 0: Graceful exit (currently not in use, as the application never stops)
/// - 1: Error during the application run (e.g. routes conflict or Rocket fairings issues)
/// - 2: Error parsing the configuration (e.g. no database URL has been defined)
pub fn start (&mut self) {
info!(target: "runner", "Server initialized.");
if let Err(error) = self.service.run() {
- error!(target: "runner", "{}", error.description());
+ error!(target: "runner", "{}", error);
process::exit(1);
}
process::exit(0);
}
pub fn start_application (routes: Vec<Route>) {
let config = <U>::parse_environment().unwrap_or_else(|_error| {
process::exit(2);
});
let mut app = Application::new(config, routes);
app.start();
}
}
/* -------------------------------------------------------------
Default application
:: Application
:: sui generis implementation, wrapper for Application
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/// The default application implements CLI program behavior to prepare a configuration from the
/// `DefaultConfig` implementation, test if it's possible to connect to the database, and if so,
/// launch a Rocket server.
///
/// # Examples
///
/// To run an application with some routes in a `requests` module:
///
/// ```
/// use limiting_factor::kernel::DefaultApplication;
/// use requests::*;
///
/// pub fn main () {
/// let routes = routes![
/// status,
/// favicon,
/// users::register,
/// users::get_player,
/// ];
///
/// DefaultApplication::start_application(routes);
/// }
/// ```
///
/// The default configuration will be used and the server started.
#[cfg(feature = "pgsql")]
pub struct DefaultApplication {}
#[cfg(feature = "pgsql")]
impl DefaultApplication {
pub fn start_application (routes: Vec<Route>) {
Application::<DefaultConfig>::start_application(routes);
}
}
/* -------------------------------------------------------------
Minimal application
:: Application
:: sui generis implementation, wrapper for Application
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
pub struct MinimalApplication {}
impl MinimalApplication {
pub fn start_application (routes: Vec<Route>) {
Application::<MinimalConfig>::start_application(routes);
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Mon, Sep 15, 08:28 (23 h, 55 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2983912
Default Alt Text
(25 KB)
Attached To
Mode
rLF Limiting Factor
Attached
Detach File
Event Timeline
Log In to Comment