/*
* Copyright (c) 2024 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
ArgumentNullException.ThrowIfNull(config);
ArgumentNullException.ThrowIfNull(property);
ArgumentNullException.ThrowIfNull(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);
ArgumentNullException.ThrowIfNull(property);
ArgumentNullException.ThrowIfNull(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 a configuration property from the specified configuration scope
/// and invokes your callback function on the element if found to transform the
/// output value, or returns the default value if the property is not found.
///
///
///
/// The name of the configuration element to get
/// The function used to set the desired value from the config element
/// The default value to return
/// The property value returned from your getter callback, or the default value if not found
///
[return:NotNullIfNotNull(nameof(defaultValue))]
public static T? GetValueOrDefault(this IConfigScope config, string property, Func getter, T defaultValue)
{
return TryGetProperty(config, property, getter, out T? value) ? value : defaultValue;
}
///
/// 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);
}
}
}