Implement configuration and environments.

This commit is contained in:
Sergio Benitez 2016-10-03 03:39:56 -07:00
parent 39c979db4c
commit 17b88d0a6b
12 changed files with 471 additions and 4 deletions

View File

@ -24,4 +24,5 @@ members = [
"examples/json",
"examples/handlebars_templates",
"examples/form_kitchen_sink",
"examples/config",
]

View File

@ -0,0 +1,9 @@
[package]
name = "config"
version = "0.0.1"
authors = ["Sergio Benitez <sb@sergio.bz>"]
workspace = "../../"
[dependencies]
rocket = { path = "../../lib" }
rocket_codegen = { path = "../../codegen" }

View File

@ -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"

View File

@ -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]);
}

View File

@ -9,3 +9,4 @@ log = "^0.3"
hyper = { version = "^0.9", default-features = false }
url = "^1"
mime = "^0.2"
toml = "^0.2"

90
lib/src/config/config.rs Normal file
View File

@ -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<String>,
pub extra: HashMap<String, Value>,
}
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(())
}
}

View File

@ -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<Environment, ConfigError> {
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<Self, Self::Err> {
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"),
}
}
}

66
lib/src/config/error.rs Normal file
View File

@ -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<ParsingError>),
}
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));
}
}
}
}
}

144
lib/src/config/mod.rs Normal file
View File

@ -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<Environment, Config>,
}
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<PathBuf, ConfigError> {
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<RocketConfig, ConfigError> {
// 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<RocketConfig, ConfigError> {
// 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
},
}
}
}

View File

@ -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()
}

View File

@ -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<Self, Self::Err> {
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,) };

View File

@ -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<u16, Catcher>,
log_set: bool,
@ -120,7 +122,7 @@ impl Rocket {
catcher.handle(Error::NoRoute, request).respond(response);
}
pub fn new<S: ToString>(address: S, port: isize) -> Rocket {
pub fn new<S: ToString>(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<Route>) {
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,
}
}
}