From 19ccdb90260cbc7b490240d4133a1aa1380e2157 Mon Sep 17 00:00:00 2001 From: MattMo Date: Fri, 3 Feb 2023 13:33:28 -0800 Subject: [PATCH] Working on a client code generation feature to help speed up using an api built with this library. --- Rest.Net.Example/Program.cs | 8 + Rest.Net/CodeGenerator.cs | 78 +++++++ Rest.Net/CodeWriter.cs | 398 ++++++++++++++++++++++++++++++++++++ Rest.Net/Route.cs | 2 +- Rest.Net/RouteGroup.cs | 37 ++++ 5 files changed, 522 insertions(+), 1 deletion(-) create mode 100644 Rest.Net/CodeGenerator.cs create mode 100644 Rest.Net/CodeWriter.cs create mode 100644 Rest.Net/RouteGroup.cs diff --git a/Rest.Net.Example/Program.cs b/Rest.Net.Example/Program.cs index d9523fc..cc46daa 100644 --- a/Rest.Net.Example/Program.cs +++ b/Rest.Net.Example/Program.cs @@ -34,6 +34,10 @@ namespace MontoyaTech.Rest.Net.Example new Route(HttpRequestMethod.Get, "/json", Json) ); + Console.WriteLine(CodeGenerator.GenerateCSharpClient(listener.Routes)); + + Console.ReadLine(); + listener.RequestPreProcessEvent += (HttpListenerContext context) => { Console.WriteLine("Request start: " + context.Request.RawUrl); return true; @@ -56,21 +60,25 @@ namespace MontoyaTech.Rest.Net.Example listener.Block(); } + [RouteGroup("Test")] public static HttpListenerResponse Status(HttpListenerContext context) { return context.Response.WithStatus(HttpStatusCode.OK).WithText("Everything is operational. 👍"); } + [RouteGroup("Test")] public static HttpListenerResponse Add(HttpListenerContext context, double a, double b) { return context.Response.WithStatus(HttpStatusCode.OK).WithText((a + b).ToString()); } + [RouteGroup("Test")] public static HttpListenerResponse Signup(HttpListenerContext context, User user) { return context.Response.WithStatus(HttpStatusCode.OK).WithText("User:" + user.Name); } + [RouteGroup("Test")] public static HttpListenerResponse Json(HttpListenerContext context) { return context.Response.WithStatus(HttpStatusCode.OK).WithJson(new User("Rest.Net")); diff --git a/Rest.Net/CodeGenerator.cs b/Rest.Net/CodeGenerator.cs new file mode 100644 index 0000000..794dd52 --- /dev/null +++ b/Rest.Net/CodeGenerator.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +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 CodeGenerator + { + /// + /// Generates a CSharp client for a given set of routes. + /// + /// + /// + public static string GenerateCSharpClient(IList routes) + { + var writer = new CodeWriter(); + + writer.WriteLine("public class Client").WriteLine("{").Indent(); + writer.WriteSpacer(); + writer.WriteLine("public string BaseUrl = null;"); + writer.WriteSpacer(); + writer.WriteLine("public Client(string baseUrl)").WriteLine("{").Indent(); + writer.WriteLine("this.BaseUrl = baseUrl;"); + writer.Outdent().WriteLine("}"); + + GenerateCSharpRouteClasses(routes, writer); + + writer.Outdent().WriteLine("}"); + + return writer.ToString(); + } + + private static void GenerateCSharpRouteClasses(IList routes, CodeWriter writer) + { + 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.Target.GetMethodInfo(); + + //See if this method defines the group for us. + var routeGroup = methodInfo.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 }); + } + } + + private static void GenerateCSharpRouteClass(string name, List routes, CodeWriter writer) + { + } + + /// + /// 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 new file mode 100644 index 0000000..df40ac3 --- /dev/null +++ b/Rest.Net/CodeWriter.cs @@ -0,0 +1,398 @@ +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 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') + { + 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/Route.cs b/Rest.Net/Route.cs index 784f67a..2484fde 100644 --- a/Rest.Net/Route.cs +++ b/Rest.Net/Route.cs @@ -25,7 +25,7 @@ namespace MontoyaTech.Rest.Net /// /// The target function to invoke if this route is invoked. /// - private Func Target; + internal Func Target; /// /// Whether or not to close the response after the route is invoked. diff --git a/Rest.Net/RouteGroup.cs b/Rest.Net/RouteGroup.cs new file mode 100644 index 0000000..3bee727 --- /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.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; + } + } +}