Page MenuHomeDevCentral

No OneTemporary

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

Mime Type
text/x-diff
Expires
Mon, Sep 15, 08:28 (10 h, 9 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2983912
Default Alt Text
(25 KB)

Event Timeline