Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/decouple env vars #105

Draft
wants to merge 12 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 4 additions & 26 deletions src/DotNetEnv/Configuration/EnvConfigurationProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,33 +21,11 @@ public EnvConfigurationProvider(

public override void Load()
{
IEnumerable<KeyValuePair<string, string>> values;
if (paths == null)
{
values = Env.Load(options: options);
}
else
{
if (paths.Length == 1)
{
values = Env.Load(paths[0], options);
}
else
{
values = Env.LoadMulti(paths, options);
}
}
var values = paths == null
? Env.Load(options: options)
: Env.LoadMulti(paths, options);

// Since the Load method does not take care of clobberring, We have to check it here!
var dictionaryOption = options.ClobberExistingVars ? CreateDictionaryOption.TakeLast : CreateDictionaryOption.TakeFirst;
var dotEnvDictionary = values.ToDotEnvDictionary(dictionaryOption);

if (!options.ClobberExistingVars)
foreach (string key in Environment.GetEnvironmentVariables().Keys)
if (dotEnvDictionary.ContainsKey(key))
dotEnvDictionary[key] = Environment.GetEnvironmentVariable(key);

foreach (var value in dotEnvDictionary)
foreach (var value in values)
Data[NormalizeKey(value.Key)] = value.Value;
}

Expand Down
89 changes: 58 additions & 31 deletions src/DotNetEnv/Env.cs
Original file line number Diff line number Diff line change
@@ -1,27 +1,31 @@
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using DotNetEnv.Extensions;

namespace DotNetEnv
{
public class Env
{
public const string DEFAULT_ENVFILENAME = ".env";

public static ConcurrentDictionary<string, string> FakeEnvVars = new ConcurrentDictionary<string, string>();

public static IEnumerable<KeyValuePair<string, string>> LoadMulti (string[] paths, LoadOptions options = null)
public static IEnumerable<KeyValuePair<string, string>> LoadMulti(string[] paths, LoadOptions options = null)
{
return paths.Aggregate(
Enumerable.Empty<KeyValuePair<string, string>>(),
(kvps, path) => kvps.Concat(Load(path, options))
Array.Empty<KeyValuePair<string, string>>(),
(kvps, path) => kvps.Concat(Load(path, options, kvps)).ToArray()
);
}

public static IEnumerable<KeyValuePair<string, string>> Load (string path = null, LoadOptions options = null)
public static IEnumerable<KeyValuePair<string, string>> Load(string path = null, LoadOptions options = null)
=> Load(path, options, null);

private static IEnumerable<KeyValuePair<string, string>> Load(string path, LoadOptions options,
IEnumerable<KeyValuePair<string, string>> previousValues)
{
if (options == null) options = LoadOptions.DEFAULT;

Expand All @@ -45,6 +49,7 @@ public static IEnumerable<KeyValuePair<string, string>> Load (string path = null
path = null;
break;
}

dir = parent.FullName;
path = Path.Combine(dir, file);
}
Expand All @@ -55,52 +60,74 @@ public static IEnumerable<KeyValuePair<string, string>> Load (string path = null
{
return Enumerable.Empty<KeyValuePair<string, string>>();
}
return LoadContents(File.ReadAllText(path), options);

return LoadContents(File.ReadAllText(path), options, previousValues);
}

public static IEnumerable<KeyValuePair<string, string>> Load (Stream file, LoadOptions options = null)
public static IEnumerable<KeyValuePair<string, string>> Load(Stream file, LoadOptions options = null)
{
using (var reader = new StreamReader(file))
{
return LoadContents(reader.ReadToEnd(), options);
}
}

public static IEnumerable<KeyValuePair<string, string>> LoadContents (string contents, LoadOptions options = null)
public static IEnumerable<KeyValuePair<string, string>> LoadContents(string contents,
LoadOptions options = null)
=> LoadContents(contents, options, null);

private static IEnumerable<KeyValuePair<string, string>> LoadContents(string contents,
LoadOptions options, IEnumerable<KeyValuePair<string, string>> previousValues)
{
if (options == null) options = LoadOptions.DEFAULT;

previousValues = previousValues?.ToArray() ?? Array.Empty<KeyValuePair<string, string>>();

var envVarSnapshot = Environment.GetEnvironmentVariables().Cast<DictionaryEntry>()
.Select(entry => new KeyValuePair<string, string>(entry.Key.ToString(), entry.Value.ToString()))
.ToArray();

var dictionaryOption = options.ClobberExistingVars
? CreateDictionaryOption.TakeLast
: CreateDictionaryOption.TakeFirst;

var actualValues = new ConcurrentDictionary<string, string>(envVarSnapshot.Concat(previousValues)
.ToDotEnvDictionary(dictionaryOption));

var pairs = Parsers.ParseDotenvFile(contents, options.ClobberExistingVars, actualValues);

// for NoClobber, remove pairs which are exactly contained in previousValues or present in EnvironmentVariables
var unClobberedPairs = (options.ClobberExistingVars
? pairs
: pairs.Where(p =>
previousValues.All(pv => pv.Key != p.Key) &&
Environment.GetEnvironmentVariable(p.Key) == null))
.ToArray();

if (options.SetEnvVars)
{
if (options.ClobberExistingVars)
{
return Parsers.ParseDotenvFile(contents, Parsers.SetEnvVar);
}
else
{
return Parsers.ParseDotenvFile(contents, Parsers.NoClobberSetEnvVar);
}
}
else
{
return Parsers.ParseDotenvFile(contents, Parsers.DoNotSetEnvVar);
}
foreach (var pair in unClobberedPairs)
Environment.SetEnvironmentVariable(pair.Key, pair.Value);

return unClobberedPairs.ToDotEnvDictionary(dictionaryOption);
}

public static string GetString (string key, string fallback = default(string)) =>
public static string GetString(string key, string fallback = default(string)) =>
Environment.GetEnvironmentVariable(key) ?? fallback;

public static bool GetBool (string key, bool fallback = default(bool)) =>
public static bool GetBool(string key, bool fallback = default(bool)) =>
bool.TryParse(Environment.GetEnvironmentVariable(key), out var value) ? value : fallback;

public static int GetInt (string key, int fallback = default(int)) =>
public static int GetInt(string key, int fallback = default(int)) =>
int.TryParse(Environment.GetEnvironmentVariable(key), out var value) ? value : fallback;

public static double GetDouble (string key, double fallback = default(double)) =>
double.TryParse(Environment.GetEnvironmentVariable(key), NumberStyles.Any, CultureInfo.InvariantCulture, out var value) ? value : fallback;
public static double GetDouble(string key, double fallback = default(double)) =>
double.TryParse(Environment.GetEnvironmentVariable(key), NumberStyles.Any, CultureInfo.InvariantCulture,
out var value)
? value
: fallback;

public static LoadOptions NoEnvVars () => LoadOptions.NoEnvVars();
public static LoadOptions NoClobber () => LoadOptions.NoClobber();
public static LoadOptions TraversePath () => LoadOptions.TraversePath();
public static LoadOptions NoEnvVars() => LoadOptions.NoEnvVars();
public static LoadOptions NoClobber() => LoadOptions.NoClobber();
public static LoadOptions TraversePath() => LoadOptions.TraversePath();
}
}
5 changes: 1 addition & 4 deletions src/DotNetEnv/IValue.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
using System;
using System.Linq;
using System.Collections.Generic;
using System.Text.RegularExpressions;

namespace DotNetEnv
{
Expand All @@ -21,8 +19,7 @@ public ValueInterpolated (string id)

public string GetValue ()
{
var val = Environment.GetEnvironmentVariable(_id);
return val ?? (Env.FakeEnvVars.TryGetValue(_id, out val) ? val : string.Empty);
return Parsers.ActualValuesSnapshot.TryGetValue(_id, out var val) ? val : string.Empty;
}
}

Expand Down
47 changes: 19 additions & 28 deletions src/DotNetEnv/Parsers.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Collections.Generic;
using System.Text;
Expand All @@ -11,28 +12,7 @@ namespace DotNetEnv
{
class Parsers
{
public static KeyValuePair<string, string> SetEnvVar (KeyValuePair<string, string> kvp)
{
Environment.SetEnvironmentVariable(kvp.Key, kvp.Value);
return kvp;
}

public static KeyValuePair<string, string> DoNotSetEnvVar (KeyValuePair<string, string> kvp)
{
Env.FakeEnvVars.AddOrUpdate(kvp.Key, kvp.Value, (_, v) => v);
return kvp;
}

public static KeyValuePair<string, string> NoClobberSetEnvVar (KeyValuePair<string, string> kvp)
{
if (Environment.GetEnvironmentVariable(kvp.Key) == null)
{
Environment.SetEnvironmentVariable(kvp.Key, kvp.Value);
}
// not sure if maybe should return something different if avoided clobber... (current value?)
// probably not since the point is to return what the dotenv file reported, but it's arguable
return kvp;
}
public static ConcurrentDictionary<string, string> ActualValuesSnapshot = new ConcurrentDictionary<string, string>();

// helpful blog I discovered only after digging through all the Sprache source myself:
// https://justinpealing.me.uk/post/2020-03-11-sprache1-chars/
Expand Down Expand Up @@ -293,13 +273,24 @@ from _c in Comment.OptionalOrDefault()
from _lt in LineTerminator
select new KeyValuePair<string, string>(null, null));

public static IEnumerable<KeyValuePair<string, string>> ParseDotenvFile (
string contents,
Func<KeyValuePair<string, string>, KeyValuePair<string, string>> tranform
)
public static IEnumerable<KeyValuePair<string, string>> ParseDotenvFile(string contents,
bool clobberExistingVariables = true, IDictionary<string, string> actualValues = null)
{
return Assignment.Select(tranform).Or(Empty).Many().AtEnd()
.Parse(contents).Where(kvp => kvp.Key != null);
ActualValuesSnapshot = new ConcurrentDictionary<string, string>(actualValues ?? new Dictionary<string, string>());

return Assignment.Select(UpdateEnvVarSnapshot).Or(Empty)
.Many()
.AtEnd()
.Parse(contents)
.Where(kvp => kvp.Key != null);

KeyValuePair<string, string> UpdateEnvVarSnapshot(KeyValuePair<string, string> pair)
{
if (clobberExistingVariables || !ActualValuesSnapshot.ContainsKey(pair.Key))
ActualValuesSnapshot.AddOrUpdate(pair.Key, pair.Value, (key, oldValue) => pair.Value);

return pair;
}
}
}
}
2 changes: 2 additions & 0 deletions test/DotNetEnv.Tests/.env2
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ WHITELEAD=' leading white space followed by comment' # comment
UNICODE="\u00ae \U0001F680 日本"
NAME=Other
ENVVAR_TEST=overridden_2
ClobberEnvVarTest=$ENVVAR_TEST
UrlFromVariable=$URL
26 changes: 21 additions & 5 deletions test/DotNetEnv.Tests/EnvConfigurationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,22 @@ public void AddSourceToBuilderAndLoadMultiWithNoClobber()
.Build();

