250 lines
8.8 KiB
C#
250 lines
8.8 KiB
C#
using Qrakhen.Qamp.Core.Collections;
|
|
using Qrakhen.Qamp.Core.Logging;
|
|
using Qrakhen.Qamp.Core.Values.Objects;
|
|
using System.Linq.Expressions;
|
|
using System.Reflection;
|
|
|
|
namespace Qrakhen.Qamp.Core.Values;
|
|
|
|
public delegate Value ExtensionDelegate(Value target, Value[] parameters);
|
|
|
|
public class ExtensionException(string message, object? context = null) : QampException(message, context);
|
|
|
|
public abstract class ExtensionAttribute : Attribute;
|
|
|
|
public abstract class TypedExtensionAttribute : ExtensionAttribute
|
|
{
|
|
public ValueType ValueType { get; set; } = ValueType.Undefined;
|
|
public Type? ValueObjectType { get; set; } = null;
|
|
public string? ValueObjectName { get; set; }
|
|
public bool Nullable { get; set; } = false;
|
|
public TypeInfo TypeInfo => new TypeInfo(ValueType, ValueObjectType, ValueObjectName, Nullable);
|
|
}
|
|
|
|
[AttributeUsage(AttributeTargets.Method)]
|
|
public class ExtensionMethodAttribute : ExtensionAttribute
|
|
{
|
|
public string? Name { get; set; } = null;
|
|
}
|
|
|
|
[AttributeUsage(AttributeTargets.Method)]
|
|
public class ReturnsAttribute : TypedExtensionAttribute;
|
|
|
|
[AttributeUsage(AttributeTargets.Parameter)]
|
|
public class SelfAttribute : TypedExtensionAttribute;
|
|
|
|
[AttributeUsage(AttributeTargets.Class)]
|
|
public class ExtensionClassAttribute : SelfAttribute
|
|
{
|
|
public string? Name { get; set; } = null;
|
|
}
|
|
|
|
[AttributeUsage(AttributeTargets.Parameter)]
|
|
public class ParameterAttribute : TypedExtensionAttribute
|
|
{
|
|
public string? Name { get; set; } = null;
|
|
public bool Optional { get; set; } = false;
|
|
}
|
|
|
|
|
|
public class ExtensionMethod(
|
|
TypeInfo targetType,
|
|
TypeInfo returnType,
|
|
string name,
|
|
ExtensionDelegate callback,
|
|
string[] parameters)
|
|
{
|
|
private static readonly Register<TypeInfo, Register<string, ExtensionMethod>> _register = [];
|
|
|
|
private static readonly ILogger _logger = LoggerService.Get<ExtensionMethod>();
|
|
|
|
public readonly string Name = name;
|
|
public readonly TypeInfo TargetType = targetType;
|
|
public readonly TypeInfo ReturnType = returnType;
|
|
public readonly ExtensionDelegate Callback = callback;
|
|
public readonly string[] Parameters = parameters.ToArray();
|
|
|
|
public readonly string Key = parameters.Length > 0 ? $"{name}_{parameters.Length}" : name;
|
|
|
|
public static ExtensionMethod? Get(TypeInfo targetType, string name, int parameters = 0)
|
|
{
|
|
if (parameters > 0)
|
|
name = $"{name}_{parameters}";
|
|
|
|
if (!_register.Has(targetType))
|
|
return null;
|
|
|
|
if (!_register[targetType].Has(name))
|
|
return null;
|
|
|
|
return _register[targetType][name];
|
|
}
|
|
|
|
public static void Register(IEnumerable<ExtensionMethod> extensions)
|
|
{
|
|
foreach (var extension in extensions)
|
|
Register(extension);
|
|
}
|
|
|
|
public static void Register(ExtensionMethod extension)
|
|
{
|
|
if (!_register.Has(extension.TargetType))
|
|
_register.Add(extension.TargetType, []);
|
|
|
|
if (_register[extension.TargetType].Has(extension.Key))
|
|
throw new ExtensionException($"Extension {extension.Key} already exists for type {extension.TargetType}.");
|
|
|
|
_register[extension.TargetType].Add(extension.Key, extension);
|
|
}
|
|
|
|
public static void Register(TypeInfo targetType,
|
|
TypeInfo returnType,
|
|
string name,
|
|
ExtensionDelegate callback,
|
|
params string[] parameters)
|
|
=> Register(new ExtensionMethod(targetType, returnType, name, callback, parameters));
|
|
|
|
private static ExtensionMethod RenderExtension(MethodInfo method)
|
|
{
|
|
_logger.Verbose($"Rendering extension from {method}...");
|
|
var attr = method.GetCustomAttribute<ExtensionMethodAttribute>();
|
|
if (attr is null)
|
|
throw new ExtensionException($"Expected an [ExtensionMethod] attribute on method {method} for extension compilation.", method);
|
|
|
|
var parameters = method.GetParameters();
|
|
if (parameters.Length < 1)
|
|
throw new ExtensionException($"Tried to define method {method} as extension, but extensions require their first parameter to be <self>, the target value of the extension.", method);
|
|
|
|
var selfAttr = parameters[0].GetCustomAttribute<SelfAttribute>() ?? method.DeclaringType?.GetCustomAttribute<ExtensionClassAttribute>();
|
|
if (selfAttr is null)
|
|
throw new ExtensionException($"Expected first parameter of {method} to have a [Self], or its declaring type to have a [ExtensionClass] attribute in order to define the extension's target type.");
|
|
|
|
TypeInfo anyType = TypeInfo.Any;
|
|
|
|
string name = attr.Name ?? method.Name;
|
|
TypeInfo selfType = selfAttr.TypeInfo;
|
|
TypeInfo returnType = method.GetCustomAttribute<ReturnsAttribute>()?.TypeInfo ?? anyType; // todo: assume returned type from actual return value
|
|
string[] parameterNames = parameters.Subset(1).Select(p => p.Name!).ToArray() ?? []; // todo: use the actual parameter attribute for this.
|
|
|
|
ExtensionDelegate callback = RenderDelegate(method);
|
|
|
|
return new ExtensionMethod(selfType, returnType, name, callback, parameterNames);
|
|
}
|
|
|
|
private static ExtensionDelegate RenderDelegate(MethodInfo method)
|
|
{
|
|
_logger.Verbose($"Rendering delegate expression...");
|
|
var selfParameter = Expression.Parameter(typeof(Value), "self");
|
|
var argsParameter = Expression.Parameter(typeof(Value[]), "args");
|
|
ParameterInfo[] parameterInfos = method.GetParameters();
|
|
|
|
var parameterExpressions = new Expression[parameterInfos.Length - 1];
|
|
|
|
for (int i = 0; i < parameterInfos.Length; i++) {
|
|
ConstantExpression index = Expression.Constant(i);
|
|
BinaryExpression indexExpression = Expression.ArrayIndex(argsParameter, index);
|
|
|
|
/* todo: unsure
|
|
Type requiredType = parameterInfos[i].ParameterType;
|
|
if (indexExpression.Type != requiredType) {
|
|
parameterExpressions[i] = Expression.Convert(indexExpression, requiredType);
|
|
}*/
|
|
|
|
parameterExpressions[i] = indexExpression;
|
|
}
|
|
|
|
Expression[] arguments = [selfParameter, .. parameterExpressions];
|
|
MethodCallExpression callExpression = Expression.Call(method, arguments);
|
|
Expression final;
|
|
|
|
if (method.ReturnType != typeof(Value)) {
|
|
ConstructorInfo? ctor = typeof(Value).GetConstructor([ method.ReturnType ]);
|
|
if (ctor is null)
|
|
throw new ExtensionException($"Can not compile extension from method {method}, its return type '{method.ReturnType}' is not accepted by any Value constructors.", method);
|
|
final = Expression.New(ctor, callExpression);
|
|
} else {
|
|
final = callExpression;
|
|
}
|
|
|
|
LambdaExpression lambda = Expression.Lambda(final, selfParameter, argsParameter);
|
|
|
|
_logger.Verbose($"Done: ", lambda);
|
|
return (ExtensionDelegate)lambda.Compile();
|
|
}
|
|
|
|
public static IEnumerable<ExtensionMethod> CompileExtensionsFromType(Type type)
|
|
{
|
|
_logger.Debug($"Compiling static extension methods from {type}...");
|
|
List<ExtensionMethod> extensions = [];
|
|
|
|
var methods = type.GetMethods(BindingFlags.Static | BindingFlags.Public)
|
|
.Where(method => method.GetCustomAttribute<ExtensionMethodAttribute>() is not null)
|
|
.ToArray();
|
|
|
|
if (methods.Length == 0) {
|
|
_logger.Warn($"Tried to compile static extension methods from {type}, but no public static methods were found declared inside it.");
|
|
return extensions;
|
|
}
|
|
|
|
foreach (var method in methods) {
|
|
extensions.Add(RenderExtension(method));
|
|
}
|
|
|
|
return extensions;
|
|
}
|
|
|
|
static ExtensionMethod()
|
|
{
|
|
Register(CompileExtensionsFromType(typeof(StringExtensions)));
|
|
}
|
|
|
|
public Value Call(Value self, Value[] parameters)
|
|
{
|
|
return Callback(self, parameters);
|
|
}
|
|
|
|
public override string ToString()
|
|
{
|
|
return $"{TargetType}.{Name}({string.Join(", ", Parameters)}) (NativeExtension)";
|
|
}
|
|
}
|
|
|
|
[ExtensionClass(ValueType = ValueType.String)]
|
|
public static class StringExtensions
|
|
{
|
|
[ExtensionMethod]
|
|
public static long Length(Objects.String self)
|
|
{
|
|
return self.Value?.Length ?? 0;
|
|
}
|
|
|
|
[ExtensionMethod]
|
|
public static long IndexOf(Objects.String self, Objects.String needle)
|
|
{
|
|
if (needle.Value is null)
|
|
return -1;
|
|
|
|
return self.Value?.IndexOf(needle.Value) ?? -1;
|
|
}
|
|
|
|
[ExtensionMethod]
|
|
public static Objects.Array Split(Objects.String self, Objects.String splitter)
|
|
{
|
|
string? _splitter = splitter.Value;
|
|
string? _string = self.Value;
|
|
|
|
if (_string is null)
|
|
return new Objects.Array([]);
|
|
|
|
if (_splitter is null)
|
|
return new Objects.Array([ Obj.Create(self) ]);
|
|
|
|
string[] parts = _string.Split(_splitter);
|
|
Value[] values = new Value[parts.Length];
|
|
for (int i = 0; i < parts.Length; i++)
|
|
values[i] = Objects.String.Make(parts[i]);
|
|
|
|
return new Objects.Array(values);
|
|
}
|
|
}
|