using System.Collections.Immutable; using System.Reflection; using System.Text; using Connected.Annotations; using Connected.Configuration.Environment; using Connected.ServiceModel; using Connected.Services; using Microsoft.AspNetCore.Http; namespace Connected.Rest; internal class ApiResolutionService : IApiResolutionService { private readonly Dictionary> _methods; private readonly Dictionary> _arguments; public ApiResolutionService(IEnvironmentService environmentService) { _methods = new(StringComparer.OrdinalIgnoreCase); _arguments = new(StringComparer.OrdinalIgnoreCase); EnvironmentService = environmentService; Initialize(); } private Dictionary> Methods => _methods; private Dictionary> Arguments => _arguments; private IEnvironmentService EnvironmentService { get; } /// /// This method tries to resolve argument implementation type based on the parameter's type interface. /// /// The implementation parameter of the method which declares the argument /// that implements 's type interface. public Type? ResolveArgument(ParameterInfo parameter) { if (!Arguments.ContainsKey(ArgumentName(parameter.ParameterType))) return null; var items = Arguments[ArgumentName(parameter.ParameterType)]; /* * If the interafce has only one implementation the air is clear. */ if (items.Count == 1) return WrapArgument(items[0].Type, parameter); /* * We have more than one implementation. We'll try to find the implementation that match * the assembly of the invoking method. This is the most probable scenario. */ foreach (var argument in items) { if (argument.Type.Assembly == parameter.ParameterType.Assembly) return WrapArgument(argument.Type, parameter); } /* * Method's assembly doesn't have an implementation, let's try to look in the * interface's assembly. */ foreach (var argument in items) { if (argument.Type.Assembly == parameter.ParameterType.Assembly) return WrapArgument(argument.Type, parameter); } /* * Nope, there must be some intermediate assembly implementing the argument and it surely must * be referenced by the method's assembly. */ return WrapArgument(items[0].Type, parameter); } private static Type WrapArgument(Type argument, ParameterInfo parameter) { if (!argument.IsGenericType) return argument; return argument.MakeGenericType(parameter.ParameterType.GetGenericArguments()); } public ApiInvokeDescriptor? ResolveMethod(HttpContext context) { var route = context.Request.Path.Value; if (route is null) return null; if (!Methods.TryGetValue(route.ToString(), out List? descriptor)) return null; //TODO: map overloads from arguments return descriptor[0]; } private void Initialize() { foreach (var type in EnvironmentService.Services.Services) InitializeApiService(type); foreach (var type in EnvironmentService.Services.Arguments) InitializeArgument(type); } private void InitializeArgument(Type type) { if (type.GetImplementedArguments() is not List arguments || !arguments.Any()) return; foreach (var argument in arguments) { var name = ArgumentName(argument); if (Arguments.TryGetValue(name, out _)) Arguments[name].Add(new ArgumentDescriptor { Type = type }); else Arguments.Add(name, new List { new ArgumentDescriptor { Type = type } }); } } private void InitializeApiService(Type type) { if (type.GetImplementedServices() is not List services || !services.Any()) return; foreach (var service in services) { var serviceUrl = ResolveServiceUrl(service); var methods = service.GetMethods(BindingFlags.Public | BindingFlags.Instance); foreach (var method in methods) { if (method.GetCustomAttribute() is not ServiceMethodAttribute attribute || attribute.Verbs == ServiceMethodVerbs.None) continue; InitializeServiceMethod(serviceUrl, service, method, attribute.Verbs); } } } private void InitializeServiceMethod(string serviceUrl, Type serviceType, MethodInfo method, ServiceMethodVerbs verbs) { var parameterTypes = new List(); foreach (var parameter in method.GetParameters()) parameterTypes.Add(parameter.ParameterType); var targetMethod = serviceType.GetMethod(method.Name, parameterTypes.ToArray()); var methodUrl = $"{serviceUrl}/{ResolveMethodUrl(targetMethod)}"; var descriptor = new ApiInvokeDescriptor { Service = serviceType, Method = targetMethod, Parameters = parameterTypes.ToArray(), Verbs = verbs }; if (Methods.TryGetValue(methodUrl, out List? items)) items.Add(descriptor); else Methods.Add(methodUrl, new List { descriptor }); } public ImmutableList> QueryRoutes() { var result = new List>(); foreach (var method in Methods) { var verbs = ServiceMethodVerbs.None; foreach (var descriptor in method.Value) verbs |= descriptor.Verbs; result.Add(Tuple.Create(method.Key, verbs)); } return result.ToImmutableList(); } private static string ResolveServiceUrl(Type type) { if (type.GetCustomAttribute() is ServiceUrlAttribute attribute) return attribute.Template; return $"{PascalNamespace(type.Namespace)}/{type.Name.ToPascalCase()}".Replace('.', '/'); } private static string ResolveMethodUrl(MethodInfo method) { if (method.GetCustomAttribute() is ServiceUrlAttribute attribute) return attribute.Template; return method.Name.ToCamelCase(); } private static string ArgumentName(Type argument) { return $"{argument.Namespace}.{argument.Name}, {argument.Assembly.FullName}"; } private static string? PascalNamespace(string? @namespace) { if (string.IsNullOrEmpty(@namespace)) return null; var tokens = @namespace.Split('.'); var result = new StringBuilder(); foreach (var token in tokens) result.Append($"{token.ToPascalCase()}."); return result.ToString().TrimEnd('.'); } }