Assert.Equal("Toni", configuration["NAME"]);
Assert.Equal("ENV value", configuration["ENVVAR_TEST"]);
Assert.Null(configuration["ENVVAR_TEST"]); // value from EnvironmentVariables is not contained for NoClobber
Assert.Equal("ENV value", configuration["ClobberEnvVarTest"]); // should contain ENVVAR_TEST from EnvironmentVariable
Assert.Equal("https://github.com/tonerdo", configuration["UrlFromVariable"]); // should contain Url from .env
}

[Fact]
public void AddSourceToBuilderAndLoadMultiWithClobber()
{
configuration = new ConfigurationBuilder()
.AddDotNetEnvMulti(new[] { "./.env", "./.env2" }, LoadOptions.NoEnvVars())
.Build();

Assert.Equal("Other", configuration["NAME"]);
Assert.Equal("overridden_2", configuration["ENVVAR_TEST"]);
Assert.Equal("overridden_2", configuration["ClobberEnvVarTest"]); // should contain ENVVAR_TEST from .env
Assert.Equal("https://github.com/tonerdo", configuration["UrlFromVariable"]); // should contain Url from .env
}

[Fact]
Expand All @@ -117,18 +132,19 @@ public void AddSourceToBuilderAndGetSection()
Assert.Equal("value2", section["Key2"]);
}

