use std::path::PathBuf;
use std::sync::Arc;
use rainier_cache::CacheManager;
use rainier_config::{Config, Env};
use rainier_container::{Application, ServiceProvider};
use rainier_crypt::{Encryption, Key, KeyRing};
use rainier_database::Database;
use rainier_events::Dispatcher;
use rainier_filesystem::Storage;
use rainier_mail::Mailer;
use rainier_middleware::MiddlewareRegistry;
use rainier_queue::QueueManager;
use rainier_routing::{Router, UrlGenerator};
use rainier_server::Kernel;
use rainier_session::{MemorySessionStore, SessionManager};
use rainier_support::{Error, Result};
use rainier_view::{BladeEngine, ViewEngine};
use crate::facades::Views;
use crate::keys;
pub struct Rainier {
base_path: PathBuf,
env: Env,
config: Config,
deferred: Option<Error>,
events: Dispatcher,
middleware: MiddlewareRegistry,
router: Router,
views: Option<Arc<dyn ViewEngine>>,
database: Option<Database>,
queue: Option<QueueManager>,
mailer: Option<Mailer>,
sessions: Option<SessionManager>,
crypt: Option<Encryption>,
cache: Option<CacheManager>,
notifier: Option<rainier_notify::Notifier>,
broadcasting: Option<rainier_broadcast::Broadcasting>,
websockets: Option<rainier_websocket::WebSocketRoutes>,
#[allow(clippy::type_complexity, reason = "one boxed closure per bound value")]
instances: Vec<Box<dyn Fn(&Application)>>,
schedule: Option<rainier_scheduler::Schedule>,
storage: Option<Storage>,
providers: Vec<Arc<dyn ServiceProvider>>,
install_facades: bool,
install_tracing: bool,
}
impl Rainier {
pub fn new(base_path: impl Into<PathBuf>) -> Self {
let base_path = base_path.into();
let env = Env::load_or_default(base_path.join(".env"));
let (config, deferred) = match default_config(&env, &base_path) {
Ok(config) => (config, None),
Err(e) => (Config::new(), Some(e)),
};
Self {
base_path,
env,
config,
deferred,
events: Dispatcher::new(),
middleware: default_middleware(),
router: Router::new(),
views: None,
database: None,
queue: None,
mailer: None,
sessions: None,
crypt: None,
cache: None,
notifier: None,
broadcasting: None,
websockets: None,
instances: Vec::new(),
schedule: None,
storage: None,
providers: Vec::new(),
install_facades: true,
install_tracing: true,
}
}
pub fn env(&self) -> &Env {
&self.env
}
pub fn config(&self) -> &Config {
&self.config
}
pub fn configure(self, adjust: impl FnOnce(&Config)) -> Self {
adjust(&self.config);
self
}
pub fn with_events(self, register: impl FnOnce(&Dispatcher)) -> Self {
register(&self.events);
self
}
pub fn with_middleware(self, register: impl FnOnce(&MiddlewareRegistry)) -> Self {
register(&self.middleware);
self
}
pub fn with_routes(mut self, declare: impl FnOnce(&mut Router)) -> Self {
declare(&mut self.router);
self
}
pub fn with_views(mut self, engine: Arc<dyn ViewEngine>) -> Self {
self.views = Some(engine);
self
}
pub fn with_database(mut self, database: Database) -> Self {
self.database = Some(database);
self
}
pub fn with_queue(mut self, queue: QueueManager) -> Self {
self.queue = Some(queue);
self
}
pub fn with_mailer(mut self, mailer: Mailer) -> Self {
self.mailer = Some(mailer);
self
}
pub fn with_sessions(mut self, sessions: SessionManager) -> Self {
self.sessions = Some(sessions);
self
}
pub fn with_cache(mut self, cache: CacheManager) -> Self {
self.cache = Some(cache);
self
}
pub fn with_notifier(mut self, notifier: rainier_notify::Notifier) -> Self {
self.notifier = Some(notifier);
self
}
pub fn with_broadcasting(mut self, broadcasting: rainier_broadcast::Broadcasting) -> Self {
self.broadcasting = Some(broadcasting);
self
}
pub fn with_websockets(mut self, routes: rainier_websocket::WebSocketRoutes) -> Self {
self.websockets = Some(routes);
self
}
pub fn with_instance<T: Send + Sync + 'static>(mut self, value: T) -> Self {
let value = Arc::new(value);
self.instances.push(Box::new(move |app| app.instance_arc(Arc::clone(&value))));
self
}
pub fn with_instance_arc<T: Send + Sync + 'static>(mut self, value: Arc<T>) -> Self {
self.instances.push(Box::new(move |app| app.instance_arc(Arc::clone(&value))));
self
}
pub fn with_schedule(mut self, declare: impl FnOnce(&mut rainier_scheduler::Schedule)) -> Self {
let mut schedule = self.schedule.take().unwrap_or_default();
declare(&mut schedule);
self.schedule = Some(schedule);
self
}
pub fn with_storage(mut self, storage: Storage) -> Self {
self.storage = Some(storage);
self
}
pub fn with_encryption(mut self, crypt: Encryption) -> Self {
self.crypt = Some(crypt);
self
}
pub fn with_provider(mut self, provider: impl ServiceProvider) -> Self {
self.providers.push(Arc::new(provider));
self
}
pub fn without_facades(mut self) -> Self {
self.install_facades = false;
self
}
pub fn without_tracing(mut self) -> Self {
self.install_tracing = false;
self
}
pub async fn boot(self) -> Result<Arc<Application>> {
if let Some(error) = self.deferred {
return Err(error);
}
if self.install_tracing {
install_tracing(&self.env);
}
let environment = self.config.get(keys::APP_ENV).unwrap_or_default().to_string();
let app = Arc::new(Application::new(self.base_path.clone()).with_environment(&environment));
if self.install_facades {
rainier_container::set_facade_application(Arc::clone(&app));
}
let views = self.views.unwrap_or_else(|| {
Arc::new(BladeEngine::new(self.base_path.join("resources").join("views")))
});
let crypt = self.crypt.unwrap_or_else(|| Encryption::from_keys(app_keys(&self.env)));
let sessions = self
.sessions
.unwrap_or_else(|| SessionManager::new(Arc::new(MemorySessionStore::default())));
app.instance(self.config);
app.instance(self.env);
app.instance(self.events);
app.instance(Views::new(views));
app.instance(crypt);
app.instance(sessions);
let cache = self.cache.unwrap_or_else(CacheManager::memory);
app.instance(rainier_cache::LockManager::new(Arc::clone(cache.store())));
app.instance(cache);
app.instance(self.schedule.unwrap_or_default());
for bind in self.instances {
bind(&app);
}
app.instance(self.websockets.unwrap_or_default());
app.instance(self.broadcasting.unwrap_or_else(rainier_broadcast::Broadcasting::log));
app.instance(
self.notifier.unwrap_or_else(|| {
rainier_notify::Notifier::new().with(rainier_notify::LogChannel)
}),
);
app.instance(
self.storage
.unwrap_or_else(|| Storage::local(self.base_path.join("storage").join("app"))),
);
if let Some(database) = self.database {
app.instance(database);
}
if let Some(queue) = self.queue {
app.instance(queue.with_locks(rainier_cache::LockManager::new(Arc::clone(
app.resolve::<CacheManager>()?.store(),
))));
}
if let Some(mailer) = self.mailer {
app.instance(mailer);
}
for provider in self.providers {
app.register_arc(provider)?;
}
let compiled = Arc::new(self.router.compile(&app)?);
let urls = UrlGenerator::from_routes(compiled.named_routes())
.with_base(app.resolve::<Config>()?.get_or(keys::APP_URL, String::new()));
let kernel =
Kernel::from_shared(Arc::clone(&compiled), self.middleware.global_stack(&app)?)
.with_debug(app.resolve::<Config>()?.get_or(keys::APP_DEBUG, false));
app.instance_arc(compiled);
app.instance(urls);
app.instance(self.middleware);
app.instance(kernel);
app.boot().await?;
Ok(app)
}
}
fn default_config(env: &Env, base_path: &std::path::Path) -> Result<Config> {
let config = Config::new();
config.set(keys::APP_NAME, env.string("APP_NAME", "Rainier"))?;
config.set(keys::APP_ENV, env.setting("APP_ENV")?)?;
config.set(keys::APP_DEBUG, env.bool("APP_DEBUG", false))?;
config.set(keys::APP_URL, env.string("APP_URL", "http://localhost:8000"))?;
config.set(keys::APP_BASE_PATH, base_path.to_string_lossy().into_owned())?;
config.set(keys::SERVER_HOST, env.string("SERVER_HOST", "127.0.0.1"))?;
config.set(keys::SERVER_PORT, env.int("SERVER_PORT", 8000) as u16)?;
config.set(keys::SERVER_MAX_BODY_BYTES, env.int("SERVER_MAX_BODY", 2 * 1024 * 1024) as u64)?;
config.set(keys::DATABASE_URL, env.string("DATABASE_URL", "sqlite::memory:"))?;
config.set(keys::CACHE_DRIVER, env.setting("CACHE_DRIVER")?)?;
config.set(keys::CACHE_REDIS_URL, env.string("REDIS_URL", "redis://127.0.0.1:6379/"))?;
config.set(keys::CACHE_MEMCACHED_URL, env.string("MEMCACHED_URL", "127.0.0.1:11211"))?;
config.set(keys::SESSION_DRIVER, env.setting("SESSION_DRIVER")?)?;
config.set(keys::SESSION_LIFETIME, env.int("SESSION_LIFETIME", 7200))?;
config.set(keys::SESSION_COOKIE, env.string("SESSION_COOKIE", "rainier_session"))?;
config.set(keys::SESSION_SECURE, env.bool("SESSION_SECURE", false))?;
config.set(keys::QUEUE_DRIVER, env.setting("QUEUE_DRIVER")?)?;
config.set(keys::QUEUE_DEFAULT, env.string("QUEUE_DEFAULT", "default"))?;
config.set(keys::MAIL_DRIVER, env.setting("MAIL_DRIVER")?)?;
config.set(keys::MAIL_FROM_ADDRESS, env.string("MAIL_FROM", "hello@example.com"))?;
config.set(keys::MAIL_FROM_NAME, env.string("MAIL_FROM_NAME", "Rainier"))?;
Ok(config)
}
fn app_keys(env: &Env) -> KeyRing {
let previous: Vec<String> =
env.get("APP_PREVIOUS_KEYS").unwrap_or_default().split(',').map(str::to_string).collect();
match env.get("APP_KEY").filter(|key| !key.trim().is_empty()) {
Some(key) => match KeyRing::from_base64(&key, &previous) {
Ok(ring) => ring,
Err(e) => {
tracing::error!(
error = %e.message(),
"APP_KEY is not usable; generating a temporary key. Encrypted values \
written before this boot cannot be read, and values written now cannot \
be read after a restart."
);
KeyRing::new(Key::generate())
}
},
None => {
tracing::warn!(
"APP_KEY is not set; generating a temporary key. Set one with \
`APP_KEY={}` or nothing encrypted will survive a restart.",
Key::generate().to_base64()
);
KeyRing::new(Key::generate())
}
}
}
fn default_middleware() -> MiddlewareRegistry {
let registry = MiddlewareRegistry::new();
registry.global(crate::groups::normalise_input());
registry
}
fn install_tracing(env: &Env) {
use tracing_subscriber::{fmt, EnvFilter};
let filter = env.string("RUST_LOG", "info");
let _ = fmt()
.with_env_filter(EnvFilter::try_new(&filter).unwrap_or_else(|_| EnvFilter::new("info")))
.try_init();
}
#[cfg(test)]
mod tests {
use super::*;
use crate::groups;
use rainier_http::{Method, Request, StatusCode};
use rainier_session::SessionRequestExt as _;
use rainier_session::StartSession;
async fn app() -> Arc<Application> {
Rainier::new(".")
.without_facades()
.without_tracing()
.with_routes(|router| {
router.get("/", || async { "home" }).name("home");
router.get("/posts/{post}", || async { "post" }).name("posts.show");
})
.boot()
.await
.expect("boots")
}
#[tokio::test]
async fn booting_binds_the_core_services() {
let app = app().await;
assert!(app.resolve::<Config>().is_ok());
assert!(app.resolve::<Dispatcher>().is_ok());
assert!(app.resolve::<Views>().is_ok());
assert!(app.resolve::<Kernel>().is_ok());
assert!(app.resolve::<UrlGenerator>().is_ok());
assert!(app.resolve::<MiddlewareRegistry>().is_ok());
assert!(app.is_booted());
}
#[tokio::test]
async fn the_kernel_serves_the_declared_routes() {
let app = app().await;
let kernel = app.resolve::<Kernel>().unwrap();
let response =
kernel.handle_request(Request::builder().method(Method::GET).uri("/").build()).await;
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test]
async fn named_routes_reach_the_url_generator() {
let app = app().await;
let urls = app.resolve::<UrlGenerator>().unwrap();
assert_eq!(urls.route("posts.show", &[("post", "7")]).unwrap(), "/posts/7");
}
#[tokio::test]
async fn the_default_config_comes_from_the_environment() {
let app = app().await;
let config = app.resolve::<Config>().unwrap();
assert_eq!(config.get(keys::APP_NAME).as_deref(), Some("Rainier"));
assert_eq!(config.get(keys::SERVER_PORT), Some(8000));
assert_eq!(config.get(keys::APP_DEBUG), Some(false));
}
#[test]
fn the_default_drivers_are_the_ones_that_need_no_infrastructure() {
let config = default_config(&Env::new(), std::path::Path::new(".")).unwrap();
assert_eq!(config.setting(keys::APP_ENV).unwrap(), rainier_config::AppEnv::Production);
assert_eq!(config.setting(keys::CACHE_DRIVER).unwrap(), rainier_cache::CacheDriver::Memory);
assert_eq!(
config.setting(keys::SESSION_DRIVER).unwrap(),
rainier_session::SessionDriver::Memory
);
assert_eq!(config.setting(keys::QUEUE_DRIVER).unwrap(), rainier_queue::QueueDriver::Sync);
assert_eq!(config.setting(keys::MAIL_DRIVER).unwrap(), rainier_mail::MailDriver::Log);
}
#[test]
fn a_driver_written_as_its_wire_spelling_reads_back_as_the_enum() {
let env = Env::parse("CACHE_DRIVER=redis-cluster\nQUEUE_DRIVER=database\nAPP_ENV=local");
let config = default_config(&env, std::path::Path::new(".")).unwrap();
assert_eq!(
config.setting(keys::CACHE_DRIVER).unwrap(),
rainier_cache::CacheDriver::RedisCluster
);
assert_eq!(
config.setting(keys::QUEUE_DRIVER).unwrap(),
rainier_queue::QueueDriver::Database
);
assert!(config.setting(keys::APP_ENV).unwrap().is_developing());
}
#[test]
fn a_misspelled_driver_is_a_configuration_error_not_a_fallback() {
let env = Env::parse("CACHE_DRIVER=redys");
let err = default_config(&env, std::path::Path::new(".")).unwrap_err();
assert!(err.message().contains("CACHE_DRIVER"), "{}", err.message());
assert!(err.message().contains("`redys`"), "{}", err.message());
assert!(err.message().contains("`redis-cluster`"), "{}", err.message());
}
#[tokio::test]
async fn a_misspelled_driver_fails_the_boot_rather_than_being_deferred_forever() {
let mut builder = Rainier::new(".").without_facades().without_tracing();
builder.deferred = Some(Error::internal("`QUEUE_DRIVER`: nope"));
let err = builder.boot().await.unwrap_err();
assert!(err.message().contains("QUEUE_DRIVER"), "{}", err.message());
}
#[tokio::test]
async fn configuration_can_be_adjusted_while_building() {
let app = Rainier::new(".")
.without_facades()
.without_tracing()
.configure(|config| {
config.set("app.name", "Custom").unwrap();
})
.boot()
.await
.unwrap();
assert_eq!(app.resolve::<Config>().unwrap().string("app.name").as_deref(), Some("Custom"));
}
#[tokio::test]
async fn the_default_middleware_normalises_input() {
let app = Rainier::new(".")
.without_facades()
.without_tracing()
.with_routes(|router| {
router.post("/echo", |request: rainier_routing::Req| async move {
request.input_or("name", "(absent)")
});
})
.boot()
.await
.unwrap();
let response = app
.resolve::<Kernel>()
.unwrap()
.handle_request(
Request::builder()
.method(Method::POST)
.uri("/echo")
.json(&serde_json::json!({ "name": " Ada " }))
.build(),
)
.await;
let body = response.into_http().into_body().collect().await.unwrap();
assert_eq!(body, "Ada", "TrimStrings should be global");
}
#[tokio::test]
async fn the_shipped_groups_all_build() {
let app = Rainier::new(".")
.without_facades()
.without_tracing()
.with_routes(|router| {
router.get("/a", || async { "a" }).middleware(groups::api());
router.get("/b", || async { "b" }).middleware(groups::web());
router.get("/c", || async { "c" }).middleware(groups::api_throttled(5));
router.get("/d", || async { "d" }).middleware(groups::session());
router.get("/e", || async { "e" }).middleware(groups::trust_local_proxies());
router.get("/f", || async { "f" }).middleware(groups::normalise_input());
})
.boot()
.await;
assert!(app.is_ok(), "{:?}", app.err().map(|e| e.message().to_string()));
}
#[tokio::test]
async fn booting_binds_encryption_and_sessions() {
let app = app().await;
assert!(app.resolve::<Encryption>().is_ok());
assert!(app.resolve::<SessionManager>().is_ok());
assert_eq!(app.resolve::<SessionManager>().unwrap().driver(), "memory");
}
#[tokio::test]
async fn encryption_round_trips_through_the_container() {
let app = app().await;
let crypt = app.resolve::<Encryption>().unwrap();
let sealed = crypt.encrypt("a secret").unwrap();
assert_eq!(crypt.decrypt(&sealed).unwrap(), "a secret");
}
#[tokio::test]
async fn an_app_key_from_the_environment_is_used() {
let key = Key::generate();
let sealed = Encryption::from_keys(KeyRing::new(key.clone())).encrypt("before").unwrap();
let app = Rainier::new(".")
.without_facades()
.without_tracing()
.configure(|_| {})
.boot()
.await
.unwrap();
assert!(app.resolve::<Encryption>().unwrap().decrypt(&sealed).is_err());
let with_key = Rainier::new(".")
.without_facades()
.without_tracing()
.with_encryption(Encryption::from_keys(KeyRing::new(key)))
.boot()
.await
.unwrap();
assert_eq!(with_key.resolve::<Encryption>().unwrap().decrypt(&sealed).unwrap(), "before");
}
#[tokio::test]
async fn the_session_middleware_gives_a_route_a_session() {
let app = Rainier::new(".")
.without_facades()
.without_tracing()
.with_routes(|router| {
router
.get("/count", |request: rainier_routing::Req| async move {
let session = request.session().expect("StartSession ran");
let seen: u64 = session.get("seen").unwrap_or(0);
session.put("seen", seen + 1).unwrap();
format!("{seen}")
})
.middleware(groups::session());
})
.boot()
.await
.unwrap();
let kernel = app.resolve::<Kernel>().unwrap();
let response = kernel
.handle_request(Request::builder().method(Method::GET).uri("/count").build())
.await;
assert!(response.header("set-cookie").is_some(), "the session should be persisted");
let body = response.into_http().into_body().collect().await.unwrap();
assert_eq!(body, "0");
}
#[tokio::test]
async fn a_route_without_the_session_middleware_has_no_session() {
let app = Rainier::new(".")
.without_facades()
.without_tracing()
.with_routes(|router| {
router.get("/plain", |request: rainier_routing::Req| async move {
format!("{}", request.session().is_some())
});
})
.boot()
.await
.unwrap();
let response = app
.resolve::<Kernel>()
.unwrap()
.handle_request(Request::builder().method(Method::GET).uri("/plain").build())
.await;
let body = response.into_http().into_body().collect().await.unwrap();
assert_eq!(body, "false");
}
#[tokio::test]
async fn middleware_that_needs_an_unbound_service_fails_at_boot() {
struct NeverBound;
let result = Rainier::new(".")
.without_facades()
.without_tracing()
.with_routes(|router| {
router.get("/a", || async { "a" }).middleware(
rainier_middleware::MiddlewareStack::new().resolved(|_: Arc<NeverBound>| {
StartSession::new(Arc::new(MemorySessionStore::default()))
}),
);
})
.boot()
.await;
let err = result.err().expect("boot should fail");
assert!(err.message().contains("/a"), "{}", err.message());
assert!(err.message().contains("NeverBound"), "{}", err.message());
}
#[tokio::test]
async fn a_route_attaches_middleware_by_value() {
let app = Rainier::new(".")
.without_facades()
.without_tracing()
.with_routes(|router| {
router
.get("/guarded", || async { "ok" })
.middleware(rainier_middleware::AddHeaders::security_defaults());
})
.boot()
.await
.unwrap();
let response = app
.resolve::<Kernel>()
.unwrap()
.handle_request(Request::builder().method(Method::GET).uri("/guarded").build())
.await;
assert!(response.header("x-content-type-options").is_some());
}
#[tokio::test]
async fn a_provider_registers_before_the_router_compiles() {
struct AliasProvider;
impl ServiceProvider for AliasProvider {
fn register(&self, app: &Application) -> Result<()> {
app.instance(42u32);
Ok(())
}
}
let app = Rainier::new(".")
.without_facades()
.without_tracing()
.with_provider(AliasProvider)
.boot()
.await
.unwrap();
assert_eq!(*app.resolve::<u32>().unwrap(), 42);
}
#[tokio::test]
async fn listeners_registered_while_building_survive_into_the_dispatcher() {
struct Ping;
let app = Rainier::new(".")
.without_facades()
.without_tracing()
.with_events(|events| {
events.listen(|_: Arc<Ping>| async { Ok(()) });
})
.boot()
.await
.unwrap();
assert!(app.resolve::<Dispatcher>().unwrap().has_listeners::<Ping>());
}
}
#[cfg(test)]
mod storage_tests {
use super::*;
use rainier_cache::CacheExt as _;
use rainier_filesystem::{FilesystemExt as _, MemoryFilesystem};
async fn app() -> Arc<Application> {
Rainier::new(".")
.without_facades()
.without_tracing()
.with_storage(Storage::new(Arc::new(MemoryFilesystem::new())))
.boot()
.await
.expect("boots")
}
#[tokio::test]
async fn booting_binds_a_cache_and_a_storage() {
let app = app().await;
assert_eq!(app.resolve::<CacheManager>().unwrap().driver(), "memory");
assert_eq!(app.resolve::<Storage>().unwrap().driver(), "memory");
}
#[tokio::test]
async fn the_default_storage_is_local_under_the_base_path() {
let app = Rainier::new(".").without_facades().without_tracing().boot().await.unwrap();
assert_eq!(app.resolve::<Storage>().unwrap().driver(), "local");
}
#[tokio::test]
async fn the_cache_round_trips_through_the_container() {
let app = app().await;
let cache = app.resolve::<CacheManager>().unwrap();
cache.put_string("k", "v", None).await.unwrap();
assert_eq!(cache.get_string("k").await.unwrap().as_deref(), Some("v"));
}
#[tokio::test]
async fn storage_round_trips_through_the_container() {
let app = app().await;
let storage = app.resolve::<Storage>().unwrap();
storage.put_string("uploads/a.txt", "hello").await.unwrap();
assert_eq!(storage.get_string("uploads/a.txt").await.unwrap().as_deref(), Some("hello"));
}
#[tokio::test]
async fn a_traversal_through_the_container_is_still_refused() {
let storage = app().await.resolve::<Storage>().unwrap();
assert!(storage.get("../../etc/passwd").await.is_err());
}
}