diff --git a/DisCatSharp.Configuration/ConfigurationExtensions.cs b/DisCatSharp.Configuration/ConfigurationExtensions.cs index 76e6d9633..931e424e8 100644 --- a/DisCatSharp.Configuration/ConfigurationExtensions.cs +++ b/DisCatSharp.Configuration/ConfigurationExtensions.cs @@ -1,227 +1,270 @@ // 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; using System.Linq; using System.Reflection; using DisCatSharp.Configuration.Models; using Microsoft.Extensions.Configuration; namespace DisCatSharp.Configuration { internal static class ConfigurationExtensions { private const string FactoryErrorMessage = "Require a function which provides a default entity to work with"; public const string DefaultRootLib = "DisCatSharp"; + private const string ConfigSuffix = "Configuration"; + /// /// 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); /// /// Skims over the configuration section and only overrides values that are explicitly defined within the config /// /// Instance of config /// Section which contains values for private static void HydrateInstance(ref object config, ConfigSection section) { PropertyInfo[] props = config.GetType().GetProperties(); foreach (var prop in props) { // Must have a set method for this to work, otherwise continue on if (prop.SetMethod == null) continue; string entry = section.GetValue(prop.Name); object? value = null; if (typeof(string) == prop.PropertyType) { // We do NOT want to override value if nothing was provided if(!string.IsNullOrEmpty(entry)) prop.SetValue(config, entry); continue; } // We need to address collections a bit differently // They can come in the form of "root:section:name" with a string representation OR // "root:section:name:0" <--- this is not detectable when checking the above path if (typeof(IEnumerable).IsAssignableFrom(prop.PropertyType)) { if (string.IsNullOrEmpty(section.GetValue(prop.Name))) value = section.Config .GetSection(section.GetPath(prop.Name)).Get(prop.PropertyType); else value = Newtonsoft.Json.JsonConvert.DeserializeObject(entry, prop.PropertyType); if (value == null) continue; prop.SetValue(config, value); } // From this point onward we require the 'entry' value to have something useful if (string.IsNullOrEmpty(entry)) continue; try { // Primitive types are simple to convert if (prop.PropertyType.IsPrimitive) value = Convert.ChangeType(entry, prop.PropertyType); 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(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 '{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 (TConfig) configInstance; } /// /// Determines if contains a particular section/object (not value) /// /// /// /// { /// "Discord": { // this is a section/object /// /// }, /// "Value": "something" // this is not a section/object /// } /// /// /// /// /// True if section exists, otherwise false public static bool HasSection(this IConfiguration config, params string[] values) { if (!values.Any()) return false; if (values.Length == 1) return config.GetChildren().Any(x => x.Key == values[0]); if (config.GetChildren().All(x => x.Key != values[0])) return false; IConfigurationSection current = config.GetSection(values[0]); for (int i = 1; i < values.Length - 1; i++) { if (current.GetChildren().All(x => x.Key != values[i])) return false; current = current.GetSection(values[i]); } return current.GetChildren().Any(x=>x.Key == values[^1]); } + + /// + /// Instantiates an instance of , then consumes any custom + /// configuration from user/developer from .
+ /// View remarks for more info + ///
+ /// + /// This is an example of how your JSON structure should look if you wish + /// to override one or more of the default values from + /// + /// { + /// "DisCatSharp": { + /// "Discord": { } + /// } + /// } + /// + ///
+ /// Alternatively, you can use the type name itself + /// + /// { + /// "DisCatSharp": { + /// "DiscordConfiguration": { } + /// } + /// } + /// + ///
+ /// + /// Instance of + public static DiscordClient BuildClient(this IConfiguration config) + { + var section = config.HasSection(DefaultRootLib, "Discord") + ? "Discord" + : config.HasSection(DefaultRootLib, $"Discord{ConfigSuffix}") + ? $"Discord:{ConfigSuffix}" + : null; + + if (string.IsNullOrEmpty(section)) + return new DiscordClient(new()); + + return new DiscordClient(config.ExtractConfig(section)); + } } } diff --git a/DisCatSharp.Configuration/DisCatSharp.Configuration.csproj b/DisCatSharp.Configuration/DisCatSharp.Configuration.csproj index 51478b6a1..14db29ff6 100644 --- a/DisCatSharp.Configuration/DisCatSharp.Configuration.csproj +++ b/DisCatSharp.Configuration/DisCatSharp.Configuration.csproj @@ -1,37 +1,41 @@ net5.0 enable DisCatSharp.Configuration DisCatSharp.Configuration DisCatSharp.Configuration Configuration for DisCatSharp. discord, discord-api, bots, discord-bots, net-sdk, dcs, discatsharp, csharp, dotnet, vb-net, fsharp LICENSE.md True + + + + diff --git a/DisCatSharp.Hosting/ConfigurationExtensions.cs b/DisCatSharp.Hosting/ConfigurationExtensions.cs index 3005a9245..947d7688c 100644 --- a/DisCatSharp.Hosting/ConfigurationExtensions.cs +++ b/DisCatSharp.Hosting/ConfigurationExtensions.cs @@ -1,232 +1,191 @@ // 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 System.Reflection; 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 { /// /// Find assemblies that match the names provided via . /// /// /// In some cases the assembly the user is after could be used in the application but /// not appear within the .
/// The workaround for this is to check the assemblies in the , as well as referenced /// assemblies. If the targeted assembly is a reference, we need to load it into our workspace to get more info. ///
/// Names of assemblies to look for /// Assemblies which meet the given names. No duplicates public static Assembly[] FindAssemblies(string[]? names) { /* There is a possibility that an assembly can be referenced in multiple assemblies. To alleviate duplicates we need to shrink our queue as we find things */ List results = new(); if (names is null) return results.ToArray(); List queue = new(names); foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) { if (!queue.Any()) break; var loadedAssemblyName = assembly.GetName().Name; // Kinda need the name to do our thing if (loadedAssemblyName == null) continue; // Is this something we're looking for? if (queue.Contains(loadedAssemblyName)) { results.Add(assembly); // Shrink queue so we don't accidentally add the same assembly > 1 times queue.Remove(loadedAssemblyName); } // Time to check if one of the referenced assemblies is something we're looking for foreach(var referencedAssembly in assembly.GetReferencedAssemblies() .Where(x => x.Name != null && queue.Contains(x.Name))) try { // Must load the assembly into our workspace so we can do stuff with it later results.Add(Assembly.Load(referencedAssembly)); #pragma warning disable 8604 queue.Remove(referencedAssembly.Name); #pragma warning restore 8604 } catch (Exception ex) { Console.Error.WriteLine($"Unable to load referenced assembly: '{referencedAssembly.Name}' \n\t{ex.Message}"); } } return results.ToArray(); } - /// - /// Instantiates an instance of , then consumes any custom - /// configuration from user/developer from .
- /// View remarks for more info - ///
- /// - /// This is an example of how your JSON structure should look if you wish - /// to override one or more of the default values from - /// - /// { - /// "DisCatSharp": { - /// "Discord": { } - /// } - /// } - /// - ///
- /// Alternatively, you can use the type name itself - /// - /// { - /// "DisCatSharp": { - /// "DiscordConfiguration": { } - /// } - /// } - /// - ///
- /// - /// Instance of - public static DiscordClient BuildClient(this IConfiguration config) - { - var section = config.HasSection(Constants.LibName, "Discord") - ? "Discord" - : config.HasSection(Constants.LibName, $"Discord{Constants.ConfigSuffix}") - ? $"Discord:{Constants.ConfigSuffix}" - : null; - - if (string.IsNullOrEmpty(section)) - return new DiscordClient(new()); - - return new DiscordClient(config.ExtractConfig(section)); - } - /// /// 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(); string[]? assemblyNames; // Has the user defined a using section within the root name? if (!configuration.HasSection(rootName, "Using")) return results; /* There are 2 ways a user could list which assemblies are used "Using": "[\"Assembly.Name\"]" "Using": ["Assembly.Name"] JSON or as Text. */ if (string.IsNullOrEmpty(configuration[configuration.ConfigPath(rootName, "Using")])) assemblyNames = configuration.GetSection(configuration.ConfigPath(rootName, "Using")).Get(); else assemblyNames = Newtonsoft.Json.JsonConvert.DeserializeObject( configuration[configuration.ConfigPath(rootName, "Using")]); #pragma warning disable 8604 foreach (var assembly in FindAssemblies(assemblyNames.Select(x=> x.StartsWith(Constants.LibName) ? x : $"{Constants.LibName}.{x}").ToArray())) { 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); // IF THE SECTION IS NOT PROVIDED --> WE WILL USE DEFAULT CONFIG IMPLEMENTATION /* 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); } } } #pragma warning restore 8604 return results; } } }