diff options
author | vnugent <public@vaughnnugent.com> | 2024-07-28 18:52:54 -0400 |
---|---|---|
committer | vnugent <public@vaughnnugent.com> | 2024-07-28 18:52:54 -0400 |
commit | 641bdbe75cb0128c09e27f1b92709c86574026ac (patch) | |
tree | eb412bcb9fa0442d2e283c54d207204034fcbe11 /lib/VNLib.Plugins.Extensions.Loading/src | |
parent | 1350c983c371fdd6a93596c8474345f9168284e1 (diff) |
Squashed commit of the following:
commit 87887c0a45c84458e81fa38f7f4dca31faa49d7c
Author: vnugent <public@vaughnnugent.com>
Date: Sat Jul 27 22:41:17 2024 -0400
package updates
commit d52772c010b10357d194239056d19c0d22d414fe
Author: vnugent <public@vaughnnugent.com>
Date: Sat Jul 27 20:50:12 2024 -0400
update secrets and remove deprecated and unused apis
commit c8567e58dc1d4135da1f6cefa6fa66af5fcd7b19
Author: vnugent <public@vaughnnugent.com>
Date: Mon Jul 15 19:05:01 2024 -0400
feat: Smiplify configuration helpers
commit 640ee6760c07b628529e3160c16641773c76e800
Author: vnugent <public@vaughnnugent.com>
Date: Thu Jul 4 23:02:56 2024 -0400
package updates
commit db5747a20600a2e2c5e8d915cf0bdbe4ec6df6a2
Author: vnugent <public@vaughnnugent.com>
Date: Fri Jun 21 17:08:16 2024 -0400
configuration validation updates
commit 9bc24801735884e0c03aa00e83804448c466bdf2
Author: vnugent <public@vaughnnugent.com>
Date: Sun Jun 16 13:16:18 2024 -0400
update plugins array instead of single path
commit 1229ed75549de1c56aaee42c921acbd96c4d4c9b
Author: vnugent <public@vaughnnugent.com>
Date: Tue Jun 11 22:13:58 2024 -0400
feat: Stage some mvc stuff
commit 35815081df2149741a6a79a880a57d63c5938a34
Author: vnugent <public@vaughnnugent.com>
Date: Sun Jun 9 13:06:45 2024 -0400
package updates
commit bcbe51bef546458cb7fee0d8f1dfd00cf936545a
Author: vnugent <public@vaughnnugent.com>
Date: Fri Jun 7 22:03:19 2024 -0400
feat: Allow S3Config type inheritence
commit 2e55ac437f44eb5f9d66f7d7fd47b5670dedc2cb
Merge: 27fb538 1350c98
Author: vnugent <public@vaughnnugent.com>
Date: Wed May 22 15:29:47 2024 -0400
Merge branch 'master' into develop
commit 27fb5382d80d9bcfb4c65974bbae20c5e7b8ccbc
Author: vnugent <public@vaughnnugent.com>
Date: Wed May 22 00:57:34 2024 -0400
feat: Vault environment vars
commit 69f13e43dfdd8069459800ccc3039f45fc884814
Author: vnugent <public@vaughnnugent.com>
Date: Wed May 15 22:04:43 2024 -0400
fix: #3 Defer vault loading until a secret actually needs it
commit c848787d4830a73e9ba93898897282be2f3752f2
Author: vnugent <public@vaughnnugent.com>
Date: Wed May 15 22:02:02 2024 -0400
package updates
commit 21c6c85f540740ac29536a7091346a731aa85148
Author: vnugent <public@vaughnnugent.com>
Date: Wed May 15 22:01:16 2024 -0400
fix: #3 Error raised when managed password type disposed
commit 8e77289041349b16536497f48f0c0a4ec6fe30f5
Author: vnugent <public@vaughnnugent.com>
Date: Thu May 2 15:44:42 2024 -0400
feat: #2 Middleware helpers, proj cleanup, fix sync secrets, vault client
commit e0a5c85297516188e57b54d9b530b2482cb03eb0
Merge: a977dab 5ad520e
Author: vnugent <public@vaughnnugent.com>
Date: Sat Apr 27 17:44:09 2024 -0400
Merge branch 'master' into develop
commit a977dabef1dec915e00f755cb3ee3363aa9985f1
Author: vnugent <public@vaughnnugent.com>
Date: Sat Apr 27 17:26:35 2024 -0400
chore: package updates
commit a2e2c3c4152d000b8df25c3c3fee14d491aab2c6
Merge: f03b727 87bfa83
Author: vnugent <public@vaughnnugent.com>
Date: Sat Apr 20 12:11:45 2024 -0400
Merge branch 'master' into develop
commit f03b727d8f8e52f1dbd6293ea5c5a492c6d8e2da
Author: vnugent <public@vaughnnugent.com>
Date: Sat Apr 20 12:02:07 2024 -0400
chore: Package updates
Diffstat (limited to 'lib/VNLib.Plugins.Extensions.Loading/src')
14 files changed, 657 insertions, 131 deletions
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Configuration/ConfigScope.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Configuration/ConfigScope.cs index 7f5c09c..d8f4347 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/Configuration/ConfigScope.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/Configuration/ConfigScope.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2023 Vaughn Nugent +* Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Extensions.Loading @@ -29,13 +29,15 @@ using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using VNLib.Utils.Resources; + namespace VNLib.Plugins.Extensions.Loading { internal sealed class ConfigScope: IConfigScope { - private readonly Lazy<IReadOnlyDictionary<string, JsonElement>> _config; + private readonly LazyInitializer<IReadOnlyDictionary<string, JsonElement>> _config; private readonly JsonElement _element; @@ -48,36 +50,40 @@ namespace VNLib.Plugins.Extensions.Loading private IReadOnlyDictionary<string, JsonElement> LoadTable() { - return _element.EnumerateObject().ToDictionary(static k => k.Name, static k => k.Value); + return _element.EnumerateObject() + .ToDictionary( + static k => k.Name, + static k => k.Value + ); } ///<inheritdoc/> - public JsonElement this[string key] => _config.Value[key]; + public JsonElement this[string key] => _config.Instance[key]; ///<inheritdoc/> - public IEnumerable<string> Keys => _config.Value.Keys; + public IEnumerable<string> Keys => _config.Instance.Keys; ///<inheritdoc/> - public IEnumerable<JsonElement> Values => _config.Value.Values; + public IEnumerable<JsonElement> Values => _config.Instance.Values; ///<inheritdoc/> - public int Count => _config.Value.Count; + public int Count => _config.Instance.Count; ///<inheritdoc/> public string ScopeName { get; } ///<inheritdoc/> - public bool ContainsKey(string key) => _config.Value.ContainsKey(key); + public bool ContainsKey(string key) => _config.Instance.ContainsKey(key); ///<inheritdoc/> public T Deserialze<T>() => _element.Deserialize<T>()!; ///<inheritdoc/> - public IEnumerator<KeyValuePair<string, JsonElement>> GetEnumerator() => _config.Value.GetEnumerator(); + public IEnumerator<KeyValuePair<string, JsonElement>> GetEnumerator() => _config.Instance.GetEnumerator(); ///<inheritdoc/> - public bool TryGetValue(string key, [MaybeNullWhen(false)] out JsonElement value) => _config.Value.TryGetValue(key, out value); + public bool TryGetValue(string key, [MaybeNullWhen(false)] out JsonElement value) => _config.Instance.TryGetValue(key, out value); - IEnumerator IEnumerable.GetEnumerator() => _config.Value.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => _config.Instance.GetEnumerator(); } } 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 0337fbd..e838822 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs @@ -24,12 +24,14 @@ using System; using System.IO; +using System.Linq; using System.Text.Json; using System.Reflection; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using VNLib.Utils.Extensions; +using VNLib.Plugins.Extensions.Loading.Configuration; namespace VNLib.Plugins.Extensions.Loading { @@ -37,22 +39,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 @@ -71,7 +68,6 @@ namespace VNLib.Plugins.Extensions.Loading 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"; /// <summary> /// Retrieves a top level configuration dictionary of elements for the specified type. @@ -80,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) { @@ -97,26 +93,12 @@ 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) - { - 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"); - } + { + return TryGetConfig(plugin, propName) + ?? throw new ConfigurationException($"Missing required top level configuration object '{propName}', in host/plugin configuration files"); } /// <summary> @@ -154,7 +136,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); @@ -178,11 +160,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> @@ -194,7 +177,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 @@ -203,13 +186,32 @@ namespace VNLib.Plugins.Extensions.Loading 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}"); - } + bool hasValue = config.TryGetValue(property, out JsonElement el); + Validate.Assert(hasValue, $"Missing required configuration property '{property}' in config {config.ScopeName}"); + + T? value = getter(el); + Validate.Assert(value is not null, $"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}"); + //Attempt to validate if the configuration inherits the interface + TryValidateConfig(value); + + return value; + } + + + /// <summary> + /// Gets a required configuration property from the specified configuration scope + /// and deserializes the json type. + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="config"></param> + /// <param name="property">The name of the property to get</param> + /// <returns>The property value deserialzied into the desired object</returns> + /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="ConfigurationException"></exception> + public static T GetRequiredProperty<T>(this IConfigScope config, string property) + { + return GetRequiredProperty(config, property, static p => p.Deserialize<T>()!); } /// <summary> @@ -261,6 +263,28 @@ namespace VNLib.Plugins.Extensions.Loading } /// <summary> + /// 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. + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="config"></param> + /// <param name="property">The name of the configuration element to get</param> + /// <param name="defaultValue">The default value to return</param> + /// <returns>The property value returned from your getter callback, or the default value if not found</returns> + /// <exception cref="ArgumentNullException"></exception> + [return: NotNullIfNotNull(nameof(defaultValue))] + public static T? GetValueOrDefault<T>(this IConfigScope config, string property, T defaultValue) + { + return GetValueOrDefault( + config, + property, + static p => p.Deserialize<T>(), + defaultValue + ); + } + + /// <summary> /// Gets the configuration property name for the type /// </summary> /// <param name="type">The type to get the configuration name for</param> @@ -289,7 +313,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) { @@ -297,11 +321,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}"); } } @@ -321,7 +345,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> @@ -331,14 +355,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; } @@ -351,7 +370,6 @@ namespace VNLib.Plugins.Extensions.Loading /// <returns>True if the plugin config contains the require configuration property</returns> public static bool HasConfigForType<T>(this PluginBase plugin) => HasConfigForType(plugin, typeof(T)); - /// <summary> /// Determines if the current plugin configuration contains the require properties to initialize /// the type @@ -373,7 +391,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> @@ -388,23 +406,13 @@ namespace VNLib.Plugins.Extensions.Loading public static TConfig GetConfigElement<TConfig>(this PluginBase plugin) { //Deserialze the element - TConfig config = plugin.GetConfigForType<TConfig>().Deserialze<TConfig>(); + 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); } @@ -416,7 +424,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> @@ -432,31 +440,36 @@ namespace VNLib.Plugins.Extensions.Loading public static TConfig GetConfigElement<TConfig>(this PluginBase plugin, string elementName) { //Deserialze the element - TConfig config = plugin.GetConfig(elementName).Deserialze<TConfig>(); + 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; } - /// <summary> /// Attempts to load the basic S3 configuration variables required /// for S3 client access @@ -492,16 +505,39 @@ namespace VNLib.Plugins.Extensions.Loading /// </summary> /// <param name="plugin"></param> /// <returns>The absolute path to the directory containing all plugins</returns> - public static string GetPluginsPath(this PluginBase plugin) + public static string[] GetPluginSearchDirs(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()!; + /* + * Hosts are allowed to define mutliple plugin loading paths. A + * single path is supported for compat. Multi path takes precidence + * of course so attempt to load a string array first + */ + + if (!config.TryGetValue("paths", out JsonElement searchPaths) + && !config.TryGetValue("path", out searchPaths)) + { + return []; + } - //Get absolute path - return Path.GetFullPath(pluginsPath); + if (searchPaths.ValueKind == JsonValueKind.Array) + { + //Get the plugins path or throw because it should ALWAYS be defined if this method is called + return searchPaths.EnumerateArray() + .Select(static p => p.GetString()!) + .Select(Path.GetFullPath) //Get absolute file paths + .ToArray(); + } + else if (searchPaths.ValueKind == JsonValueKind.String) + { + return [Path.GetFullPath(searchPaths.GetString()!)]; + } + else + { + return []; + } } } } 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 b65c5e6..ca897e6 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs @@ -26,6 +26,7 @@ using System; using System.IO; using System.Linq; using System.Text.Json; +using System.Threading; using System.Reflection; using System.Runtime.Loader; using System.Threading.Tasks; @@ -104,22 +105,30 @@ namespace VNLib.Plugins.Extensions.Loading plugin.ThrowIfUnloaded(); ArgumentNullException.ThrowIfNull(assemblyName); + string[] searchDirs; + /* * Allow an assets directory to limit the scope of the search for the desired * assembly, otherwise search all plugins directories */ string? assetDir = plugin.GetAssetsPath(); - assetDir ??= plugin.GetPluginsPath(); + + searchDirs = assetDir is null + ? plugin.GetPluginSearchDirs() + : ([assetDir]); /* - * This should never happen since this method can only be called from a - * plugin context, which means this path was used to load the current plugin - */ - ArgumentNullException.ThrowIfNull(assetDir, "No plugin asset directory is defined for the current host configuration, this is likely a bug"); + * This should never happen since this method can only be called from a + * plugin context, which means this path was used to load the current plugin + */ + if (searchDirs.Length == 0) + { + throw new ConfigurationException("No plugin asset directory is defined for the current host configuration, this is likely a bug"); + } //Get the first file that matches the search file - return Directory.EnumerateFiles(assetDir, assemblyName, searchOption).FirstOrDefault(); + return searchDirs.SelectMany(d => Directory.EnumerateFiles(d, assemblyName, searchOption)).FirstOrDefault(); } /// <summary> @@ -260,9 +269,10 @@ namespace VNLib.Plugins.Extensions.Loading /// </summary> /// <param name="plugin"></param> /// <exception cref="ObjectDisposedException"></exception> - public static void ThrowIfUnloaded(this PluginBase plugin) + public static void ThrowIfUnloaded(this PluginBase? plugin) { //See if the plugin was unlaoded + ArgumentNullException.ThrowIfNull(plugin); ObjectDisposedException.ThrowIf(plugin.UnloadToken.IsCancellationRequested, plugin); } @@ -294,6 +304,12 @@ namespace VNLib.Plugins.Extensions.Loading //Optional delay await Task.Delay(delayMs); + //If plugin unloads during delay, bail + if (plugin.UnloadToken.IsCancellationRequested) + { + return; + } + //Run on ts Task deferred = Task.Run(asyncTask); @@ -348,7 +364,7 @@ namespace VNLib.Plugins.Extensions.Loading static async Task WaitForUnload(PluginBase pb, Action callback) { //Wait for unload as a task on the threadpool to avoid deadlocks - await pb.UnloadToken.WaitHandle.WaitAsync() + await pb.UnloadToken.WaitHandle.NoSpinWaitAsync(Timeout.Infinite) .ConfigureAwait(false); callback(); @@ -649,9 +665,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; @@ -747,6 +769,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 { diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Routing/EndpointLogNameAttribute.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Routing/EndpointLogNameAttribute.cs new file mode 100644 index 0000000..d47be22 --- /dev/null +++ b/lib/VNLib.Plugins.Extensions.Loading/src/Routing/EndpointLogNameAttribute.cs @@ -0,0 +1,41 @@ +/* +* 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; + +namespace VNLib.Plugins.Extensions.Loading.Routing +{ + + /// <summary> + /// Defines configurable settings for an endpoint + /// </summary> + [AttributeUsage(AttributeTargets.Class)] + public sealed class EndpointLogNameAttribute(string logName) : Attribute + { + /// <summary> + /// The name of the logging scope for the endpoint + /// </summary> + public string LogName { get; } = logName; + } +} diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Routing/EndpointPathAttribute.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Routing/EndpointPathAttribute.cs new file mode 100644 index 0000000..a5ab355 --- /dev/null +++ b/lib/VNLib.Plugins.Extensions.Loading/src/Routing/EndpointPathAttribute.cs @@ -0,0 +1,40 @@ +/* +* 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; + +namespace VNLib.Plugins.Extensions.Loading.Routing +{ + /// <summary> + /// Defines configurable settings for an endpoint + /// </summary> + [AttributeUsage(AttributeTargets.Class)] + public sealed class EndpointPathAttribute(string path) : Attribute + { + /// <summary> + /// Sets the endpoint path (or configuration template if set) + /// </summary> + public string Path { get; } = path; + } +} diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Routing/Mvc/HttpControllerAttribute.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Routing/Mvc/HttpControllerAttribute.cs new file mode 100644 index 0000000..94771ab --- /dev/null +++ b/lib/VNLib.Plugins.Extensions.Loading/src/Routing/Mvc/HttpControllerAttribute.cs @@ -0,0 +1,36 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Loading +* File: HttpControllerAttribute.cs +* +* HttpControllerAttribute.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.Routing.Mvc +{ + /// <summary> + /// Attribute to define a controller for http routing. The class must be decorated + /// with this attribute to be recognized as a controller + /// </summary> + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] + public sealed class HttpControllerAttribute : Attribute + { } +} diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Routing/Mvc/HttpEndpointAttribute.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Routing/Mvc/HttpEndpointAttribute.cs new file mode 100644 index 0000000..d89df63 --- /dev/null +++ b/lib/VNLib.Plugins.Extensions.Loading/src/Routing/Mvc/HttpEndpointAttribute.cs @@ -0,0 +1,52 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Loading +* File: HttpEndpointAttribute.cs +* +* HttpEndpointAttribute.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 VNLib.Net.Http; + +namespace VNLib.Plugins.Extensions.Loading.Routing.Mvc +{ + + /// <summary> + /// Attribute to define an http endpoint for a controller. The class + /// must be decorated with the <see cref="HttpControllerAttribute"/> attribute + /// </summary> + /// <param name="path">The endpoint path</param> + /// <param name="method">The method (or methods) allowed to be filtered by this endpoint</param> + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public sealed class HttpEndpointAttribute(string path, HttpMethod method) : Attribute + { + /// <summary> + /// The path of the endpoint + /// </summary> + public string Path { get; } = path; + + /// <summary> + /// The http method of the endpoint. You may set more than one method + /// for a given endpoint + /// </summary> + public HttpMethod Method { get; } = method; + } +} diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Routing/RoutingExtensions.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Routing/RoutingExtensions.cs index a1817a8..6665a75 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/Routing/RoutingExtensions.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/Routing/RoutingExtensions.cs @@ -24,10 +24,19 @@ using System; using System.Reflection; +using System.Threading.Tasks; +using System.Collections.Frozen; using System.Collections.Generic; +using System.Text.RegularExpressions; using System.Runtime.CompilerServices; +using VNLib.Net.Http; +using VNLib.Utils.Logging; +using VNLib.Utils.Resources; using VNLib.Plugins.Essentials.Runtime; +using VNLib.Plugins.Essentials; +using VNLib.Plugins.Essentials.Endpoints; +using VNLib.Plugins.Extensions.Loading.Routing.Mvc; namespace VNLib.Plugins.Extensions.Loading.Routing { @@ -35,7 +44,7 @@ namespace VNLib.Plugins.Extensions.Loading.Routing /// <summary> /// Provides advanced QOL features to plugin loading /// </summary> - public static class RoutingExtensions + public static partial class RoutingExtensions { private static readonly ConditionalWeakTable<IEndpoint, PluginBase?> _pluginRefs = new(); private static readonly ConditionalWeakTable<PluginBase, EndpointCollection> _pluginEndpoints = new(); @@ -52,11 +61,14 @@ namespace VNLib.Plugins.Extensions.Loading.Routing T endpoint = plugin.CreateService<T>(); //Route the endpoint - plugin.Route(endpoint); + Route(plugin, endpoint); //Store ref to plugin for endpoint _pluginRefs.Add(endpoint, plugin); + //Function that initalizes the endpoint's path and logging variables + InitEndpointSettings(plugin, endpoint); + return endpoint; } @@ -96,15 +108,119 @@ namespace VNLib.Plugins.Extensions.Loading.Routing { _ = _pluginRefs.TryGetValue(ep, out PluginBase? pBase); return pBase ?? throw new InvalidOperationException("Endpoint was not dynamically routed"); - } - + } + + private static readonly Regex ConfigSyntaxParser = ParserRegex(); + private delegate void InitFunc(string path, ILogProvider log); + + [GeneratedRegex("{{(.*?)}}", RegexOptions.Compiled)] + private static partial Regex ParserRegex(); + + private static void InitEndpointSettings<T>(PluginBase plugin, T endpoint) where T : IEndpoint + { + //Load optional config + IConfigScope config = plugin.GetConfigForType<T>(); + + ILogProvider logger = plugin.Log; + + EndpointPathAttribute? pathAttr = typeof(T).GetCustomAttribute<EndpointPathAttribute>(); + + /* + * gets the protected function for assigning the endpoint path + * and logger instance. + */ + InitFunc? initPathAndLog = ManagedLibrary.TryGetMethod<InitFunc>(endpoint, "InitPathAndLog", BindingFlags.NonPublic); + + if (pathAttr is null || initPathAndLog is null) + { + return; + } + + string? logName = typeof(T).GetCustomAttribute<EndpointLogNameAttribute>()?.LogName; + + if (!string.IsNullOrWhiteSpace(logName)) + { + logger = plugin.Log.CreateScope(SubsituteValue(logName, config)); + } + try + { + + //Invoke init function and pass in variable names + initPathAndLog( + path: SubsituteValue(pathAttr.Path, config), + logger + ); + } + catch (ConfigurationException) + { + throw; + } + catch(Exception e) + { + throw new ConfigurationException($"Failed to initalize endpoint {endpoint.GetType().Name}", e); + } + + static string SubsituteValue(string pathVar, IConfigScope? config) + { + if (config is null) + { + return pathVar; + } + + // Replace the matched pattern with the corresponding value from the dictionary + return ConfigSyntaxParser.Replace(pathVar, match => + { + string varName = match.Groups[1].Value; + + //Get the value from the config scope or return the original variable unmodified + return config.GetValueOrDefault(varName, varName); + }); + } + } private sealed class EndpointCollection : IVirtualEndpointDefinition { public List<IEndpoint> Endpoints { get; } = new(); ///<inheritdoc/> - IEnumerable<IEndpoint> IVirtualEndpointDefinition.GetEndpoints() => Endpoints; + IEnumerable<IEndpoint> IVirtualEndpointDefinition.GetEndpoints() => Endpoints; + } + + + private delegate ValueTask<VfReturnType> EndpointWorkFunc(HttpEntity entity); + + sealed record class HttpControllerEndpoint(MethodInfo MethodInfo, HttpEndpointAttribute Attr) + { + public string Path => Attr.Path; + + public HttpMethod Method => Attr.Method; + + public EndpointWorkFunc Func { get; } = MethodInfo.CreateDelegate<EndpointWorkFunc>(); + } + + private sealed class EndpointWrapper + : ResourceEndpointBase + { + + private readonly FrozenDictionary<HttpMethod, EndpointWorkFunc> _wrappers; + + public EndpointWrapper(FrozenDictionary<HttpMethod, EndpointWorkFunc> table, string path, ILogProvider log) + { + _wrappers = table; + InitPathAndLog(path, log); + } + + protected override ValueTask<VfReturnType> OnProcessAsync(HttpEntity entity) + { + ref readonly EndpointWorkFunc func = ref _wrappers.GetValueRefOrNullRef(entity.Server.Method); + + if (Unsafe.IsNullRef(in func)) + { + return ValueTask.FromResult(VfReturnType.ProcessAsFile); + } + + return func(entity); + } } } } diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/S3Config.cs b/lib/VNLib.Plugins.Extensions.Loading/src/S3Config.cs index 11f101f..0574cb0 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/S3Config.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/S3Config.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2023 Vaughn Nugent +* Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Extensions.Loading @@ -26,7 +26,12 @@ using System.Text.Json.Serialization; namespace VNLib.Plugins.Extensions.Loading { - public sealed class S3Config + + /// <summary> + /// A common json-serializable configuration for S3 storage + /// in an attempt to unify S3 configuration. + /// </summary> + public class S3Config { [JsonPropertyName("server_address")] public string? ServerAddress { get; init; } diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/HCVaultClient.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/HCVaultClient.cs index 885f22f..fae22c8 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/HCVaultClient.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/HCVaultClient.cs @@ -163,7 +163,7 @@ namespace VNLib.Plugins.Extensions.Loading using HttpResponseMessage response = await _client.SendAsync(ms, HttpCompletionOption.ResponseHeadersRead); //Check if an error occured in the response - await ProcessVaultErrorResponseAsync(response); + await ProcessVaultErrorResponseAsync(secretName, response); //Read the response async using SecretResponse res = await ReadSecretResponse(response.Content); @@ -266,7 +266,7 @@ namespace VNLib.Plugins.Extensions.Loading return null; } - private static ValueTask ProcessVaultErrorResponseAsync(HttpResponseMessage response) + private static ValueTask ProcessVaultErrorResponseAsync(string secretName, HttpResponseMessage response) { if (response.IsSuccessStatusCode) { @@ -278,7 +278,7 @@ namespace VNLib.Plugins.Extensions.Loading if(!ctLen.HasValue || ctLen.Value == 0) { return ValueTask.FromException( - new HttpRequestException($"Failed to fetch secret from vault with error code {response.StatusCode}") + new HttpRequestException($"Failed to fetch secret '{secretName}' from vault with error code {response.StatusCode}") ); } @@ -300,15 +300,15 @@ namespace VNLib.Plugins.Extensions.Loading ); } - return ExceptionsFromContentAsync(response); + return ExceptionsFromContentAsync(secretName, response); - static ValueTask ExceptionFromVaultErrors(HttpStatusCode code, VaultErrorMessage? errs) + static ValueTask ExceptionFromVaultErrors(string secretName, HttpStatusCode code, VaultErrorMessage? errs) { //If the error message is null, raise an exception if (errs?.Errors is null || errs.Errors.Length == 0) { return ValueTask.FromException( - new HttpRequestException($"Failed to fetch secret from vault with error code {code}") + new HttpRequestException($"Failed to fetch secret '{secretName}' from vault with error code {code}") ); } @@ -318,17 +318,17 @@ namespace VNLib.Plugins.Extensions.Loading //Finally raise the exception with all the returned errors return ValueTask.FromException( - new HttpRequestException($"Failed to fetch secre from vault with {code}, errors:\n {errStr}") + new HttpRequestException($"Failed to fetch secret `{secretName}` from vault with {code}, errors:\n {errStr}") ); } - static async ValueTask ExceptionsFromContentAsync(HttpResponseMessage response) + static async ValueTask ExceptionsFromContentAsync(string secretName, HttpResponseMessage response) { //Read stream async and deserialize async using Stream stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); VaultErrorMessage? errs = await JsonSerializer.DeserializeAsync<VaultErrorMessage>(stream); - await ExceptionFromVaultErrors(response.StatusCode, errs); + await ExceptionFromVaultErrors(secretName, response.StatusCode, errs); } } |