From 74f8921f7ae6219b80fbdc26507afd0ce7197988 Mon Sep 17 00:00:00 2001 From: MattMo Date: Sat, 4 Feb 2023 10:37:30 -0800 Subject: [PATCH] Improved the code generator. More to come. --- Rest.Net.Example/Client.cs | 82 ++++--- Rest.Net.Example/Program.cs | 4 + Rest.Net/CodeGenerator.cs | 441 ++++++++++++++++++++++++++++++++---- Rest.Net/CodeWriter.cs | 22 +- 4 files changed, 475 insertions(+), 74 deletions(-) diff --git a/Rest.Net.Example/Client.cs b/Rest.Net.Example/Client.cs index 7401906..7a4392b 100644 --- a/Rest.Net.Example/Client.cs +++ b/Rest.Net.Example/Client.cs @@ -6,42 +6,68 @@ using System.Threading.Tasks; namespace MontoyaTech.Rest.Net.Example { + using System; + using System.Net.Http; + public class Client { - public string BaseUrl = null; + public string BaseUrl; - public TestFunctions Test; + private HttpClient HttpClient; - public class TestFunctions - { - public Client Client; - - public TestFunctions(Client client) - { - this.Client = client; - } - - public void Status() - { - } - - public void Add() - { - } - - public void Signup() - { - } - - public void Json() - { - } - } + public TestApi Test; public Client(string baseUrl) { this.BaseUrl = baseUrl; - this.Test = new TestFunctions(this); + this.Test = new TestApi(this); + this.HttpClient = new HttpClient(); + this.HttpClient.DefaultRequestHeaders.Add("Accept", "*/*"); + this.HttpClient.DefaultRequestHeaders.Add("Connection", "keep-alive"); + this.HttpClient.DefaultRequestHeaders.Add("Accept-Encoding", "identity"); + } + + public class TestApi + { + public Client Client; + + public TestApi(Client client) + { + this.Client = client; + } + + public string Status() + { + var request = new HttpRequestMessage(HttpMethod.Get, $"{this.Client.BaseUrl}/status"); + return default; + } + + public string Add(double a, double b) + { + var request = new HttpRequestMessage(HttpMethod.Post, $"{this.Client.BaseUrl}/add/{a}/{b}"); + return default; + } + + public string Signup(User user) + { + var request = new HttpRequestMessage(HttpMethod.Post, $"{this.Client.BaseUrl}/signup/{user}"); + return default; + } + + public User Json() + { + var request = new HttpRequestMessage(HttpMethod.Get, $"{this.Client.BaseUrl}/json"); + return default; + } + } + + public class User + { + public string Name; + + public System.Collections.Generic.List List; + + public ulong Property { get; set; } } } } diff --git a/Rest.Net.Example/Program.cs b/Rest.Net.Example/Program.cs index b46a3f4..febc655 100644 --- a/Rest.Net.Example/Program.cs +++ b/Rest.Net.Example/Program.cs @@ -14,6 +14,10 @@ namespace MontoyaTech.Rest.Net.Example { public string Name = null; + public List List = null; + + public ulong Property { get; set; } + public User(string name) { this.Name = name; diff --git a/Rest.Net/CodeGenerator.cs b/Rest.Net/CodeGenerator.cs index 449522b..b10f414 100644 --- a/Rest.Net/CodeGenerator.cs +++ b/Rest.Net/CodeGenerator.cs @@ -1,10 +1,13 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.Data; using System.Linq; +using System.Net.Http; using System.Reflection; using System.Text; using System.Threading.Tasks; +using System.Xml.Linq; namespace MontoyaTech.Rest.Net { @@ -14,45 +17,249 @@ namespace MontoyaTech.Rest.Net /// public class CodeGenerator { - /// - /// Generates a CSharp client for a given set of routes. - /// - /// - /// - public static string GenerateCSharpClient(IList routes) + private static bool IsTypeDotNet(Type type) { - var writer = new CodeWriter(); + if (type.Assembly.GetName().Name == "System.Private.CoreLib") + return true; - writer.WriteLine("public class Client").WriteLine("{").Indent(); - writer.WriteLine("public string BaseUrl = null;"); - - GenerateCSharpRouteClasses(routes, writer, out List routeClasses); - - writer.WriteBreak(); - writer.WriteLine("public Client(string baseUrl)").WriteLine("{").Indent(); - writer.WriteLine("this.BaseUrl = baseUrl;"); - - foreach (var @class in routeClasses) - writer.WriteLine($"this.{@class} = new {@class}Functions(this);"); - - writer.Outdent().WriteLine("}"); - - writer.Outdent().WriteLine("}"); - - return writer.ToString(); + return false; } - private static void GenerateCSharpRouteClasses(IList routes, CodeWriter writer, out List routeClasses) + private static List FindTypeDependencies(Type type) + { + var dependencies = new HashSet(); + + if (IsTypeDotNet(type)) + return dependencies.ToList(); + + dependencies.Add(type); + + var arguments = type.GetGenericArguments(); + + if (arguments != null) + { + foreach (var argument in arguments) + { + var types = FindTypeDependencies(argument); + + for (int i = 0; i < types.Count; i++) + if (!dependencies.Contains(types[i])) + dependencies.Add(types[i]); + } + } + + var fields = type.GetFields(); + + if (fields != null) + { + foreach (var field in fields) + { + if (field.IsPublic) + { + var types = FindTypeDependencies(field.FieldType); + + for (int i = 0; i < types.Count; i++) + if (!dependencies.Contains(types[i])) + dependencies.Add(types[i]); + } + } + } + + var properties = type.GetProperties(); + + if (properties != null) + { + foreach (var property in properties) + { + if (property.GetSetMethod() != null && property.GetGetMethod() != null) + { + var types = FindTypeDependencies(property.PropertyType); + + for (int i = 0; i < types.Count; i++) + if (!dependencies.Contains(types[i])) + dependencies.Add(types[i]); + } + } + } + + return dependencies.ToList(); + } + + private static List FindRouteDependencies(Route route) + { + var dependencies = new HashSet(); + + var methodInfo = route.GetTarget().GetMethodInfo(); + + if (methodInfo != null) + { + var parameters = methodInfo.GetParameters(); + + if (parameters != null) + { + for (int i = 1; i < parameters.Length; i++) + { + var types = FindTypeDependencies(parameters[i].ParameterType); + + foreach (var type in types) + if (!dependencies.Contains(type)) + dependencies.Add(type); + } + } + + var routeRequest = methodInfo.GetCustomAttribute(); + + if (routeRequest != null) + { + var types = FindTypeDependencies(routeRequest.RequestType); + + foreach (var type in types) + if (!dependencies.Contains(type)) + dependencies.Add(type); + } + + var routeResponse = methodInfo.GetCustomAttribute(); + + if (routeResponse != null) + { + var types = FindTypeDependencies(routeResponse.ResponseType); + + foreach (var type in types) + if (!dependencies.Contains(type)) + dependencies.Add(type); + } + } + + return dependencies.ToList(); + } + + private static List FindRoutesDependencies(List routes) + { + var dependencies = new HashSet(); + + foreach (var route in routes) + { + var types = FindRouteDependencies(route); + + if (types != null) + for (int i = 0; i < types.Count; i++) + if (!dependencies.Contains(types[i])) + dependencies.Add(types[i]); + } + + return dependencies.ToList(); + } + + private static string GetTypeFullyResolvedName(Type type) + { + if (IsTypeDotNet(type)) + { + var typeCode = Type.GetTypeCode(type); + + switch (typeCode) + { + case TypeCode.Boolean: + return "bool"; + case TypeCode.Byte: + return "byte"; + case TypeCode.Char: + return "char"; + case TypeCode.DateTime: + return "DateTime"; + case TypeCode.Decimal: + return "decimal"; + case TypeCode.Double: + return "double"; + case TypeCode.Int16: + return "short"; + case TypeCode.Int32: + return "int"; + case TypeCode.Int64: + return "long"; + case TypeCode.SByte: + return "sbyte"; + case TypeCode.Single: + return "float"; + case TypeCode.String: + return "string"; + case TypeCode.UInt16: + return "ushort"; + case TypeCode.UInt32: + return "uint"; + case TypeCode.UInt64: + return "ulong"; + default: + var builder = new StringBuilder(); + + builder.Append(type.Namespace); + + int genericSymbol = type.Name.IndexOf('`'); + + if (genericSymbol == -1) + builder.Append(".").Append(type.Name); + else + builder.Append(".").Append(type.Name.Substring(0, genericSymbol)); + + var genericArguments = type.GetGenericArguments(); + + if (genericArguments != null && genericArguments.Length > 0) + { + builder.Append("<"); + + for (int i = 0; i < genericArguments.Length; i++) + { + if (i > 0) + builder.Append(", "); + + builder.Append(GetTypeFullyResolvedName(genericArguments[i])); + } + + builder.Append(">"); + } + + return builder.ToString(); + } + } + else + { + var builder = new StringBuilder(); + + int genericSymbol = type.Name.IndexOf('`'); + + if (genericSymbol == -1) + builder.Append(type.Name); + else + builder.Append(type.Name.Substring(0, genericSymbol)); + + var genericArguments = type.GetGenericArguments(); + + if (genericArguments != null && genericArguments.Length > 0) + { + builder.Append("<"); + + for (int i = 0; i < genericArguments.Length; i++) + { + if (i > 0) + builder.Append(", "); + + builder.Append(GetTypeFullyResolvedName(genericArguments[i])); + } + + builder.Append(">"); + } + + return builder.ToString(); + } + } + + private static Dictionary> FindRouteGroups(List routes) { var groups = new Dictionary>(); - //Go through all the routes and group them. foreach (var route in routes) { - //Get the method that this route is tied to. var methodInfo = route.GetTarget().GetMethodInfo(); - //See if this method defines the group for us. var routeGroup = methodInfo.GetCustomAttribute(); //If there wasn't a route group on the method, see if there is one on the declaring type. @@ -68,30 +275,121 @@ namespace MontoyaTech.Rest.Net groups.Add(group, new List() { route }); } - //Generate classes for these groups - foreach (var group in groups) - GenerateCSharpRouteClass(group.Key, group.Value, writer); - - //Set the route classes - routeClasses = new List(); - routeClasses.AddRange(groups.Select(group => group.Key)); + return groups; } - private static void GenerateCSharpRouteClass(string name, List routes, CodeWriter writer) + public static string GenerateCSharpClient(List routes) + { + var includedTypes = FindRoutesDependencies(routes); + + var routeGroups = FindRouteGroups(routes); + + var writer = new CodeWriter(); + + writer.WriteLine("using System;"); + writer.WriteLine("using System.Net.Http;"); + + writer.WriteBreak().WriteLine("public class Client").WriteLine("{").Indent(); + + writer.WriteBreak().WriteLine("public string BaseUrl;"); + + writer.WriteBreak().WriteLine("private HttpClient HttpClient;"); + + //Create fields foreach route group so they can be accessed. + foreach (var group in routeGroups) + writer.WriteBreak().WriteLine($"public {group.Key}Api {group.Key};"); + + //Create the client constructor + writer.WriteBreak().WriteLine("public Client(string baseUrl)").WriteLine("{").Indent(); + + //Init the base url + writer.WriteLine("this.BaseUrl = baseUrl;"); + + //Init all the route group fields + foreach (var group in routeGroups) + writer.WriteLine($"this.{group.Key} = new {group.Key}Api(this);"); + + //Init the http client + writer.WriteLine("this.HttpClient = new HttpClient();"); + writer.WriteLine(@"this.HttpClient.DefaultRequestHeaders.Add(""Accept"", ""*/*"");"); + writer.WriteLine(@"this.HttpClient.DefaultRequestHeaders.Add(""Connection"", ""keep-alive"");"); + writer.WriteLine(@"this.HttpClient.DefaultRequestHeaders.Add(""Accept-Encoding"", ""identity"");"); + + writer.Outdent().WriteLine("}"); + + GenerateCSharpRouteGroups(routeGroups, writer); + + GenerateCSharpIncludedTypes(includedTypes, writer); + + writer.Outdent().WriteLine("}"); + + return writer.ToString(); + } + + private static void GenerateCSharpIncludedTypes(List types, CodeWriter writer) + { + foreach (var type in types) + GenerateCSharpIncludedType(type, writer); + } + + private static void GenerateCSharpIncludedType(Type type, CodeWriter writer) { writer.WriteBreak(); - writer.WriteLine($"public {name}Functions {name};"); + writer.WriteLine($"public class {type.Name}").WriteLine("{").Indent(); + var fields = type.GetFields(); + + if (fields != null) + foreach (var field in fields) + if (field.IsPublic) + GenerateCSharpIncludedField(field, writer); + + var properties = type.GetProperties(); + + if (properties != null) + foreach (var property in properties) + if (property.GetSetMethod() != null && property.GetGetMethod() != null) + GenerateCSharpIncludedProperty(property, writer); + + writer.Outdent().WriteLine("}"); + + System.Diagnostics.Debugger.Break(); + } + + private static void GenerateCSharpIncludedField(FieldInfo field, CodeWriter writer) + { writer.WriteBreak(); - writer.WriteLine($"public class {name}Functions").WriteLine("{").Indent(); + writer.WriteLine($"public {GetTypeFullyResolvedName(field.FieldType)} {field.Name};"); + } + + private static void GenerateCSharpIncludedProperty(PropertyInfo property, CodeWriter writer) + { + writer.WriteBreak(); + + writer.WriteLine($"public {GetTypeFullyResolvedName(property.PropertyType)} {property.Name} {{ get; set; }}"); + } + + private static void GenerateCSharpRouteGroups(Dictionary> groups, CodeWriter writer) + { + foreach (var group in groups) + GenerateCSharpRouteGroup(group.Key, group.Value, writer); + } + + private static void GenerateCSharpRouteGroup(string name, List routes, CodeWriter writer) + { + writer.WriteBreak(); + + writer.WriteLine($"public class {name}Api").WriteLine("{").Indent(); + + writer.WriteBreak(); writer.WriteLine("public Client Client;"); writer.WriteBreak(); - writer.WriteLine($"public {name}Functions(Client client)").WriteLine("{").Indent(); + writer.WriteLine($"public {name}Api(Client client)").WriteLine("{").Indent(); writer.WriteLine("this.Client = client;"); @@ -109,16 +407,69 @@ namespace MontoyaTech.Rest.Net var methodInfo = route.GetTarget().GetMethodInfo(); - writer.WriteLine($"public void {methodInfo.Name}()").WriteLine("{").Indent(); + var routeRequest = methodInfo.GetCustomAttribute(); + + var routeResponse = methodInfo.GetCustomAttribute(); + + writer.Write($"public {(routeResponse == null ? "void" : GetTypeFullyResolvedName(routeResponse.ResponseType))} {methodInfo.Name}("); + + var parameters = methodInfo.GetParameters(); + + if (parameters != null) + { + for (int i = 1; i < parameters.Length; i++) + { + writer.WriteSeparator(); + writer.Write(GetTypeFullyResolvedName(parameters[i].ParameterType)).Write(" ").Write(parameters[i].Name); + } + } + + if (routeRequest != null) + { + writer.WriteSeparator(); + writer.Write(GetTypeFullyResolvedName(routeRequest.RequestType)).Write(" request"); + } + + writer.WriteLine(")").WriteLine("{").Indent(); + + //Generate code to send a request + /* + * var response = HttpClient.Send(new HttpRequestMessage(HttpMethod.Post, $"{Auth0Url}/oauth/token") + { + Content = new FormUrlEncodedContent(new[] + { + new KeyValuePair("grant_type", "authorization_code"), + new KeyValuePair("code", code), + new KeyValuePair("redirect_uri", redirectUrl), + new KeyValuePair("client_id", ClientId) + }) + }) + */ + + writer.Write($"var request = new HttpRequestMessage("); + + switch (route.Method.ToLower()) + { + case "post": + writer.Write("HttpMethod.Post"); + break; + case "get": + writer.Write("HttpMethod.Get"); + break; + default: + throw new NotSupportedException("Unsupport route method:" + route.Method); + } + + writer.WriteSeparator().Write('$').WriteString($"{{this.Client.BaseUrl}}/{route.Syntax}"); + + writer.WriteLine(");"); + + if (routeResponse != null) + writer.WriteLine("return default;"); writer.Outdent().WriteLine("}"); } - /// - /// Generates a Javascript client for a given set of routes. - /// - /// - /// public static string GenerateJavascriptClient(IList routes) { return null; diff --git a/Rest.Net/CodeWriter.cs b/Rest.Net/CodeWriter.cs index df40ac3..b28521a 100644 --- a/Rest.Net/CodeWriter.cs +++ b/Rest.Net/CodeWriter.cs @@ -77,6 +77,26 @@ namespace MontoyaTech.Rest.Net return this; } + /// + /// Writes text to the writer surrounded by quotes. + /// + /// + /// + public CodeWriter WriteString(string text) + { + if (this.PendingIndent) + { + for (var i = 0; i < Indents; i++) + this.Builder.Append(' '); + + this.PendingIndent = false; + } + + this.Builder.Append('"').Append(text).Append('"'); + + return this; + } + /// /// Writes a character to the writer. /// @@ -306,7 +326,7 @@ namespace MontoyaTech.Rest.Net { var prevChar = this.Builder[this.Builder.Length - 2]; - if (prevChar != '\n') + if (prevChar != '\n' && prevChar != '{' && prevChar != '(' && prevChar != '[') { this.Builder.Append('\n'); this.PendingIndent = true;