(#50, #55) Config centralization, backwards compat (#59)

Addresses #50, #55.

Co-authored-by: Daniel Valentine <Daniel-Valentine@users.noreply.github.com>
This commit is contained in:
Matt 2022-10-31 20:08:22 -04:00 committed by GitHub
parent 401061b7b7
commit 6a68241a7c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 243 additions and 8 deletions

94
Cargo.lock generated
View File

@ -356,14 +356,17 @@ dependencies = [
"hyper-rustls",
"libflate",
"lipsum",
"once_cell",
"percent-encoding",
"regex",
"route-recognizer",
"rust-embed",
"sealed_test",
"serde",
"serde_json",
"time",
"tokio",
"toml",
"url",
]
@ -382,6 +385,12 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "fs_extra"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2022715d62ab30faffd124d40b76f4134a550a87792276512b18d63272333394"
[[package]]
name = "futures"
version = "0.3.24"
@ -768,9 +777,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.15.0"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1"
checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
[[package]]
name = "openssl-probe"
@ -846,6 +855,12 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "quick-error"
version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
[[package]]
name = "quote"
version = "1.0.21"
@ -911,6 +926,15 @@ version = "0.6.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244"
[[package]]
name = "remove_dir_all"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
dependencies = [
"winapi",
]
[[package]]
name = "ring"
version = "0.16.20"
@ -1005,6 +1029,18 @@ dependencies = [
"base64",
]
[[package]]
name = "rusty-forkfork"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ce85af4dfa2fb0c0143121ab5e424c71ea693867357c9159b8777b59984c218"
dependencies = [
"fnv",
"quick-error",
"tempfile",
"wait-timeout",
]
[[package]]
name = "ryu"
version = "1.0.11"
@ -1046,6 +1082,28 @@ dependencies = [
"untrusted",
]
[[package]]
name = "sealed_test"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a608d94641cc17fe203b102db2ae86d47a236630192f0244ddbbbb0044c0272"
dependencies = [
"fs_extra",
"rusty-forkfork",
"sealed_test_derive",
"tempfile",
]
[[package]]
name = "sealed_test_derive"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b672e005ae58fef5da619d90b9f1c5b44b061890f4a371b3c96257a8a15e697"
dependencies = [
"quote",
"syn",
]
[[package]]
name = "security-framework"
version = "2.7.0"
@ -1168,6 +1226,20 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "tempfile"
version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
dependencies = [
"cfg-if",
"fastrand",
"libc",
"redox_syscall",
"remove_dir_all",
"winapi",
]
[[package]]
name = "textwrap"
version = "0.15.1"
@ -1284,6 +1356,15 @@ dependencies = [
"tracing",
]
[[package]]
name = "toml"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7"
dependencies = [
"serde",
]
[[package]]
name = "tower-service"
version = "0.3.2"
@ -1375,6 +1456,15 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "wait-timeout"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
dependencies = [
"libc",
]
[[package]]
name = "waker-fn"
version = "1.1.0"

View File

@ -27,6 +27,9 @@ url = "2.2.2"
rust-embed = "6.4.0"
libflate = "1.2.0"
brotli = { version = "3.3.4", features = ["std"] }
toml = "0.5.9"
once_cell = "1.16.0"
[dev-dependencies]
lipsum = "0.8.2"
sealed_test = "1.0.0"

142
src/config.rs Normal file
View File

@ -0,0 +1,142 @@
use once_cell::sync::Lazy;
use std::env::var;
// Waiting for https://github.com/rust-lang/rust/issues/74465 to land, so we
// can reduce reliance on once_cell.
//
// This is the local static that is initialized at runtime (technically at
// first request) and contains the instance settings.
static CONFIG: Lazy<Config> = Lazy::new(Config::load);
/// Stores the configuration parsed from the environment variables and the
/// config file. `Config::Default()` contains None for each setting.
#[derive(Default, serde::Deserialize)]
pub struct Config {
#[serde(rename = "FERRIT_SFW_ONLY")]
sfw_only: Option<String>,
#[serde(rename = "FERRIT_DEFAULT_THEME")]
default_theme: Option<String>,
#[serde(rename = "FERRIT_DEFAULT_FRONT_PAGE")]
default_front_page: Option<String>,
#[serde(rename = "FERRIT_DEFAULT_LAYOUT")]
default_layout: Option<String>,
#[serde(rename = "FERRIT_DEFAULT_WIDE")]
default_wide: Option<String>,
#[serde(rename = "FERRIT_DEFAULT_COMMENT_SORT")]
default_comment_sort: Option<String>,
#[serde(rename = "FERRIT_DEFAULT_POST_SORT")]
default_post_sort: Option<String>,
#[serde(rename = "FERRIT_DEFAULT_SHOW_NSFW")]
default_show_nsfw: Option<String>,
#[serde(rename = "FERRIT_DEFAULT_BLUR_NSFW")]
default_blur_nsfw: Option<String>,
#[serde(rename = "FERRIT_DEFAULT_USE_HLS")]
default_use_hls: Option<String>,
#[serde(rename = "FERRIT_DEFAULT_HIDE_HLS_NOTIFICATION")]
default_hide_hls_notification: Option<String>,
}
impl Config {
/// Load the configuration from the environment variables and the config file.
/// In the case that there are no environment variables set and there is no
/// config file, this function returns a Config that contains all None values.
pub fn load() -> Self {
// Read from ferrit.toml config file. If for any reason, it fails, the
// default `Config` is used (all None values)
let config: Config = toml::from_str(&std::fs::read_to_string("ferrit.toml").unwrap_or_default()).unwrap_or_default();
// This function defines the order of preference - first check for
// environment variables with "FERRIT", then check for environment variables
// with "LIBREDDIT" for reverse compatibility, then check the config, then if
// both are `None`, return a `None` via the `map_or_else` function
let parse = |key: &str| -> Option<String> {
var(key)
.ok()
.map_or_else(|| var(key.replace("FERRIT", "LIBREDDIT")).ok(), Some)
.map_or_else(|| get_setting_from_config(key, &config), Some)
};
Self {
sfw_only: parse("FERRIT_SFW_ONLY"),
default_theme: parse("FERRIT_DEFAULT_THEME"),
default_front_page: parse("FERRIT_DEFAULT_FRONT_PAGE"),
default_layout: parse("FERRIT_DEFAULT_LAYOUT"),
default_post_sort: parse("FERRIT_DEFAULT_POST_SORT"),
default_wide: parse("FERRIT_DEFAULT_WIDE"),
default_comment_sort: parse("FERRIT_DEFAULT_COMMENT_SORT"),
default_show_nsfw: parse("FERRIT_DEFAULT_SHOW_NSFW"),
default_blur_nsfw: parse("FERRIT_DEFAULT_BLUR_NSFW"),
default_use_hls: parse("FERRIT_DEFAULT_USE_HLS"),
default_hide_hls_notification: parse("FERRIT_DEFAULT_HIDE_HLS"),
}
}
}
fn get_setting_from_config(name: &str, config: &Config) -> Option<String> {
match name {
"FERRIT_SFW_ONLY" => config.sfw_only.clone(),
"FERRIT_DEFAULT_THEME" => config.default_theme.clone(),
"FERRIT_DEFAULT_FRONT_PAGE" => config.default_front_page.clone(),
"FERRIT_DEFAULT_LAYOUT" => config.default_layout.clone(),
"FERRIT_DEFAULT_COMMENT_SORT" => config.default_comment_sort.clone(),
"FERRIT_DEFAULT_POST_SORT" => config.default_post_sort.clone(),
"FERRIT_DEFAULT_SHOW_NSFW" => config.default_show_nsfw.clone(),
"FERRIT_DEFAULT_BLUR_NSFW" => config.default_blur_nsfw.clone(),
"FERRIT_DEFAULT_USE_HLS" => config.default_use_hls.clone(),
"FERRIT_DEFAULT_HIDE_HLS_NOTIFICATION" => config.default_hide_hls_notification.clone(),
"FERRIT_DEFAULT_WIDE" => config.default_wide.clone(),
_ => None,
}
}
/// Retrieves setting from environment variable or config file.
pub(crate) fn get_setting(name: &str) -> Option<String> {
get_setting_from_config(name, &CONFIG)
}
#[cfg(test)]
use sealed_test::prelude::*;
#[test]
#[sealed_test(env = [("FERRIT_SFW_ONLY", "1")])]
fn test_env_var() {
assert!(crate::utils::sfw_only())
}
#[test]
#[sealed_test(env = [("FERRIT_DEFAULT_COMMENT_SORT", "top"), ("LIBREDDIT_DEFAULT_COMMENT_SORT", "best")])]
fn test_env_precedence() {
assert_eq!(crate::config::get_setting("FERRIT_DEFAULT_COMMENT_SORT"), Some("top".into()))
}
#[test]
#[sealed_test]
fn test_config() {
let config_to_write = r#"FERRIT_DEFAULT_COMMENT_SORT = "best""#;
std::fs::write("ferrit.toml", config_to_write).unwrap();
assert_eq!(crate::config::get_setting("FERRIT_DEFAULT_COMMENT_SORT"), Some("best".into()));
}
#[test]
#[sealed_test(env = [("FERRIT_DEFAULT_COMMENT_SORT", "top")])]
fn test_env_config_precedence() {
let config_to_write = r#"FERRIT_DEFAULT_COMMENT_SORT = "best""#;
std::fs::write("ferrit.toml", config_to_write).unwrap();
assert_eq!(crate::config::get_setting("FERRIT_DEFAULT_COMMENT_SORT"), Some("top".into()))
}
#[test]
#[sealed_test(env = [("LIBREDDIT_DEFAULT_COMMENT_SORT", "top")])]
fn test_alt_env_config_precedence() {
let config_to_write = r#"FERRIT_DEFAULT_COMMENT_SORT = "best""#;
std::fs::write("Ferrit.toml", config_to_write).unwrap();
assert_eq!(crate::config::get_setting("FERRIT_DEFAULT_COMMENT_SORT"), Some("top".into()))
}

View File

@ -3,6 +3,7 @@
#![allow(clippy::cmp_owned)]
// Reference local files
mod config;
mod duplicates;
mod post;
mod search;

View File

@ -680,8 +680,8 @@ pub fn setting(req: &Request<Body>, name: &str) -> String {
req
.cookie(name)
.unwrap_or_else(|| {
// If there is no cookie for this setting, try receiving a default from an environment variable
if let Ok(default) = std::env::var(format!("FERRIT_DEFAULT_{}", name.to_uppercase())) {
// If there is no cookie for this setting, try receiving a default from the config
if let Some(default) = crate::config::get_setting(&format!("FERRIT_DEFAULT_{}", name.to_uppercase())) {
Cookie::new(name, default)
} else {
Cookie::named(name)
@ -866,11 +866,10 @@ pub async fn error(req: Request<Body>, msg: impl ToString) -> Result<Response<Bo
Ok(Response::builder().status(404).header("content-type", "text/html").body(body.into()).unwrap_or_default())
}
/// Set the FERRIT_SFW_ONLY environment variable to anything, to enable SFW-only mode.
/// If environment variable is set, this bool will be true. Otherwise it will be false.
/// This variable is set by the instance operator, and as such, side-steps the user config
/// Retrieve the `FERRIT_SFW_ONLY` setting value from the config, based on
/// environment variables and the config file.
pub fn sfw_only() -> bool {
env::var("FERRIT_SFW_ONLY").is_ok()
crate::config::get_setting("FERRIT_SFW_ONLY").is_some()
}
/// Render the landing page for NSFW content when the user has not enabled