diff --git a/DisCatSharp.Configuration.Tests/ConfigurationExtensionTests.cs b/DisCatSharp.Configuration.Tests/ConfigurationExtensionTests.cs index 2b6030919..967867424 100644 --- a/DisCatSharp.Configuration.Tests/ConfigurationExtensionTests.cs +++ b/DisCatSharp.Configuration.Tests/ConfigurationExtensionTests.cs @@ -1,113 +1,135 @@ using System; using System.Collections.Generic; using DisCatSharp.Common.Configuration; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Xunit; namespace DisCatSharp.Configuration.Tests { public class ConfigurationExtensionTests { 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(); + [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); } [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.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); + } } } diff --git a/DisCatSharp.Configuration/ConfigurationExtensions.cs b/DisCatSharp.Configuration/ConfigurationExtensions.cs index 4932c7af2..b2dda8cf9 100644 --- a/DisCatSharp.Configuration/ConfigurationExtensions.cs +++ b/DisCatSharp.Configuration/ConfigurationExtensions.cs @@ -1,102 +1,103 @@ // 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 { /// /// 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. /// /// 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) + public static TConfig ExtractConfig(this IConfiguration config, string sectionName, string? rootSectionName = "DisCatSharp") where TConfig : new() { - var section = new ConfigSection(ref config, sectionName); + 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(); 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); } 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}"); } } return configInstance; } } } diff --git a/DisCatSharp.Configuration/Models/ConfigSection.cs b/DisCatSharp.Configuration/Models/ConfigSection.cs index 51b8ab0ed..eb9c34801 100644 --- a/DisCatSharp.Configuration/Models/ConfigSection.cs +++ b/DisCatSharp.Configuration/Models/ConfigSection.cs @@ -1,73 +1,82 @@ // 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 Microsoft.Extensions.Configuration; namespace DisCatSharp.Common.Configuration.Models { /// /// Represents an object in /// internal readonly struct ConfigSection { /// /// Key within which represents an object containing multiple values /// public string SectionName { get;} + /// + /// Optional used to indicate this section is nested within another + /// + public string? Root { get; } + /// /// Reference to used within application /// public IConfiguration Config { get; } - public ConfigSection(ref IConfiguration config, string sectionName) + /// Reference to config + /// Section of interest + /// (Optional) Indicates is nested within this name. Default value is DisCatSharp + public ConfigSection(ref IConfiguration config, string sectionName, string? rootName = "DisCatSharp") { this.Config = config; this.SectionName = sectionName; + this.Root = rootName; } /// /// Checks if key exists in /// /// Property / Key to search for in section /// Config path to key /// (Optional) Root key to use. Default is DisCatSharp because configs in library should be consolidated /// True if key exists, otherwise false. Outputs path to config regardless - public bool ContainsKey(string name, out string path, string root = "DisCatSharp") + public bool ContainsKey(string name, out string path) { - path = string.IsNullOrEmpty(root) + path = string.IsNullOrEmpty(this.Root) ? this.Config.ConfigPath(this.SectionName, name) - : this.Config.ConfigPath(root, this.SectionName, name); + : this.Config.ConfigPath(this.Root, this.SectionName, name); return !string.IsNullOrEmpty(this.Config[path]); } /// - /// Attempts to get value associated to the config path. + /// Attempts to get value associated to the config path.
Should be used in unison with ///
/// Config path to value /// Value found at public string GetValue(string path) => this.Config[path]; } }