diff --git a/DubUrl.Core/ConnectionUrlFactory.cs b/DubUrl.Core/ConnectionUrlFactory.cs index a3dc6eb2..82b03691 100644 --- a/DubUrl.Core/ConnectionUrlFactory.cs +++ b/DubUrl.Core/ConnectionUrlFactory.cs @@ -23,6 +23,7 @@ public ConnectionUrlFactory(SchemeMapperBuilder builder) internal ConnectionUrlFactory(IParser parser, SchemeMapperBuilder builder) => (Parser, SchemeMapperBuilder) = (parser, builder); - public virtual ConnectionUrl Instantiate(string url) => new (url, Parser, SchemeMapperBuilder); + public virtual ConnectionUrl Instantiate(string url) + => new (url, Parser, SchemeMapperBuilder); } } diff --git a/DubUrl.Extensions.Testing/Configuration/ConfiguredConnectionUrlFactoryTest.cs b/DubUrl.Extensions.Testing/Configuration/ConfiguredConnectionUrlFactoryTest.cs new file mode 100644 index 00000000..c79e18fe --- /dev/null +++ b/DubUrl.Extensions.Testing/Configuration/ConfiguredConnectionUrlFactoryTest.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using NUnit.Framework; +using DubUrl.Extensions.Configuration; +using DubUrl.Mapping; + +namespace DubUrl.Extensions.Testing.Configuration +{ + public class ConfiguredConnectionUrlFactoryTest + { + [Test()] + public void FromConfiguration_ExistingConnectionString_ValueReturned() + { + var connectionUrl = "mssql://localhost/Customers"; + var connectionName = "Customers"; + var connectionStrings = new Dictionary + { + [$"ConnectionStrings:{connectionName}"] = connectionUrl + }; + var config = new ConfigurationBuilder() + .AddInMemoryCollection(connectionStrings) + .Build(); + + var factory = new ConfiguredConnectionUrlFactory(new SchemeMapperBuilder(), config); + Assert.That(factory.InstantiateFromConnectionStrings(connectionName).Url, Is.EqualTo(connectionUrl)); + } + + [Test()] + public void FromConfiguration_ExistingKeys_ValueReturned() + { + var connectionUrl = "mssql://localhost/Customers"; + var key = "Databases:Customers:ConnectionUrl"; + var databases = new Dictionary + { + [key] = connectionUrl + }; + var config = new ConfigurationBuilder() + .AddInMemoryCollection(databases) + .Build(); + + var factory = new ConfiguredConnectionUrlFactory(new SchemeMapperBuilder(), config); + Assert.That(factory.InstantiateFromConfiguration(key.Split(':')).Url, Is.EqualTo(connectionUrl)); + } + + [Test()] + [TestCase("Foo")] + [TestCase("Databases:Foo")] + [TestCase("Databases:Customers:Foo")] + public void FromConfiguration_NotExistingKeys_Throws(string keys) + { + var connectionUrl = "mssql://localhost/Customers"; + var key = "Databases:Customers:ConnectionUrl"; + var databases = new Dictionary + { + [key] = connectionUrl + }; + var config = new ConfigurationBuilder() + .AddInMemoryCollection(databases) + .Build(); + + var factory = new ConfiguredConnectionUrlFactory(new SchemeMapperBuilder(), config); + Assert.Throws(() => factory.InstantiateFromConfiguration(keys.Split(':'))); + } + + [Test()] + public void BindFromConfiguration_ExistingKeyWithDetails_ReturnsValue() + { + var connectionUrl = "mssql://foo:bar@localhost:1234/Customers?foo=1&bar=2"; + var key = "Databases:Customers:ConnectionUrl"; + var databases = new Dictionary + { + [$"{key}:scheme"] = "mssql", + [$"{key}:host"] = "localhost", + [$"{key}:port"] = "1234", + [$"{key}:username"] = "foo", + [$"{key}:password"] = "bar", + [$"{key}:segments:0"] = "Customers", + [$"{key}:keys:0"] = "foo", + [$"{key}:values:0"] = "1", + [$"{key}:keys:1"] = "bar", + [$"{key}:values:1"] = "2", + }; + var config = new ConfigurationBuilder() + .AddInMemoryCollection(databases) + .Build(); + + var factory = new ConfiguredConnectionUrlFactory(new SchemeMapperBuilder(), config); + Assert.That(factory.InstantiateWithBind(key.Split(':')).Url, Is.EqualTo(connectionUrl)); + } + } +} diff --git a/DubUrl.Extensions/Configuration/ConfiguredConnectionUrlFactory.cs b/DubUrl.Extensions/Configuration/ConfiguredConnectionUrlFactory.cs new file mode 100644 index 00000000..382e7032 --- /dev/null +++ b/DubUrl.Extensions/Configuration/ConfiguredConnectionUrlFactory.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using DubUrl.Mapping; +using Microsoft.Extensions.Configuration; + +namespace DubUrl.Extensions.Configuration +{ + public class ConfiguredConnectionUrlFactory : ConnectionUrlFactory + { + private IConfigurationRoot Configuration { get; } + + public ConfiguredConnectionUrlFactory(SchemeMapperBuilder builder) + : base(builder) + { + Configuration = new ConfigurationBuilder() + .AddJsonFile("appsettings.json") + .AddEnvironmentVariables() + .Build(); + } + + public ConfiguredConnectionUrlFactory(SchemeMapperBuilder builder, IConfigurationRoot config) + : base(builder) + => Configuration = config; + + public ConnectionUrl InstantiateFromConnectionStrings(string name) + => Instantiate(Configuration.GetConnectionString(name) + ?? throw new ArgumentOutOfRangeException(nameof(name), $"Cannot find a connection string named '{name}' in the section 'ConnectionStrings'")); + + public ConnectionUrl InstantiateFromConfiguration(string[] keys) + => Instantiate(GetSection(Configuration, keys).Value + ?? throw new NullReferenceException($"Value of the key '{string.Join('.', keys)}' is null.")); + + public ConnectionUrl InstantiateWithBind(string[] keys) + => Instantiate(GetSection(Configuration, keys).Get().ToString()); + + private static IConfigurationSection GetSection(IConfigurationRoot config, string[] keys) + { + IConfigurationSection? section = null; + if (!keys.Any()) + throw new ArgumentOutOfRangeException(nameof(keys), $"The provided keys cannot be an empty array."); + for (int i = 0; i < keys.Length; i++) + { + section = section is not null ? section.GetSection(keys[i]) : config.GetSection(keys[i]); + if (!section.Exists()) + if (i == 0) + throw new KeyNotFoundException($"Cannot find the configuration key '{keys[i]}' at the root of configuration."); + else + throw new KeyNotFoundException($"Cannot find the configuration key '{keys[i]}' under the section '{string.Join('.', keys.Take(i))}'."); + } + return section!; + } + } +} diff --git a/DubUrl.Extensions/Configuration/ConnectionUrlSettings.cs b/DubUrl.Extensions/Configuration/ConnectionUrlSettings.cs index fc0785d1..31220fad 100644 --- a/DubUrl.Extensions/Configuration/ConnectionUrlSettings.cs +++ b/DubUrl.Extensions/Configuration/ConnectionUrlSettings.cs @@ -15,11 +15,12 @@ public record struct ConnectionUrlSettings string? Username, string? Password, string[]? Segments, + string? Segment, string[]? Keys, string[]? Values ) { - public override string ToString() + public override readonly string ToString() { IDictionary parameters; if (Keys is not null || Values is not null) @@ -50,12 +51,17 @@ public override string ToString() builder.UserName = encoder.Encode(Username); if (!string.IsNullOrEmpty(Password)) builder.Password = encoder.Encode(Password); + if (Segments is not null && Segments.Any() && !string.IsNullOrEmpty(Segment)) + throw new InvalidOperationException("Cannot define both an array of segments and a segment."); + if (Segments is not null && Segments.Any()) + builder.Path = string.Join('/', Segments.Select(encoder.Encode)); + else if (!string.IsNullOrEmpty(Segment)) + builder.Path = string.Join('/', Segment.Split('/').Select(encoder.Encode)); if (Segments is not null && Segments.Any()) builder.Path = string.Join('/', Segments.Select(encoder.Encode)); if (parameters.Any()) builder.Query = string.Join("&", parameters.Select(x => $"{encoder.Encode(x.Key)}={encoder.Encode(x.Value)}")); return builder.ToString(); } - } } diff --git a/DubUrl.QA/Configuration.cs b/DubUrl.QA/Configuration.cs index bf97e93e..334e3365 100644 --- a/DubUrl.QA/Configuration.cs +++ b/DubUrl.QA/Configuration.cs @@ -7,34 +7,61 @@ using DubUrl.Registering; using NUnit.Framework; using DubUrl.Extensions.Configuration; +using Microsoft.Extensions.Configuration; namespace DubUrl.QA { public class Configuration { + private IConfigurationRoot IniConfig { get; set; } + [OneTimeSetUp] public virtual void SetupFixture() - => new ProviderFactoriesRegistrator().Register(); - + { + new ProviderFactoriesRegistrator().Register(); + IniConfig = new ConfigurationBuilder().AddIniFile("appsettings.ini").Build(); + } + [Test] public void ReadFromAppSettingsJson_ConnectionStrings() { - var factory = new ConnectionUrlFactory(new SchemeMapperBuilder()); - Assert.That(factory.FromConfiguration("Customers").Url, Is.EqualTo("mssql://localhost/Customers")); + var factory = new ConfiguredConnectionUrlFactory(new SchemeMapperBuilder()); + Assert.That(factory.InstantiateFromConnectionStrings("Customers").Url, Is.EqualTo("mssql://localhost/Customers")); } [Test] public void ReadFromAppSettingsJson_Key() { - var factory = new ConnectionUrlFactory(new SchemeMapperBuilder()); - Assert.That(factory.FromConfiguration(new[] { "Databases", "Customers", "ConnectionUrl" }).Url, Is.EqualTo("pgsql://127.0.0.1/Customers")); + var factory = new ConfiguredConnectionUrlFactory(new SchemeMapperBuilder()); + Assert.That(factory.InstantiateFromConfiguration(new[] { "Databases", "Customers", "ConnectionUrl" }).Url, Is.EqualTo("pgsql://127.0.0.1/Customers")); } [Test] public void BindFromAppSettingsJson_Key() { - var factory = new ConnectionUrlFactory(new SchemeMapperBuilder()); - Assert.That(factory.BindFromConfiguration(new[] { "Databases", "Customers", "Details" }).Url, Is.EqualTo("mysql://remote.database.org:1234/myInstance/Customers")); + var factory = new ConfiguredConnectionUrlFactory(new SchemeMapperBuilder()); + Assert.That(factory.InstantiateWithBind(new[] { "Databases", "Customers", "Details" }).Url, Is.EqualTo("mysql://remote.database.org:1234/myInstance/Customers")); + } + + [Test] + public void ReadFromIni_ConnectionStrings() + { + var factory = new ConfiguredConnectionUrlFactory(new SchemeMapperBuilder(), IniConfig); + Assert.That(factory.InstantiateFromConnectionStrings("Customers").Url, Is.EqualTo("duckdb://localhost/Customers")); + } + + [Test] + public void ReadFromIni_Key() + { + var factory = new ConfiguredConnectionUrlFactory(new SchemeMapperBuilder(), IniConfig); + Assert.That(factory.InstantiateFromConfiguration(new[] { "Databases", "Customers" }).Url, Is.EqualTo("sqlite://localhost/Customers")); + } + + [Test] + public void BindFromIni_Key() + { + var factory = new ConfiguredConnectionUrlFactory(new SchemeMapperBuilder(), IniConfig); + Assert.That(factory.InstantiateWithBind(new[] { "Details" }).Url, Is.EqualTo("firebird://remote.database.org:1234/myInstance/Customers")); } } } diff --git a/DubUrl.QA/DubUrl.QA.csproj b/DubUrl.QA/DubUrl.QA.csproj index 133a12c6..98c3097b 100644 --- a/DubUrl.QA/DubUrl.QA.csproj +++ b/DubUrl.QA/DubUrl.QA.csproj @@ -68,24 +68,35 @@ - - + + + + + + + + + + + + + + + + - - - @@ -96,6 +107,9 @@ + + PreserveNewest + PreserveNewest diff --git a/DubUrl.QA/appsettings.ini b/DubUrl.QA/appsettings.ini new file mode 100644 index 00000000..3f63129c --- /dev/null +++ b/DubUrl.QA/appsettings.ini @@ -0,0 +1,12 @@ +[ConnectionStrings] +Customers=duckdb://localhost/Customers + +[Databases] +Customers=sqlite://localhost/Customers + +[Details] +scheme=firebird +host=remote.database.org +port=1234 +segment=myInstance/Customers +