diff --git a/DisCatSharp.Configuration.Tests/ConfigurationExtensionTests.cs b/DisCatSharp.Configuration.Tests/ConfigurationExtensionTests.cs index 967867424..d7e09b302 100644 --- a/DisCatSharp.Configuration.Tests/ConfigurationExtensionTests.cs +++ b/DisCatSharp.Configuration.Tests/ConfigurationExtensionTests.cs @@ -1,135 +1,208 @@ using System; using System.Collections.Generic; using DisCatSharp.Common.Configuration; +using DisCatSharp.Common.Configuration.Models; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Xunit; namespace DisCatSharp.Configuration.Tests { public class ConfigurationExtensionTests { + class SampleClass + { + public int Amount { get; set; } + public string Email { get; set; } + } + + class SampleClass2 + { + public TimeSpan Timeout { get; set; } = TimeSpan.FromMinutes(7); + public string Name { get; set; } = "Sample"; + + public string ConstructorValue { get; } + + public SampleClass2(string value) + { + this.ConstructorValue = value; + } + } + private IConfiguration BasicDiscordConfiguration() => new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary() { {"DisCatSharp:Discord:Token", "1234567890"}, {"DisCatSharp:Discord:TokenType", "Bot" }, {"DisCatSharp:Discord:MinimumLogLevel", "Information"}, {"DisCatSharp:Discord:UseRelativeRateLimit", "true"}, {"DisCatSharp:Discord:LogTimestampFormat", "yyyy-MM-dd HH:mm:ss zzz"}, {"DisCatSharp:Discord:LargeThreshold", "250"}, {"DisCatSharp:Discord:AutoReconnect", "true"}, {"DisCatSharp:Discord:ShardId", "123123"}, {"DisCatSharp:Discord:GatewayCompressionLevel", "Stream"}, {"DisCatSharp:Discord:MessageCacheSize", "1024"}, {"DisCatSharp:Discord:HttpTimeout", "00:00:20"}, {"DisCatSharp:Discord:ReconnectIndefinitely", "false"}, {"DisCatSharp:Discord:AlwaysCacheMembers", "true" }, {"DisCatSharp:Discord:DiscordIntents", "AllUnprivileged"}, {"DisCatSharp:Discord:MobileStatus", "false"}, {"DisCatSharp:Discord:UseCanary", "false"}, {"DisCatSharp:Discord:AutoRefreshChannelCache", "false"}, {"DisCatSharp:Discord:Intents", "AllUnprivileged"} }) .Build(); private IConfiguration DiscordIntentsConfig() => new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { {"DisCatSharp:Discord:Intents", "GuildEmojisAndStickers,GuildMembers,GuildInvites,GuildMessageReactions"} }) .Build(); private IConfiguration DiscordHaphazardConfig() => new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { { "DisCatSharp:Discord:Intents", "GuildEmojisAndStickers,GuildMembers,Guilds" }, { "DisCatSharp:Discord:MobileStatus", "true" }, { "DisCatSharp:Discord:LargeThreshold", "1000" }, { "DisCatSharp:Discord:HttpTimeout", "10:00:00" } }) .Build(); private IConfiguration SampleConfig() => new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { { "Sample:Amount", "200" }, { "Sample:Email", "test@gmail.com" } }) .Build(); + private IConfiguration SampleClass2Configuration_Default() => new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + {"Random:Stuff", "Meow"}, + {"SampleClass2:Name", "Purfection"} + }) + .Build(); + + private IConfiguration SampleClass2Configuration_Change() => new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + { "SampleClass:Timeout", "01:30:00" }, { "SampleClass:NotValid", "Something" } + }) + .Build(); + [Fact] public void TestExtractDiscordConfig_Intents() { var source = this.DiscordIntentsConfig(); DiscordConfiguration config = source.ExtractConfig("Discord"); var expected = DiscordIntents.GuildEmojisAndStickers | DiscordIntents.GuildMembers | DiscordIntents.GuildInvites | DiscordIntents.GuildMessageReactions; Assert.Equal(expected, config.Intents); } [Fact] public void TestExtractDiscordConfig_Haphzard() { var source = this.DiscordHaphazardConfig(); DiscordConfiguration config = source.ExtractConfig("Discord"); var expectedIntents = DiscordIntents.GuildEmojisAndStickers | DiscordIntents.GuildMembers | DiscordIntents.Guilds; Assert.Equal(expectedIntents, config.Intents); Assert.True(config.MobileStatus); Assert.Equal(1000, config.LargeThreshold); - - var expectedTimeout = TimeSpan.FromHours(10); - Assert.Equal(expectedTimeout, config.HttpTimeout); + Assert.Equal(TimeSpan.FromHours(10), config.HttpTimeout); } [Fact] public void TestExtractDiscordConfig_Default() { var source = this.BasicDiscordConfiguration(); DiscordConfiguration config = source.ExtractConfig("Discord"); Assert.Equal("1234567890", config.Token); Assert.Equal(TokenType.Bot, config.TokenType); Assert.Equal(LogLevel.Information, config.MinimumLogLevel); Assert.True(config.UseRelativeRatelimit); Assert.Equal("yyyy-MM-dd HH:mm:ss zzz", config.LogTimestampFormat); Assert.Equal(250, config.LargeThreshold); Assert.True(config.AutoReconnect); Assert.Equal(123123, config.ShardId); Assert.Equal(GatewayCompressionLevel.Stream, config.GatewayCompressionLevel); Assert.Equal(1024, config.MessageCacheSize); - - TimeSpan timeout = TimeSpan.FromSeconds(20); - Assert.Equal(timeout, config.HttpTimeout); + Assert.Equal(TimeSpan.FromSeconds(20), config.HttpTimeout); Assert.False(config.ReconnectIndefinitely); Assert.True(config.AlwaysCacheMembers); Assert.Equal(DiscordIntents.AllUnprivileged, config.Intents); Assert.False(config.MobileStatus); Assert.False(config.UseCanary); Assert.False(config.AutoRefreshChannelCache); } - class SampleClass - { - public int Amount { get; set; } - public string Email { get; set; } - } - [Fact] public void TestSection() { var source = this.SampleConfig(); SampleClass config = source.ExtractConfig("Sample", null); Assert.Equal(200, config.Amount); Assert.Equal("test@gmail.com", config.Email); } + + [Fact] + public void TestExtractConfig_V2_Default() + { + var source = this.SampleClass2Configuration_Default(); + var config = (SampleClass2) source.ExtractConfig("SampleClass", () => new SampleClass2("Test"), null); + Assert.Equal(TimeSpan.FromMinutes(7), config.Timeout); + Assert.Equal("Test", config.ConstructorValue); + Assert.Equal("Sample", config.Name); + } + + [Fact] + public void TestExtractConfig_V2_Change() + { + var source = this.SampleClass2Configuration_Change(); + var config = (SampleClass2) source.ExtractConfig("SampleClass", () => new SampleClass2("Test123"), null); + var span = new TimeSpan(0, 1, 30, 0); + Assert.Equal(span, config.Timeout); + Assert.Equal("Test123", config.ConstructorValue); + Assert.Equal("Sample", config.Name); + } + + [Fact] + public void TestExtractConfig_V3_Default() + { + var source = this.SampleClass2Configuration_Default(); + var config = + (SampleClass2)new ConfigSection(ref source, "SampleClass", null).ExtractConfig(() => + new SampleClass2("Meow")); + + Assert.Equal("Meow", config.ConstructorValue); + Assert.Equal(TimeSpan.FromMinutes(7), config.Timeout); + Assert.Equal("Sample", config.Name); + } + + [Fact] + public void TestExtractConfig_V3_Change() + { + var source = this.SampleClass2Configuration_Change(); + var config = + (SampleClass2)new ConfigSection(ref source, "SampleClass", null).ExtractConfig(() => + new SampleClass2("Meow")); + + Assert.Equal("Meow", config.ConstructorValue); + var span = new TimeSpan(0, 1, 30, 0); + Assert.Equal(span, config.Timeout); + Assert.Equal("Sample", config.Name); + } } } diff --git a/DisCatSharp.Configuration/ConfigurationExtensions.cs b/DisCatSharp.Configuration/ConfigurationExtensions.cs index b2dda8cf9..d6448dce2 100644 --- a/DisCatSharp.Configuration/ConfigurationExtensions.cs +++ b/DisCatSharp.Configuration/ConfigurationExtensions.cs @@ -1,103 +1,159 @@ // This file is part of the DisCatSharp project, a fork of DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Reflection; using DisCatSharp.Common.Configuration.Models; using Microsoft.Extensions.Configuration; namespace DisCatSharp.Common.Configuration { internal static class ConfigurationExtensions { + private const string FactoryErrorMessage = "Require a function which provides a default entity to work with"; + private const string DefaultRootLib = "DisCatSharp"; + /// /// Easily piece together paths that will work within /// /// (not used - only for adding context based functionality) /// The strings to piece together /// Strings joined together via ':' public static string ConfigPath(this IConfiguration config, params string[] values) => string.Join(":", values); /// - /// Instantiate a new instance of , then walk through the specified - /// in . Translate user-defined config values to the instance. + /// Skims over the configuration section and only overrides values that are explicitly defined within the config /// - /// Loaded App Configuration - /// Name of section to load - /// (Optional) Used when section is nested with another. Default value is DisCatSharp - /// Type of instance that represents - /// Hydrated instance of which contains the user-defined values (if any). - public static TConfig ExtractConfig(this IConfiguration config, string sectionName, string? rootSectionName = "DisCatSharp") - where TConfig : new() + /// Instance of config + /// Section which contains values for + private static void HydrateInstance(ref object config, ConfigSection section) { - var section = new ConfigSection(ref config, sectionName, rootSectionName); - - // Default values should hopefully be provided from the constructor - TConfig configInstance = new(); - - PropertyInfo[] props = typeof(TConfig).GetProperties(); + PropertyInfo[] props = config.GetType().GetProperties(); foreach (var prop in props) // If found in the config -- user/dev wants to override default value if (section.ContainsKey(prop.Name, out string path)) { // Must have a set method for this to work, otherwise continue on if (prop.SetMethod == null) continue; string entry = section.GetValue(path); try { object? value = null; // Primitive types are simple to convert if (prop.PropertyType.IsPrimitive) value = Convert.ChangeType(entry, prop.PropertyType); else if (prop.PropertyType == typeof(string)) value = entry; else { // The following types require a different approach if (prop.PropertyType.IsEnum) value = Enum.Parse(prop.PropertyType, entry); else if (typeof(TimeSpan) == prop.PropertyType) value = TimeSpan.Parse(entry); else if (typeof(DateTime) == prop.PropertyType) value = DateTime.Parse(entry); else if (typeof(DateTimeOffset) == prop.PropertyType) value = DateTimeOffset.Parse(entry); } // Update value within our config instance - prop.SetValue(configInstance, value); + prop.SetValue(config, value); } catch (Exception ex) { - Console.Error.WriteLine($"Unable to convert value of '{entry}' to type '{prop.PropertyType.Name}' for prop '{prop.Name}' in config '{typeof(TConfig).Name}'\n\t\t{ex.Message}"); + Console.Error.WriteLine($"Unable to convert value of '{entry}' to type '{prop.PropertyType.Name}' for prop '{prop.Name}' in config '{config.GetType().Name}'\n\t\t{ex.Message}"); } } + } + + /// + /// Instantiate an entity using then walk through the specified + /// and translate user-defined config values to the instantiated instance from + /// + /// Section containing values for targeted config + /// Function which generates a default entity + /// Hydrated instance of an entity which contains user-defined values (if any) + /// When is null + public static object ExtractConfig(this ConfigSection section, Func factory) + { + if (factory == null) + throw new ArgumentNullException(nameof(factory),FactoryErrorMessage); + + // Create default instance + object config = factory(); + + HydrateInstance(ref config, section); + + return config; + } + + /// + /// Instantiate an entity using then walk through the specified + /// in . Translate user-defined config values to the instantiated instance from + /// + /// Loaded App Configuration + /// Name of section to load + /// Function which creates a default entity to work with + /// (Optional) Used when section is nested within another. Default value is + /// Hydrated instance of an entity which contains user-defined values (if any) + /// When is null + public static object ExtractConfig(this IConfiguration config, string sectionName, Func factory, + string? rootSectionName = DefaultRootLib) + { + if (factory == null) + throw new ArgumentNullException(nameof(factory), FactoryErrorMessage); + + // create default instance + object instance = factory(); + + HydrateInstance(ref instance, new ConfigSection(ref config, sectionName, rootSectionName)); + + return instance; + } + + /// + /// Instantiate a new instance of , then walk through the specified + /// in . Translate user-defined config values to the instance. + /// + /// Loaded App Configuration + /// Name of section to load + /// (Optional) Used when section is nested with another. Default value is + /// Type of instance that represents + /// Hydrated instance of which contains the user-defined values (if any). + public static TConfig ExtractConfig(this IConfiguration config, string sectionName, string? rootSectionName = DefaultRootLib) + where TConfig : new() + { + // Default values should hopefully be provided from the constructor + object configInstance = new TConfig(); + + HydrateInstance(ref configInstance, new ConfigSection(ref config, sectionName, rootSectionName)); - return configInstance; + return (TConfig) configInstance; } } } diff --git a/DisCatSharp/Properties/AssemblyProperties.cs b/DisCatSharp/Properties/AssemblyProperties.cs index d9af161e0..c7c443bde 100644 --- a/DisCatSharp/Properties/AssemblyProperties.cs +++ b/DisCatSharp/Properties/AssemblyProperties.cs @@ -1,34 +1,35 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System.Runtime.CompilerServices; -[assembly: InternalsVisibleTo("DisCatSharp.Common")] [assembly: InternalsVisibleTo("DisCatSharp.ApplicationCommands")] [assembly: InternalsVisibleTo("DisCatSharp.CommandsNext")] +[assembly: InternalsVisibleTo("DisCatSharp.Common")] +[assembly: InternalsVisibleTo("DisCatSharp.Configuration.Tests")] [assembly: InternalsVisibleTo("DisCatSharp.Interactivity")] [assembly: InternalsVisibleTo("DisCatSharp.Lavalink")] +[assembly: InternalsVisibleTo("DisCatSharp.Phabricator")] +[assembly: InternalsVisibleTo("DisCatSharp.Support")] [assembly: InternalsVisibleTo("DisCatSharp.VoiceNext")] [assembly: InternalsVisibleTo("DisCatSharp.VoiceNext.Natives")] -[assembly: InternalsVisibleTo("Nyaw")] // Ignore pls, DisCatSharp Dev Debug -[assembly: InternalsVisibleTo("DisCatSharp.Configuration.Tests")] - +[assembly: InternalsVisibleTo("Nyaw")]