/* * Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Extensions.Loading * File: ConfigurationExtensions.cs * * ConfigurationExtensions.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger * VNLib collection of libraries and utilities. * * VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ using System; using System.IO; using System.Text.Json; using System.Reflection; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using VNLib.Utils.Extensions; namespace VNLib.Plugins.Extensions.Loading { /// /// Specifies a configuration variable name in the plugin's configuration /// containing data specific to the type /// [AttributeUsage(AttributeTargets.Class)] public sealed class ConfigurationNameAttribute : Attribute { /// /// /// public string ConfigVarName { get; } /// /// Initializes a new /// /// The name of the configuration variable for the class public ConfigurationNameAttribute(string configVarName) { ConfigVarName = configVarName; } /// /// When true or not configured, signals that the type requires a configuration scope /// when loaded. When false, and configuration is not found, signals to the service loading /// system to continue without configuration /// public bool Required { get; init; } = true; } /// /// Contains extensions for plugin configuration specifc extensions /// public static class ConfigurationExtensions { public const string S3_CONFIG = "s3_config"; public const string S3_SECRET_KEY = "s3_secret"; public const string PLUGIN_ASSET_KEY = "assets"; public const string PLUGINS_HOST_KEY = "plugins"; public const string PLUGIN_PATH_KEY = "path"; /// /// Retrieves a top level configuration dictionary of elements for the specified type. /// The type must contain a /// /// The type to get the configuration of /// /// A of top level configuration elements for the type /// /// public static IConfigScope GetConfigForType(this PluginBase plugin) { Type t = typeof(T); return plugin.GetConfigForType(t); } /// /// Retrieves a top level configuration dictionary of elements with the specified property name. /// /// /// Search order: Plugin config, fall back to host config, throw if not found /// /// /// The config property name to retrieve /// A of top level configuration elements for the type /// /// public static IConfigScope GetConfig(this PluginBase plugin, string propName) { plugin.ThrowIfUnloaded(); try { //Try to get the element from the plugin config first if (!plugin.PluginConfig.TryGetProperty(propName, out JsonElement el)) { //Fallback to the host config el = plugin.HostConfig.GetProperty(propName); } //Get the top level config as a dictionary return new ConfigScope(el, propName); } catch (KeyNotFoundException) { throw new KeyNotFoundException($"Missing required top level configuration object '{propName}', in host/plugin configuration files"); } } /// /// Retrieves a top level configuration dictionary of elements with the specified property name, /// or null if no configuration could be found /// /// /// Search order: Plugin config, fall back to host config, null not found /// /// /// The config property name to retrieve /// A of top level configuration elements for the type /// public static IConfigScope? TryGetConfig(this PluginBase plugin, string propName) { plugin.ThrowIfUnloaded(); //Try to get the element from the plugin config first, or fallback to host if (plugin.PluginConfig.TryGetProperty(propName, out JsonElement el) || plugin.HostConfig.TryGetProperty(propName, out el)) { //Get the top level config as a dictionary return new ConfigScope(el, propName); } //No config found return null; } /// /// Retrieves a top level configuration dictionary of elements for the specified type. /// The type must contain a /// /// /// The type to get configuration data for /// A of top level configuration elements for the type /// public static IConfigScope GetConfigForType(this PluginBase plugin, Type type) { _ = type ?? throw new ArgumentNullException(nameof(type)); string? configName = GetConfigNameForType(type); if (configName == null) { ThrowConfigNotFoundForType(type); } return plugin.GetConfig(configName); } /// /// Gets a required configuration property from the specified configuration scope /// /// /// /// The name of the property to get /// A function to get the value from the json type /// The property value /// public static T? GetProperty(this IConfigScope config, string property, Func getter) { //Check null _ = config ?? throw new ArgumentNullException(nameof(config)); _ = property ?? throw new ArgumentNullException(nameof(property)); _ = getter ?? throw new ArgumentNullException(nameof(getter)); return !config.TryGetValue(property, out JsonElement el) ? default : getter(el); } /// /// Gets a required configuration property from the specified configuration scope /// /// /// /// The name of the property to get /// A function to get the value from the json type /// The property value /// /// public static T GetRequiredProperty(this IConfigScope config, string property, Func getter) { //Check null _ = config ?? throw new ArgumentNullException(nameof(config)); _ = property ?? throw new ArgumentNullException(nameof(property)); _ = getter ?? throw new ArgumentNullException(nameof(getter)); //Get the property if(!config.TryGetValue(property, out JsonElement el)) { throw new KeyNotFoundException($"Missing required configuration property '{property}' in config {config.ScopeName}"); } //Even if the getter returns null, throw return getter(el) ?? throw new KeyNotFoundException($"Missing required configuration property '{property}' in config {config.ScopeName}"); } /// /// Attempts to get a configuration property from the specified configuration scope /// and invokes your callback function on the element if found to transform the /// output value /// /// /// /// The name of the configuration element to get /// The function used to set the desired value from the config element /// The output value to set /// A value that indicates if the property was found public static bool TryGetProperty(this IConfigScope config, string property, Func getter, out T? value) { //Check null ArgumentNullException.ThrowIfNull(config, nameof(config)); ArgumentNullException.ThrowIfNull(property, nameof(property)); ArgumentNullException.ThrowIfNull(getter, nameof(getter)); //Get the property if (config.TryGetValue(property, out JsonElement el)) { //Safe to invoke callback function on the element and set the return value value = getter(el); return true; } value = default; return false; } /// /// Gets the configuration property name for the type /// /// The type to get the configuration name for /// The configuration property element name public static string? GetConfigNameForType(Type type) { //Get config name attribute from plugin type return type.GetCustomAttribute()?.ConfigVarName; } /// /// Determines if the type requires a configuration element. /// /// The type to determine config required status /// /// True if the configuration is required, or false if the /// was not declared, or is false /// public static bool ConfigurationRequired(Type type) { return type.GetCustomAttribute()?.Required ?? false; } /// /// Throws a with proper diagnostic information /// for missing configuration for a given type /// /// The type to raise exception for /// [DoesNotReturn] public static void ThrowConfigNotFoundForType(Type type) { //Try to get the config property name for the type string? configName = GetConfigNameForType(type); if (configName != null) { throw new KeyNotFoundException($"Missing required configuration key '{configName}' for type {type.Name}"); } else { throw new KeyNotFoundException($"Missing required configuration key for type {type.Name}"); } } /// /// Shortcut extension for to get /// config of current class /// /// The object that a configuration can be retrieved for /// The plugin containing configuration variables /// A of top level configuration elements for the type /// public static IConfigScope GetConfig(this PluginBase plugin, object obj) { Type t = obj.GetType(); return plugin.GetConfigForType(t); } /// /// Deserialzes the configuration to the desired object and calls its /// method. Validation exceptions /// are wrapped in a /// /// /// /// /// public static T DeserialzeAndValidate(this IConfigScope scope) where T : IOnConfigValidation { T conf = scope.Deserialze(); try { conf.Validate(); } catch(Exception ex) { throw new ConfigurationValidationException($"Configuration validation failed for type {typeof(T).Name}", ex); } return conf; } /// /// Determines if the current plugin configuration contains the require properties to initialize /// the type /// /// /// /// True if the plugin config contains the require configuration property public static bool HasConfigForType(this PluginBase plugin) => HasConfigForType(plugin, typeof(T)); /// /// Determines if the current plugin configuration contains the require properties to initialize /// the type /// /// The type to get the configuration for /// /// True if the plugin config contains the require configuration property public static bool HasConfigForType(this PluginBase plugin, Type type) { ConfigurationNameAttribute? configName = type.GetCustomAttribute(); //See if the plugin contains a configuration varables return configName != null && ( plugin.PluginConfig.TryGetProperty(configName.ConfigVarName, out _) || plugin.HostConfig.TryGetProperty(configName.ConfigVarName, out _) ); } /// /// Gets a given configuration element from the global configuration scope /// and deserializes it into the desired type. /// /// If the type inherits the /// method is invoked, and exceptions are warpped in /// /// /// If the type inherits the /// method is called by the service scheduler /// /// /// The configuration type /// /// The deserialzed configuration element /// public static TConfig GetConfigElement(this PluginBase plugin) { //Deserialze the element TConfig config = plugin.GetConfigForType().Deserialze(); //If the type is validatable, validate it if(config is IOnConfigValidation conf) { try { conf.Validate(); } catch (Exception ex) { throw new ConfigurationValidationException($"Configuration validation failed for type {typeof(TConfig).Name}", ex); } } //If async config, load async if(config is IAsyncConfigurable ac) { _ = plugin.ConfigureServiceAsync(ac); } return config; } /// /// Gets a given configuration element from the global configuration scope /// and deserializes it into the desired type. /// /// If the type inherits the /// method is invoked, and exceptions are warpped in /// /// /// If the type inherits the /// method is called by the service scheduler /// /// /// The configuration type /// /// The configuration element name override /// The deserialzed configuration element /// public static TConfig GetConfigElement(this PluginBase plugin, string elementName) { //Deserialze the element TConfig config = plugin.GetConfig(elementName).Deserialze(); //If the type is validatable, validate it if (config is IOnConfigValidation conf) { try { conf.Validate(); } catch (Exception ex) { throw new ConfigurationValidationException($"Configuration validation failed for type {typeof(TConfig).Name}", ex); } } //If async config, load async if (config is IAsyncConfigurable ac) { _ = plugin.ConfigureServiceAsync(ac); } return config; } /// /// Attempts to load the basic S3 configuration variables required /// for S3 client access /// /// /// The S3 configuration object found in the plugin/host configuration public static S3Config? TryGetS3Config(this PluginBase plugin) { //Try get the config IConfigScope? s3conf = plugin.TryGetConfig(S3_CONFIG); return s3conf?.Deserialze(); } /// /// Trys to get the optional assets directory from the plugin configuration /// /// /// The absolute path to the assets directory if defined, null otherwise public static string? GetAssetsPath(this PluginBase plugin) { //Get global plugin config element IConfigScope config = plugin.GetConfig(PLUGINS_HOST_KEY); //Try to get the assets path if its defined string? assetsPath = config.GetPropString(PLUGIN_ASSET_KEY); //Try to get the full path for the assets if we can return assetsPath != null ? Path.GetFullPath(assetsPath) : null; } /// /// Gets the absolute path to the plugins directory as defined in the host configuration /// /// /// The absolute path to the directory containing all plugins public static string GetPluginsPath(this PluginBase plugin) { //Get global plugin config element IConfigScope config = plugin.GetConfig(PLUGINS_HOST_KEY); //Get the plugins path or throw because it should ALWAYS be defined if this method is called string pluginsPath = config[PLUGIN_PATH_KEY].GetString()!; //Get absolute path return Path.GetFullPath(pluginsPath); } } }