525 lines
22 KiB
C#
525 lines
22 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)
|
|
{
|
|
var includedTypes = this.FindRoutesDependencies(routes);
|
|
|
|
var routeGroups = this.FindRouteGroups(routes);
|
|
|
|
var writer = new CodeWriter();
|
|
|
|
writer.WriteLine("using System;");
|
|
writer.WriteLine("using System.Net;");
|
|
writer.WriteLine("using System.Net.Http;");
|
|
writer.WriteLine("using Newtonsoft.Json;");
|
|
|
|
writer.WriteBreak().WriteLine($"public class {this.ClientName}").WriteLine("{").Indent();
|
|
|
|
//Create the base url field
|
|
if (this.StaticCode)
|
|
writer.WriteBreak().WriteLine("public static string BaseUrl;");
|
|
else
|
|
writer.WriteBreak().WriteLine("public string BaseUrl;");
|
|
|
|
//Create the cookie container field
|
|
if (this.StaticCode)
|
|
writer.WriteBreak().WriteLine("public static CookieContainer CookieContainer;");
|
|
else
|
|
writer.WriteBreak().WriteLine("public CookieContainer CookieContainer;");
|
|
|
|
//Create the client handler field
|
|
if (this.StaticCode)
|
|
writer.WriteBreak().WriteLine("public static HttpClientHandler ClientHandler;");
|
|
else
|
|
writer.WriteBreak().WriteLine("public HttpClientHandler ClientHandler;");
|
|
|
|
//Create the http client field
|
|
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();
|
|
|
|
//Make sure the base url isn't null or whitespace
|
|
writer.WriteBreak().WriteLine("if (string.IsNullOrWhiteSpace(baseUrl))");
|
|
writer.Indent().WriteLine(@"throw new ArgumentException(""baseUrl must not be null or whitespace."");").Outdent();
|
|
|
|
//If the baseUrl ends with a /, remove it.
|
|
writer.WriteBreak().WriteLine("if (baseUrl.EndsWith('/'))");
|
|
writer.Indent().WriteLine("baseUrl = baseUrl.Substring(0, baseUrl.Length - 1);").Outdent();
|
|
|
|
//Init the base url
|
|
writer.WriteBreak().WriteLine($"{this.ClientName}.BaseUrl = baseUrl;");
|
|
|
|
//Init the cookie container
|
|
writer.WriteBreak().WriteLine($"{this.ClientName}.CookieContainer = new CookieContainer();");
|
|
|
|
//Init the client handler
|
|
writer.WriteBreak().WriteLine($"{this.ClientName}.ClientHandler = new HttpClientHandler()");
|
|
writer.WriteLine("{").Indent();
|
|
writer.WriteLine("AllowAutoRedirect = true,");
|
|
writer.WriteLine("UseCookies = true,");
|
|
writer.WriteLine($"CookieContainer = {this.ClientName}.CookieContainer,");
|
|
writer.WriteLine("AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate");
|
|
writer.Outdent().WriteLine("};");
|
|
|
|
//Init the http client
|
|
writer.WriteBreak().WriteLine($"{this.ClientName}.HttpClient = new HttpClient({this.ClientName}.ClientHandler);");
|
|
writer.WriteBreak().WriteLine(@$"{this.ClientName}.HttpClient.DefaultRequestHeaders.Add(""Accept"", ""*/*"");");
|
|
writer.WriteBreak().WriteLine(@$"{this.ClientName}.HttpClient.DefaultRequestHeaders.Add(""Connection"", ""keep-alive"");");
|
|
writer.WriteBreak().WriteLine(@$"{this.ClientName}.HttpClient.DefaultRequestHeaders.Add(""Accept-Encoding"", ""identity"");");
|
|
|
|
writer.Outdent().WriteLine("}");
|
|
}
|
|
else
|
|
{
|
|
writer.WriteBreak().WriteLine("public Client(string baseUrl)").WriteLine("{").Indent();
|
|
|
|
//Make sure the base url isn't null or whitespace
|
|
writer.WriteBreak().WriteLine("if (string.IsNullOrWhiteSpace(baseUrl))");
|
|
writer.Indent().WriteLine(@"throw new ArgumentException(""baseUrl must not be null or whitespace."");").Outdent();
|
|
|
|
//If the baseUrl ends with a /, remove it.
|
|
writer.WriteBreak().WriteLine("if (baseUrl.EndsWith('/'))");
|
|
writer.Indent().WriteLine("baseUrl = baseUrl.Substring(0, baseUrl.Length - 1);").Outdent();
|
|
|
|
//Init the base url
|
|
writer.WriteBreak().WriteLine("this.BaseUrl = baseUrl;");
|
|
|
|
//Init the cookie container
|
|
writer.WriteBreak().WriteLine("this.CookieContainer = new CookieContainer();");
|
|
|
|
//Init the client handler
|
|
writer.WriteBreak().WriteLine("this.ClientHandler = new HttpClientHandler()");
|
|
writer.WriteLine("{").Indent();
|
|
writer.WriteLine("AllowAutoRedirect = true,");
|
|
writer.WriteLine("UseCookies = true,");
|
|
writer.WriteLine("CookieContainer = this.CookieContainer,");
|
|
writer.WriteLine("AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate");
|
|
writer.Outdent().WriteLine("};");
|
|
|
|
//Init the http client
|
|
writer.WriteBreak().WriteLine("this.HttpClient = new HttpClient(this.ClientHandler);");
|
|
writer.WriteBreak().WriteLine(@"this.HttpClient.DefaultRequestHeaders.Add(""Accept"", ""*/*"");");
|
|
writer.WriteBreak().WriteLine(@"this.HttpClient.DefaultRequestHeaders.Add(""Connection"", ""keep-alive"");");
|
|
writer.WriteBreak().WriteLine(@"this.HttpClient.DefaultRequestHeaders.Add(""Accept-Encoding"", ""identity"");");
|
|
|
|
//Init all the route group fields
|
|
foreach (var group in routeGroups)
|
|
writer.WriteBreak().WriteLine($"this.{group.Key} = new {group.Key}Api(this);");
|
|
|
|
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 internal virtual void GenerateCSharpIncludedTypes(List<Type> types, CodeWriter writer)
|
|
{
|
|
foreach (var type in types)
|
|
{
|
|
bool subType = false;
|
|
|
|
//See if this type belongs to another type in the list.
|
|
if (type.DeclaringType != null)
|
|
for (int i = 0; i < types.Count && !subType; i++)
|
|
if (type.DeclaringType == types[i])
|
|
subType = true;
|
|
|
|
//If not, generate the C# for this type.
|
|
if (!subType)
|
|
this.GenerateCSharpIncludedType(type, types, writer);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generates C# for a given included type.
|
|
/// </summary>
|
|
/// <param name="type"></param>
|
|
/// <param name="writer"></param>
|
|
protected internal virtual void GenerateCSharpIncludedType(Type type, List<Type> types, CodeWriter writer)
|
|
{
|
|
writer.WriteBreak();
|
|
|
|
var newName = type.GetCustomAttribute<RouteTypeName>();
|
|
|
|
writer.Write($"public {(type.IsEnum ? "enum" : "class")} {(newName != null ? newName.Name : type.Name)}");
|
|
|
|
if (type.IsEnum)
|
|
writer.Write(" : ").Write(this.GetTypeFullyResolvedName(Enum.GetUnderlyingType(type)));
|
|
else if (!(IsTypeDotNet(type.BaseType) && type.BaseType.Name == "Object"))
|
|
writer.Write(" : ").Write(this.GetTypeFullyResolvedName(type.BaseType));
|
|
|
|
writer.NewLine().WriteLine("{").Indent();
|
|
|
|
FieldInfo[] fields;
|
|
|
|
if (type.IsEnum)
|
|
fields = type.GetFields(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Static);
|
|
else
|
|
fields = type.GetFields(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public);
|
|
|
|
if (fields != null)
|
|
foreach (var field in fields)
|
|
if (field.IsPublic && !field.IsSpecialName)
|
|
this.GenerateCSharpIncludedField(field, writer);
|
|
|
|
var properties = type.GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public);
|
|
|
|
if (properties != null)
|
|
foreach (var property in properties)
|
|
if (!property.IsSpecialName && property.GetSetMethod() != null && property.GetGetMethod() != null)
|
|
this.GenerateCSharpIncludedProperty(property, writer);
|
|
|
|
//Generate C# for any types that belong to this one.
|
|
for (int i = 0; i < types.Count; i++)
|
|
if (types[i].DeclaringType == type)
|
|
GenerateCSharpIncludedType(types[i], types, writer);
|
|
|
|
writer.Outdent().WriteLine("}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generates C# for a field inside an included type.
|
|
/// </summary>
|
|
/// <param name="field"></param>
|
|
/// <param name="writer"></param>
|
|
protected internal virtual void GenerateCSharpIncludedField(FieldInfo field, CodeWriter writer)
|
|
{
|
|
writer.WriteBreak();
|
|
|
|
if (field.DeclaringType.IsEnum)
|
|
writer.WriteLine($"{field.Name} = {field.GetRawConstantValue()},");
|
|
else
|
|
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 internal 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 internal 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 internal 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 internal 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($"{{{this.ClientName}.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 = {this.ClientName}.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.IsSuccessStatusCode)").WriteLine("{").Indent();
|
|
|
|
writer.WriteBreak().WriteLine("var content = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();");
|
|
|
|
writer.WriteBreak().WriteLine("if (string.IsNullOrEmpty(content))").Indent().WriteLine("return default;").Outdent();
|
|
|
|
if (routeResponse.Json)
|
|
{
|
|
writer.WriteBreak().WriteLine($"return JsonConvert.DeserializeObject<{this.GetTypeFullyResolvedName(routeResponse.ResponseType)}>(content);");
|
|
}
|
|
else
|
|
{
|
|
switch (Type.GetTypeCode(routeResponse.ResponseType))
|
|
{
|
|
case TypeCode.Boolean:
|
|
writer.WriteBreak().WriteLine("return bool.Parse(content);");
|
|
break;
|
|
|
|
case TypeCode.Byte:
|
|
writer.WriteBreak().WriteLine("return byte.Parse(content);");
|
|
break;
|
|
|
|
case TypeCode.Char:
|
|
writer.WriteBreak().WriteLine("return content[0];");
|
|
break;
|
|
|
|
case TypeCode.DateTime:
|
|
writer.WriteBreak().WriteLine("return DateTime.Parse(content);");
|
|
break;
|
|
|
|
case TypeCode.Decimal:
|
|
writer.WriteBreak().WriteLine("return decimal.Parse(content);");
|
|
break;
|
|
|
|
case TypeCode.Double:
|
|
writer.WriteBreak().WriteLine("return double.Parse(content);");
|
|
break;
|
|
|
|
case TypeCode.Int16:
|
|
writer.WriteBreak().WriteLine("return short.Parse(content);");
|
|
break;
|
|
|
|
case TypeCode.Int32:
|
|
writer.WriteBreak().WriteLine("return int.Parse(content);");
|
|
break;
|
|
|
|
case TypeCode.Int64:
|
|
writer.WriteBreak().WriteLine("return long.Parse(content);");
|
|
break;
|
|
|
|
case TypeCode.SByte:
|
|
writer.WriteBreak().WriteLine("return sbyte.Parse(content);");
|
|
break;
|
|
|
|
case TypeCode.Single:
|
|
writer.WriteBreak().WriteLine("return float.Parse(content);");
|
|
break;
|
|
|
|
case TypeCode.String:
|
|
writer.WriteBreak().WriteLine("return content;");
|
|
break;
|
|
|
|
case TypeCode.UInt16:
|
|
writer.WriteBreak().WriteLine("return ushort.Parse(content);");
|
|
break;
|
|
|
|
case TypeCode.UInt32:
|
|
writer.WriteBreak().WriteLine("return uint.Parse(content);");
|
|
break;
|
|
|
|
case TypeCode.UInt64:
|
|
writer.WriteBreak().WriteLine("return ulong.Parse(content);");
|
|
break;
|
|
|
|
case TypeCode.Object:
|
|
throw new NotSupportedException("ResponseType isn't JSON but is an object.");
|
|
}
|
|
}
|
|
|
|
writer.Outdent().WriteLine("}").WriteLine("else").WriteLine("{").Indent();
|
|
writer.WriteLine(@"throw new Exception(""Unexpected Http Response StatusCode:"" + response.StatusCode);");
|
|
writer.Outdent().WriteLine("}");
|
|
}
|
|
else
|
|
{
|
|
writer.WriteBreak().WriteLine("if (!response.IsSuccessStatusCode)").Indent();
|
|
writer.WriteLine(@"throw new Exception(""Unexpected Http Response StatusCode:"" + response.StatusCode);").Outdent();
|
|
}
|
|
|
|
//Close off the route function.
|
|
writer.Outdent().WriteLine("}");
|
|
}
|
|
}
|
|
}
|