diff --git a/DisCatSharp.Hosting.Tests/ExtensionTests.cs b/DisCatSharp.Hosting.Tests/ExtensionTests.cs index eb459adbc..c77be6ac3 100644 --- a/DisCatSharp.Hosting.Tests/ExtensionTests.cs +++ b/DisCatSharp.Hosting.Tests/ExtensionTests.cs @@ -1,108 +1,108 @@ using System; using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Configuration; using Xunit; using DisCatSharp.Interactivity; using DisCatSharp.Lavalink; namespace DisCatSharp.Hosting.Tests { public class HostExtensionTests { #region Reference to external assemblies - required to ensure they're loaded private InteractivityConfiguration interactivityConfig = null; private LavalinkConfiguration lavalinkConfig = null; private DiscordConfiguration discordConfig = null; #endregion private Dictionary DefaultDiscord() => new() { { "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" } }; public IConfiguration DiscordInteractivityConfiguration() => new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary(this.DefaultDiscord()) { - {"DisCatSharp:Interactivity", ""} // this should be enough to automatically add the extension + {"DisCatSharp:Using", "[\"DisCatSharp.Interactivity\"]"} // this should be enough to automatically add the extension }) .Build(); public IConfiguration DiscordOnlyConfiguration() => new ConfigurationBuilder() .AddInMemoryCollection(this.DefaultDiscord()) .Build(); public IConfiguration DiscordInteractivityAndLavaLinkConfiguration() => new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary(this.DefaultDiscord()) { - { "DisCatSharp:Interactivity", "" }, { "DisCatSharp:LavaLink", "" } + {"DisCatSharp:Using", "[\"DisCatSharp.Interactivity\",\"DisCatSharp.Lavalink\"]"} }) .Build(); [Fact] public void HasSection_Tests() { var source = this.DiscordInteractivityConfiguration(); bool hasDisCatSharp = source.HasSection("DisCatSharp"); Assert.True(hasDisCatSharp); bool hasDiscord = source.HasSection("DisCatSharp", "Discord"); Assert.True(hasDiscord); } [Fact] public void DiscoverExtensions_Interactivity() { var source = this.DiscordInteractivityConfiguration(); var discovered = source.FindImplementedExtensions(); // Remember that DiscordConfiguration does not have an implementation type which is assignable to BaseExtension - Assert.Equal(2,discovered.Count); + Assert.Equal(1,discovered.Count); var item = discovered.First(); Assert.Equal(typeof(InteractivityConfiguration), item.Value.ConfigType); Assert.Equal(typeof(InteractivityExtension), item.Value.ImplementationType); Assert.Equal("InteractivityExtension", item.Key); } [Fact] public void DiscoverExtensions_InteractivityAndLavaLink() { var source = this.DiscordInteractivityAndLavaLinkConfiguration(); var discovered = source.FindImplementedExtensions(); Assert.Equal(2, discovered.Count); var first = discovered.First(); var last = discovered.Last(); Assert.Equal(typeof(InteractivityConfiguration), first.Value.ConfigType); Assert.Equal(typeof(InteractivityExtension), first.Value.ImplementationType); Assert.True("InteractivityExtension".Equals(first.Key, StringComparison.OrdinalIgnoreCase)); Assert.Equal(typeof(LavalinkConfiguration), last.Value.ConfigType); Assert.Equal(typeof(LavalinkExtension), last.Value.ImplementationType); Assert.True("LavalinkExtension".Equals(last.Key, StringComparison.OrdinalIgnoreCase)); } } } diff --git a/DisCatSharp.Hosting.Tests/HostTests.cs b/DisCatSharp.Hosting.Tests/HostTests.cs index 364eb3a9f..7ee80a7b1 100644 --- a/DisCatSharp.Hosting.Tests/HostTests.cs +++ b/DisCatSharp.Hosting.Tests/HostTests.cs @@ -1,145 +1,145 @@ // 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.Collections.Generic; using DisCatSharp.Interactivity; using DisCatSharp.Lavalink; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Xunit; namespace DisCatSharp.Hosting.Tests { public class HostTests { private Dictionary DefaultDiscord() => new() { { "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" } }; public Dictionary DiscordInteractivity() => new (this.DefaultDiscord()) { - { "DisCatSharp:Interactivity", "" } // this should be enough to automatically add the extension + {"DisCatSharp:Using","[\"DisCatSharp.Interactivity\"]"}, }; public Dictionary DiscordInteractivityAndLavalink() => new (this.DefaultDiscord()) { - { "DisCatSharp:Interactivity", "" }, { "DisCatSharp:LavaLink", "" } + {"DisCatSharp:Using","[\"DisCatSharp.Interactivity\", \"DisCatSharp.Lavalink\"]"}, }; IHostBuilder Create(Dictionary configValues) => Host.CreateDefaultBuilder() .ConfigureServices(services => { services.AddSingleton(); }) .ConfigureHostConfiguration(builder => { builder.AddInMemoryCollection(configValues); }); [Fact] public void TestNoExtensions() { IHost? host = null; try { host = this.Create(this.DefaultDiscord()).Build(); var service = host.Services.GetRequiredService(); Assert.NotNull(service); Assert.NotNull(service.Client); } finally { host?.Dispose(); } } [Fact] public void TestInteractivityExtension() { IHost? host = null; try { host = this.Create(this.DiscordInteractivity()).Build(); var service = host.Services.GetRequiredService(); Assert.NotNull(service); Assert.NotNull(service.Client); Assert.NotNull(service.Client.GetExtension()); // Also verify IDiscordHostedService.Extensions - Assert.Equal(2, service.Extensions.Count); + Assert.Equal(1, service.Extensions.Count); } finally { host?.Dispose(); } } [Fact] public void TestInteractivityLavalinkExtensions() { IHost? host = null; try { host = this.Create(this.DiscordInteractivityAndLavalink()).Build(); var service = host.Services.GetRequiredService(); Assert.NotNull(service); Assert.NotNull(service.Client); Assert.NotNull(service.Client.GetExtension()); Assert.NotNull(service.Client.GetExtension()); Assert.Equal(2, service.Extensions.Count); } finally { host?.Dispose(); } } } } diff --git a/DisCatSharp.Hosting/ConfigurationExtensions.cs b/DisCatSharp.Hosting/ConfigurationExtensions.cs index b846f3e04..e4ef7b7e0 100644 --- a/DisCatSharp.Hosting/ConfigurationExtensions.cs +++ b/DisCatSharp.Hosting/ConfigurationExtensions.cs @@ -1,124 +1,133 @@ // 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.Collections.Generic; using System.Linq; using DisCatSharp.Configuration; using DisCatSharp.Configuration.Models; using Microsoft.Extensions.Configuration; namespace DisCatSharp.Hosting { internal struct ExtensionConfigResult { public ConfigSection Section { get; set; } public Type ConfigType { get; set; } public Type ImplementationType { get; set; } } internal static class ConfigurationExtensions { public static bool HasSection(this IConfiguration config, params string[] values) { // We require something to be passed in if (!values.Any()) return false; Queue queue = new(values); IConfigurationSection section = config.GetSection(queue.Dequeue()); while (section != null && queue.Any()) config.GetSection(queue.Dequeue()); return section != null; } /// /// Easily identify which configuration types have been added to the
/// This way we can dynamically load extensions without explicitly doing so ///
/// /// /// Dictionary where Key -> Name of implemented type
Value ->
public static Dictionary FindImplementedExtensions(this IConfiguration configuration, string rootName = Configuration.ConfigurationExtensions.DefaultRootLib) { if (string.IsNullOrEmpty(rootName)) throw new ArgumentNullException(nameof(rootName), "Root name must be provided"); Dictionary results = new(); + // Has the user defined a using section within the root name? + if (!configuration.HasSection(rootName, "Using") || + string.IsNullOrEmpty(configuration[configuration.ConfigPath(rootName,"Using")])) + return results; + + string usingAssemblies = configuration[configuration.ConfigPath(rootName, "Using")]; + + var assemblyNames = Newtonsoft.Json.JsonConvert.DeserializeObject(usingAssemblies); + // Assemblies managed by DisCatSharp var assemblies = AppDomain.CurrentDomain.GetAssemblies() - .Where(x => x.FullName != null && x.FullName.StartsWith(Configuration.ConfigurationExtensions.DefaultRootLib)); + .Where(x => assemblyNames.Contains(x.GetName().Name)); foreach (var assembly in assemblies) { ExtensionConfigResult result = new(); foreach (var type in assembly.ExportedTypes .Where(x => x.Name.EndsWith(Constants.ConfigSuffix) && !x.IsAbstract && !x.IsInterface)) { string sectionName = type.Name; string prefix = type.Name.Replace(Constants.ConfigSuffix, ""); result.ConfigType = type; // Does a section exist with the classname? (DiscordConfiguration - for instance) if(configuration.HasSection(rootName, sectionName)) result.Section = new ConfigSection(ref configuration, type.Name, rootName); // Does a section exist with the classname minus Configuration? (Discord - for Instance) else if (configuration.HasSection(rootName, prefix)) result.Section = new ConfigSection(ref configuration, prefix, rootName); // We require the implemented type to exist so we'll continue onward else continue; /* Now we need to find the type which should consume our config In the event a user has some "fluff" between prefix and suffix we'll just check for beginning and ending values. Type should not be an interface or abstract, should also be assignable to BaseExtension */ var implementationType = assembly.ExportedTypes.FirstOrDefault(x => !x.IsAbstract && !x.IsInterface && x.Name.StartsWith(prefix) && x.Name.EndsWith(Constants.ExtensionSuffix) && x.IsAssignableTo(typeof(BaseExtension))); // If the implementation type was found we can add it to our result set if (implementationType != null) { result.ImplementationType = implementationType; results.Add(implementationType.Name, result); } } } return results; } } }