Rest.Net/Rest.Net/RestCSharpClientGenerator.cs

445 lines
18 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
namespace MontoyaTech.Rest.Net
{
/// <summary>
/// The outline of a Rest Client Generator that can generate a C# Client.
/// </summary>
public class RestCSharpClientGenerator : RestClientGenerator
{
/// <summary>
/// Whether or not to generate static code, if true the client will be static.
/// </summary>
public bool StaticCode = false;
/// <summary>
/// Generates a CSharp Client from a given set of routes and returns it.
/// </summary>
/// <param name="routes"></param>
/// <returns></returns>
public override string Generate(List<Route> routes)
{
//Remove any hidden routes from code generation.
for (int i = 0; i < routes.Count; i++)
{
var methodInfo = routes[i].GetTarget().GetMethodInfo();
var routeHidden = methodInfo.GetCustomAttribute<RouteHidden>();
if (routeHidden != null)
{
routes.RemoveAt(i);
i--;
}
}
var includedTypes = this.FindRoutesDependencies(routes);
var routeGroups = this.FindRouteGroups(routes);
var writer = new CodeWriter();
writer.WriteLine("using System;");
writer.WriteLine("using System.Net.Http;");
writer.WriteLine("using Newtonsoft.Json;");
writer.WriteBreak().WriteLine($"public class {this.ClientName}").WriteLine("{").Indent();
if (this.StaticCode)
writer.WriteBreak().WriteLine("public static string BaseUrl;");
else
writer.WriteBreak().WriteLine("public string BaseUrl;");
if (this.StaticCode)
writer.WriteBreak().WriteLine("public static HttpClient HttpClient;");
else
writer.WriteBreak().WriteLine("public HttpClient HttpClient;");
//Create fields foreach route group so they can be accessed.
if (!this.StaticCode)
foreach (var group in routeGroups)
writer.WriteBreak().WriteLine($"public {group.Key}Api {group.Key};");
//Create the client constructor or init method
if (this.StaticCode)
{
writer.WriteBreak().WriteLine("public static void Init(string baseUrl)").WriteLine("{").Indent();
//Init the base url
writer.WriteLine($"{this.ClientName}.BaseUrl = baseUrl;");
//Init the http client
writer.WriteLine($"{this.ClientName}.HttpClient = new HttpClient();");
writer.WriteLine(@$"{this.ClientName}.HttpClient.DefaultRequestHeaders.Add(""Accept"", ""*/*"");");
writer.WriteLine(@$"{this.ClientName}.HttpClient.DefaultRequestHeaders.Add(""Connection"", ""keep-alive"");");
writer.WriteLine(@$"{this.ClientName}.HttpClient.DefaultRequestHeaders.Add(""Accept-Encoding"", ""identity"");");
writer.Outdent().WriteLine("}");
}
else
{
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("}");
}
this.GenerateCSharpRouteGroups(routeGroups, writer);
this.GenerateCSharpIncludedTypes(includedTypes, writer);
writer.Outdent().WriteLine("}");
return writer.ToString();
}
/// <summary>
/// Generates C# for a set of included types.
/// </summary>
/// <param name="types"></param>
/// <param name="writer"></param>
protected virtual void GenerateCSharpIncludedTypes(List<Type> types, CodeWriter writer)
{
foreach (var type in types)
this.GenerateCSharpIncludedType(type, writer);
}
/// <summary>
/// Generates C# for a given included type.
/// </summary>
/// <param name="type"></param>
/// <param name="writer"></param>
protected virtual void GenerateCSharpIncludedType(Type type, CodeWriter writer)
{
writer.WriteBreak();
writer.WriteLine($"public class {type.Name}").WriteLine("{").Indent();
var fields = type.GetFields();
if (fields != null)
foreach (var field in fields)
if (field.IsPublic)
this.GenerateCSharpIncludedField(field, writer);
var properties = type.GetProperties();
if (properties != null)
foreach (var property in properties)
if (property.GetSetMethod() != null && property.GetGetMethod() != null)
this.GenerateCSharpIncludedProperty(property, writer);
writer.Outdent().WriteLine("}");
}
/// <summary>
/// Generates C# for a field inside an included type.
/// </summary>
/// <param name="field"></param>
/// <param name="writer"></param>
protected virtual void GenerateCSharpIncludedField(FieldInfo field, CodeWriter writer)
{
writer.WriteBreak();
writer.WriteLine($"public {this.GetTypeFullyResolvedName(field.FieldType)} {field.Name};");
}
/// <summary>
/// Generates C# for a property inside an included type.
/// </summary>
/// <param name="property"></param>
/// <param name="writer"></param>
protected virtual void GenerateCSharpIncludedProperty(PropertyInfo property, CodeWriter writer)
{
writer.WriteBreak();
writer.WriteLine($"public {this.GetTypeFullyResolvedName(property.PropertyType)} {property.Name} {{ get; set; }}");
}
/// <summary>
/// Generates C# for a set of route groups.
/// </summary>
/// <param name="groups"></param>
/// <param name="writer"></param>
protected virtual void GenerateCSharpRouteGroups(Dictionary<string, List<Route>> groups, CodeWriter writer)
{
foreach (var group in groups)
this.GenerateCSharpRouteGroup(group.Key, group.Value, writer);
}
/// <summary>
/// Generates C# for a given route group.
/// </summary>
/// <param name="name"></param>
/// <param name="routes"></param>
/// <param name="writer"></param>
protected virtual void GenerateCSharpRouteGroup(string name, List<Route> routes, CodeWriter writer)
{
writer.WriteBreak();
//Output the class header
if (this.StaticCode)
writer.WriteLine($"public class {name}").WriteLine("{").Indent();
else
writer.WriteLine($"public class {name}Api").WriteLine("{").Indent();
//Output the client instance
if (!this.StaticCode)
writer.WriteBreak().WriteLine("public Client Client;");
//Output the constuctor if not static.
if (!this.StaticCode)
{
writer.WriteBreak().WriteLine($"public {name}Api(Client client)").WriteLine("{").Indent();
writer.WriteLine("this.Client = client;");
writer.Outdent().WriteLine("}");
}
//Output all the route functions.
foreach (var route in routes)
this.GenerateCSharpRouteFunction(route, writer);
writer.Outdent().WriteLine("}");
}
/// <summary>
/// Generates a C# function for a given route.
/// </summary>
/// <param name="route"></param>
/// <param name="writer"></param>
/// <exception cref="NotSupportedException"></exception>
protected virtual void GenerateCSharpRouteFunction(Route route, CodeWriter writer)
{
writer.WriteBreak();
var methodInfo = route.GetTarget().GetMethodInfo();
var routeName = methodInfo.GetCustomAttribute<RouteName>();
var routeRequest = methodInfo.GetCustomAttribute<RouteRequest>();
var routeResponse = methodInfo.GetCustomAttribute<RouteResponse>();
//Generate the route function header
if (this.StaticCode)
writer.Write($"public static {(routeResponse == null ? "void" : this.GetTypeFullyResolvedName(routeResponse.ResponseType))} {(routeName == null ? methodInfo.Name : routeName.Name)}(");
else
writer.Write($"public {(routeResponse == null ? "void" : this.GetTypeFullyResolvedName(routeResponse.ResponseType))} {(routeName == null ? methodInfo.Name : routeName.Name)}(");
//Generate the functions parameters
var parameters = methodInfo.GetParameters();
if (parameters != null)
{
for (int i = 1; i < parameters.Length; i++)
{
writer.WriteSeparator();
writer.Write(this.GetTypeFullyResolvedName(parameters[i].ParameterType)).Write(" ").Write(parameters[i].Name);
}
}
if (routeRequest != null)
{
writer.WriteSeparator();
writer.Write(this.GetTypeFullyResolvedName(routeRequest.RequestType)).Write(" request");
}
writer.WriteLine(")").WriteLine("{").Indent();
//Generate the message code
writer.WriteBreak().Write($"var message = new HttpRequestMessage(");
switch (route.Method.ToLower())
{
case "post":
writer.Write("HttpMethod.Post");
break;
case "get":
writer.Write("HttpMethod.Get");
break;
case "delete":
writer.Write("HttpMethod.Delete");
break;
case "put":
writer.Write("HttpMethod.Put");
break;
case "options":
writer.Write("HttpMethod.Options");
break;
case "patch":
writer.Write("HttpMethod.Patch");
break;
case "head":
writer.Write("HttpMethod.Head");
break;
case "trace":
writer.Write("HttpMethod.Trace");
break;
default:
throw new NotSupportedException("Unsupport route method:" + route.Method);
}
if (this.StaticCode)
writer.WriteSeparator().Write('$').Write('"').Write("{Client.BaseUrl}");
else
writer.WriteSeparator().Write('$').Write('"').Write("{this.Client.BaseUrl}");
//Reconstruct the route syntax into a request url.
var components = route.Syntax.Split('/');
int argumentIndex = 0;
foreach (var component in components)
{
if (!string.IsNullOrWhiteSpace(component))
{
writer.Write('/');
if (component.StartsWith("{"))
{
writer.Write("{").Write(parameters[argumentIndex++ + 1].Name).Write("}");
}
else if (component == "*")
{
writer.Write("*");
}
else if (component == "**")
{
break;
}
else
{
writer.Write(component);
}
}
}
writer.Write('"').WriteLine(");");
//Add the request content if any.
if (routeRequest != null)
{
if (routeRequest.Json)
writer.WriteBreak().WriteLine("message.Content = new StringContent(JsonConvert.SerializeObject(request));");
else
writer.WriteBreak().WriteLine("message.Content = new StringContent(request.ToString());");
}
//Generate the response code
if (this.StaticCode)
writer.WriteBreak().WriteLine("var response = Client.HttpClient.Send(message);");
else
writer.WriteBreak().WriteLine("var response = this.Client.HttpClient.Send(message);");
//Handle the response
if (routeResponse != null)
{
writer.WriteBreak().WriteLine("if (response.StatusCode == System.Net.HttpStatusCode.OK)").Indent();
if (routeResponse.Json)
{
writer.WriteLine($"return JsonConvert.DeserializeObject<{this.GetTypeFullyResolvedName(routeResponse.ResponseType)}>(response.Content.ReadAsStringAsync().GetAwaiter().GetResult());");
}
else
{
switch (Type.GetTypeCode(routeResponse.ResponseType))
{
case TypeCode.Boolean:
writer.WriteLine("return bool.Parse(response.Content.ReadAsStringAsync().GetAwaiter().GetResult());");
break;
case TypeCode.Byte:
writer.WriteLine("return byte.Parse(response.Content.ReadAsStringAsync().GetAwaiter().GetResult());");
break;
case TypeCode.Char:
writer.WriteLine("return response.Content.ReadAsStringAsync().GetAwaiter().GetResult()[0];");
break;
case TypeCode.DateTime:
writer.WriteLine("return DateTime.Parse(response.Content.ReadAsStringAsync().GetAwaiter().GetResult());");
break;
case TypeCode.Decimal:
writer.WriteLine("return decimal.Parse(response.Content.ReadAsStringAsync().GetAwaiter().GetResult());");
break;
case TypeCode.Double:
writer.WriteLine("return double.Parse(response.Content.ReadAsStringAsync().GetAwaiter().GetResult());");
break;
case TypeCode.Int16:
writer.WriteLine("return short.Parse(response.Content.ReadAsStringAsync().GetAwaiter().GetResult());");
break;
case TypeCode.Int32:
writer.WriteLine("return int.Parse(response.Content.ReadAsStringAsync().GetAwaiter().GetResult());");
break;
case TypeCode.Int64:
writer.WriteLine("return long.Parse(response.Content.ReadAsStringAsync().GetAwaiter().GetResult());");
break;
case TypeCode.SByte:
writer.WriteLine("return sbyte.Parse(response.Content.ReadAsStringAsync().GetAwaiter().GetResult());");
break;
case TypeCode.Single:
writer.WriteLine("return float.Parse(response.Content.ReadAsStringAsync().GetAwaiter().GetResult());");
break;
case TypeCode.String:
writer.WriteLine("return response.Content.ReadAsStringAsync().GetAwaiter().GetResult();");
break;
case TypeCode.UInt16:
writer.WriteLine("return ushort.Parse(response.Content.ReadAsStringAsync().GetAwaiter().GetResult());");
break;
case TypeCode.UInt32:
writer.WriteLine("return uint.Parse(response.Content.ReadAsStringAsync().GetAwaiter().GetResult());");
break;
case TypeCode.UInt64:
writer.WriteLine("return ulong.Parse(response.Content.ReadAsStringAsync().GetAwaiter().GetResult());");
break;
case TypeCode.Object:
throw new NotSupportedException("ResponseType isn't JSON but is an object.");
}
}
writer.Outdent().WriteLine("else").Indent();
writer.WriteLine(@"throw new Exception(""Unexpected Http Response StatusCode:"" + response.StatusCode);").Outdent();
}
else
{
writer.WriteBreak().WriteLine("if (response.StatusCode == System.Net.HttpStatusCode.OK)").Indent();
writer.WriteLine(@"throw new Exception(""Unexpected Http Response StatusCode:"" + response.StatusCode);").Outdent();
}
//Close off the route function.
writer.Outdent().WriteLine("}");
}
}
}