Improved the code generator. More to come.

This commit is contained in:
MattMo 2023-02-04 10:37:30 -08:00
parent 6bb01464e7
commit 74f8921f7a
4 changed files with 475 additions and 74 deletions

View File

@ -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<string> List;
public ulong Property { get; set; }
}
}
}

View File

@ -14,6 +14,10 @@ namespace MontoyaTech.Rest.Net.Example
{
public string Name = null;
public List<string> List = null;
public ulong Property { get; set; }
public User(string name)
{
this.Name = name;

View File

@ -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
/// </summary>
public class CodeGenerator
{
/// <summary>
/// Generates a CSharp client for a given set of routes.
/// </summary>
/// <param name="routes"></param>
/// <returns></returns>
public static string GenerateCSharpClient(IList<Route> 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<string> 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<Route> routes, CodeWriter writer, out List<string> routeClasses)
private static List<Type> FindTypeDependencies(Type type)
{
var dependencies = new HashSet<Type>();
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<Type> FindRouteDependencies(Route route)
{
var dependencies = new HashSet<Type>();
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<RouteRequest>();
if (routeRequest != null)
{
var types = FindTypeDependencies(routeRequest.RequestType);
foreach (var type in types)
if (!dependencies.Contains(type))
dependencies.Add(type);
}
var routeResponse = methodInfo.GetCustomAttribute<RouteResponse>();
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<Type> FindRoutesDependencies(List<Route> routes)
{
var dependencies = new HashSet<Type>();
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<string, List<Route>> FindRouteGroups(List<Route> routes)
{
var groups = new Dictionary<string, List<Route>>();
//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<RouteGroup>();
//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>() { route });
}
//Generate classes for these groups
foreach (var group in groups)
GenerateCSharpRouteClass(group.Key, group.Value, writer);
//Set the route classes
routeClasses = new List<string>();
routeClasses.AddRange(groups.Select(group => group.Key));
return groups;
}
private static void GenerateCSharpRouteClass(string name, List<Route> routes, CodeWriter writer)
public static string GenerateCSharpClient(List<Route> 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<Type> 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<string, List<Route>> groups, CodeWriter writer)
{
foreach (var group in groups)
GenerateCSharpRouteGroup(group.Key, group.Value, writer);
}
private static void GenerateCSharpRouteGroup(string name, List<Route> 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<RouteRequest>();
var routeResponse = methodInfo.GetCustomAttribute<RouteResponse>();
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<string, string>("grant_type", "authorization_code"),
new KeyValuePair<string, string>("code", code),
new KeyValuePair<string, string>("redirect_uri", redirectUrl),
new KeyValuePair<string, string>("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("}");
}
/// <summary>
/// Generates a Javascript client for a given set of routes.
/// </summary>
/// <param name="routes"></param>
/// <returns></returns>
public static string GenerateJavascriptClient(IList<Route> routes)
{
return null;

View File

@ -77,6 +77,26 @@ namespace MontoyaTech.Rest.Net
return this;
}
/// <summary>
/// Writes text to the writer surrounded by quotes.
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
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;
}
/// <summary>
/// Writes a character to the writer.
/// </summary>
@ -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;