/*
* Copyright (c) 2023 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Plugins.Extensions.Loading
* File: RoutingExtensions.cs
*
* RoutingExtensions.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.Linq;
using System.Reflection;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using VNLib.Plugins.Extensions.Loading.Events;
namespace VNLib.Plugins.Extensions.Loading.Routing
{
///
/// Provides advanced QOL features to plugin loading
///
public static class RoutingExtensions
{
private static readonly ConditionalWeakTable _pluginRefs = new();
///
/// Constructs and routes the specific endpoint type for the current plugin
///
/// The type
///
/// If true, requires the configuration exist in the config file
/// The path to the plugin sepcific configuration property
///
public static T Route(this PluginBase plugin, string? pluginConfigPathName, bool configRequired = true) where T : IEndpoint
{
Type endpointType = typeof(T);
T endpoint;
//If the config attribute is not set, then ignore the config variables
if (string.IsNullOrWhiteSpace(pluginConfigPathName))
{
ConstructorInfo? constructor = endpointType.GetConstructor(new Type[] { typeof(PluginBase) });
_ = constructor ?? throw new EntryPointNotFoundException($"No constructor t(PluginBase p) found for {endpointType.Name}");
//Create the new endpoint and pass the plugin instance
endpoint = (T)constructor.Invoke(new object[] { plugin });
//Register event handlers for the endpoint
ScheduleIntervals(plugin, endpoint, endpointType, null);
}
else
{
//Try to get config but allow null if not required
IConfigScope? config = plugin.TryGetConfig(pluginConfigPathName);
if(configRequired && config == null)
{
ConfigurationExtensions.ThrowConfigNotFoundForType(endpointType);
return default;
}
//Choose constructor based on config
if (config != null)
{
ConstructorInfo? constructor = endpointType.GetConstructor(new Type[] { typeof(PluginBase), typeof(IConfigScope) });
//Make sure the constructor exists
_ = constructor ?? throw new EntryPointNotFoundException($"No constructor t(PluginBase p, IConfigScope cs) found for {endpointType.Name}");
//Create the new endpoint and pass the plugin instance along with the configuration object
endpoint = (T)constructor.Invoke(new object[] { plugin, config });
//Register event handlers for the endpoint
ScheduleIntervals(plugin, endpoint, endpointType, config);
}
else
{
//Config does not exist, so use the default constructor
ConstructorInfo? constructor = endpointType.GetConstructor(new Type[] { typeof(PluginBase) });
_ = constructor ?? throw new EntryPointNotFoundException($"No constructor t(PluginBase p) found for {endpointType.Name}");
//Create the new endpoint and pass the plugin instance
endpoint = (T)constructor.Invoke(new object[] { plugin });
//Register event handlers for the endpoint
ScheduleIntervals(plugin, endpoint, endpointType, null);
}
}
//Route the endpoint
plugin.Route(endpoint);
//Store ref to plugin for endpoint
_pluginRefs.Add(endpoint, plugin);
//See if the endpoint is disposable
if (endpoint is IDisposable dis)
{
//Register dispose for unload
_ = plugin.RegisterForUnload(dis.Dispose);
}
return endpoint;
}
///
/// Constructs and routes the specific endpoint type for the current plugin
///
/// The type
///
///
public static T Route(this PluginBase plugin) where T : IEndpoint
{
Type endpointType = typeof(T);
//Get config name attribute
ConfigurationNameAttribute? configAttr = endpointType.GetCustomAttribute();
//Route using attribute
return plugin.Route(configAttr?.ConfigVarName, configAttr?.Required == true);
}
///
/// Gets the plugin that loaded the current endpoint
///
///
/// The plugin that loaded the current endpoint
///
public static PluginBase GetPlugin(this IEndpoint ep)
{
_ = _pluginRefs.TryGetValue(ep, out PluginBase? pBase);
return pBase ?? throw new InvalidOperationException("Endpoint was not dynamically routed");
}
private static void ScheduleIntervals(PluginBase plugin, T endpointInstance, Type epType, IConfigScope? endpointLocalConfig) where T : IEndpoint
{
//Get all methods that have the configurable async interval attribute specified
IEnumerable> confIntervals = epType.GetMethods()
.Where(m => m.GetCustomAttribute() != null)
.Select(m => new Tuple
(m.GetCustomAttribute()!, m.CreateDelegate(endpointInstance)));
//If the endpoint has a local config, then use it to find the interval
if (endpointLocalConfig != null)
{
//Schedule event handlers on the current plugin
foreach (Tuple interval in confIntervals)
{
int value = endpointLocalConfig[interval.Item1.IntervalPropertyName].GetInt32();
//Get the timeout from its resolution variable
TimeSpan timeout = interval.Item1.Resolution switch
{
IntervalResultionType.Seconds => TimeSpan.FromSeconds(value),
IntervalResultionType.Minutes => TimeSpan.FromMinutes(value),
IntervalResultionType.Hours => TimeSpan.FromHours(value),
_ => TimeSpan.FromMilliseconds(value),
};
//Schedule
plugin.ScheduleInterval(interval.Item2, timeout);
}
}
//Get all methods that have the async interval attribute specified
IEnumerable> intervals = epType.GetMethods()
.Where(m => m.GetCustomAttribute() != null)
.Select(m => new Tuple(
m.GetCustomAttribute()!, m.CreateDelegate(endpointInstance))
);
//Schedule event handlers on the current plugin
foreach (Tuple interval in intervals)
{
plugin.ScheduleInterval(interval.Item2, interval.Item1.Interval);
}
}
}
}