Page MenuHomeDevCentral

No OneTemporary

diff --git a/Cargo.toml b/Cargo.toml
index a1e9b53..777f044 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,27 +1,33 @@
[package]
name = "limiting-factor"
version = "0.1.0"
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"] }
+diesel = { version = "^1.0.0", features = ["postgres", "r2d2", "chrono"], optional = true }
dotenv = "0.9.0"
log = "^0.4.4"
-r2d2 = "^0.8.2"
+r2d2 = { version = "^0.8.2", optional = true }
rocket = "^0.3.16"
rocket_contrib = { version = "^0.3.16", features = [ "json" ] }
+
+[features]
+default = ["pgsql"]
+minimal = []
+
+pgsql = ["diesel", "r2d2"]
diff --git a/src/api/replies.rs b/src/api/replies.rs
index 9f15880..189d0e3 100644
--- a/src/api/replies.rs
+++ b/src/api/replies.rs
@@ -1,128 +1,133 @@
//! # API standard and JSON responses.
//!
//! This module provides useful traits and methods to craft API replies from an existing type.
-use std::error::Error;
-
-use diesel::result::DatabaseErrorInformation;
-use diesel::result::DatabaseErrorKind;
+#[cfg(feature = "pgsql")]
+use diesel::result::{DatabaseErrorInformation, DatabaseErrorKind, QueryResult};
+#[cfg(feature = "pgsql")]
use diesel::result::Error as ResultError;
-use diesel::result::QueryResult;
+
use rocket::http::Status;
use rocket::response::Failure;
use rocket_contrib::Json;
+#[cfg(feature = "pgsql")]
+use std::error::Error;
+
/* -------------------------------------------------------------
Custom types
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
pub type ApiJsonResponse<T> = Result<Json<T>, Failure>;
/* -------------------------------------------------------------
API Response
:: Implementation for QueryResult (Diesel ORM)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/// This trait allows to consume an object into an HTTP response.
pub trait ApiResponse<T> {
/// Consumes the value and creates a JSON or a Failure 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(any database error)
///
/// # Examples
///
/// To offer a /player/foo route to serve player information from the player 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.
fn into_json_response(self) -> ApiJsonResponse<T> {
match self {
// CASE I - The query returns one value, we return a JSON representation fo the item
Ok(item) => Ok(Json(item)),
Err(error) => match error {
// Case II - The query returns no result, we return a 404 Not found response
ResultError::NotFound => Err(Failure::from(Status::NotFound)),
// Case III - We need to handle a database error, which could be a 400/409/500
ResultError::DatabaseError(kind, details) => Err(build_database_error_response(kind, details)),
// Case IV - The error is probably server responsbility, log it and throw a 500
_ => Err(error.into_failure_response()),
}
}
}
}
/* -------------------------------------------------------------
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) -> Failure;
}
+#[cfg(feature = "pgsql")]
impl FailureResponse for ResultError {
/// Consumes the error and creates a Failure 500 Internal server error response.
fn into_failure_response(self) -> Failure {
build_internal_server_error_response(self.description())
}
}
/* -------------------------------------------------------------
Helper methods to prepare API responses
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
pub fn build_internal_server_error_response(message: &str) -> Failure {
warn!(target:"api", "{}", message);
Failure::from(Status::InternalServerError)
}
+#[cfg(feature = "pgsql")]
fn build_database_error_response(error_kind: DatabaseErrorKind, info: Box<dyn DatabaseErrorInformation>) -> Failure {
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 => Failure::from(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 => Failure::from(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 1c4fb58..44a084c 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -1,98 +1,187 @@
//! # 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_database_url(&self) -> &str { &self.database_url }
- fn get_entry_point(&self) -> &str {
- &self.entry_point
- }
+ fn get_entry_point(&self) -> &str { &self.entry_point }
- fn get_database_pool_size(&self) -> u32 {
- self.database_pool_size
+ 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)
}
}
-impl DefaultConfig {
- pub const DEFAULT_DATABASE_POOL_SIZE: u32 = 4;
-
- pub fn parse_environment() -> ErrorResult<Self> {
+#[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());
};
+ let with_database = env::var("LF_DISABLE_DATABASE").is_err();
+
let database_url = match env::var("DATABASE_URL") {
Ok(url) => url,
Err(e) => {
- error!(target: "config", "You need to specify a DATABASE_URL variable in the environment (or .env file).");
- return Err(Box::new(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());
+ };
+
+ let entry_point = env::var("API_ENTRY_POINT").unwrap_or(String::from("/"));
+
+ Ok(MinimalConfig {
+ entry_point,
+ })
+ }
+}
diff --git a/src/kernel.rs b/src/kernel.rs
index 46382fa..2f49cef 100644
--- a/src/kernel.rs
+++ b/src/kernel.rs
@@ -1,135 +1,240 @@
//! # Service execution utilities.
//!
//! Provides methods to start the server and handle the application
-use config::Config;
+use config::{Config, MinimalConfig};
+#[cfg(feature = "pgsql")]
use config::DefaultConfig;
-use database::initialize_database_pool;
-use database::test_database_connection;
+#[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;
/* -------------------------------------------------------------
- Application
+ Service
Allow to define config and routes. Launch a server.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
-pub trait Application {
+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();
- ignite()
- .manage(
+ 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 run (&mut self) -> ErrorResult<()> {
- info!(target: "runner", "Server started.");
-
- // Initial connection to test if the database configuration works
- {
- let config = self.get_config();
+ 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.");
}
- self.launch_server()?;
+ 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(()) }
}
/* -------------------------------------------------------------
- Default application
+ 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());
+ 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.
-pub struct DefaultApplication {
- config: DefaultConfig,
- routes: Box<Vec<Route>>,
-}
-
-impl Application for DefaultApplication {
- fn get_config(&self) -> &dyn Config {
- &self.config
- }
-
- fn get_routes(&self) -> &[Route] {
- self.routes.as_slice()
- }
-}
+#[cfg(feature = "pgsql")]
+pub struct DefaultApplication {}
+#[cfg(feature = "pgsql")]
impl DefaultApplication {
- pub fn new (config: DefaultConfig, routes: Vec<Route>) -> Self {
- DefaultApplication {
- config,
- routes: Box::new(routes),
- }
- }
-
- /// Starts the application, prepares default configuration
- ///
- /// # 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_application (routes: Vec<Route>) {
- info!(target: "runner", "Server initialized.");
+ Application::<DefaultConfig>::start_application(routes);
+ }
+}
- let config = DefaultConfig::parse_environment().unwrap_or_else(|_error| {
- process::exit(2);
- });
+/* -------------------------------------------------------------
+ Minimal application
- let mut app = Self::new(config, routes);
+ :: Application
+ :: sui generis implementation, wrapper for Application
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
- if let Err(error) = app.run() {
- error!(target: "runner", "{}", error.description());
- process::exit(1);
- }
+pub struct MinimalApplication {}
- process::exit(0);
+impl MinimalApplication {
+ pub fn start_application (routes: Vec<Route>) {
+ Application::<MinimalConfig>::start_application(routes);
}
}
diff --git a/src/lib.rs b/src/lib.rs
index 12c774c..9a5d291 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,52 +1,64 @@
//! A library with components to implement a REST API.
//!
//! The goal of this crate is to provide:
//!
//! - boilerplate to parse environment and run a Rocket server
//! - glue code for Rocket and Diesel to use a database in the web service
//! - standard API replies
//!
//! That allows an API or a back-end web server to focus on requests and data model.
//!
//! # Examples
//!
//! A simple server serving a 200 ALIVE response on /status :
//!
//! ```no_run
//! use limiting_factor::kernel::DefaultApplication;
//!
//! pub fn run () {
//! let routes = routes![
//! status,
//! ];
//!
//! DefaultApplication::start_application(routes);
//! }
//!
//! #[get("/status")]
//! pub fn status() -> &'static str {
//! "ALIVE"
//! }
//! ```
+//!
+//! Replacing `DefaultApplication` by `MinimalApplication` allows to use a lighter version
+//! of the library without Diesel dependencies or database use.
+#[cfg(feature = "pgsql")]
extern crate diesel;
extern crate dotenv;
-#[macro_use] extern crate log;
+#[macro_use]
+extern crate log;
+#[cfg(feature = "pgsql")]
extern crate r2d2;
extern crate rocket;
extern crate rocket_contrib;
/* -------------------------------------------------------------
Public modules offered by this crate
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
pub mod api;
pub mod config;
-pub mod database;
pub mod kernel;
+/* -------------------------------------------------------------
+ Optional public features modules offered by this crate
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
+
+#[cfg(feature = "pgsql")]
+pub mod database;
+
/* -------------------------------------------------------------
Custom types
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
pub type ErrorResult<T> = Result<T, Box<dyn std::error::Error>>;

File Metadata

Mime Type
text/x-diff
Expires
Sat, Oct 11, 22:59 (1 d, 10 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3064245
Default Alt Text
(23 KB)

Event Timeline