diff --git a/Rest.Net.Example/Client.cs b/Rest.Net.Example/Client.cs new file mode 100644 index 0000000..7401906 --- /dev/null +++ b/Rest.Net.Example/Client.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MontoyaTech.Rest.Net.Example +{ + public class Client + { + public string BaseUrl = null; + + public TestFunctions Test; + + 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 Client(string baseUrl) + { + this.BaseUrl = baseUrl; + this.Test = new TestFunctions(this); + } + } +} diff --git a/Rest.Net.Example/Program.cs b/Rest.Net.Example/Program.cs index cc46daa..b46a3f4 100644 --- a/Rest.Net.Example/Program.cs +++ b/Rest.Net.Example/Program.cs @@ -34,7 +34,9 @@ namespace MontoyaTech.Rest.Net.Example new Route(HttpRequestMethod.Get, "/json", Json) ); - Console.WriteLine(CodeGenerator.GenerateCSharpClient(listener.Routes)); + string code = CodeGenerator.GenerateCSharpClient(listener.Routes); + + Console.WriteLine(code); Console.ReadLine(); @@ -61,24 +63,35 @@ namespace MontoyaTech.Rest.Net.Example } [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()); } [RouteGroup("Test")] + [RouteResponse(typeof(string))] public static HttpListenerResponse Signup(HttpListenerContext context, User user) { return context.Response.WithStatus(HttpStatusCode.OK).WithText("User:" + user.Name); } [RouteGroup("Test")] + [RouteRequest(typeof(User))] + public static HttpListenerResponse SignupRequest(HttpListenerContext context) + { + return context.Response.WithStatus(HttpStatusCode.OK); + } + + [RouteGroup("Test")] + [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/CodeGenerator.cs b/Rest.Net/CodeGenerator.cs index 794dd52..449522b 100644 --- a/Rest.Net/CodeGenerator.cs +++ b/Rest.Net/CodeGenerator.cs @@ -24,21 +24,25 @@ namespace MontoyaTech.Rest.Net var writer = new CodeWriter(); writer.WriteLine("public class Client").WriteLine("{").Indent(); - writer.WriteSpacer(); writer.WriteLine("public string BaseUrl = null;"); - writer.WriteSpacer(); + + GenerateCSharpRouteClasses(routes, writer, out List routeClasses); + + writer.WriteBreak(); writer.WriteLine("public Client(string baseUrl)").WriteLine("{").Indent(); writer.WriteLine("this.BaseUrl = baseUrl;"); - writer.Outdent().WriteLine("}"); - GenerateCSharpRouteClasses(routes, writer); + foreach (var @class in routeClasses) + writer.WriteLine($"this.{@class} = new {@class}Functions(this);"); + + writer.Outdent().WriteLine("}"); writer.Outdent().WriteLine("}"); return writer.ToString(); } - private static void GenerateCSharpRouteClasses(IList routes, CodeWriter writer) + private static void GenerateCSharpRouteClasses(IList routes, CodeWriter writer, out List routeClasses) { var groups = new Dictionary>(); @@ -46,11 +50,15 @@ namespace MontoyaTech.Rest.Net foreach (var route in routes) { //Get the method that this route is tied to. - var methodInfo = route.Target.GetMethodInfo(); + var methodInfo = route.GetTarget().GetMethodInfo(); //See if this method defines the group for us. 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); @@ -59,10 +67,51 @@ namespace MontoyaTech.Rest.Net else groups.Add(group, new List() { route }); } + + //Generate classes for these groups + foreach (var group in groups) + GenerateCSharpRouteClass(group.Key, group.Value, writer); + + //Set the route classes + routeClasses = new List(); + routeClasses.AddRange(groups.Select(group => group.Key)); } private static void GenerateCSharpRouteClass(string name, List routes, CodeWriter writer) { + writer.WriteBreak(); + + writer.WriteLine($"public {name}Functions {name};"); + + writer.WriteBreak(); + + writer.WriteLine($"public class {name}Functions").WriteLine("{").Indent(); + + writer.WriteLine("public Client Client;"); + + writer.WriteBreak(); + + writer.WriteLine($"public {name}Functions(Client client)").WriteLine("{").Indent(); + + writer.WriteLine("this.Client = client;"); + + writer.Outdent().WriteLine("}"); + + foreach (var route in routes) + GenerateCSharpRouteFunction(route, writer); + + writer.Outdent().WriteLine("}"); + } + + private static void GenerateCSharpRouteFunction(Route route, CodeWriter writer) + { + writer.WriteBreak(); + + var methodInfo = route.GetTarget().GetMethodInfo(); + + writer.WriteLine($"public void {methodInfo.Name}()").WriteLine("{").Indent(); + + writer.Outdent().WriteLine("}"); } /// diff --git a/Rest.Net/Route.cs b/Rest.Net/Route.cs index 2484fde..304442a 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. /// - internal Func Target; + private Func Target; /// /// Whether or not to close the response after the route is invoked. @@ -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 index 3bee727..bb8ee78 100644 --- a/Rest.Net/RouteGroup.cs +++ b/Rest.Net/RouteGroup.cs @@ -9,7 +9,7 @@ namespace MontoyaTech.Rest.Net /// /// The outline of an attribute that controls what group a route belongs to. /// - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] public class RouteGroup : Attribute { /// 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; + } + } +}