Fix: Environment variables do not overwrite Config.toml options (#2433)

* Fix environment variable parsing

* Fix failing tests

dfgdfgfd

dsf

* Add new test
This commit is contained in:
Gunnar Raßmann 2024-03-30 01:02:52 +01:00 committed by GitHub
parent 642504f2ba
commit d528cbd828
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 172 additions and 67 deletions

View file

@ -10,7 +10,7 @@ readme = "../README.md"
rust-version.workspace = true rust-version.workspace = true
[dependencies] [dependencies]
config = { version = "0.14", default-features = false, features = ["toml"] } config = { version = "0.14", default-features = false, features = ["toml", "convert-case"] }
regex = "1.7.0" regex = "1.7.0"
serde = { version = "1.0.151", features = ["derive"] } serde = { version = "1.0.151", features = ["derive"] }
thiserror = "1.0.38" thiserror = "1.0.38"
@ -19,3 +19,4 @@ typed-builder = "0.18"
[dev-dependencies] [dev-dependencies]
tokio = { version = "1", features = ["rt", "macros"] } tokio = { version = "1", features = ["rt", "macros"] }
tempfile = "3" tempfile = "3"
temp-env = { version = "0.3.6", features = ["async_closure"] }

View file

@ -3,7 +3,7 @@
pub mod errors; pub mod errors;
use crate::errors::LeptosConfigError; use crate::errors::LeptosConfigError;
use config::{Config, File, FileFormat}; use config::{Case, Config, File, FileFormat};
use regex::Regex; use regex::Regex;
use std::{ use std::{
convert::TryFrom, env::VarError, fs, net::SocketAddr, path::Path, convert::TryFrom, env::VarError, fs, net::SocketAddr, path::Path,
@ -300,7 +300,9 @@ impl TryFrom<String> for ReloadWSProtocol {
/// Loads [LeptosOptions] from a Cargo.toml text content with layered overrides. /// Loads [LeptosOptions] from a Cargo.toml text content with layered overrides.
/// If an env var is specified, like `LEPTOS_ENV`, it will override a setting in the file. /// If an env var is specified, like `LEPTOS_ENV`, it will override a setting in the file.
pub fn get_config_from_str(text: &str) -> Result<ConfFile, LeptosConfigError> { pub fn get_config_from_str(
text: &str,
) -> Result<LeptosOptions, LeptosConfigError> {
let re: Regex = Regex::new(r"(?m)^\[package.metadata.leptos\]").unwrap(); let re: Regex = Regex::new(r"(?m)^\[package.metadata.leptos\]").unwrap();
let re_workspace: Regex = let re_workspace: Regex =
Regex::new(r"(?m)^\[\[workspace.metadata.leptos\]\]").unwrap(); Regex::new(r"(?m)^\[\[workspace.metadata.leptos\]\]").unwrap();
@ -324,14 +326,18 @@ pub fn get_config_from_str(text: &str) -> Result<ConfFile, LeptosConfigError> {
// so that serde error messages have right line number // so that serde error messages have right line number
let newlines = text[..start].matches('\n').count(); let newlines = text[..start].matches('\n').count();
let input = "\n".repeat(newlines) + &text[start..]; let input = "\n".repeat(newlines) + &text[start..];
let toml = input.replace(metadata_name, "[leptos-options]"); // so the settings will be interpreted as root level settings
let toml = input.replace(metadata_name, "");
let settings = Config::builder() let settings = Config::builder()
// Read the "default" configuration file // Read the "default" configuration file
.add_source(File::from_str(&toml, FileFormat::Toml)) .add_source(File::from_str(&toml, FileFormat::Toml))
// Layer on the environment-specific values. // Layer on the environment-specific values.
// Add in settings from environment variables (with a prefix of LEPTOS and '_' as separator) // Add in settings from environment variables (with a prefix of LEPTOS)
// E.g. `LEPTOS_RELOAD_PORT=5001 would set `LeptosOptions.reload_port` // E.g. `LEPTOS_RELOAD_PORT=5001 would set `LeptosOptions.reload_port`
.add_source(config::Environment::with_prefix("LEPTOS").separator("_")) .add_source(
config::Environment::with_prefix("LEPTOS")
.convert_case(Case::Kebab),
)
.build()?; .build()?;
settings settings
@ -361,7 +367,8 @@ pub async fn get_config_from_file<P: AsRef<Path>>(
) -> Result<ConfFile, LeptosConfigError> { ) -> Result<ConfFile, LeptosConfigError> {
let text = fs::read_to_string(path) let text = fs::read_to_string(path)
.map_err(|_| LeptosConfigError::ConfigNotFound)?; .map_err(|_| LeptosConfigError::ConfigNotFound)?;
get_config_from_str(&text) let leptos_options = get_config_from_str(&text)?;
Ok(ConfFile { leptos_options })
} }
/// Loads [LeptosOptions] from environment variables or rely on the defaults /// Loads [LeptosOptions] from environment variables or rely on the defaults

View file

@ -30,44 +30,53 @@ fn ws_from_str_test() {
#[test] #[test]
fn env_w_default_test() { fn env_w_default_test() {
std::env::set_var("LEPTOS_CONFIG_ENV_TEST", "custom"); _ = temp_env::with_var("LEPTOS_CONFIG_ENV_TEST", Some("custom"), || {
assert_eq!( assert_eq!(
env_w_default("LEPTOS_CONFIG_ENV_TEST", "default").unwrap(), env_w_default("LEPTOS_CONFIG_ENV_TEST", "default").unwrap(),
String::from("custom") String::from("custom")
); );
std::env::remove_var("LEPTOS_CONFIG_ENV_TEST"); });
assert_eq!(
env_w_default("LEPTOS_CONFIG_ENV_TEST", "default").unwrap(), _ = temp_env::with_var_unset("LEPTOS_CONFIG_ENV_TEST", || {
String::from("default") assert_eq!(
); env_w_default("LEPTOS_CONFIG_ENV_TEST", "default").unwrap(),
String::from("default")
);
});
} }
#[test] #[test]
fn env_wo_default_test() { fn env_wo_default_test() {
std::env::set_var("LEPTOS_CONFIG_ENV_TEST", "custom"); _ = temp_env::with_var("LEPTOS_CONFIG_ENV_TEST", Some("custom"), || {
assert_eq!( assert_eq!(
env_wo_default("LEPTOS_CONFIG_ENV_TEST").unwrap(), env_wo_default("LEPTOS_CONFIG_ENV_TEST").unwrap(),
Some(String::from("custom")) Some(String::from("custom"))
); );
std::env::remove_var("LEPTOS_CONFIG_ENV_TEST"); });
assert_eq!(env_wo_default("LEPTOS_CONFIG_ENV_TEST").unwrap(), None);
_ = temp_env::with_var_unset("LEPTOS_CONFIG_ENV_TEST", || {
assert_eq!(env_wo_default("LEPTOS_CONFIG_ENV_TEST").unwrap(), None);
});
} }
#[test] #[test]
fn try_from_env_test() { fn try_from_env_test() {
// Test config values from environment variables // Test config values from environment variables
std::env::set_var("LEPTOS_OUTPUT_NAME", "app_test"); let config = temp_env::with_vars(
std::env::set_var("LEPTOS_SITE_ROOT", "my_target/site"); [
std::env::set_var("LEPTOS_SITE_PKG_DIR", "my_pkg"); ("LEPTOS_OUTPUT_NAME", Some("app_test")),
std::env::set_var("LEPTOS_SITE_ADDR", "0.0.0.0:80"); ("LEPTOS_SITE_ROOT", Some("my_target/site")),
std::env::set_var("LEPTOS_RELOAD_PORT", "8080"); ("LEPTOS_SITE_PKG_DIR", Some("my_pkg")),
std::env::set_var("LEPTOS_RELOAD_EXTERNAL_PORT", "8080"); ("LEPTOS_SITE_ADDR", Some("0.0.0.0:80")),
std::env::set_var("LEPTOS_ENV", "PROD"); ("LEPTOS_RELOAD_PORT", Some("8080")),
std::env::set_var("LEPTOS_RELOAD_WS_PROTOCOL", "WSS"); ("LEPTOS_RELOAD_EXTERNAL_PORT", Some("8080")),
("LEPTOS_ENV", Some("PROD")),
("LEPTOS_RELOAD_WS_PROTOCOL", Some("WSS")),
],
|| LeptosOptions::try_from_env().unwrap(),
);
let config = LeptosOptions::try_from_env().unwrap();
assert_eq!(config.output_name, "app_test"); assert_eq!(config.output_name, "app_test");
assert_eq!(config.site_root, "my_target/site"); assert_eq!(config.site_root, "my_target/site");
assert_eq!(config.site_pkg_dir, "my_pkg"); assert_eq!(config.site_pkg_dir, "my_pkg");
assert_eq!( assert_eq!(

View file

@ -23,13 +23,7 @@ env = "PROD"
const CARGO_TOML_CONTENT_ERR: &str = r#"\ const CARGO_TOML_CONTENT_ERR: &str = r#"\
[package.metadata.leptos] [package.metadata.leptos]
_output-name = "app-test" - invalid toml -
_site-root = "my_target/site"
_site-pkg-dir = "my_pkg"
_site-addr = "0.0.0.0:80"
_reload-port = "8080"
_reload-external-port = "8080"
_env = "PROD"
"#; "#;
#[tokio::test] #[tokio::test]
@ -43,10 +37,23 @@ async fn get_configuration_from_file_ok() {
let path: &Path = cargo_tmp.as_ref(); let path: &Path = cargo_tmp.as_ref();
let path_s = path.to_string_lossy().to_string(); let path_s = path.to_string_lossy().to_string();
let config = get_configuration(Some(&path_s)) let config = temp_env::async_with_vars(
.await [
.unwrap() ("LEPTOS_OUTPUT_NAME", None::<&str>),
.leptos_options; ("LEPTOS_SITE_ROOT", None::<&str>),
("LEPTOS_SITE_PKG_DIR", None::<&str>),
("LEPTOS_SITE_ADDR", None::<&str>),
("LEPTOS_RELOAD_PORT", None::<&str>),
("LEPTOS_RELOAD_EXTERNAL_PORT", None::<&str>),
],
async {
get_configuration(Some(&path_s))
.await
.unwrap()
.leptos_options
},
)
.await;
assert_eq!(config.output_name, "app-test"); assert_eq!(config.output_name, "app-test");
assert_eq!(config.site_root, "my_target/site"); assert_eq!(config.site_root, "my_target/site");
@ -91,10 +98,23 @@ async fn get_config_from_file_ok() {
write!(output, "{CARGO_TOML_CONTENT_OK}").unwrap(); write!(output, "{CARGO_TOML_CONTENT_OK}").unwrap();
} }
let config = get_config_from_file(&cargo_tmp) let config = temp_env::async_with_vars(
.await [
.unwrap() ("LEPTOS_OUTPUT_NAME", None::<&str>),
.leptos_options; ("LEPTOS_SITE_ROOT", None::<&str>),
("LEPTOS_SITE_PKG_DIR", None::<&str>),
("LEPTOS_SITE_ADDR", None::<&str>),
("LEPTOS_RELOAD_PORT", None::<&str>),
("LEPTOS_RELOAD_EXTERNAL_PORT", None::<&str>),
],
async {
get_config_from_file(&cargo_tmp)
.await
.unwrap()
.leptos_options
},
)
.await;
assert_eq!(config.output_name, "app-test"); assert_eq!(config.output_name, "app-test");
assert_eq!(config.site_root, "my_target/site"); assert_eq!(config.site_root, "my_target/site");
@ -129,9 +149,18 @@ async fn get_config_from_file_empty() {
#[test] #[test]
fn get_config_from_str_content() { fn get_config_from_str_content() {
let config = get_config_from_str(CARGO_TOML_CONTENT_OK) let config = temp_env::with_vars_unset(
.unwrap() [
.leptos_options; "LEPTOS_OUTPUT_NAME",
"LEPTOS_SITE_ROOT",
"LEPTOS_SITE_PKG_DIR",
"LEPTOS_SITE_ADDR",
"LEPTOS_RELOAD_PORT",
"LEPTOS_RELOAD_EXTERNAL_PORT",
],
|| get_config_from_str(CARGO_TOML_CONTENT_OK).unwrap(),
);
assert_eq!(config.output_name, "app-test"); assert_eq!(config.output_name, "app-test");
assert_eq!(config.site_root, "my_target/site"); assert_eq!(config.site_root, "my_target/site");
assert_eq!(config.site_pkg_dir, "my_pkg"); assert_eq!(config.site_pkg_dir, "my_pkg");
@ -146,16 +175,20 @@ fn get_config_from_str_content() {
#[tokio::test] #[tokio::test]
async fn get_config_from_env() { async fn get_config_from_env() {
// Test config values from environment variables // Test config values from environment variables
std::env::set_var("LEPTOS_OUTPUT_NAME", "app-test"); let config = temp_env::async_with_vars(
std::env::set_var("LEPTOS_SITE_ROOT", "my_target/site"); [
std::env::set_var("LEPTOS_SITE_PKG_DIR", "my_pkg"); ("LEPTOS_OUTPUT_NAME", Some("app-test")),
std::env::set_var("LEPTOS_SITE_ADDR", "0.0.0.0:80"); ("LEPTOS_SITE_ROOT", Some("my_target/site")),
std::env::set_var("LEPTOS_RELOAD_PORT", "8080"); ("LEPTOS_SITE_PKG_DIR", Some("my_pkg")),
std::env::set_var("LEPTOS_RELOAD_EXTERNAL_PORT", "8080"); ("LEPTOS_SITE_ADDR", Some("0.0.0.0:80")),
("LEPTOS_RELOAD_PORT", Some("8080")),
("LEPTOS_RELOAD_EXTERNAL_PORT", Some("8080")),
],
async { get_configuration(None).await.unwrap().leptos_options },
)
.await;
let config = get_configuration(None).await.unwrap().leptos_options;
assert_eq!(config.output_name, "app-test"); assert_eq!(config.output_name, "app-test");
assert_eq!(config.site_root, "my_target/site"); assert_eq!(config.site_root, "my_target/site");
assert_eq!(config.site_pkg_dir, "my_pkg"); assert_eq!(config.site_pkg_dir, "my_pkg");
assert_eq!( assert_eq!(
@ -166,13 +199,19 @@ async fn get_config_from_env() {
assert_eq!(config.reload_external_port, Some(8080)); assert_eq!(config.reload_external_port, Some(8080));
// Test default config values // Test default config values
std::env::remove_var("LEPTOS_SITE_ROOT"); let config = temp_env::async_with_vars(
std::env::remove_var("LEPTOS_SITE_PKG_DIR"); [
std::env::remove_var("LEPTOS_SITE_ADDR"); ("LEPTOS_OUTPUT_NAME", None::<&str>),
std::env::remove_var("LEPTOS_RELOAD_PORT"); ("LEPTOS_SITE_ROOT", None::<&str>),
std::env::set_var("LEPTOS_RELOAD_EXTERNAL_PORT", "443"); ("LEPTOS_SITE_PKG_DIR", None::<&str>),
("LEPTOS_SITE_ADDR", None::<&str>),
("LEPTOS_RELOAD_PORT", None::<&str>),
("LEPTOS_RELOAD_EXTERNAL_PORT", None::<&str>),
],
async { get_configuration(None).await.unwrap().leptos_options },
)
.await;
let config = get_configuration(None).await.unwrap().leptos_options;
assert_eq!(config.site_root, "target/site"); assert_eq!(config.site_root, "target/site");
assert_eq!(config.site_pkg_dir, "pkg"); assert_eq!(config.site_pkg_dir, "pkg");
assert_eq!( assert_eq!(
@ -180,7 +219,7 @@ async fn get_config_from_env() {
SocketAddr::from_str("127.0.0.1:3000").unwrap() SocketAddr::from_str("127.0.0.1:3000").unwrap()
); );
assert_eq!(config.reload_port, 3001); assert_eq!(config.reload_port, 3001);
assert_eq!(config.reload_external_port, Some(443)); assert_eq!(config.reload_external_port, None);
} }
#[test] #[test]
@ -197,3 +236,52 @@ fn leptos_options_builder_default() {
assert_eq!(conf.reload_port, 3001); assert_eq!(conf.reload_port, 3001);
assert_eq!(conf.reload_external_port, None); assert_eq!(conf.reload_external_port, None);
} }
#[test]
fn environment_variable_override() {
// first check without variables set
let config = temp_env::with_vars_unset(
[
"LEPTOS_OUTPUT_NAME",
"LEPTOS_SITE_ROOT",
"LEPTOS_SITE_PKG_DIR",
"LEPTOS_SITE_ADDR",
"LEPTOS_RELOAD_PORT",
"LEPTOS_RELOAD_EXTERNAL_PORT",
],
|| get_config_from_str(CARGO_TOML_CONTENT_OK).unwrap(),
);
assert_eq!(config.output_name, "app-test");
assert_eq!(config.site_root, "my_target/site");
assert_eq!(config.site_pkg_dir, "my_pkg");
assert_eq!(
config.site_addr,
SocketAddr::from_str("0.0.0.0:80").unwrap()
);
assert_eq!(config.reload_port, 8080);
assert_eq!(config.reload_external_port, Some(8080));
// check the override
let config = temp_env::with_vars(
[
("LEPTOS_OUTPUT_NAME", Some("app-test2")),
("LEPTOS_SITE_ROOT", Some("my_target/site2")),
("LEPTOS_SITE_PKG_DIR", Some("my_pkg2")),
("LEPTOS_SITE_ADDR", Some("0.0.0.0:82")),
("LEPTOS_RELOAD_PORT", Some("8082")),
("LEPTOS_RELOAD_EXTERNAL_PORT", Some("8082")),
],
|| get_config_from_str(CARGO_TOML_CONTENT_OK).unwrap(),
);
assert_eq!(config.output_name, "app-test2");
assert_eq!(config.site_root, "my_target/site2");
assert_eq!(config.site_pkg_dir, "my_pkg2");
assert_eq!(
config.site_addr,
SocketAddr::from_str("0.0.0.0:82").unwrap()
);
assert_eq!(config.reload_port, 8082);
assert_eq!(config.reload_external_port, Some(8082));
}