diff --git a/src/config.rs b/src/config.rs index a330405..5e84024 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,186 +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 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) -> Box; } /* ------------------------------------------------------------- EnvironmentConfigurable - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ /// This trait allows to configure the object from the environment pub trait EnvironmentConfigurable { fn parse_environment() -> ErrorResult 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 mounting 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) -> Box { let service = DefaultService { config: self, - routes: Box::new(routes), + routes, }; Box::new(service) } } #[cfg(feature = "pgsql")] impl EnvironmentConfigurable for DefaultConfig { fn parse_environment() -> ErrorResult { if let Err(error) = dotenv() { 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::() { 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 mounting 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), + 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); }; 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 3ed1545..77fdd6f 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>, + pub routes: Vec, } #[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>, + pub routes: Vec, } 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 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); 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. /// /// # 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) { Application::::start_application(routes); } } /* ------------------------------------------------------------- Minimal application :: Application :: sui generis implementation, wrapper for Application - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ pub struct MinimalApplication {} impl MinimalApplication { pub fn start_application (routes: Vec) { Application::::start_application(routes); } }