diff options
author | vnugent <public@vaughnnugent.com> | 2024-06-21 17:08:16 -0400 |
---|---|---|
committer | vnugent <public@vaughnnugent.com> | 2024-06-21 17:08:16 -0400 |
commit | db5747a20600a2e2c5e8d915cf0bdbe4ec6df6a2 (patch) | |
tree | 4aea2b29379239d0f6bb894c9700eda2b04ae7b3 /lib/VNLib.Plugins.Extensions.Loading | |
parent | 9bc24801735884e0c03aa00e83804448c466bdf2 (diff) |
configuration validation updates
Diffstat (limited to 'lib/VNLib.Plugins.Extensions.Loading')
6 files changed, 229 insertions, 66 deletions
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Configuration/IOnConfigValidation.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Configuration/IOnConfigValidation.cs index 6d4641b..6eeba78 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/Configuration/IOnConfigValidation.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/Configuration/IOnConfigValidation.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2023 Vaughn Nugent +* Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Extensions.Loading @@ -33,6 +33,6 @@ namespace VNLib.Plugins.Extensions.Loading /// <summary> /// Validates a json configuration during deserialzation /// </summary> - void Validate(); + void OnValidate(); } } diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Configuration/Validate.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Configuration/Validate.cs new file mode 100644 index 0000000..1bb6787 --- /dev/null +++ b/lib/VNLib.Plugins.Extensions.Loading/src/Configuration/Validate.cs @@ -0,0 +1,114 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Loading +* File: Validate.cs +* +* Validate.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.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +using VNLib.Utils.IO; + +namespace VNLib.Plugins.Extensions.Loading.Configuration +{ + /// <summary> + /// A class that allows for easy configuration validation + /// </summary> + public sealed class Validate + { + /// <summary> + /// Ensures the object is not null and not an empty string, + /// otherwise a <see cref="ConfigurationValidationException"/> is raised + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="obj">The object to test</param> + /// <param name="message">The message to display to the user on loading</param> + /// <exception cref="ConfigurationValidationException"></exception> + [DoesNotReturn] + public static void NotNull<T>(T? obj, string message) where T : class + { + if (obj is null) + { + throw new ConfigurationValidationException(message); + } + + if (obj is string s && string.IsNullOrWhiteSpace(s)) + { + throw new ConfigurationValidationException(message); + } + } + + /// <summary> + /// + /// </summary> + /// <param name="condition"></param> + /// <param name="message"></param> + /// <exception cref="ConfigurationValidationException"></exception> + public static void Assert([DoesNotReturnIf(false)] bool condition, string message) + { + if (!condition) + { + throw new ConfigurationValidationException(message); + } + } + + public static void NotEqual<T>(T a, T b, string message) + { + if (a is null || b is null) + { + throw new ConfigurationValidationException(message); + } + + if (a.Equals(b)) + { + throw new ConfigurationValidationException(message); + } + } + + public static void Range2<T>(T value, T min, T max, string message) + where T : IComparable<T> + { + //Compare the value against min/max calues and raise exception if it is + if (value.CompareTo(min) < 0 || value.CompareTo(max) > 0) + { + throw new ConfigurationValidationException(message); + } + } + + + public static void Range<T>(T value, T min, T max, [CallerArgumentExpression(nameof(value))] string? paramName = null) + where T : IComparable<T> + { + + Range2(value, min, max, $"Value for {paramName} must be between {min} and {max}. Value: {value}"); + } + + + public static void FileExists(string path) + { + if (!FileOperations.FileExists(path)) + { + throw new ConfigurationValidationException($"Required file: {path} not found"); + } + } + } +} diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs b/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs index 3258e27..0a1bc7f 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs @@ -38,22 +38,17 @@ namespace VNLib.Plugins.Extensions.Loading /// Specifies a configuration variable name in the plugin's configuration /// containing data specific to the type /// </summary> + /// <remarks> + /// Initializes a new <see cref="ConfigurationNameAttribute"/> + /// </remarks> + /// <param name="configVarName">The name of the configuration variable for the class</param> [AttributeUsage(AttributeTargets.Class)] - public sealed class ConfigurationNameAttribute : Attribute + public sealed class ConfigurationNameAttribute(string configVarName) : Attribute { /// <summary> /// /// </summary> - public string ConfigVarName { get; } - - /// <summary> - /// Initializes a new <see cref="ConfigurationNameAttribute"/> - /// </summary> - /// <param name="configVarName">The name of the configuration variable for the class</param> - public ConfigurationNameAttribute(string configVarName) - { - ConfigVarName = configVarName; - } + public string ConfigVarName { get; } = configVarName; /// <summary> /// When true or not configured, signals that the type requires a configuration scope @@ -81,7 +76,7 @@ namespace VNLib.Plugins.Extensions.Loading /// <typeparam name="T">The type to get the configuration of</typeparam> /// <param name="plugin"></param> /// <returns>A <see cref="Dictionary{TKey, TValue}"/> of top level configuration elements for the type</returns> - /// <exception cref="KeyNotFoundException"></exception> + /// <exception cref="ConfigurationException"></exception> /// <exception cref="ObjectDisposedException"></exception> public static IConfigScope GetConfigForType<T>(this PluginBase plugin) { @@ -98,7 +93,7 @@ namespace VNLib.Plugins.Extensions.Loading /// <param name="plugin"></param> /// <param name="propName">The config property name to retrieve</param> /// <returns>A <see cref="IConfigScope"/> of top level configuration elements for the type</returns> - /// <exception cref="KeyNotFoundException"></exception> + /// <exception cref="ConfigurationException"></exception> /// <exception cref="ObjectDisposedException"></exception> public static IConfigScope GetConfig(this PluginBase plugin, string propName) { @@ -116,7 +111,7 @@ namespace VNLib.Plugins.Extensions.Loading } catch (KeyNotFoundException) { - throw new KeyNotFoundException($"Missing required top level configuration object '{propName}', in host/plugin configuration files"); + throw new ConfigurationException($"Missing required top level configuration object '{propName}', in host/plugin configuration files"); } } @@ -155,7 +150,7 @@ namespace VNLib.Plugins.Extensions.Loading /// <exception cref="ObjectDisposedException"></exception> public static IConfigScope GetConfigForType(this PluginBase plugin, Type type) { - _ = type ?? throw new ArgumentNullException(nameof(type)); + ArgumentNullException.ThrowIfNull(type); string? configName = GetConfigNameForType(type); @@ -179,11 +174,12 @@ namespace VNLib.Plugins.Extensions.Loading public static T? GetProperty<T>(this IConfigScope config, string property, Func<JsonElement, T> 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); + ArgumentNullException.ThrowIfNull(config); + ArgumentNullException.ThrowIfNull(getter); + ArgumentException.ThrowIfNullOrWhiteSpace(property); + return !config.TryGetValue(property, out JsonElement el) + ? default + : getter(el); } /// <summary> @@ -195,7 +191,7 @@ namespace VNLib.Plugins.Extensions.Loading /// <param name="getter">A function to get the value from the json type</param> /// <returns>The property value</returns> /// <exception cref="ArgumentNullException"></exception> - /// <exception cref="KeyNotFoundException"></exception> + /// <exception cref="ConfigurationException"></exception> public static T GetRequiredProperty<T>(this IConfigScope config, string property, Func<JsonElement, T> getter) { //Check null @@ -206,11 +202,11 @@ namespace VNLib.Plugins.Extensions.Loading //Get the property if (!config.TryGetValue(property, out JsonElement el)) { - throw new KeyNotFoundException($"Missing required configuration property '{property}' in config {config.ScopeName}"); + throw new ConfigurationException($"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}"); + return getter(el) ?? throw new ConfigurationException($"Missing required configuration property '{property}' in config {config.ScopeName}"); } /// <summary> @@ -290,7 +286,7 @@ namespace VNLib.Plugins.Extensions.Loading /// for missing configuration for a given type /// </summary> /// <param name="type">The type to raise exception for</param> - /// <exception cref="KeyNotFoundException"></exception> + /// <exception cref="ConfigurationException"></exception> [DoesNotReturn] public static void ThrowConfigNotFoundForType(Type type) { @@ -298,11 +294,11 @@ namespace VNLib.Plugins.Extensions.Loading string? configName = GetConfigNameForType(type); if (configName != null) { - throw new KeyNotFoundException($"Missing required configuration key '{configName}' for type {type.Name}"); + throw new ConfigurationException($"Missing required configuration key '{configName}' for type {type.Name}"); } else { - throw new KeyNotFoundException($"Missing required configuration key for type {type.Name}"); + throw new ConfigurationException($"Missing required configuration key for type {type.Name}"); } } @@ -322,7 +318,7 @@ namespace VNLib.Plugins.Extensions.Loading /// <summary> /// Deserialzes the configuration to the desired object and calls its - /// <see cref="IOnConfigValidation.Validate"/> method. Validation exceptions + /// <see cref="IOnConfigValidation.OnValidate"/> method. Validation exceptions /// are wrapped in a <see cref="ConfigurationValidationException"/> /// </summary> /// <typeparam name="T"></typeparam> @@ -332,14 +328,9 @@ namespace VNLib.Plugins.Extensions.Loading public static T DeserialzeAndValidate<T>(this IConfigScope scope) where T : IOnConfigValidation { T conf = scope.Deserialze<T>(); - try - { - conf.Validate(); - } - catch(Exception ex) - { - throw new ConfigurationValidationException($"Configuration validation failed for type {typeof(T).Name}", ex); - } + + TryValidateConfig(conf); + return conf; } @@ -374,7 +365,7 @@ namespace VNLib.Plugins.Extensions.Loading /// Gets a given configuration element from the global configuration scope /// and deserializes it into the desired type. /// <para> - /// If the type inherits <see cref="IOnConfigValidation"/> the <see cref="IOnConfigValidation.Validate"/> + /// If the type inherits <see cref="IOnConfigValidation"/> the <see cref="IOnConfigValidation.OnValidate"/> /// method is invoked, and exceptions are warpped in <see cref="ConfigurationValidationException"/> /// </para> /// <para> @@ -391,21 +382,10 @@ namespace VNLib.Plugins.Extensions.Loading //Deserialze the element TConfig config = plugin.GetConfigForType<TConfig>().Deserialze<TConfig>(); - //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); - } - } + TryValidateConfig(config); //If async config, load async - if(config is IAsyncConfigurable ac) + if (config is IAsyncConfigurable ac) { _ = plugin.ConfigureServiceAsync(ac); } @@ -417,7 +397,7 @@ namespace VNLib.Plugins.Extensions.Loading /// Gets a given configuration element from the global configuration scope /// and deserializes it into the desired type. /// <para> - /// If the type inherits <see cref="IOnConfigValidation"/> the <see cref="IOnConfigValidation.Validate"/> + /// If the type inherits <see cref="IOnConfigValidation"/> the <see cref="IOnConfigValidation.OnValidate"/> /// method is invoked, and exceptions are warpped in <see cref="ConfigurationValidationException"/> /// </para> /// <para> @@ -435,26 +415,31 @@ namespace VNLib.Plugins.Extensions.Loading //Deserialze the element TConfig config = plugin.GetConfig(elementName).Deserialze<TConfig>(); + TryValidateConfig(config); + + //If async config, load async + if (config is IAsyncConfigurable ac) + { + _ = plugin.ConfigureServiceAsync(ac); + } + + return config; + } + + private static void TryValidateConfig<TConfig>(TConfig config) + { //If the type is validatable, validate it if (config is IOnConfigValidation conf) { try { - conf.Validate(); + conf.OnValidate(); } 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; } diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Exceptions/ConfigurationException.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Exceptions/ConfigurationException.cs new file mode 100644 index 0000000..edfb002 --- /dev/null +++ b/lib/VNLib.Plugins.Extensions.Loading/src/Exceptions/ConfigurationException.cs @@ -0,0 +1,42 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Loading +* File: ConfigurationException.cs +* +* ConfigurationException.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; + +namespace VNLib.Plugins.Extensions.Loading +{ + /// <summary> + /// A base plugin configuration exception + /// </summary> + public class ConfigurationException : Exception + { + public ConfigurationException(string message) : base(message) + { } + + public ConfigurationException(string message, Exception innerException) : base(message, innerException) + { } + public ConfigurationException() + { } + } +} diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Exceptions/ConfigurationValidationException.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Exceptions/ConfigurationValidationException.cs index ebf4d9e..cedc41a 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/Exceptions/ConfigurationValidationException.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/Exceptions/ConfigurationValidationException.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2023 Vaughn Nugent +* Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Extensions.Loading @@ -26,17 +26,18 @@ using System; namespace VNLib.Plugins.Extensions.Loading { + /// <summary> /// An exception raised when a configuration validation exception has occured /// </summary> - public class ConfigurationValidationException : Exception + public class ConfigurationValidationException : ConfigurationException { public ConfigurationValidationException(string message) : base(message) - {} + { } public ConfigurationValidationException(string message, Exception innerException) : base(message, innerException) - {} + { } public ConfigurationValidationException() - {} + { } } } diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs b/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs index c62dff9..4ffb3a1 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs @@ -657,9 +657,15 @@ namespace VNLib.Plugins.Extensions.Loading } catch(TargetInvocationException te) when (te.InnerException != null) { + FindNestedConfigurationException(te); FindAndThrowInnerException(te); throw; } + catch(Exception ex) + { + FindNestedConfigurationException(ex); + throw; + } Task? loading = null; @@ -755,6 +761,21 @@ namespace VNLib.Plugins.Extensions.Loading } } + internal static void FindNestedConfigurationException(Exception ex) + { + if(ex is ConfigurationException ce) + { + ExceptionDispatchInfo.Throw(ce); + } + + //Recurse + if(ex.InnerException is not null) + { + FindNestedConfigurationException(ex.InnerException); + } + + //No more exceptions + } private sealed class PluginLocalCache { |