From 17b88d0a6b28fbc71981e53c5b80ae5509f01392 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Mon, 3 Oct 2016 03:39:56 -0700 Subject: [PATCH] Implement configuration and environments. --- Cargo.toml | 1 + examples/config/Cargo.toml | 9 +++ examples/config/Rocket.toml | 21 +++++ examples/config/src/main.rs | 13 +++ lib/Cargo.toml | 1 + lib/src/config/config.rs | 90 +++++++++++++++++++++ lib/src/config/environment.rs | 52 ++++++++++++ lib/src/config/error.rs | 66 ++++++++++++++++ lib/src/config/mod.rs | 144 ++++++++++++++++++++++++++++++++++ lib/src/lib.rs | 7 ++ lib/src/logger.rs | 18 ++++- lib/src/rocket.rs | 53 ++++++++++++- 12 files changed, 471 insertions(+), 4 deletions(-) create mode 100644 examples/config/Cargo.toml create mode 100644 examples/config/Rocket.toml create mode 100644 examples/config/src/main.rs create mode 100644 lib/src/config/config.rs create mode 100644 lib/src/config/environment.rs create mode 100644 lib/src/config/error.rs create mode 100644 lib/src/config/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 921cb4b7..bc65db66 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,4 +24,5 @@ members = [ "examples/json", "examples/handlebars_templates", "examples/form_kitchen_sink", + "examples/config", ] diff --git a/examples/config/Cargo.toml b/examples/config/Cargo.toml new file mode 100644 index 00000000..413d7cdc --- /dev/null +++ b/examples/config/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "config" +version = "0.0.1" +authors = ["Sergio Benitez "] +workspace = "../../" + +[dependencies] +rocket = { path = "../../lib" } +rocket_codegen = { path = "../../codegen" } diff --git a/examples/config/Rocket.toml b/examples/config/Rocket.toml new file mode 100644 index 00000000..592ccf9c --- /dev/null +++ b/examples/config/Rocket.toml @@ -0,0 +1,21 @@ +# None of these are actually needed as Rocket has sane defaults for each. We +# show all of them here explicitly for demonstrative purposes. + +[development] +address = "localhost" +port = 8000 +log = "normal" + +[staging] +address = "0.0.0.0" +port = 80 +log = "normal" +# don't use this key! generate your own and keep it private! +session_key = "VheMwXIBygSmOlZAhuWl2B+zgvTN3WW5" + +[production] +address = "0.0.0.0" +port = 80 +log = "critical" +# don't use this key! generate your own and keep it private! +session_key = "adL5fFIPmZBrlyHk2YT4NLV3YCk2gFXz" diff --git a/examples/config/src/main.rs b/examples/config/src/main.rs new file mode 100644 index 00000000..8adf87aa --- /dev/null +++ b/examples/config/src/main.rs @@ -0,0 +1,13 @@ +#![feature(plugin)] +#![plugin(rocket_codegen)] + +extern crate rocket; + +#[get("/")] +fn hello() -> &'static str { + "Hello, world!" +} + +fn main() { + rocket::ignite().mount_and_launch("/hello", routes![hello]); +} diff --git a/lib/Cargo.toml b/lib/Cargo.toml index a9c4e142..b1b6b0d3 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -9,3 +9,4 @@ log = "^0.3" hyper = { version = "^0.9", default-features = false } url = "^1" mime = "^0.2" +toml = "^0.2" diff --git a/lib/src/config/config.rs b/lib/src/config/config.rs new file mode 100644 index 00000000..3c955416 --- /dev/null +++ b/lib/src/config/config.rs @@ -0,0 +1,90 @@ +use super::Environment::*; +use super::Environment; + +use logger::LoggingLevel; +use toml::Value; + +use std::collections::HashMap; + +#[derive(Debug)] +pub struct Config { + pub address: String, + pub port: usize, + pub log_level: LoggingLevel, + pub session_key: Option, + pub extra: HashMap, +} + +macro_rules! parse { + ($val:expr, as_str) => ( + match $val.as_str() { + Some(v) => v, + None => return Err("a string") + } + ); + + ($val:expr, as_integer) => ( + match $val.as_integer() { + Some(v) => v, + None => return Err("an integer") + } + ); +} + +impl Config { + pub fn default_for(env: Environment) -> Config { + match env { + Development => { + Config { + address: "localhost".to_string(), + port: 8000, + log_level: LoggingLevel::Normal, + session_key: None, + extra: HashMap::new(), + } + } + Staging => { + Config { + address: "0.0.0.0".to_string(), + port: 80, + log_level: LoggingLevel::Normal, + session_key: None, + extra: HashMap::new(), + } + } + Production => { + Config { + address: "0.0.0.0".to_string(), + port: 80, + log_level: LoggingLevel::Critical, + session_key: None, + extra: HashMap::new(), + } + } + } + } + + pub fn set(&mut self, name: &str, value: &Value) -> Result<(), &'static str> { + if name == "address" { + self.address = parse!(value, as_str).to_string(); + } else if name == "port" { + self.port = parse!(value, as_integer) as usize; + } else if name == "session_key" { + let key = parse!(value, as_str); + if key.len() != 32 { + return Err("a 192-bit base64 encoded string") + } + + self.session_key = Some(key.to_string()); + } else if name == "log" { + self.log_level = match parse!(value, as_str).parse() { + Ok(level) => level, + Err(_) => return Err("log level ('normal', 'critical', 'debug')"), + }; + } else { + self.extra.insert(name.into(), value.clone()); + } + + Ok(()) + } +} diff --git a/lib/src/config/environment.rs b/lib/src/config/environment.rs new file mode 100644 index 00000000..e8093c13 --- /dev/null +++ b/lib/src/config/environment.rs @@ -0,0 +1,52 @@ +use super::ConfigError; + +use std::fmt; +use std::str::FromStr; +use std::env; + +use self::Environment::*; + +const CONFIG_ENV: &'static str = "ROCKET_ENV"; + +#[derive(Hash, PartialEq, Eq, Debug, Clone, Copy)] +pub enum Environment { + Development, + Staging, + Production, +} + +impl Environment { + pub fn active() -> Result { + let env_str = env::var(CONFIG_ENV).unwrap_or(Development.to_string()); + env_str.parse().map_err(|_| ConfigError::BadEnv(env_str)) + } + + /// Returns a string with a comma-seperated list of valid environments. + pub fn valid() -> &'static str { + "development, staging, production" + } +} + +impl FromStr for Environment { + type Err = (); + fn from_str(s: &str) -> Result { + let env = match s { + "dev" | "devel" | "development" => Development, + "stage" | "staging" => Staging, + "prod" | "production" => Production, + _ => return Err(()), + }; + + Ok(env) + } +} + +impl fmt::Display for Environment { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Development => write!(f, "development"), + Staging => write!(f, "staging"), + Production => write!(f, "production"), + } + } +} diff --git a/lib/src/config/error.rs b/lib/src/config/error.rs new file mode 100644 index 00000000..f64576ca --- /dev/null +++ b/lib/src/config/error.rs @@ -0,0 +1,66 @@ +use super::Environment; + +use term_painter::Color::White; +use term_painter::ToStyle; + +#[derive(Debug)] +pub struct ParsingError { + pub byte_range: (usize, usize), + pub start: (usize, usize), + pub end: (usize, usize), + pub desc: String, +} + +#[derive(Debug)] +pub enum ConfigError { + BadCWD, + NotFound, + IOError, + /// (environment_name) + BadEnv(String), + /// (environment_name, filename) + BadEntry(String, String), + /// (entry_name, expected_type, actual_type, filename) + BadType(String, &'static str, &'static str, String), + /// (toml_source_string, filename, error_list) + ParseError(String, String, Vec), +} + +impl ConfigError { + pub fn pretty_print(&self) { + use self::ConfigError::*; + + let valid_envs = Environment::valid(); + match *self { + BadCWD => error!("couldn't get current working directory"), + NotFound => error!("config file was not found"), + IOError => error!("failed reading the config file: IO error"), + BadEntry(ref name, ref filename) => { + error!("[{}] is not a known configuration environment", name); + info_!("in {}", White.paint(filename)); + info_!("valid environments are: {}", White.paint(valid_envs)); + } + BadEnv(ref name) => { + error!("'{}' is not a valid ROCKET_ENV", name); + info_!("valid environments are: {}", White.paint(valid_envs)); + } + BadType(ref name, ref expected, ref actual, ref filename) => { + error!("'{}' key could not be parsed", name); + info_!("in {}", White.paint(filename)); + info_!("expected value to be {}, but found {}", + White.paint(expected), White.paint(actual)); + } + ParseError(ref source, ref filename, ref errors) => { + for error in errors { + let (lo, hi) = error.byte_range; + let (line, col) = error.start; + let error_source = &source[lo..hi]; + + error!("config file could not be parsed as TOML"); + info_!("at {}:{}:{}", White.paint(filename), line + 1, col + 1); + trace_!("'{}' - {}", error_source, White.paint(&error.desc)); + } + } + } + } +} diff --git a/lib/src/config/mod.rs b/lib/src/config/mod.rs new file mode 100644 index 00000000..1acec8a2 --- /dev/null +++ b/lib/src/config/mod.rs @@ -0,0 +1,144 @@ +mod error; +mod environment; +mod config; + +pub use self::error::{ConfigError, ParsingError}; +pub use self::environment::Environment; + +use toml::{self, Table}; + +use self::Environment::*; +use self::config::Config; + +use std::fs::{self, File}; +use std::collections::HashMap; +use std::io::Read; +use std::path::PathBuf; +use std::env; + +const CONFIG_FILENAME: &'static str = "Rocket.toml"; + +#[derive(Debug)] +pub struct RocketConfig { + pub active_env: Environment, + config: HashMap, +} + +impl RocketConfig { + /// Iteratively search for `file` in `pwd` and its parents, returning the path + /// to the file or an Error::NoKey if the file couldn't be found. + fn find() -> Result { + let cwd = env::current_dir().map_err(|_| ConfigError::BadCWD)?; + let mut current = cwd.as_path(); + + loop { + let manifest = current.join(CONFIG_FILENAME); + if fs::metadata(&manifest).is_ok() { + return Ok(manifest) + } + + match current.parent() { + Some(p) => current = p, + None => break, + } + } + + Err(ConfigError::NotFound) + } + + fn set(&mut self, env: Environment, kvs: &Table, filename: &str) + -> Result<(), ConfigError> { + let config = self.config.entry(env).or_insert(Config::default_for(env)); + for (key, value) in kvs { + if let Err(expected) = config.set(key, value) { + let name = format!("{}.{}", env, key); + return Err(ConfigError::BadType( + name, expected, value.type_str(), filename.to_string() + )) + } + } + + Ok(()) + } + + pub fn get(&self, env: Environment) -> &Config { + if let Some(config) = self.config.get(&env) { + config + } else { + panic!("No value from environment: {:?}", env); + } + } + + pub fn active(&self) -> &Config { + self.get(self.active_env) + } + + fn parse(src: String, filename: &str) -> Result { + // Parse the source as TOML, if possible. + let mut parser = toml::Parser::new(&src); + let toml = parser.parse().ok_or(ConfigError::ParseError( + src.clone(), filename.into(), + parser.errors.iter().map(|error| ParsingError { + byte_range: (error.lo, error.hi), + start: parser.to_linecol(error.lo), + end: parser.to_linecol(error.hi), + desc: error.desc.clone(), + }).collect() + ))?; + + // Create a config with the defaults, but the set the env to the active + let mut config = RocketConfig::default(); + config.active_env = Environment::active()?; + + // Parse the values from the TOML file. + for (entry, value) in toml { + // Parse the environment from the table entry name. + let env = entry.as_str().parse().map_err(|_| { + ConfigError::BadEntry(entry.clone(), filename.into()) + })?; + + // Each environment must be a table. + let kv_pairs = match value.as_table() { + Some(table) => table, + None => return Err(ConfigError::BadType( + entry, "a table", value.type_str(), filename.into() + )) + }; + + // Set the environment configuration from the kv pairs. + config.set(env, &kv_pairs, filename)?; + } + + Ok(config) + } + + pub fn read() -> Result { + // Find the config file, starting from the `cwd` and working backwords. + let file = RocketConfig::find()?; + + // Try to open the config file for reading. + let mut handle = File::open(&file).map_err(|_| ConfigError::IOError)?; + + // Read the configure file to a string for parsing. + let mut contents = String::new(); + handle.read_to_string(&mut contents).map_err(|_| ConfigError::IOError)?; + + // Parse the contents from the file. + RocketConfig::parse(contents, &file.to_string_lossy()) + } +} + +impl Default for RocketConfig { + fn default() -> RocketConfig { + RocketConfig { + active_env: Environment::Development, + config: { + let mut default_config = HashMap::new(); + default_config.insert(Development, Config::default_for(Development)); + default_config.insert(Staging, Config::default_for(Staging)); + default_config.insert(Production, Config::default_for(Production)); + default_config + }, + } + } +} diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 1610778b..336101b7 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -62,6 +62,7 @@ extern crate term_painter; extern crate hyper; extern crate url; extern crate mime; +extern crate toml; #[macro_use] extern crate log; #[doc(hidden)] #[macro_use] pub mod logger; @@ -76,6 +77,7 @@ mod router; mod rocket; mod codegen; mod catcher; +mod config; /// Defines the types for request and error handlers. pub mod handler { @@ -99,3 +101,8 @@ pub use error::Error; pub use router::{Router, Route}; pub use catcher::Catcher; pub use rocket::Rocket; + +/// Alias to Rocket::ignite(). +pub fn ignite() -> Rocket { + Rocket::ignite() +} diff --git a/lib/src/logger.rs b/lib/src/logger.rs index 82a96fd0..a9b6470d 100644 --- a/lib/src/logger.rs +++ b/lib/src/logger.rs @@ -1,5 +1,7 @@ //! Rocket's logging infrastructure. +use std::str::FromStr; + use log::{self, Log, LogLevel, LogRecord, LogMetadata}; use term_painter::Color::*; use term_painter::ToStyle; @@ -7,7 +9,7 @@ use term_painter::ToStyle; struct RocketLogger(LoggingLevel); /// Defines the different levels for log messages. -#[derive(PartialEq)] +#[derive(PartialEq, Eq, Debug, Clone, Copy)] pub enum LoggingLevel { /// Only shows errors and warning. Critical, @@ -28,6 +30,20 @@ impl LoggingLevel { } } +impl FromStr for LoggingLevel { + type Err = (); + fn from_str(s: &str) -> Result { + let level = match s { + "critical" => LoggingLevel::Critical, + "normal" => LoggingLevel::Normal, + "debug" => LoggingLevel::Debug, + _ => return Err(()) + }; + + Ok(level) + } +} + #[macro_export] macro_rules! log_ { ($name:ident: $format:expr) => { log_!($name: $format,) }; diff --git a/lib/src/rocket.rs b/lib/src/rocket.rs index a3b7f493..eaa80b74 100644 --- a/lib/src/rocket.rs +++ b/lib/src/rocket.rs @@ -2,10 +2,12 @@ use super::*; use response::{FreshHyperResponse, Outcome}; use request::HyperRequest; use catcher; +use config::RocketConfig; use std::collections::HashMap; use std::str::from_utf8_unchecked; use std::cmp::min; +use std::process; use term_painter::Color::*; use term_painter::ToStyle; @@ -16,7 +18,7 @@ use hyper::header::SetCookie; pub struct Rocket { address: String, - port: isize, + port: usize, router: Router, catchers: HashMap, log_set: bool, @@ -120,7 +122,7 @@ impl Rocket { catcher.handle(Error::NoRoute, request).respond(response); } - pub fn new(address: S, port: isize) -> Rocket { + pub fn new(address: S, port: usize) -> Rocket { Rocket { address: address.to_string(), port: port, @@ -193,14 +195,59 @@ impl Rocket { } let full_addr = format!("{}:{}", self.address, self.port); + let server = match HyperServer::http(full_addr.as_str()) { + Ok(hyper_server) => hyper_server, + Err(e) => { + error!("failed to start server."); + error_!("{}", e); + process::exit(1); + } + }; + info!("🚀 {} {}...", White.paint("Rocket has launched from"), White.bold().paint(&full_addr)); - let _ = HyperServer::http(full_addr.as_str()).unwrap().handle(self); + + server.handle(self).unwrap(); } pub fn mount_and_launch(mut self, base: &'static str, routes: Vec) { self.mount(base, routes); self.launch(); } + + pub fn ignite() -> Rocket { + use config::ConfigError::*; + let config = match RocketConfig::read() { + Ok(config) => config, + Err(e@ParseError(..)) | Err(e@BadEntry(..)) | + Err(e@BadEnv(..)) | Err(e@BadType(..)) => { + logger::init(LoggingLevel::Debug); + e.pretty_print(); + process::exit(1) + } + Err(IOError) | Err(BadCWD) => { + warn!("error reading Rocket config file; using defaults."); + RocketConfig::default() + } + Err(NotFound) => RocketConfig::default() + }; + + logger::init(config.active().log_level); + info!("🔧 Configured for {}.", config.active_env); + info_!("listening: {}:{}", + White.paint(&config.active().address), + White.paint(&config.active().port)); + info_!("logging: {:?}", White.paint(config.active().log_level)); + info_!("session key: {}", + White.paint(config.active().session_key.is_some())); + + Rocket { + address: config.active().address.clone(), + port: config.active().port, + router: Router::new(), + catchers: catcher::defaults::get(), + log_set: true, + } + } }