[Fact()]
public void AddSourceToBuilderAndParseInterpolatedTest()
[Theory]
[InlineData(true)]
[InlineData(false)]
public void AddSourceToBuilderAndParseInterpolatedTest(bool setEnvVars)
{
Environment.SetEnvironmentVariable("EXISTING_ENVIRONMENT_VARIABLE", "value");
Environment.SetEnvironmentVariable("DNE_VAR", null);

// Have to remove since it's recursive and can be set by the `EnvTests.cs`
Environment.SetEnvironmentVariable("TEST4", null);
Env.FakeEnvVars.Clear();

configuration = new ConfigurationBuilder()
.AddDotNetEnv("./.env_embedded")
.AddDotNetEnv("./.env_embedded", new LoadOptions(setEnvVars: setEnvVars))
.Build();

Assert.Equal("test", configuration["TEST"]);
Expand Down
22 changes: 15 additions & 7 deletions test/DotNetEnv.Tests/EnvTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,19 @@ public void LoadMultiTest()
Assert.Equal("Person", Environment.GetEnvironmentVariable("NAME"));
}

[Fact]
public void LoadMultiTestNoEnvVars()
{
var pairs = DotNetEnv.Env.NoEnvVars().LoadMulti(new[] { "./.env", "./.env2" });
Assert.Equal("Other", pairs.LastOrDefault(x => x.Key == "NAME").Value);
Environment.SetEnvironmentVariable("NAME", null);
pairs = DotNetEnv.Env.NoEnvVars().NoClobber().LoadMulti(new[] { "./.env", "./.env2" });
Assert.Equal("Toni", pairs.FirstOrDefault(x => x.Key == "NAME").Value);
Environment.SetEnvironmentVariable("NAME", "Person");
pairs = DotNetEnv.Env.NoEnvVars().NoClobber().LoadMulti(new[] { "./.env", "./.env2" });
Assert.Null(pairs.FirstOrDefault(x => x.Key == "NAME").Value); // value from EnvironmentVariables is not contained with NoClobber
}

[Fact]
public void LoadNoClobberTest()
{
Expand Down Expand Up @@ -390,16 +403,11 @@ public void OtherTest()
Environment.SetEnvironmentVariable("NVAR2", "_nvar2_");

var kvps = DotNetEnv.Env.Load("./.env_other").ToArray();
Assert.Equal(35, kvps.Length);
var dict = kvps.ToDotEnvDictionary();
Assert.Equal(34, kvps.Length);

// note that env vars get only the final assignment, but all are returned
Assert.Equal("dupe2", Environment.GetEnvironmentVariable("DUPLICATE"));
Assert.Equal("dupe2", dict["DUPLICATE"]);
Assert.Equal("DUPLICATE", kvps[0].Key);
Assert.Equal("DUPLICATE", kvps[1].Key);
Assert.Equal("dupe1", kvps[0].Value);
Assert.Equal("dupe2", kvps[1].Value);
Assert.Equal("dupe2", kvps[0].Value);

Assert.Equal("bar", Environment.GetEnvironmentVariable("TEST_KEYWORD_1"));
Assert.Equal("12345", Environment.GetEnvironmentVariable("TEST_KEYWORD_2"));
Expand Down
Loading