From 182e5a54326f5deeca0894d82bd682ccc804a908 Mon Sep 17 00:00:00 2001 From: Louis <contact@louiscap.co> Date: Sun, 5 Jan 2025 00:03:36 +0000 Subject: [PATCH] Make 'apply' work with EnvFile and Result<EnvFile, T> --- src/env_file.rs | 65 ++++++++++++++++++++++++++++++++++++++++---- src/filesystem.rs | 17 ++++++++---- src/lib.rs | 4 +-- src/parser.rs | 2 +- tests/integration.rs | 31 +++++++++++++++++++-- 5 files changed, 101 insertions(+), 18 deletions(-) diff --git a/src/env_file.rs b/src/env_file.rs index c013c25..97ae492 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 af50b9f..7f17af6 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 82f59c5..72fc2a5 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 efb5b59..2d41e13 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 bbdd0a4..4161db4 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 -- GitLab