diff --git a/Cargo.toml b/Cargo.toml --- a/Cargo.toml +++ b/Cargo.toml @@ -19,9 +19,15 @@ 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 --- a/src/api/replies.rs +++ b/src/api/replies.rs @@ -2,15 +2,15 @@ //! //! 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; +use std::error::Error; /* ------------------------------------------------------------- Custom types @@ -30,6 +30,7 @@ fn into_json_response(self) -> ApiJsonResponse; } +#[cfg(feature = "pgsql")] impl ApiResponse for QueryResult { /// Prepares an API response from a query result. /// @@ -92,6 +93,7 @@ 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 { @@ -109,6 +111,7 @@ Failure::from(Status::InternalServerError) } +#[cfg(feature = "pgsql")] fn build_database_error_response(error_kind: DatabaseErrorKind, info: Box) -> Failure { match error_kind { // Case IIIa - The query tries to do an INSERT violating an unique constraint diff --git a/src/config.rs b/src/config.rs --- a/src/config.rs +++ b/src/config.rs @@ -21,6 +21,7 @@ fn get_database_url(&self) -> &str; fn get_entry_point(&self) -> &str; fn get_database_pool_size(&self) -> u32; + fn with_database(&self) -> bool; } /* ------------------------------------------------------------- @@ -40,6 +41,7 @@ database_url: String, entry_point: String, database_pool_size: u32, + with_database: bool, } impl Config for DefaultConfig { @@ -54,6 +56,8 @@ fn get_database_pool_size(&self) -> u32 { self.database_pool_size } + + fn with_database(&self) -> bool { self.with_database } } impl DefaultConfig { @@ -64,11 +68,17 @@ 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() } }; @@ -92,7 +102,54 @@ 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 } +} + +impl MinimalConfig { + pub fn parse_environment() -> ErrorResult { + 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 --- a/src/kernel.rs +++ b/src/kernel.rs @@ -4,6 +4,7 @@ use config::Config; use config::DefaultConfig; +use config::MinimalConfig; use database::initialize_database_pool; use database::test_database_connection; use ErrorResult; @@ -26,10 +27,15 @@ 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(); @@ -42,8 +48,10 @@ // Initial connection to test if the database configuration works { let config = self.get_config(); - test_database_connection(config.get_database_url())?; - info!(target: "runner", "Connection to database established."); + if config.with_database() { + test_database_connection(config.get_database_url())?; + info!(target: "runner", "Connection to database established."); + } } self.launch_server()?; @@ -53,12 +61,73 @@ } /* ------------------------------------------------------------- - Default application + Base application as concrete implementation :: Application :: sui generis implementation - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ +/// The base application is a generic Application structure to implement the Application trait +/// with a CLI program behavior to prepare a configuration, for example +/// from environment (like DefaultApplication and MinimalApplication do), +/// check if the configuration works fine and if so launch a Rocket server. +pub struct BaseApplication + where T: Config +{ + config: T, + routes: Box>, +} + +impl Application for BaseApplication + where T: Config +{ + fn get_config(&self) -> &dyn Config { + &self.config + } + + fn get_routes(&self) -> &[Route] { + self.routes.as_slice() + } +} + +impl BaseApplication + where T: Config +{ + pub fn new (config: T, routes: Vec) -> Self { + BaseApplication { + config, + routes: Box::new(routes), + } + } + + /// 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.run() { + error!(target: "runner", "{}", error.description()); + process::exit(1); + } + + process::exit(0); + } +} + +/* ------------------------------------------------------------- + Default application + + :: Application + :: sui generis implementation, wrapper for BaseApplication + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ + /// 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. @@ -84,52 +153,50 @@ /// ``` /// /// The default configuration will be used and the server started. -pub struct DefaultApplication { - config: DefaultConfig, - routes: Box>, -} - -impl Application for DefaultApplication { - fn get_config(&self) -> &dyn Config { - &self.config - } - - fn get_routes(&self) -> &[Route] { - self.routes.as_slice() - } -} +pub struct DefaultApplication {} impl DefaultApplication { - pub fn new (config: DefaultConfig, routes: Vec) -> Self { - DefaultApplication { + pub fn new (config: DefaultConfig, routes: Vec) -> BaseApplication { + BaseApplication { 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) { - info!(target: "runner", "Server initialized."); - let config = DefaultConfig::parse_environment().unwrap_or_else(|_error| { process::exit(2); }); - let mut app = Self::new(config, routes); + let mut app = BaseApplication::new(config, routes); + app.start(); + } +} - if let Err(error) = app.run() { - error!(target: "runner", "{}", error.description()); - process::exit(1); +/* ------------------------------------------------------------- + Minimal application + + :: Application + :: sui generis implementation, wrapper for BaseApplication + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ + +pub struct MinimalApplication {} + +impl MinimalApplication { + pub fn new (config: MinimalConfig, routes: Vec) -> BaseApplication { + BaseApplication { + config, + routes: Box::new(routes), } + } - process::exit(0); + pub fn start_application (routes: Vec) { + let config = MinimalConfig::parse_environment().unwrap_or_else(|_error| { + process::exit(2); + }); + + let mut app = BaseApplication::new(config, routes); + app.start(); } } + diff --git a/src/lib.rs b/src/lib.rs --- a/src/lib.rs +++ b/src/lib.rs @@ -13,14 +13,14 @@ //! A simple server serving a 200 ALIVE response on /status : //! //! ```no_run -//! use limiting_factor::kernel::DefaultApplication; +//! use limiting_factor::kernel::BaseApplication; //! //! pub fn run () { //! let routes = routes![ //! status, //! ]; //! -//! DefaultApplication::start_application(routes); +//! BaseApplication::start_application(routes); //! } //! //! #[get("/status")] @@ -29,9 +29,12 @@ //! } //! ``` +#[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; @@ -42,9 +45,15 @@ 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 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */