mirror of https://github.com/rwf2/Rocket.git
Implement configuration and environments.
This commit is contained in:
parent
39c979db4c
commit
17b88d0a6b
|
@ -24,4 +24,5 @@ members = [
|
|||
"examples/json",
|
||||
"examples/handlebars_templates",
|
||||
"examples/form_kitchen_sink",
|
||||
"examples/config",
|
||||
]
|
||||
|
|
|
@ -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" }
|
|
@ -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"
|
|
@ -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]);
|
||||
}
|
|
@ -9,3 +9,4 @@ log = "^0.3"
|
|||
hyper = { version = "^0.9", default-features = false }
|
||||
url = "^1"
|
||||
mime = "^0.2"
|
||||
toml = "^0.2"
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
|
@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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,) };
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue