diff --git a/Rest.Net.Example/Client.cs b/Rest.Net.Example/Client.cs new file mode 100644 index 0000000..4307c2e --- /dev/null +++ b/Rest.Net.Example/Client.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MontoyaTech.Rest.Net.Example +{ + using System; + using System.Net.Http; + using Newtonsoft.Json; + + public class Client + { + public string BaseUrl; + + public HttpClient HttpClient; + + public TestApi Test; + + public AuthApi Auth; + + public Client(string baseUrl) + { + this.BaseUrl = baseUrl; + this.Test = new TestApi(this); + this.Auth = new AuthApi(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 message = new HttpRequestMessage(HttpMethod.Get, $"{this.Client.BaseUrl}/status"); + + var response = this.Client.HttpClient.Send(message); + + if (response.StatusCode == System.Net.HttpStatusCode.OK) + return JsonConvert.DeserializeObject(response.Content.ReadAsStringAsync().GetAwaiter().GetResult()); + else + throw new Exception("Unexpected Http Response StatusCode:" + response.StatusCode); + } + + public string Add(double a, double b) + { + var message = new HttpRequestMessage(HttpMethod.Post, $"{this.Client.BaseUrl}/add/{a}/{b}"); + + var response = this.Client.HttpClient.Send(message); + + if (response.StatusCode == System.Net.HttpStatusCode.OK) + return JsonConvert.DeserializeObject(response.Content.ReadAsStringAsync().GetAwaiter().GetResult()); + else + throw new Exception("Unexpected Http Response StatusCode:" + response.StatusCode); + } + } + + public class AuthApi + { + public Client Client; + + public AuthApi(Client client) + { + this.Client = client; + } + + public bool UserExists(string name) + { + var message = new HttpRequestMessage(HttpMethod.Get, $"{this.Client.BaseUrl}/auth/{name}"); + + var response = this.Client.HttpClient.Send(message); + + if (response.StatusCode == System.Net.HttpStatusCode.OK) + return JsonConvert.DeserializeObject(response.Content.ReadAsStringAsync().GetAwaiter().GetResult()); + else + throw new Exception("Unexpected Http Response StatusCode:" + response.StatusCode); + } + + public void Signup(User request) + { + var message = new HttpRequestMessage(HttpMethod.Post, $"{this.Client.BaseUrl}/auth/signup"); + + message.Content = new StringContent(JsonConvert.SerializeObject(request)); + + var response = this.Client.HttpClient.Send(message); + + if (response.StatusCode == System.Net.HttpStatusCode.OK) + throw new Exception("Unexpected Http Response StatusCode:" + response.StatusCode); + } + + public User Get() + { + var message = new HttpRequestMessage(HttpMethod.Get, $"{this.Client.BaseUrl}/auth"); + + var response = this.Client.HttpClient.Send(message); + + if (response.StatusCode == System.Net.HttpStatusCode.OK) + return JsonConvert.DeserializeObject(response.Content.ReadAsStringAsync().GetAwaiter().GetResult()); + else + throw new Exception("Unexpected Http Response StatusCode:" + response.StatusCode); + } + } + + 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 d9523fc..a4e6d87 100644 --- a/Rest.Net.Example/Program.cs +++ b/Rest.Net.Example/Program.cs @@ -5,6 +5,7 @@ using System.Text; using System.Threading.Tasks; using System.Net; using MontoyaTech.Rest.Net; +using System.Net.Mime; namespace MontoyaTech.Rest.Net.Example { @@ -14,15 +15,16 @@ namespace MontoyaTech.Rest.Net.Example { public string Name = null; + public List List = null; + + public ulong Property { get; set; } + + public User() { } + public User(string name) { this.Name = name; } - - public static explicit operator User(string input) - { - return new User(input.ToString()); - } } public static void Main(string[] args) @@ -30,18 +32,23 @@ namespace MontoyaTech.Rest.Net.Example var listener = new RouteListener(8080, new Route(HttpRequestMethod.Get, "/status", Status), new Route(HttpRequestMethod.Post, "/add/{a}/{b}", Add), - new Route(HttpRequestMethod.Post, "/signup/{username}", Signup), - new Route(HttpRequestMethod.Get, "/json", Json) + new Route(HttpRequestMethod.Get, "/auth/{username}", Exists), + new Route(HttpRequestMethod.Post, "/auth/signup", Signup), + new Route(HttpRequestMethod.Get, "/auth/", Json) ); + string code = ClientCodeGenerator.GenerateCSharpClient(listener.Routes); + + Console.WriteLine(code); + listener.RequestPreProcessEvent += (HttpListenerContext context) => { - Console.WriteLine("Request start: " + context.Request.RawUrl); + Console.WriteLine($"[{context.Request.HttpMethod}] Request start: " + context.Request.RawUrl); return true; }; listener.RequestPostProcessEvent += (HttpListenerContext context) => { - Console.WriteLine("Request end: " + context.Request.RawUrl); + Console.WriteLine($"[{context.Request.HttpMethod}] Request end: " + context.Request.RawUrl); }; listener.Start(); @@ -51,26 +58,57 @@ namespace MontoyaTech.Rest.Net.Example foreach (var route in listener.Routes) Console.WriteLine($"- [{route.Method}] {route.Syntax}"); - Console.WriteLine($"Rest api server running at http://localhost:{listener.Port}"); + Console.WriteLine($"Rest api server running at {listener.BaseUrl}"); + + var client = new Client(listener.BaseUrl); + + var result = client.Auth.Get(); listener.Block(); } + [RouteGroup("Test")] + [RouteResponse(typeof(string))] public static HttpListenerResponse Status(HttpListenerContext context) { return context.Response.WithStatus(HttpStatusCode.OK).WithText("Everything is operational. 👍"); } + [RouteGroup("Test")] + [RouteResponse(typeof(string))] public static HttpListenerResponse Add(HttpListenerContext context, double a, double b) { return context.Response.WithStatus(HttpStatusCode.OK).WithText((a + b).ToString()); } - public static HttpListenerResponse Signup(HttpListenerContext context, User user) + + [RouteGroup("Test")] + [RouteRequest(typeof(User))] + public static HttpListenerResponse SignupRequest(HttpListenerContext context) { - return context.Response.WithStatus(HttpStatusCode.OK).WithText("User:" + user.Name); + return context.Response.WithStatus(HttpStatusCode.OK); } + [RouteGroup("Auth")] + [RouteName("UserExists")] + [RouteResponse(typeof(bool))] + public static HttpListenerResponse Exists(HttpListenerContext context, string name) + { + Console.WriteLine("Auth.Exists called, name:" + name); + + return context.Response.WithStatus(HttpStatusCode.OK).WithJson(true); + } + + [RouteGroup("Auth")] + [RouteRequest(typeof(User))] + public static HttpListenerResponse Signup(HttpListenerContext context) + { + return context.Response.WithStatus(HttpStatusCode.OK); + } + + [RouteGroup("Auth")] + [RouteName("Get")] + [RouteResponse(typeof(User))] public static HttpListenerResponse Json(HttpListenerContext context) { return context.Response.WithStatus(HttpStatusCode.OK).WithJson(new User("Rest.Net")); diff --git a/Rest.Net/ClientCodeGenerator.cs b/Rest.Net/ClientCodeGenerator.cs new file mode 100644 index 0000000..9b7f815 --- /dev/null +++ b/Rest.Net/ClientCodeGenerator.cs @@ -0,0 +1,605 @@ +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 +{ + /// + /// A class that can take a set of routes and generate code + /// for a client that can be used to interact with them. + /// + public class ClientCodeGenerator + { + /// + /// Returns whether or not a given type belongs to DotNet. + /// + /// + /// + private static bool IsTypeDotNet(Type type) + { + if (type.Assembly.GetName().Name == "System.Private.CoreLib") + return true; + + return false; + } + + /// + /// Finds all the dependencies for a given type and returns them. + /// + /// + /// + 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(); + } + + /// + /// Finds all the types that a given route depends on to function. + /// + /// + /// + 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(); + } + + /// + /// Finds all the types that a given set of routes depend on to function. + /// + /// + /// + 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(); + } + + /// + /// Returns the fully resolve name for a given type. + /// + /// + /// + 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(); + } + } + + /// + /// Finds all the route groupings from a set of routes and returns those groups. + /// + /// + /// + private static Dictionary> FindRouteGroups(List routes) + { + var groups = new Dictionary>(); + + foreach (var route in routes) + { + var methodInfo = route.GetTarget().GetMethodInfo(); + + var routeGroup = methodInfo.GetCustomAttribute(); + + //If there wasn't a route group on the method, see if there is one on the declaring type. + if (routeGroup == null) + routeGroup = methodInfo.DeclaringType.GetCustomAttribute(); + + //Group this route. + string group = (routeGroup != null ? routeGroup.Name : methodInfo.DeclaringType.Name); + + if (groups.ContainsKey(group)) + groups[group].Add(route); + else + groups.Add(group, new List() { route }); + } + + return groups; + } + + /// + /// Generates a CSharpClient from a RouteListener. + /// + /// + /// The name of the Client class, default is Client. + /// The generated C# code. + public static string GenerateCSharpClient(RouteListener listener, string name = "Client") + { + return GenerateCSharpClient(listener.Routes, name); + } + + /// + /// Generates a CSharpClient from a given set of Routes. + /// + /// + /// The name of the Client class, default is Client. + /// + public static string GenerateCSharpClient(List routes, string name = "Client") + { + var includedTypes = FindRoutesDependencies(routes); + + var routeGroups = 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 {name}").WriteLine("{").Indent(); + + writer.WriteBreak().WriteLine("public string BaseUrl;"); + + writer.WriteBreak().WriteLine("public 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(); + } + + /// + /// Generates C# for a set of included types. + /// + /// + /// + private static void GenerateCSharpIncludedTypes(List types, CodeWriter writer) + { + foreach (var type in types) + GenerateCSharpIncludedType(type, writer); + } + + /// + /// Generates C# for a given included type. + /// + /// + /// + private static 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) + 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("}"); + } + + /// + /// Generates C# for a field inside an included type. + /// + /// + /// + private static void GenerateCSharpIncludedField(FieldInfo field, CodeWriter writer) + { + writer.WriteBreak(); + + writer.WriteLine($"public {GetTypeFullyResolvedName(field.FieldType)} {field.Name};"); + } + + /// + /// Generates C# for a property inside an included type. + /// + /// + /// + private static void GenerateCSharpIncludedProperty(PropertyInfo property, CodeWriter writer) + { + writer.WriteBreak(); + + writer.WriteLine($"public {GetTypeFullyResolvedName(property.PropertyType)} {property.Name} {{ get; set; }}"); + } + + /// + /// Generates C# for a set of route groups. + /// + /// + /// + private static void GenerateCSharpRouteGroups(Dictionary> groups, CodeWriter writer) + { + foreach (var group in groups) + GenerateCSharpRouteGroup(group.Key, group.Value, writer); + } + + /// + /// Generates C# for a given route group. + /// + /// + /// + /// + 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}Api(Client client)").WriteLine("{").Indent(); + + writer.WriteLine("this.Client = client;"); + + writer.Outdent().WriteLine("}"); + + foreach (var route in routes) + GenerateCSharpRouteFunction(route, writer); + + writer.Outdent().WriteLine("}"); + } + + /// + /// Generates a C# function for a given route. + /// + /// + /// + /// + private static void GenerateCSharpRouteFunction(Route route, CodeWriter writer) + { + writer.WriteBreak(); + + var methodInfo = route.GetTarget().GetMethodInfo(); + + var routeName = methodInfo.GetCustomAttribute(); + + var routeRequest = methodInfo.GetCustomAttribute(); + + var routeResponse = methodInfo.GetCustomAttribute(); + + //Generate the route function header + writer.Write($"public {(routeResponse == null ? "void" : 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(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 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; + default: + throw new NotSupportedException("Unsupport route method:" + route.Method); + } + + 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) + writer.WriteBreak().WriteLine("message.Content = new StringContent(JsonConvert.SerializeObject(request));"); + + //Generate the response code + 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(); + writer.WriteLine($"return JsonConvert.DeserializeObject<{GetTypeFullyResolvedName(routeResponse.ResponseType)}>(response.Content.ReadAsStringAsync().GetAwaiter().GetResult());"); + 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("}"); + } + + /// + /// Generates a Javascript client from a given set of routes. + /// + /// + /// + /// + public static string GenerateJavascriptClient(List routes) + { + throw new NotImplementedException(); + } + } +} diff --git a/Rest.Net/CodeWriter.cs b/Rest.Net/CodeWriter.cs new file mode 100644 index 0000000..b28521a --- /dev/null +++ b/Rest.Net/CodeWriter.cs @@ -0,0 +1,418 @@ +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MontoyaTech.Rest.Net +{ + /// + /// The outline of a writer that helps with generating code. + /// + internal class CodeWriter + { + /// + /// The internal string builder. + /// + private StringBuilder Builder = new StringBuilder(); + + /// + /// The current number of characters written.. + /// + public int Length + { + get + { + return Builder.Length; + } + } + + /// + /// The current number of indents. + /// + private int Indents = 0; + + /// + /// Whether or not the writer is pending an indent that needs + /// to be handled. + /// + private bool PendingIndent = false; + + /// + /// Creates a new default CodeWriter. + /// + public CodeWriter() { } + + /// + /// Creates a new default text writer and copies the indent data from the passed + /// text writer. + /// + /// + public CodeWriter(CodeWriter writer) + { + this.Indents = writer.Indents; + + //If we have indents, then we must set pending indent. + if (this.Indents > 0) + this.PendingIndent = true; + } + + /// + /// Writes text to the writer. + /// + /// + public CodeWriter Write(string text) + { + if (this.PendingIndent) + { + for (var i = 0; i < Indents; i++) + this.Builder.Append(' '); + + this.PendingIndent = false; + } + + this.Builder.Append(text); + + 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. + /// + /// + public CodeWriter Write(char @char) + { + if (this.PendingIndent) + { + for (var i = 0; i < Indents; i++) + this.Builder.Append(' '); + + this.PendingIndent = false; + } + + this.Builder.Append(@char); + + return this; + } + + /// + /// Writes text to the writer and a newline. + /// + /// + public CodeWriter WriteLine(string text) + { + if (this.PendingIndent) + { + for (var i = 0; i < Indents; i++) + this.Builder.Append(' '); + + this.PendingIndent = false; + } + + this.Builder.Append(text); + this.Builder.Append('\n'); + this.PendingIndent = true; + + return this; + } + + /// + /// Writes a character and new line to the writer. + /// + /// + public CodeWriter WriteLine(char @char) + { + if (this.PendingIndent) + { + for (var i = 0; i < Indents; i++) + this.Builder.Append(' '); + + this.PendingIndent = false; + } + + this.Builder.Append(@char); + this.Builder.Append('\n'); + this.PendingIndent = true; + + return this; + } + + /// + /// Writes text to the writer if the condition is met. + /// + /// + /// + /// + public CodeWriter WriteAssert(bool condition, string text) + { + if (condition) + return this.Write(text); + + return this; + } + + /// + /// Writes a character to the writer if the condition is met. + /// + /// + /// + /// + public CodeWriter WriteAssert(bool condition, char @char) + { + if (condition) + return this.Write(@char); + + return this; + } + + /// + /// Writes text to the writer and a newline if the condition is met. + /// + /// + /// + /// + public CodeWriter WriteLineAssert(bool condition, string text) + { + if (condition) + return this.WriteLine(text); + + return this; + } + + /// + /// Writes a character and new line to the writer if the condition is met. + /// + /// + /// + /// + public CodeWriter WriteLineAssert(bool condition, char @char) + { + if (condition) + return this.WriteLine(@char); + + return this; + } + + /// + /// Writes the contents of another text writer into this one. + /// + /// + public CodeWriter Write(CodeWriter writer) + { + this.Builder.Append(writer.Builder.ToString()); + + return this; + } + + /// + /// Inserts a new line to the writer. + /// + public CodeWriter NewLine() + { + this.Builder.Append('\n'); + this.PendingIndent = true; + + return this; + } + + /// + /// Inserts a new line to the writer if the condition is met. + /// + public CodeWriter NewLineAssert(bool condition) + { + if (condition) + { + this.Builder.Append('\n'); + this.PendingIndent = true; + } + + return this; + } + + /// + /// Writes a space to the writer unless there is already one. + /// + /// + public CodeWriter WriteSpacer() + { + if (this.Builder.Length >= 1 && this.Builder[this.Builder.Length - 1] != ' ') + this.Builder.Append(' '); + + return this; + } + + /// + /// Writes a separator to the writer unless one wouldn't make sense. + /// + /// + public CodeWriter WriteSeparator() + { + if (this.Builder.Length > 0) + { + var offset = this.Builder.Length - 1; + var prev = this.Builder[offset]; + while (offset >= 0 && (prev == ' ' || prev == '\t' || prev == '\n' || prev == '\r')) + { + offset--; + prev = this.Builder[offset]; + } + + if (prev != '(' && prev != '{' && prev != '[' && prev != ',') + this.Builder.Append(", "); + } + + return this; + } + + /// + /// Writes a separator to the writer and text if a condition is met unless a separator here isn't needed, then just text is written. + /// + /// + /// + /// + public CodeWriter WriteSeparatorAssert(bool condition, string text) + { + if (condition) + { + if (this.Builder.Length > 0) + { + var offset = this.Builder.Length - 1; + var prev = this.Builder[offset]; + while (offset >= 0 && (prev == ' ' || prev == '\t' || prev == '\n' || prev == '\r')) + { + offset--; + prev = this.Builder[offset]; + } + + if (prev != '(' && prev != '{' && prev != '[' && prev != ',') + this.Builder.Append(", "); + } + + if (text != null) + this.Builder.Append(text); + } + + return this; + } + + /// + /// Writes a blank line as a spacer to the writer + /// unless there is already one. + /// + public CodeWriter WriteBreak() + { + if (this.Builder.Length >= 2) + { + var prevChar = this.Builder[this.Builder.Length - 2]; + + if (prevChar != '\n' && prevChar != '{' && prevChar != '(' && prevChar != '[') + { + this.Builder.Append('\n'); + this.PendingIndent = true; + } + } + else + { + this.Builder.Append('\n'); + this.PendingIndent = true; + } + + return this; + } + + /// + /// Indents the writer one time. + /// + public CodeWriter Indent() + { + this.Indents += 4; + + return this; + } + + /// + /// Indents the writer one time if the condition is met. + /// + public CodeWriter IndentAssert(bool condition) + { + if (condition) + this.Indents += 4; + + return this; + } + + /// + /// Removes one indent from the writer. + /// + public CodeWriter Outdent() + { + if (this.Indents > 0) + this.Indents -= 4; + + return this; + } + + /// + /// Removes one indent from the writer if the condition is met. + /// + public CodeWriter OutdentAssert(bool condition) + { + if (condition && this.Indents > 0) + this.Indents -= 4; + + return this; + } + + /// + /// Resets all indents from the writer. + /// + public CodeWriter ResetIndents() + { + this.Indents = 0; + + return this; + } + + /// + /// Removes the last character from the writer. + /// + /// + public CodeWriter Remove() + { + if (this.Builder.Length > 0) + this.Builder.Remove(this.Builder.Length - 1, 1); + + return this; + } + + /// + /// Gets all the written data from the writer. + /// + /// + public override string ToString() + { + return this.Builder.ToString(); + } + } +} diff --git a/Rest.Net/Rest.Net.csproj b/Rest.Net/Rest.Net.csproj index 34bfb69..66a48a1 100644 --- a/Rest.Net/Rest.Net.csproj +++ b/Rest.Net/Rest.Net.csproj @@ -17,7 +17,7 @@ MontoyaTech.Rest.Net MontoyaTech.Rest.Net True - 1.1.8 + 1.1.9 Logo_Symbol_Black_Outline.png diff --git a/Rest.Net/Route.cs b/Rest.Net/Route.cs index 784f67a..304442a 100644 --- a/Rest.Net/Route.cs +++ b/Rest.Net/Route.cs @@ -47,6 +47,13 @@ namespace MontoyaTech.Rest.Net /// public Route(string method, string syntax, Func target, bool closeResponse = true) { + if (target == null) + throw new ArgumentNullException(nameof(target)); + else if (string.IsNullOrWhiteSpace(method)) + throw new ArgumentException("Cannot be null or empty", nameof(method)); + else if (string.IsNullOrWhiteSpace(syntax)) + throw new ArgumentException("Cannot be null or empty", nameof(syntax)); + this.Method = method; this.Syntax = syntax; this.Target = target; @@ -117,6 +124,15 @@ namespace MontoyaTech.Rest.Net { this.Target.Invoke(context); } + + /// + /// Returns the target delegate for this route. + /// + /// + public virtual Delegate GetTarget() + { + return this.Target; + } } public class Route : Route @@ -125,6 +141,13 @@ namespace MontoyaTech.Rest.Net public Route(string method, string syntax, Func target, bool closeResponse = true) { + if (target == null) + throw new ArgumentNullException(nameof(target)); + else if (string.IsNullOrWhiteSpace(method)) + throw new ArgumentException("Cannot be null or empty", nameof(method)); + else if (string.IsNullOrWhiteSpace(syntax)) + throw new ArgumentException("Cannot be null or empty", nameof(syntax)); + this.Method = method; this.Syntax = syntax; this.Target = target; @@ -138,6 +161,11 @@ namespace MontoyaTech.Rest.Net { this.Target.DynamicInvoke(context, RouteArgumentConverter.Convert(arguments[0])); } + + public override Delegate GetTarget() + { + return this.Target; + } } public class Route : Route @@ -146,6 +174,13 @@ namespace MontoyaTech.Rest.Net public Route(string method, string syntax, Func target, bool closeResponse = true) { + if (target == null) + throw new ArgumentNullException(nameof(target)); + else if (string.IsNullOrWhiteSpace(method)) + throw new ArgumentException("Cannot be null or empty", nameof(method)); + else if (string.IsNullOrWhiteSpace(syntax)) + throw new ArgumentException("Cannot be null or empty", nameof(syntax)); + this.Method = method; this.Syntax = syntax; this.Target = target; @@ -163,6 +198,11 @@ namespace MontoyaTech.Rest.Net RouteArgumentConverter.Convert(arguments[1]) ); } + + public override Delegate GetTarget() + { + return this.Target; + } } public class Route : Route @@ -171,6 +211,13 @@ namespace MontoyaTech.Rest.Net public Route(string method, string syntax, Func target, bool closeResponse = true) { + if (target == null) + throw new ArgumentNullException(nameof(target)); + else if (string.IsNullOrWhiteSpace(method)) + throw new ArgumentException("Cannot be null or empty", nameof(method)); + else if (string.IsNullOrWhiteSpace(syntax)) + throw new ArgumentException("Cannot be null or empty", nameof(syntax)); + this.Method = method; this.Syntax = syntax; this.Target = target; @@ -189,6 +236,11 @@ namespace MontoyaTech.Rest.Net RouteArgumentConverter.Convert(arguments[2]) ); } + + public override Delegate GetTarget() + { + return this.Target; + } } public class Route : Route @@ -197,6 +249,13 @@ namespace MontoyaTech.Rest.Net public Route(string method, string syntax, Func target, bool closeResponse = true) { + if (target == null) + throw new ArgumentNullException(nameof(target)); + else if (string.IsNullOrWhiteSpace(method)) + throw new ArgumentException("Cannot be null or empty", nameof(method)); + else if (string.IsNullOrWhiteSpace(syntax)) + throw new ArgumentException("Cannot be null or empty", nameof(syntax)); + this.Method = method; this.Syntax = syntax; this.Target = target; @@ -216,6 +275,11 @@ namespace MontoyaTech.Rest.Net RouteArgumentConverter.Convert(arguments[3]) ); } + + public override Delegate GetTarget() + { + return this.Target; + } } public class Route : Route @@ -224,6 +288,13 @@ namespace MontoyaTech.Rest.Net public Route(string method, string syntax, Func target, bool closeResponse = true) { + if (target == null) + throw new ArgumentNullException(nameof(target)); + else if (string.IsNullOrWhiteSpace(method)) + throw new ArgumentException("Cannot be null or empty", nameof(method)); + else if (string.IsNullOrWhiteSpace(syntax)) + throw new ArgumentException("Cannot be null or empty", nameof(syntax)); + this.Method = method; this.Syntax = syntax; this.Target = target; @@ -244,6 +315,11 @@ namespace MontoyaTech.Rest.Net RouteArgumentConverter.Convert(arguments[4]) ); } + + public override Delegate GetTarget() + { + return this.Target; + } } public class Route : Route @@ -252,6 +328,13 @@ namespace MontoyaTech.Rest.Net public Route(string method, string syntax, Func target, bool closeResponse = true) { + if (target == null) + throw new ArgumentNullException(nameof(target)); + else if (string.IsNullOrWhiteSpace(method)) + throw new ArgumentException("Cannot be null or empty", nameof(method)); + else if (string.IsNullOrWhiteSpace(syntax)) + throw new ArgumentException("Cannot be null or empty", nameof(syntax)); + this.Method = method; this.Syntax = syntax; this.Target = target; @@ -273,6 +356,11 @@ namespace MontoyaTech.Rest.Net RouteArgumentConverter.Convert(arguments[5]) ); } + + public override Delegate GetTarget() + { + return this.Target; + } } public class Route : Route @@ -281,6 +369,13 @@ namespace MontoyaTech.Rest.Net public Route(string method, string syntax, Func target, bool closeResponse = true) { + if (target == null) + throw new ArgumentNullException(nameof(target)); + else if (string.IsNullOrWhiteSpace(method)) + throw new ArgumentException("Cannot be null or empty", nameof(method)); + else if (string.IsNullOrWhiteSpace(syntax)) + throw new ArgumentException("Cannot be null or empty", nameof(syntax)); + this.Method = method; this.Syntax = syntax; this.Target = target; @@ -303,6 +398,11 @@ namespace MontoyaTech.Rest.Net RouteArgumentConverter.Convert(arguments[6]) ); } + + public override Delegate GetTarget() + { + return this.Target; + } } public class Route : Route @@ -311,6 +411,13 @@ namespace MontoyaTech.Rest.Net public Route(string method, string syntax, Func target, bool closeResponse = true) { + if (target == null) + throw new ArgumentNullException(nameof(target)); + else if (string.IsNullOrWhiteSpace(method)) + throw new ArgumentException("Cannot be null or empty", nameof(method)); + else if (string.IsNullOrWhiteSpace(syntax)) + throw new ArgumentException("Cannot be null or empty", nameof(syntax)); + this.Method = method; this.Syntax = syntax; this.Target = target; @@ -334,5 +441,10 @@ namespace MontoyaTech.Rest.Net RouteArgumentConverter.Convert(arguments[7]) ); } + + public override Delegate GetTarget() + { + return this.Target; + } } } diff --git a/Rest.Net/RouteGroup.cs b/Rest.Net/RouteGroup.cs new file mode 100644 index 0000000..bb8ee78 --- /dev/null +++ b/Rest.Net/RouteGroup.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MontoyaTech.Rest.Net +{ + /// + /// The outline of an attribute that controls what group a route belongs to. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public class RouteGroup : Attribute + { + /// + /// The name of the group this Route belongs to. + /// + public string Name = null; + + /// + /// Creates a new default route group. + /// + public RouteGroup() { } + + /// + /// Creates a new route group with the name of the group. + /// + /// + public RouteGroup(string name) + { + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentException("Must not be null or empty", nameof(name)); + + this.Name = name; + } + } +} diff --git a/Rest.Net/RouteListener.cs b/Rest.Net/RouteListener.cs index 60ab12f..67dd520 100644 --- a/Rest.Net/RouteListener.cs +++ b/Rest.Net/RouteListener.cs @@ -29,6 +29,17 @@ namespace MontoyaTech.Rest.Net /// public ushort Port = 8081; + /// + /// Returns the BaseUrl for this RouteListener. + /// + public string BaseUrl + { + get + { + return $"http://localhost:{this.Port}"; + } + } + /// /// An event to preprocess routes before the route is executed. /// @@ -208,5 +219,15 @@ namespace MontoyaTech.Rest.Net if (!Thread.Yield()) Thread.Sleep(1000); } + + /// + /// Generates a C# client from this Route Listener and returns the code. + /// + /// The name of the Client. Default is Client. + /// + public string GenerateCSharpClient(string name = "Client") + { + return ClientCodeGenerator.GenerateCSharpClient(this, name); + } } } diff --git a/Rest.Net/RouteName.cs b/Rest.Net/RouteName.cs new file mode 100644 index 0000000..467c268 --- /dev/null +++ b/Rest.Net/RouteName.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MontoyaTech.Rest.Net +{ + /// + /// The outline of an attribute that allows you to rename an attribute in + /// the output code generation. + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public class RouteName : Attribute + { + /// + /// The name of this Route. + /// + public string Name = null; + + /// + /// Creates a new default RouteName. + /// + public RouteName() { } + + /// + /// Creates a new RouteName with a given name. + /// + /// + public RouteName(string name) + { + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentException("Cannot be null or whitespace", nameof(name)); + + this.Name = name; + } + } +} diff --git a/Rest.Net/RouteRequest.cs b/Rest.Net/RouteRequest.cs new file mode 100644 index 0000000..4ab72e0 --- /dev/null +++ b/Rest.Net/RouteRequest.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MontoyaTech.Rest.Net +{ + /// + /// The outline of an attribute that defines a routes request type. + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public class RouteRequest : Attribute + { + /// + /// The type of the request. + /// + public Type RequestType = null; + + /// + /// Creates a default route request. + /// + public RouteRequest() { } + + /// + /// Creates a route request with the request type. + /// + /// + public RouteRequest(Type requestType) + { + this.RequestType = requestType; + } + } +} diff --git a/Rest.Net/RouteResponse.cs b/Rest.Net/RouteResponse.cs new file mode 100644 index 0000000..525fc04 --- /dev/null +++ b/Rest.Net/RouteResponse.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MontoyaTech.Rest.Net +{ + /// + /// The outline of an attribute that defines a routes response type. + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public class RouteResponse : Attribute + { + /// + /// The type of the request. + /// + public Type ResponseType = null; + + /// + /// Creates a default route response. + /// + public RouteResponse() { } + + /// + /// Creates a route response with the response type. + /// + /// + public RouteResponse(Type responseType) + { + this.ResponseType = responseType; + } + } +}