| | | 1 | | using System.Reflection; |
| | | 2 | | using TeleFlow.Annotations; |
| | | 3 | | |
| | | 4 | | namespace TeleFlow.Telegram.Internal.Handlers; |
| | | 5 | | |
| | | 6 | | internal static class TelegramErrorHandlerDescriptorBuilder |
| | | 7 | | { |
| | | 8 | | public static IReadOnlyList<TelegramErrorHandlerDescriptor> Build( |
| | | 9 | | Type handlerType, |
| | | 10 | | int firstRegistrationOrder) |
| | | 11 | | { |
| | 332 | 12 | | ArgumentNullException.ThrowIfNull(handlerType); |
| | | 13 | | |
| | 332 | 14 | | if (handlerType.IsAbstract || handlerType.IsInterface) |
| | | 15 | | { |
| | 0 | 16 | | throw new InvalidOperationException( |
| | 0 | 17 | | $"Telegram error handler type '{handlerType.FullName}' must be a concrete class."); |
| | | 18 | | } |
| | | 19 | | |
| | 332 | 20 | | var registrationOrder = firstRegistrationOrder; |
| | 332 | 21 | | var descriptors = new List<TelegramErrorHandlerDescriptor>(); |
| | 332 | 22 | | var moduleName = GetModuleName(handlerType); |
| | | 23 | | |
| | 1337 | 24 | | foreach (var method in GetDeclaredCandidateMethods(handlerType)) |
| | | 25 | | { |
| | 337 | 26 | | var attributes = method.GetCustomAttributes<ErrorAttribute>(inherit: true).ToArray(); |
| | | 27 | | |
| | 337 | 28 | | if (attributes.Length == 0) |
| | | 29 | | { |
| | | 30 | | continue; |
| | | 31 | | } |
| | | 32 | | |
| | 25 | 33 | | ValidateReturnType(method); |
| | | 34 | | |
| | 96 | 35 | | foreach (var attribute in attributes) |
| | | 36 | | { |
| | 24 | 37 | | var exceptionType = attribute.ExceptionType; |
| | | 38 | | |
| | 24 | 39 | | if (exceptionType is not null && !typeof(Exception).IsAssignableFrom(exceptionType)) |
| | | 40 | | { |
| | 0 | 41 | | throw CreateSignatureException( |
| | 0 | 42 | | method, |
| | 0 | 43 | | $"{nameof(ErrorAttribute)} exception type must derive from {nameof(Exception)}."); |
| | | 44 | | } |
| | | 45 | | |
| | 24 | 46 | | var parameters = BuildParameterDescriptors(method, exceptionType, out var telegramContextType); |
| | | 47 | | |
| | 24 | 48 | | descriptors.Add(new TelegramErrorHandlerDescriptor( |
| | 24 | 49 | | handlerType, |
| | 24 | 50 | | method, |
| | 24 | 51 | | exceptionType, |
| | 24 | 52 | | telegramContextType, |
| | 24 | 53 | | registrationOrder, |
| | 24 | 54 | | moduleName, |
| | 24 | 55 | | parameters)); |
| | 24 | 56 | | registrationOrder++; |
| | | 57 | | } |
| | | 58 | | } |
| | | 59 | | |
| | 331 | 60 | | if (HasInheritedErrorMethods(handlerType)) |
| | | 61 | | { |
| | 0 | 62 | | throw new InvalidOperationException( |
| | 0 | 63 | | $"Telegram error handler type '{handlerType.FullName}' inherits error handler methods. " + |
| | 0 | 64 | | "Inherited error handler methods must be overridden in the concrete handler type for generated registrat |
| | | 65 | | } |
| | | 66 | | |
| | 331 | 67 | | return descriptors; |
| | | 68 | | } |
| | | 69 | | |
| | | 70 | | private static IEnumerable<MethodInfo> GetDeclaredCandidateMethods(Type handlerType) |
| | | 71 | | { |
| | 332 | 72 | | return handlerType |
| | 332 | 73 | | .GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly) |
| | 337 | 74 | | .Where(static method => !method.IsSpecialName) |
| | 669 | 75 | | .OrderBy(static method => method.MetadataToken); |
| | | 76 | | } |
| | | 77 | | |
| | | 78 | | private static bool HasInheritedErrorMethods(Type handlerType) |
| | | 79 | | { |
| | 331 | 80 | | return handlerType |
| | 331 | 81 | | .GetMethods(BindingFlags.Instance | BindingFlags.Public) |
| | 1660 | 82 | | .Where(method => method.DeclaringType != handlerType) |
| | 1655 | 83 | | .Any(static method => method.GetCustomAttribute<ErrorAttribute>(inherit: true) is not null); |
| | | 84 | | } |
| | | 85 | | |
| | | 86 | | private static string? GetModuleName(Type handlerType) |
| | | 87 | | { |
| | 332 | 88 | | return handlerType.GetCustomAttribute<TelegramModuleAttribute>(inherit: false)?.Name; |
| | | 89 | | } |
| | | 90 | | |
| | | 91 | | private static List<TelegramErrorHandlerParameterDescriptor> BuildParameterDescriptors( |
| | | 92 | | MethodInfo method, |
| | | 93 | | Type? exceptionType, |
| | | 94 | | out Type? telegramContextType) |
| | | 95 | | { |
| | 24 | 96 | | var parameters = method.GetParameters(); |
| | | 97 | | |
| | 81 | 98 | | if (parameters.Count(static parameter => parameter.ParameterType == typeof(TelegramErrorContext)) > 1) |
| | | 99 | | { |
| | 0 | 100 | | throw CreateSignatureException(method, $"An error handler method can declare at most one {nameof(TelegramErr |
| | | 101 | | } |
| | | 102 | | |
| | 81 | 103 | | if (parameters.Count(static parameter => parameter.ParameterType == typeof(CancellationToken)) > 1) |
| | | 104 | | { |
| | 0 | 105 | | throw CreateSignatureException(method, "An error handler method can declare at most one CancellationToken pa |
| | | 106 | | } |
| | | 107 | | |
| | 24 | 108 | | var exceptionParameters = parameters |
| | 57 | 109 | | .Where(static parameter => typeof(Exception).IsAssignableFrom(parameter.ParameterType)) |
| | 24 | 110 | | .ToArray(); |
| | | 111 | | |
| | 24 | 112 | | if (exceptionParameters.Length > 1) |
| | | 113 | | { |
| | 0 | 114 | | throw CreateSignatureException(method, "An error handler method can declare at most one exception parameter. |
| | | 115 | | } |
| | | 116 | | |
| | 24 | 117 | | var contextParameters = parameters |
| | 57 | 118 | | .Where(static parameter => typeof(TelegramUpdateContext).IsAssignableFrom(parameter.ParameterType)) |
| | 24 | 119 | | .ToArray(); |
| | | 120 | | |
| | 24 | 121 | | if (contextParameters.Length > 1) |
| | | 122 | | { |
| | 0 | 123 | | throw CreateSignatureException(method, "An error handler method can declare at most one Telegram context par |
| | | 124 | | } |
| | | 125 | | |
| | 24 | 126 | | telegramContextType = contextParameters.FirstOrDefault()?.ParameterType; |
| | | 127 | | |
| | 24 | 128 | | var descriptors = new List<TelegramErrorHandlerParameterDescriptor>(parameters.Length); |
| | | 129 | | |
| | 162 | 130 | | foreach (var parameter in parameters) |
| | | 131 | | { |
| | 57 | 132 | | if (parameter.ParameterType == typeof(TelegramErrorContext)) |
| | | 133 | | { |
| | 3 | 134 | | descriptors.Add(new TelegramErrorHandlerParameterDescriptor(parameter, TelegramErrorHandlerParameterKind |
| | 3 | 135 | | continue; |
| | | 136 | | } |
| | | 137 | | |
| | 54 | 138 | | if (parameter.ParameterType == typeof(CancellationToken)) |
| | | 139 | | { |
| | 2 | 140 | | descriptors.Add(new TelegramErrorHandlerParameterDescriptor(parameter, TelegramErrorHandlerParameterKind |
| | 2 | 141 | | continue; |
| | | 142 | | } |
| | | 143 | | |
| | 52 | 144 | | if (typeof(Exception).IsAssignableFrom(parameter.ParameterType)) |
| | | 145 | | { |
| | 24 | 146 | | ValidateExceptionParameter(method, exceptionType, parameter); |
| | 24 | 147 | | descriptors.Add(new TelegramErrorHandlerParameterDescriptor(parameter, TelegramErrorHandlerParameterKind |
| | 24 | 148 | | continue; |
| | | 149 | | } |
| | | 150 | | |
| | 28 | 151 | | if (typeof(TelegramUpdateContext).IsAssignableFrom(parameter.ParameterType)) |
| | | 152 | | { |
| | 3 | 153 | | descriptors.Add(new TelegramErrorHandlerParameterDescriptor(parameter, TelegramErrorHandlerParameterKind |
| | 3 | 154 | | continue; |
| | | 155 | | } |
| | | 156 | | |
| | 25 | 157 | | if (IsRouteValueParameterType(parameter.ParameterType)) |
| | | 158 | | { |
| | 3 | 159 | | descriptors.Add(new TelegramErrorHandlerParameterDescriptor(parameter, TelegramErrorHandlerParameterKind |
| | 3 | 160 | | continue; |
| | | 161 | | } |
| | | 162 | | |
| | 22 | 163 | | descriptors.Add(new TelegramErrorHandlerParameterDescriptor(parameter, TelegramErrorHandlerParameterKind.Ser |
| | | 164 | | } |
| | | 165 | | |
| | 24 | 166 | | return descriptors; |
| | | 167 | | } |
| | | 168 | | |
| | | 169 | | private static void ValidateExceptionParameter( |
| | | 170 | | MethodInfo method, |
| | | 171 | | Type? exceptionType, |
| | | 172 | | ParameterInfo parameter) |
| | | 173 | | { |
| | 24 | 174 | | if (exceptionType is null) |
| | | 175 | | { |
| | 5 | 176 | | if (parameter.ParameterType != typeof(Exception)) |
| | | 177 | | { |
| | 0 | 178 | | throw CreateSignatureException( |
| | 0 | 179 | | method, |
| | 0 | 180 | | $"Catch-all {nameof(ErrorAttribute)} methods must bind exception parameters as {nameof(Exception)}." |
| | | 181 | | } |
| | | 182 | | |
| | 5 | 183 | | return; |
| | | 184 | | } |
| | | 185 | | |
| | 19 | 186 | | if (!parameter.ParameterType.IsAssignableFrom(exceptionType)) |
| | | 187 | | { |
| | 0 | 188 | | throw CreateSignatureException( |
| | 0 | 189 | | method, |
| | 0 | 190 | | $"Exception parameter '{parameter.Name}' must be assignable from {exceptionType.Name}."); |
| | | 191 | | } |
| | 19 | 192 | | } |
| | | 193 | | |
| | | 194 | | private static bool IsRouteValueParameterType(Type type) |
| | | 195 | | { |
| | 25 | 196 | | var valueType = Nullable.GetUnderlyingType(type) ?? type; |
| | | 197 | | |
| | 25 | 198 | | return valueType == typeof(string) || |
| | 25 | 199 | | valueType == typeof(int) || |
| | 25 | 200 | | valueType == typeof(long); |
| | | 201 | | } |
| | | 202 | | |
| | | 203 | | private static void ValidateReturnType(MethodInfo method) |
| | | 204 | | { |
| | 25 | 205 | | if (method.ReturnType == typeof(TelegramErrorHandlingResult) || |
| | 25 | 206 | | IsErrorHandlingTaskResult(method.ReturnType, typeof(Task<>)) || |
| | 25 | 207 | | IsErrorHandlingTaskResult(method.ReturnType, typeof(ValueTask<>))) |
| | | 208 | | { |
| | 24 | 209 | | return; |
| | | 210 | | } |
| | | 211 | | |
| | 1 | 212 | | throw CreateSignatureException( |
| | 1 | 213 | | method, |
| | 1 | 214 | | $"A Telegram error handler method must return {nameof(TelegramErrorHandlingResult)}, Task<{nameof(TelegramEr |
| | | 215 | | } |
| | | 216 | | |
| | | 217 | | private static bool IsErrorHandlingTaskResult(Type returnType, Type taskType) |
| | | 218 | | { |
| | 8 | 219 | | return returnType.IsGenericType && |
| | 8 | 220 | | returnType.GetGenericTypeDefinition() == taskType && |
| | 8 | 221 | | returnType.GetGenericArguments()[0] == typeof(TelegramErrorHandlingResult); |
| | | 222 | | } |
| | | 223 | | |
| | | 224 | | private static InvalidOperationException CreateSignatureException(MethodInfo method, string reason) |
| | | 225 | | { |
| | 1 | 226 | | return new InvalidOperationException( |
| | 1 | 227 | | $"Invalid Telegram error handler signature '{method.DeclaringType?.FullName}.{method.Name}': {reason}"); |
| | | 228 | | } |
| | | 229 | | } |