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 @@ -8,6 +8,10 @@ //! 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; @@ -21,6 +25,17 @@ 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) -> Box; +} + +/* ------------------------------------------------------------- + EnvironmentConfigurable + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ + +/// This trait allows to configure the object from the environment +pub trait EnvironmentConfigurable { + fn parse_environment() -> ErrorResult where Self: Sized; } /* ------------------------------------------------------------- @@ -36,39 +51,57 @@ /// - `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) -> Box { + 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 { +#[cfg(feature = "pgsql")] +impl EnvironmentConfigurable for DefaultConfig { + fn parse_environment() -> ErrorResult { 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() } }; @@ -92,7 +125,63 @@ 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) -> Box { + let service = MinimalService { + config: self, + routes: Box::new(routes), + }; + + Box::new(service) + } +} + +impl EnvironmentConfigurable for MinimalConfig { + 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,61 +4,191 @@ use config::Config; use config::DefaultConfig; -use database::initialize_database_pool; -use database::test_database_connection; +use config::MinimalConfig; +#[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>, +} + +#[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>, +} + +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 + where U: Config +{ + service: Box, + config_type: PhantomData, +} + +impl Application + where U: Config + EnvironmentConfigurable +{ + pub fn new (config: U, routes: Vec) -> 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) { + let config = ::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. @@ -84,52 +214,25 @@ /// ``` /// /// 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 { - 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."); + Application::::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) { + Application::::start_application(routes); } } 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 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */