Skip to content

Commit

Permalink
Merge pull request #92 from MrDave1999/feature/issue_73
Browse files Browse the repository at this point in the history
Added support for binding a configuration class with the keys of the .env file
  • Loading branch information
MrDave1999 authored May 10, 2022
2 parents 5d92b0e + 21cb63e commit 436bbda
Show file tree
Hide file tree
Showing 8 changed files with 290 additions and 0 deletions.
27 changes: 27 additions & 0 deletions src/Binder/BinderException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;

namespace DotEnv.Core
{
/// <summary>
/// The exception that is thrown when the binder encounters one or more errors.
/// </summary>
public class BinderException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="BinderException" /> class.
/// </summary>
public BinderException()
{

}

/// <summary>
/// Initializes a new instance of the <see cref="BinderException" /> class with the a specified error message.
/// </summary>
/// <param name="message">The message that describes the error.</param>
public BinderException(string message) : base(message)
{
}
}
}
78 changes: 78 additions & 0 deletions src/Binder/EnvBinder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using static DotEnv.Core.ExceptionMessages;

namespace DotEnv.Core
{
/// <inheritdoc cref="IEnvBinder" />
public class EnvBinder : IEnvBinder
{
/// <summary>
/// Allows access to the configuration options for the binder.
/// </summary>
private readonly EnvBinderOptions _configuration = new();

/// <summary>
/// Allows access to the errors container of the binder.
/// </summary>
private readonly EnvValidationResult _validationResult = new();

/// <summary>
/// Initializes a new instance of the <see cref="EnvBinder" /> class.
/// </summary>
public EnvBinder()
{

}

/// <summary>
/// Initializes a new instance of the <see cref="EnvBinder" /> class with environment variables provider.
/// </summary>
/// <param name="provider">The environment variables provider.</param>
public EnvBinder(IEnvironmentVariablesProvider provider)
{
_configuration.EnvVars = provider;
}

/// <inheritdoc />
public TSettings Bind<TSettings>() where TSettings : new()
=> Bind<TSettings>(out _);

/// <inheritdoc />
public TSettings Bind<TSettings>(out EnvValidationResult result) where TSettings : new()
{
var settings = new TSettings();
var type = typeof(TSettings);
result = _validationResult;
foreach (PropertyInfo property in type.GetProperties())
{
var envKeyAttribute = (EnvKeyAttribute)Attribute.GetCustomAttribute(property, typeof(EnvKeyAttribute));
var variableName = envKeyAttribute is not null ? envKeyAttribute.Name : property.Name;
var retrievedValue = _configuration.EnvVars[variableName];

if (retrievedValue is null)
{
var errorMsg = envKeyAttribute is not null ? string.Format(KeyAssignedToPropertyIsNotSet, type.Name, property.Name, envKeyAttribute.Name)
: string.Format(PropertyDoesNotMatchConfigKeyMessage, property.Name);
_validationResult.Add(errorMsg);
continue;
}

try
{
property.SetValue(settings, Convert.ChangeType(retrievedValue, property.PropertyType));
}
catch (FormatException)
{
_validationResult.Add(errorMsg: string.Format(FailedConvertConfigurationValueMessage, variableName, property.PropertyType.Name, retrievedValue, property.PropertyType.Name));
}
}

if(_validationResult.HasError())
throw new BinderException(message: _validationResult.ErrorMessages);

return settings;
}
}
}
16 changes: 16 additions & 0 deletions src/Binder/EnvBinderOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;

namespace DotEnv.Core
{
/// <summary>
/// Represents the options for configuring various behaviors of the binder.
/// </summary>
public class EnvBinderOptions
{
/// <summary>
/// Gets or sets the environment variables provider.
/// </summary>
public IEnvironmentVariablesProvider EnvVars { get; set; } = new DefaultEnvironmentProvider();
}
}
32 changes: 32 additions & 0 deletions src/Binder/EnvKeyAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;

namespace DotEnv.Core
{
/// <summary>
/// Represents the key of a .env file that is assigned to a property.
/// </summary>
public class EnvKeyAttribute : Attribute
{
/// <summary>
/// Gets the name of the key the property is mapped to.
/// </summary>
public string Name { get; }

/// <summary>
/// Initializes a new instance of the <see cref="EnvKeyAttribute" /> class.
/// </summary>
public EnvKeyAttribute()
{

}
/// <summary>
/// Initializes a new instance of the <see cref="EnvKeyAttribute" /> class with the name of the key.
/// </summary>
/// <param name="name">The name of the key the property is mapped to.</param>
public EnvKeyAttribute(string name)
{
Name = name;
}
}
}
25 changes: 25 additions & 0 deletions src/Binder/IEnvBinder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;

namespace DotEnv.Core
{
/// <summary>
/// Allows binding strongly typed objects to configuration values.
/// </summary>
public interface IEnvBinder
{
/// <param name="result">The result contains the errors found by the binder.</param>
/// <inheritdoc cref="Bind()" />
TSettings Bind<TSettings>(out EnvValidationResult result) where TSettings : new();

/// <summary>
/// Binds the instance of the environment variables provider to a new instance of type TSettings.
/// </summary>
/// <typeparam name="TSettings">The type of the new instance to bind.</typeparam>
/// <exception cref="BinderException">
/// If the binder encounters one or more errors.
/// </exception>
/// <returns>The new instance of TSettings.</returns>
TSettings Bind<TSettings>() where TSettings : new();
}
}
3 changes: 3 additions & 0 deletions src/Constants/ExceptionMessages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,8 @@ public class ExceptionMessages
public const string RequiredKeysNotPresentMessage = "'{0}' is a key required by the application.";
public const string LengthOfParamsListIsZeroMessage = "The length of the params list is zero.";
public const string EncodingNotFoundMessage = "'{0}' is not a supported encoding name. For information on defining a custom encoding, see the documentation for the Encoding.RegisterProvider method.";
public const string PropertyDoesNotMatchConfigKeyMessage = "The '{0}' property does not match any configuration key.";
public const string KeyAssignedToPropertyIsNotSet = "Could not set the value in the '{0}.{1}' property because the '{2}' key is not set.";
public const string FailedConvertConfigurationValueMessage = "Failed to convert configuration value of '{0}' to type '{1}'. '{2}' is not a valid value for {3}.";
}
}
33 changes: 33 additions & 0 deletions tests/Binder/AppSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
namespace DotEnv.Core.Tests.Binder;

public class AppSettings
{
[EnvKey("BIND_JWT_SECRET")]
public string JwtSecret { get; set; }

[EnvKey("BIND_TOKEN_ID")]
public string TokenId { get; set; }

[EnvKey("BIND_RACE_TIME")]
public int RaceTime { get; set; }

public string BindSecretKey { get; set; }
public string BindJwtSecret { get; set; }
}

public class SettingsExample1
{
public string SecretKey { get; set; }
}

public class SettingsExample2
{
[EnvKey("SECRET_KEY")]
public string SecretKey { get; set; }
}

public class SettingsExample3
{
[EnvKey("BIND_WEATHER_ID")]
public int WeatherId { get; set; }
}
76 changes: 76 additions & 0 deletions tests/Binder/EnvBinderTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
namespace DotEnv.Core.Tests.Binder;

[TestClass]
public class EnvBinderTests
{
[TestMethod]
public void Bind_WhenPropertiesAreLinkedToTheDefaultProviderInstance_ShouldReturnsSettingsInstance()
{
SetEnvironmentVariable("BIND_JWT_SECRET", "12example");
SetEnvironmentVariable("BIND_TOKEN_ID", "e32d");
SetEnvironmentVariable("BIND_RACE_TIME", "23");
SetEnvironmentVariable("BindSecretKey", "12example");
SetEnvironmentVariable("BindJwtSecret", "secret123");

var settings = new EnvBinder().Bind<AppSettings>();

Assert.AreEqual(expected: "12example", actual: settings.JwtSecret);
Assert.AreEqual(expected: "e32d", actual: settings.TokenId);
Assert.AreEqual(expected: 23, actual: settings.RaceTime);
Assert.AreEqual(expected: "12example", actual: settings.BindSecretKey);
Assert.AreEqual(expected: "secret123", actual: settings.BindJwtSecret);
}

[TestMethod]
public void Bind_WhenPropertiesAreLinkedToTheCustomProviderInstance_ShouldReturnsSettingsInstance()
{
var customProvider = new CustomEnvironmentVariablesProvider();
customProvider["BIND_JWT_SECRET"] = "13example";
customProvider["BIND_TOKEN_ID"] = "e31d";
customProvider["BIND_RACE_TIME"] = "24";
customProvider["BindSecretKey"] = "13example";
customProvider["BindJwtSecret"] = "secret124";

var settings = new EnvBinder(customProvider).Bind<AppSettings>();

Assert.AreEqual(expected: "13example", actual: settings.JwtSecret);
Assert.AreEqual(expected: "e31d", actual: settings.TokenId);
Assert.AreEqual(expected: 24, actual: settings.RaceTime);
Assert.AreEqual(expected: "13example", actual: settings.BindSecretKey);
Assert.AreEqual(expected: "secret124", actual: settings.BindJwtSecret);
}

[TestMethod]
public void Bind_WhenPropertyDoesNotMatchConfigurationKey_ShouldThrowBinderException()
{
var binder = new EnvBinder();

void action() => binder.Bind<SettingsExample1>();

var ex = Assert.ThrowsException<BinderException>(action);
StringAssert.Contains(ex.Message, string.Format(PropertyDoesNotMatchConfigKeyMessage, "SecretKey"));
}

[TestMethod]
public void Bind_WhenKeyAssignedToThePropertyIsNotSet_ShouldThrowBinderException()
{
var binder = new EnvBinder();

void action() => binder.Bind<SettingsExample2>();

var ex = Assert.ThrowsException<BinderException>(action);
StringAssert.Contains(ex.Message, string.Format(KeyAssignedToPropertyIsNotSet, "SettingsExample2", "SecretKey", "SECRET_KEY"));
}

[TestMethod]
public void Bind_WhenConfigurationValueCannotBeConvertedToAnotherDataType_ShouldThrowBinderException()
{
var binder = new EnvBinder();
SetEnvironmentVariable("BIND_WEATHER_ID", "This is not an int");

void action() => binder.Bind<SettingsExample3>();

var ex = Assert.ThrowsException<BinderException>(action);
StringAssert.Contains(ex.Message, string.Format(FailedConvertConfigurationValueMessage, "BIND_WEATHER_ID", "Int32", "This is not an int", "Int32"));
}
}

0 comments on commit 436bbda

Please sign in to comment.