From db5747a20600a2e2c5e8d915cf0bdbe4ec6df6a2 Mon Sep 17 00:00:00 2001 From: vnugent Date: Fri, 21 Jun 2024 17:08:16 -0400 Subject: configuration validation updates --- .../src/Configuration/IOnConfigValidation.cs | 4 +- .../src/Configuration/Validate.cs | 114 +++++++++++++++++++++ .../src/ConfigurationExtensions.cs | 103 ++++++++----------- .../src/Exceptions/ConfigurationException.cs | 42 ++++++++ .../Exceptions/ConfigurationValidationException.cs | 11 +- .../src/LoadingExtensions.cs | 21 ++++ 6 files changed, 229 insertions(+), 66 deletions(-) create mode 100644 lib/VNLib.Plugins.Extensions.Loading/src/Configuration/Validate.cs create mode 100644 lib/VNLib.Plugins.Extensions.Loading/src/Exceptions/ConfigurationException.cs (limited to 'lib/VNLib.Plugins.Extensions.Loading') 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 /// /// Validates a json configuration during deserialzation /// - 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 +{ + /// + /// A class that allows for easy configuration validation + /// + public sealed class Validate + { + /// + /// Ensures the object is not null and not an empty string, + /// otherwise a is raised + /// + /// + /// The object to test + /// The message to display to the user on loading + /// + [DoesNotReturn] + public static void NotNull(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); + } + } + + /// + /// + /// + /// + /// + /// + public static void Assert([DoesNotReturnIf(false)] bool condition, string message) + { + if (!condition) + { + throw new ConfigurationValidationException(message); + } + } + + public static void NotEqual(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 value, T min, T max, string message) + where T : IComparable + { + //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 value, T min, T max, [CallerArgumentExpression(nameof(value))] string? paramName = null) + where T : IComparable + { + + 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 /// + /// + /// Initializes a new + /// + /// The name of the configuration variable for the class [AttributeUsage(AttributeTargets.Class)] - public sealed class ConfigurationNameAttribute : Attribute + public sealed class ConfigurationNameAttribute(string configVarName) : Attribute { /// /// /// - public string ConfigVarName { get; } - - /// - /// Initializes a new - /// - /// The name of the configuration variable for the class - public ConfigurationNameAttribute(string configVarName) - { - ConfigVarName = configVarName; - } + public string ConfigVarName { get; } = configVarName; /// /// When true or not configured, signals that the type requires a configuration scope @@ -81,7 +76,7 @@ namespace VNLib.Plugins.Extensions.Loading /// The type to get the configuration of /// /// A of top level configuration elements for the type - /// + /// /// public static IConfigScope GetConfigForType(this PluginBase plugin) { @@ -98,7 +93,7 @@ namespace VNLib.Plugins.Extensions.Loading /// /// The config property name to retrieve /// A of top level configuration elements for the type - /// + /// /// 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 /// 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(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); + ArgumentNullException.ThrowIfNull(config); + ArgumentNullException.ThrowIfNull(getter); + ArgumentException.ThrowIfNullOrWhiteSpace(property); + return !config.TryGetValue(property, out JsonElement el) + ? default + : getter(el); } /// @@ -195,7 +191,7 @@ namespace VNLib.Plugins.Extensions.Loading /// 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 @@ -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}"); } /// @@ -290,7 +286,7 @@ namespace VNLib.Plugins.Extensions.Loading /// for missing configuration for a given type /// /// The type to raise exception for - /// + /// [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 /// /// Deserialzes the configuration to the desired object and calls its - /// method. Validation exceptions + /// method. Validation exceptions /// are wrapped in a /// /// @@ -332,14 +328,9 @@ namespace VNLib.Plugins.Extensions.Loading 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); - } + + 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. /// - /// If the type inherits the + /// If the type inherits the /// method is invoked, and exceptions are warpped in /// /// @@ -391,21 +382,10 @@ namespace VNLib.Plugins.Extensions.Loading //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); - } - } + 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. /// - /// If the type inherits the + /// If the type inherits the /// method is invoked, and exceptions are warpped in /// /// @@ -435,26 +415,31 @@ namespace VNLib.Plugins.Extensions.Loading //Deserialze the element TConfig config = plugin.GetConfig(elementName).Deserialze(); + TryValidateConfig(config); + + //If async config, load async + if (config is IAsyncConfigurable ac) + { + _ = plugin.ConfigureServiceAsync(ac); + } + + return config; + } + + private static void TryValidateConfig(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 +{ + /// + /// A base plugin configuration exception + /// + 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 { + /// /// An exception raised when a configuration validation exception has occured /// - 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 { -- cgit