use crate::conf::global_settings;
use serde::Deserialize;
use std::sync::{Arc, OnceLock};
static SETTINGS: OnceLock<Arc<Settings>> = OnceLock::new();
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)]
#[serde(default)]
pub struct TemplateConfig {
pub backend: String,
pub dirs: Vec<String>,
pub app_dirs: bool,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(default)]
pub struct Settings {
pub debug: bool,
pub secret_key: String,
pub secret_key_fallbacks: Vec<String>,
pub allowed_hosts: Vec<String>,
pub internal_ips: Vec<String>,
pub database_url: String,
pub installed_apps: Vec<String>,
pub middleware: Vec<String>,
pub root_urlconf: String,
pub append_slash: bool,
pub prepend_www: bool,
pub language_code: String,
pub time_zone: String,
pub use_tz: bool,
pub use_i18n: bool,
pub use_l10n: bool,
pub static_url: String,
pub static_root: Option<String>,
pub staticfiles_dirs: Vec<String>,
pub templates: Vec<TemplateConfig>,
pub csrf_cookie_name: String,
pub csrf_cookie_secure: bool,
pub session_cookie_name: String,
pub session_cookie_secure: bool,
pub secure_ssl_redirect: bool,
pub email_backend: String,
pub default_from_email: String,
pub logging_level: String,
}
impl Settings {
#[must_use]
pub fn builder() -> SettingsBuilder {
SettingsBuilder {
inner: Self::default(),
}
}
}
pub struct SettingsBuilder {
inner: Settings,
}
impl SettingsBuilder {
#[must_use]
pub fn debug(mut self, value: bool) -> Self {
self.inner.debug = value;
self
}
#[must_use]
pub fn secret_key(mut self, value: impl Into<String>) -> Self {
self.inner.secret_key = value.into();
self
}
#[must_use]
pub fn secret_key_fallbacks<I, S>(mut self, values: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.inner.secret_key_fallbacks = values.into_iter().map(Into::into).collect();
self
}
#[must_use]
pub fn allowed_hosts<I, S>(mut self, values: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.inner.allowed_hosts = values.into_iter().map(Into::into).collect();
self
}
#[must_use]
pub fn internal_ips<I, S>(mut self, values: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.inner.internal_ips = values.into_iter().map(Into::into).collect();
self
}
#[must_use]
pub fn database_url(mut self, value: impl Into<String>) -> Self {
self.inner.database_url = value.into();
self
}
#[must_use]
pub fn installed_apps<I, S>(mut self, values: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.inner.installed_apps = values.into_iter().map(Into::into).collect();
self
}
#[must_use]
pub fn middleware<I, S>(mut self, values: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.inner.middleware = values.into_iter().map(Into::into).collect();
self
}
#[must_use]
pub fn root_urlconf(mut self, value: impl Into<String>) -> Self {
self.inner.root_urlconf = value.into();
self
}
#[must_use]
pub fn append_slash(mut self, value: bool) -> Self {
self.inner.append_slash = value;
self
}
#[must_use]
pub fn prepend_www(mut self, value: bool) -> Self {
self.inner.prepend_www = value;
self
}
#[must_use]
pub fn language_code(mut self, value: impl Into<String>) -> Self {
self.inner.language_code = value.into();
self
}
#[must_use]
pub fn time_zone(mut self, value: impl Into<String>) -> Self {
self.inner.time_zone = value.into();
self
}
#[must_use]
pub fn use_tz(mut self, value: bool) -> Self {
self.inner.use_tz = value;
self
}
#[must_use]
pub fn use_i18n(mut self, value: bool) -> Self {
self.inner.use_i18n = value;
self
}
#[must_use]
pub fn use_l10n(mut self, value: bool) -> Self {
self.inner.use_l10n = value;
self
}
#[must_use]
pub fn static_url(mut self, value: impl Into<String>) -> Self {
self.inner.static_url = value.into();
self
}
#[must_use]
pub fn static_root(mut self, value: Option<String>) -> Self {
self.inner.static_root = value;
self
}
#[must_use]
pub fn staticfiles_dirs<I, S>(mut self, values: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.inner.staticfiles_dirs = values.into_iter().map(Into::into).collect();
self
}
#[must_use]
pub fn templates<I>(mut self, values: I) -> Self
where
I: IntoIterator<Item = TemplateConfig>,
{
self.inner.templates = values.into_iter().collect();
self
}
#[must_use]
pub fn csrf_cookie_name(mut self, value: impl Into<String>) -> Self {
self.inner.csrf_cookie_name = value.into();
self
}
#[must_use]
pub fn csrf_cookie_secure(mut self, value: bool) -> Self {
self.inner.csrf_cookie_secure = value;
self
}
#[must_use]
pub fn session_cookie_name(mut self, value: impl Into<String>) -> Self {
self.inner.session_cookie_name = value.into();
self
}
#[must_use]
pub fn session_cookie_secure(mut self, value: bool) -> Self {
self.inner.session_cookie_secure = value;
self
}
#[must_use]
pub fn secure_ssl_redirect(mut self, value: bool) -> Self {
self.inner.secure_ssl_redirect = value;
self
}
#[must_use]
pub fn email_backend(mut self, value: impl Into<String>) -> Self {
self.inner.email_backend = value.into();
self
}
#[must_use]
pub fn default_from_email(mut self, value: impl Into<String>) -> Self {
self.inner.default_from_email = value.into();
self
}
#[must_use]
pub fn logging_level(mut self, value: impl Into<String>) -> Self {
self.inner.logging_level = value.into();
self
}
#[must_use]
pub fn build(self) -> Settings {
self.inner
}
}
impl Default for Settings {
fn default() -> Self {
Self {
debug: global_settings::DEBUG,
secret_key: global_settings::SECRET_KEY.to_owned(),
secret_key_fallbacks: owned_strings(global_settings::SECRET_KEY_FALLBACKS),
allowed_hosts: owned_strings(global_settings::ALLOWED_HOSTS),
internal_ips: owned_strings(global_settings::INTERNAL_IPS),
database_url: global_settings::DATABASE_URL.to_owned(),
installed_apps: owned_strings(global_settings::INSTALLED_APPS),
middleware: owned_strings(global_settings::MIDDLEWARE),
root_urlconf: global_settings::ROOT_URLCONF.to_owned(),
append_slash: global_settings::APPEND_SLASH,
prepend_www: global_settings::PREPEND_WWW,
language_code: global_settings::LANGUAGE_CODE.to_owned(),
time_zone: global_settings::TIME_ZONE.to_owned(),
use_tz: global_settings::USE_TZ,
use_i18n: global_settings::USE_I18N,
use_l10n: global_settings::USE_L10N,
static_url: global_settings::STATIC_URL.to_owned(),
static_root: global_settings::STATIC_ROOT.map(str::to_owned),
staticfiles_dirs: owned_strings(global_settings::STATICFILES_DIRS),
templates: Vec::new(),
csrf_cookie_name: global_settings::CSRF_COOKIE_NAME.to_owned(),
csrf_cookie_secure: global_settings::CSRF_COOKIE_SECURE,
session_cookie_name: global_settings::SESSION_COOKIE_NAME.to_owned(),
session_cookie_secure: global_settings::SESSION_COOKIE_SECURE,
secure_ssl_redirect: global_settings::SECURE_SSL_REDIRECT,
email_backend: global_settings::EMAIL_BACKEND.to_owned(),
default_from_email: global_settings::DEFAULT_FROM_EMAIL.to_owned(),
logging_level: global_settings::LOGGING_LEVEL.to_owned(),
}
}
}
pub fn init(settings: Settings) {
SETTINGS
.set(Arc::new(settings))
.expect("Settings already initialized");
}
#[must_use]
pub fn settings() -> &'static Arc<Settings> {
SETTINGS
.get()
.expect("Settings not initialized. Call rjango::conf::init() first.")
}
#[must_use]
fn owned_strings(values: &[&str]) -> Vec<String> {
values.iter().map(|value| (*value).to_owned()).collect()
}
#[cfg(test)]
mod tests {
use super::*;
use std::process::Command;
use std::sync::Arc;
const INIT_ACCESSOR_SUBPROCESS_ENV: &str = "RJANGO_CONF_INIT_ACCESSOR_SUBPROCESS";
const DOUBLE_INIT_SUBPROCESS_ENV: &str = "RJANGO_CONF_DOUBLE_INIT_SUBPROCESS";
#[test]
fn default_settings_match_django_defaults() {
let s = Settings::default();
assert!(!s.debug);
assert_eq!(s.secret_key, "");
assert_eq!(s.secret_key_fallbacks, Vec::<String>::new());
assert!(s.allowed_hosts.is_empty());
assert!(s.internal_ips.is_empty());
assert_eq!(s.database_url, "");
assert!(s.installed_apps.is_empty());
assert!(s.middleware.is_empty());
assert_eq!(s.root_urlconf, "");
assert!(s.append_slash);
assert!(!s.prepend_www);
assert_eq!(s.language_code, "en-us");
assert_eq!(s.time_zone, "America/Chicago");
assert!(s.use_tz);
assert!(s.use_i18n);
assert!(s.use_l10n);
assert_eq!(s.static_url, "");
assert_eq!(s.static_root, None);
assert!(s.staticfiles_dirs.is_empty());
assert!(s.templates.is_empty());
assert_eq!(s.csrf_cookie_name, "csrftoken");
assert!(!s.csrf_cookie_secure);
assert_eq!(s.session_cookie_name, "sessionid");
assert!(!s.session_cookie_secure);
assert!(!s.secure_ssl_redirect);
assert_eq!(
s.email_backend,
"django.core.mail.backends.smtp.EmailBackend"
);
assert_eq!(s.default_from_email, "webmaster@localhost");
assert_eq!(s.logging_level, "INFO");
}
#[test]
fn builder_overrides_each_field() {
let templates = vec![TemplateConfig {
backend: "django.template.backends.django.DjangoTemplates".to_owned(),
dirs: vec!["templates".to_owned()],
app_dirs: true,
}];
let settings = Settings::builder()
.debug(true)
.secret_key("test-secret")
.secret_key_fallbacks(["old-secret", "older-secret"])
.allowed_hosts(["example.com", "localhost"])
.internal_ips(["127.0.0.1", "::1"])
.database_url("postgres://localhost/rjango")
.installed_apps(["rjango.auth", "rjango.sessions"])
.middleware(["middleware.security", "middleware.sessions"])
.root_urlconf("project.urls")
.append_slash(false)
.prepend_www(true)
.language_code("fr-ca")
.time_zone("UTC")
.use_tz(false)
.use_i18n(false)
.use_l10n(false)
.static_url("/assets/")
.static_root(Some("/srv/static".to_owned()))
.staticfiles_dirs(["assets", "vendor/assets"])
.templates(templates.clone())
.csrf_cookie_name("csrf-test")
.csrf_cookie_secure(true)
.session_cookie_name("session-test")
.session_cookie_secure(true)
.secure_ssl_redirect(true)
.email_backend("tests.email.Backend")
.default_from_email("noreply@example.com")
.logging_level("DEBUG")
.build();
assert!(settings.debug);
assert_eq!(settings.secret_key, "test-secret");
assert_eq!(
settings.secret_key_fallbacks,
vec!["old-secret", "older-secret"]
);
assert_eq!(settings.allowed_hosts, vec!["example.com", "localhost"]);
assert_eq!(settings.internal_ips, vec!["127.0.0.1", "::1"]);
assert_eq!(settings.database_url, "postgres://localhost/rjango");
assert_eq!(
settings.installed_apps,
vec!["rjango.auth", "rjango.sessions"]
);
assert_eq!(
settings.middleware,
vec!["middleware.security", "middleware.sessions"]
);
assert_eq!(settings.root_urlconf, "project.urls");
assert!(!settings.append_slash);
assert!(settings.prepend_www);
assert_eq!(settings.language_code, "fr-ca");
assert_eq!(settings.time_zone, "UTC");
assert!(!settings.use_tz);
assert!(!settings.use_i18n);
assert!(!settings.use_l10n);
assert_eq!(settings.static_url, "/assets/");
assert_eq!(settings.static_root.as_deref(), Some("/srv/static"));
assert_eq!(settings.staticfiles_dirs, vec!["assets", "vendor/assets"]);
assert_eq!(settings.templates, templates);
assert_eq!(settings.csrf_cookie_name, "csrf-test");
assert!(settings.csrf_cookie_secure);
assert_eq!(settings.session_cookie_name, "session-test");
assert!(settings.session_cookie_secure);
assert!(settings.secure_ssl_redirect);
assert_eq!(settings.email_backend, "tests.email.Backend");
assert_eq!(settings.default_from_email, "noreply@example.com");
assert_eq!(settings.logging_level, "DEBUG");
}
#[test]
fn test_default_settings() {
let settings = Settings::default();
assert!(!settings.debug);
assert_eq!(settings.secret_key, "");
assert_eq!(settings.allowed_hosts, Vec::<String>::new());
assert_eq!(settings.root_urlconf, "");
assert!(settings.append_slash);
assert_eq!(settings.language_code, "en-us");
assert_eq!(settings.time_zone, "America/Chicago");
assert!(settings.use_tz);
assert!(settings.use_i18n);
assert_eq!(
settings.email_backend,
"django.core.mail.backends.smtp.EmailBackend"
);
assert_eq!(settings.default_from_email, "webmaster@localhost");
}
#[test]
fn test_override_settings() {
let default_settings = Settings::default();
let overridden = Settings::builder()
.debug(true)
.secret_key("override-secret")
.allowed_hosts(["example.com"])
.append_slash(false)
.build();
assert!(overridden.debug);
assert_eq!(overridden.secret_key, "override-secret");
assert_eq!(overridden.allowed_hosts, vec!["example.com"]);
assert!(!overridden.append_slash);
assert_eq!(overridden.language_code, default_settings.language_code);
assert_eq!(overridden.time_zone, default_settings.time_zone);
assert_eq!(overridden.email_backend, default_settings.email_backend);
}
#[test]
fn test_settings_access() {
if std::env::var_os(INIT_ACCESSOR_SUBPROCESS_ENV).is_some() {
let configured = Settings::builder().secret_key("subprocess-secret").build();
init(configured);
let shared = settings();
assert_eq!(shared.secret_key, "subprocess-secret");
assert!(Arc::ptr_eq(shared, settings()));
return;
}
let output = run_subprocess_test(
"conf::settings::tests::test_settings_access",
INIT_ACCESSOR_SUBPROCESS_ENV,
);
assert!(
output.status.success(),
"subprocess failed:\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn global_init_and_accessor_work() {
if std::env::var_os(INIT_ACCESSOR_SUBPROCESS_ENV).is_some() {
let configured = Settings::builder().secret_key("subprocess-secret").build();
init(configured);
let shared = settings();
assert_eq!(shared.secret_key, "subprocess-secret");
assert!(Arc::ptr_eq(shared, settings()));
return;
}
let output = run_subprocess_test(
"conf::settings::tests::global_init_and_accessor_work",
INIT_ACCESSOR_SUBPROCESS_ENV,
);
assert!(
output.status.success(),
"subprocess failed:\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn init_panics_on_double_init() {
if std::env::var_os(DOUBLE_INIT_SUBPROCESS_ENV).is_some() {
init(Settings::default());
init(Settings::default());
return;
}
let output = run_subprocess_test(
"conf::settings::tests::init_panics_on_double_init",
DOUBLE_INIT_SUBPROCESS_ENV,
);
assert!(
!output.status.success(),
"double init unexpectedly succeeded"
);
let combined = format!(
"{}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
assert!(
combined.contains("Settings already initialized"),
"panic output was: {combined}"
);
}
fn run_subprocess_test(test_name: &str, env_name: &str) -> std::process::Output {
Command::new(std::env::current_exe().expect("current test executable path"))
.arg("--exact")
.arg(test_name)
.arg("--nocapture")
.env(env_name, "1")
.output()
.expect("spawn subprocess test")
}
}