diff --git a/src/env_file.rs b/src/env_file.rs index c013c255468d54cc7f7167cd81ef75f25c1ecf9b..97ae492db2b1be889c41e9d9931789b3f6025709 100644 --- a/src/env_file.rs +++ b/src/env_file.rs @@ -1,5 +1,6 @@ use std::env; use std::env::VarError; +use std::fmt::Display; use crate::parser::{file, FileLine}; use nom_locate::LocatedSpan; use std::str::FromStr; @@ -102,12 +103,52 @@ impl EnvironmentFile { pub fn is_empty(&self) -> bool { self.lines.is_empty() } +} - pub fn apply(&self) -> Result<(), EnvironmentFileError> { - set_from_file(self) +#[derive(Clone, Debug, Default)] +pub struct ApplyOptions { + pub prefix: Option<String>, + pub overwrite: bool, +} + +impl ApplyOptions { + pub fn new(prefix: impl Display, overwrite: bool) -> Self { + Self { + prefix: Some(prefix.to_string()), + overwrite, + } + } + + pub fn with_prefix(prefix: impl Display) -> Self { + Self::new(prefix, false) + } + + pub fn with_overwrite(overwrite: bool) -> Self { + Self { + prefix: None, + overwrite, + } } } +pub trait ApplyEnvironmentFile { + fn apply(&self, options: ApplyOptions); +} + +impl ApplyEnvironmentFile for EnvironmentFile { + fn apply(&self, options: ApplyOptions) { + let _ = set_from_file(self, options); + } +} + +impl <E> ApplyEnvironmentFile for Result<EnvironmentFile, E> { + fn apply(&self, options: ApplyOptions) { + if let Ok(file) = self { + file.apply(options); + } + } +} + pub struct EnvFileIterator<'a> { lines: &'a [FileLine], current: usize, @@ -166,14 +207,21 @@ fn is_missing_key(key: &str) -> Result<bool, EnvironmentFileError> { } } -fn set_from_file(file: &EnvironmentFile) -> Result<(), EnvironmentFileError> { +fn set_from_file(file: &EnvironmentFile, options: ApplyOptions) -> Result<(), EnvironmentFileError> { let mut defferred = Vec::with_capacity(file.len()); for line in file.lines_kv() { if let FileLine::KeyValue { key, .. } = &line { - if is_missing_key(key)? { + let key_name = if let Some(prefix) = &options.prefix { + format!("{}{}", prefix, key) + } else { + key.to_string() + }; + + if options.overwrite || is_missing_key(&key_name)? { if line.is_complete() { - env::set_var(key, line.assemble_value()); + eprintln!("Setting {} with value {}", key_name, line.assemble_value()); + env::set_var(key_name, line.assemble_value()); } else { defferred.push(line); } @@ -183,7 +231,12 @@ fn set_from_file(file: &EnvironmentFile) -> Result<(), EnvironmentFileError> { for line in defferred { if let FileLine::KeyValue { key, .. } = line { - env::set_var(key, line.assemble_value()); + let key_name = if let Some(prefix) = &options.prefix { + format!("{}{}", prefix, key) + } else { + key.to_string() + }; + env::set_var(key_name, line.assemble_value()); } } diff --git a/src/filesystem.rs b/src/filesystem.rs index af50b9f5c720952614b73dad5fc3ca48b245c7e1..7f17af6141e7a86b551d05c51988357881dcdac9 100644 --- a/src/filesystem.rs +++ b/src/filesystem.rs @@ -3,6 +3,7 @@ use std::fmt::Display; use std::fs::File; use std::io::Read; use std::path::Path; +use crate::env_file::{ApplyEnvironmentFile, ApplyOptions}; #[derive(Debug, thiserror::Error)] #[allow(clippy::enum_variant_names)] @@ -31,17 +32,21 @@ pub fn env_file_from_path(path: impl AsRef<Path>) -> Result<EnvironmentFile, Env } pub fn dotenv() -> Result<(), EnvFsError> { - let file = env_file()?; - file.apply().map_err(|_| EnvFsError::EnvironmentError) + env_file()?.apply(Default::default()); + Ok(()) +} +pub fn dotenv_opts(options: ApplyOptions) -> Result<(), EnvFsError> { + env_file()?.apply(options); + Ok(()) } pub fn dotenv_suffix(environment: impl Display) -> Result<(), EnvFsError> { - let file = env_file_suffix(environment)?; - file.apply().map_err(|_| EnvFsError::EnvironmentError) + env_file_suffix(environment)?.apply(Default::default()); + Ok(()) } pub fn dotenv_from(path: impl AsRef<Path>) -> Result<(), EnvFsError> { - let file = env_file_from_path(path)?; - file.apply().map_err(|_| EnvFsError::EnvironmentError) + env_file_from_path(path)?.apply(Default::default()); + Ok(()) } diff --git a/src/lib.rs b/src/lib.rs index 82f59c5ca4aca804c4c0244938c53264d586be73..72fc2a577b453f449d0e1c40a5c1eb53f80f026a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,8 +5,8 @@ mod env_file; mod filesystem; mod parser; -pub use env_file::{EnvironmentFile, EnvironmentFileError}; +pub use env_file::{EnvironmentFile, EnvironmentFileError, ApplyEnvironmentFile, ApplyOptions}; pub use parser::{FileLine, ValuePart}; #[cfg(feature = "fs")] -pub use filesystem::{dotenv, dotenv_from, dotenv_suffix}; +pub use filesystem::{dotenv, dotenv_from, dotenv_suffix, dotenv_opts}; diff --git a/src/parser.rs b/src/parser.rs index efb5b59c57b7572331c03b1095d9a309e8995550..2d41e13ea98e5d13b6a28c6197faa41a8462e2c6 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -123,7 +123,7 @@ impl FileLine { Self::KeyValue { key, value } => Self::KeyValue { key: key.clone(), value: value - .into_iter() + .iter() .filter(|part| !matches!(part, ValuePart::Comment(..))) .cloned() .collect(), diff --git a/tests/integration.rs b/tests/integration.rs index bbdd0a4bf3b743003d30b62f264249de7b4cec5e..4161db4a9e0f764a06ee3f5b250d0c9c38673380 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -1,4 +1,4 @@ -use envish::EnvironmentFile; +use envish::{ApplyEnvironmentFile, ApplyOptions, EnvironmentFile}; #[test] fn it_parses_basic_dotenv_file() { @@ -14,12 +14,13 @@ fn it_parses_basic_dotenv_file() { std::env::var("SOME_OTHER_VARIABLE").expect_err("SOME_OTHER_VARIABLE should not be set"); let file = EnvironmentFile::parse(file_contents).expect("Failed to parse environment file"); - file.apply().expect("Failed to apply environment file"); + file.apply(Default::default()); assert_eq!(std::env::var("MY_BEST_VARIABLE").unwrap(), "some_value"); assert_eq!(std::env::var("SOME_OTHER_VARIABLE").unwrap(), "1234"); } +#[test] fn it_parses_dotenv_file_with_interpolation() { let file_contents = r#" # This value won't be set in the test, and this comment will be ignored @@ -36,9 +37,33 @@ fn it_parses_dotenv_file_with_interpolation() { std::env::var("INTERPOLATED_VARIABLE").expect_err("INTERPOLATED_VARIABLE should not be set"); let file = EnvironmentFile::parse(file_contents).expect("Failed to parse environment file"); - file.apply().expect("Failed to apply environment file"); + file.apply(Default::default()); assert_eq!(std::env::var("MY_BEST_VARIABLE").unwrap(), "some_value"); assert_eq!(std::env::var("SOME_OTHER_VARIABLE").unwrap(), "1234"); assert_eq!(std::env::var("INTERPOLATED_VARIABLE").unwrap(), "1234567"); +} + +#[test] +fn it_parses_dotenv_file_with_interpolation_and_prefix_option() { + let file_contents = r#" + # This value won't be set in the test, and this comment will be ignored + MY_BEST_VARIABLE=some_value + # This variable is also not defined, and it'll still be a string, + # because all environment variables are strings without being converted to other data types + SOME_OTHER_VARIABLE=1234 + # This variable contains an interpolated value + INTERPOLATED_VARIABLE=${SOME_OTHER_VARIABLE}567 + "#; + + std::env::var("MY_BEST_VARIABLE").expect_err("MY_BEST_VARIABLE should not be set"); + std::env::var("SOME_OTHER_VARIABLE").expect_err("SOME_OTHER_VARIABLE should not be set"); + std::env::var("INTERPOLATED_VARIABLE").expect_err("INTERPOLATED_VARIABLE should not be set"); + + let file = EnvironmentFile::parse(file_contents).expect("Failed to parse environment file"); + file.apply(ApplyOptions::with_prefix("APP_")); + + assert_eq!(std::env::var("APP_MY_BEST_VARIABLE").unwrap(), "some_value"); + assert_eq!(std::env::var("APP_SOME_OTHER_VARIABLE").unwrap(), "1234"); + assert_eq!(std::env::var("APP_INTERPOLATED_VARIABLE").unwrap(), "1234567"); } \ No newline at end of file