From 446247a549a5eb3ba0e349552d3bbad08f89ecea Mon Sep 17 00:00:00 2001 From: Harald Wolff Date: Sat, 5 Dec 2020 23:07:18 +0100 Subject: [PATCH] Initial Commit --- .gitignore | 46 +++++ ln.http.api.demo/Program.cs | 71 ++++++++ ln.http.api.demo/ln.http.api.demo.csproj | 15 ++ ln.http.api.sln | 48 ++++++ ln.http.api/HttpMethod.cs | 10 ++ ln.http.api/HttpResponseExtensions.cs | 19 +++ ln.http.api/WebApiController.cs | 159 ++++++++++++++++++ ln.http.api/attributes/ArgumentSource.cs | 13 ++ .../attributes/ControllerAttributes.cs | 90 ++++++++++ ln.http.api/ln.http.api.csproj | 21 +++ 10 files changed, 492 insertions(+) create mode 100644 .gitignore create mode 100644 ln.http.api.demo/Program.cs create mode 100644 ln.http.api.demo/ln.http.api.demo.csproj create mode 100644 ln.http.api.sln create mode 100644 ln.http.api/HttpMethod.cs create mode 100644 ln.http.api/HttpResponseExtensions.cs create mode 100644 ln.http.api/WebApiController.cs create mode 100644 ln.http.api/attributes/ArgumentSource.cs create mode 100644 ln.http.api/attributes/ControllerAttributes.cs create mode 100644 ln.http.api/ln.http.api.csproj diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..542b474 --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +# Autosave files +*~ + +# build +[Oo]bj/ +[Bb]in/ +packages/ +TestResults/ + +# globs +Makefile.in +*.DS_Store +*.sln.cache +*.suo +*.cache +*.pidb +*.userprefs +*.usertasks +config.log +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.user +*.tar.gz +tarballs/ +test-results/ +Thumbs.db +.vs/ + +# Mac bundle stuff +*.dmg +*.app + +# resharper +*_Resharper.* +*.Resharper + +# dotCover +*.dotCover + +*.log +*.log.old +.vscode +.build \ No newline at end of file diff --git a/ln.http.api.demo/Program.cs b/ln.http.api.demo/Program.cs new file mode 100644 index 0000000..1d3ae30 --- /dev/null +++ b/ln.http.api.demo/Program.cs @@ -0,0 +1,71 @@ +using System.Reflection.Metadata; +using ln.http.api.attributes; +using ln.http.router; +using ln.json.mapping; +using ln.type; + +namespace ln.http.api.demo +{ + class Program + { + static void Main(string[] args) + { + DemoApi demoApi = new DemoApi(); + + SimpleRouter webRouter = new SimpleRouter(); + + webRouter.AddSimpleRoute("/api/v1/*", demoApi); + + HTTPServer httpServer = new HTTPServer(new LoggingRouter(webRouter)); + httpServer.AddEndpoint(new Endpoint(IPv6.ANY, 3434)); + + httpServer.Start(); + } + } + + + class DemoApi : WebApiController + { + + [POST("/helloworld/:n")] + [POST("/helloworld")] + public string GetHelloWorld([FromContent( SourceName = "m")]int n, [FromParameter( SourceName = "n")]int i = -1) + { + return string.Format("Hello World. You gave me a n={0} / i={1}!", n, i); + } + + + [GET("/users/:id")] + public User GetUser(int id) + { + return new User(); + } + + [POST("/users")] + public User CreateUser([FromContent]User user) + { + user.PrimaryGroup += " / or another one"; + return user; + } + + [GET("/users")] + public User[] GetUsers() + { + return new User[]{ + new User(), + new User(), + new User() + }; + } + + } + + class User + { + public string Username { get; set;} = "niclasundharald"; + public string PrimaryGroup { get; set; } = "admins"; + + } + + +} diff --git a/ln.http.api.demo/ln.http.api.demo.csproj b/ln.http.api.demo/ln.http.api.demo.csproj new file mode 100644 index 0000000..e7fd6ad --- /dev/null +++ b/ln.http.api.demo/ln.http.api.demo.csproj @@ -0,0 +1,15 @@ + + + + + + + + + + + Exe + netcoreapp3.1 + + + diff --git a/ln.http.api.sln b/ln.http.api.sln new file mode 100644 index 0000000..1088b74 --- /dev/null +++ b/ln.http.api.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ln.http.api", "ln.http.api\ln.http.api.csproj", "{A8CE1889-2BFC-4EB1-A0E8-52A9CA30C3A2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ln.http.api.demo", "ln.http.api.demo\ln.http.api.demo.csproj", "{DA9FFEBD-1422-4062-94FC-88AEF2CEC3C4}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A8CE1889-2BFC-4EB1-A0E8-52A9CA30C3A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A8CE1889-2BFC-4EB1-A0E8-52A9CA30C3A2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A8CE1889-2BFC-4EB1-A0E8-52A9CA30C3A2}.Debug|x64.ActiveCfg = Debug|Any CPU + {A8CE1889-2BFC-4EB1-A0E8-52A9CA30C3A2}.Debug|x64.Build.0 = Debug|Any CPU + {A8CE1889-2BFC-4EB1-A0E8-52A9CA30C3A2}.Debug|x86.ActiveCfg = Debug|Any CPU + {A8CE1889-2BFC-4EB1-A0E8-52A9CA30C3A2}.Debug|x86.Build.0 = Debug|Any CPU + {A8CE1889-2BFC-4EB1-A0E8-52A9CA30C3A2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A8CE1889-2BFC-4EB1-A0E8-52A9CA30C3A2}.Release|Any CPU.Build.0 = Release|Any CPU + {A8CE1889-2BFC-4EB1-A0E8-52A9CA30C3A2}.Release|x64.ActiveCfg = Release|Any CPU + {A8CE1889-2BFC-4EB1-A0E8-52A9CA30C3A2}.Release|x64.Build.0 = Release|Any CPU + {A8CE1889-2BFC-4EB1-A0E8-52A9CA30C3A2}.Release|x86.ActiveCfg = Release|Any CPU + {A8CE1889-2BFC-4EB1-A0E8-52A9CA30C3A2}.Release|x86.Build.0 = Release|Any CPU + {DA9FFEBD-1422-4062-94FC-88AEF2CEC3C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DA9FFEBD-1422-4062-94FC-88AEF2CEC3C4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DA9FFEBD-1422-4062-94FC-88AEF2CEC3C4}.Debug|x64.ActiveCfg = Debug|Any CPU + {DA9FFEBD-1422-4062-94FC-88AEF2CEC3C4}.Debug|x64.Build.0 = Debug|Any CPU + {DA9FFEBD-1422-4062-94FC-88AEF2CEC3C4}.Debug|x86.ActiveCfg = Debug|Any CPU + {DA9FFEBD-1422-4062-94FC-88AEF2CEC3C4}.Debug|x86.Build.0 = Debug|Any CPU + {DA9FFEBD-1422-4062-94FC-88AEF2CEC3C4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DA9FFEBD-1422-4062-94FC-88AEF2CEC3C4}.Release|Any CPU.Build.0 = Release|Any CPU + {DA9FFEBD-1422-4062-94FC-88AEF2CEC3C4}.Release|x64.ActiveCfg = Release|Any CPU + {DA9FFEBD-1422-4062-94FC-88AEF2CEC3C4}.Release|x64.Build.0 = Release|Any CPU + {DA9FFEBD-1422-4062-94FC-88AEF2CEC3C4}.Release|x86.ActiveCfg = Release|Any CPU + {DA9FFEBD-1422-4062-94FC-88AEF2CEC3C4}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/ln.http.api/HttpMethod.cs b/ln.http.api/HttpMethod.cs new file mode 100644 index 0000000..ba16c92 --- /dev/null +++ b/ln.http.api/HttpMethod.cs @@ -0,0 +1,10 @@ + + +namespace ln.http.api +{ + + public enum HttpMethod { + HEAD, GET, POST, PUT, PATCH, DELETE, CONNECT, OPTIONS, TRACE + } + +} \ No newline at end of file diff --git a/ln.http.api/HttpResponseExtensions.cs b/ln.http.api/HttpResponseExtensions.cs new file mode 100644 index 0000000..9c4a853 --- /dev/null +++ b/ln.http.api/HttpResponseExtensions.cs @@ -0,0 +1,19 @@ +using ln.json; + +namespace ln.http.api +{ + + public static class HttpResponseExtensions + { + + public static HttpResponse Content(this HttpResponse response, JSONValue json) + { + response.SetHeader("Content-type", "application/json"); + response.ContentWriter.Write(json.ToString()); + response.ContentWriter.Flush(); + return response; + } + + } + +} diff --git a/ln.http.api/WebApiController.cs b/ln.http.api/WebApiController.cs new file mode 100644 index 0000000..36d5e31 --- /dev/null +++ b/ln.http.api/WebApiController.cs @@ -0,0 +1,159 @@ + + +using System; +using System.Collections.Generic; +using System.Reflection; +using ln.http.api.attributes; +using ln.http.exceptions; +using ln.http.router; +using ln.json; +using ln.json.mapping; + +namespace ln.http.api +{ + + public abstract class WebApiController : SimpleRouter + { + Dictionary methodCaches = new Dictionary(); + + public WebApiController() + { + Initialize(); + } + + void Initialize() + { + foreach (MethodInfo methodInfo in GetType().GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) + { + foreach (EndpointAttribute endpointAttribute in methodInfo.GetCustomAttributes()) + { + if (endpointAttribute != null) + { + EndpointMethodCache methodCache = GetOrCreateMethodCache(endpointAttribute.Route); + methodCache.Add(endpointAttribute.Method, methodInfo); + } + } + } + } + + HttpResponse MapRequest(HttpRequest request, MethodInfo methodInfo) + { + MapArguments(request, methodInfo, out object[] arguments); + + object result = methodInfo.Invoke(this, arguments); + + if (!methodInfo.ReturnType.Equals(typeof(HttpResponse))) + { + if (!typeof(JSONValue).IsAssignableFrom(methodInfo.ReturnType)) + { + if (!JSONMapper.DefaultMapper.Serialize(result, out JSONValue json)) + return HttpResponse.InternalServerError().Content("Method result could not be serialized"); + + result = json; + } + return HttpResponse.OK().Content((JSONValue)result); + } + + return HttpResponse.OK().Content(result.ToString()); + } + + void MapArguments(HttpRequest request, MethodInfo methodInfo,out object[] arguments) + { + ParameterInfo[] parameterInfos = methodInfo.GetParameters(); + arguments = new object[parameterInfos.Length]; + + JSONValue jsonContent = null; + + if (request.GetRequestHeader("content-type", "").Equals("application/json")) + { + jsonContent = JSONParser.Parse(request.ContentReader.ReadToEnd()); + } + + for (int n=0;n() ?? ArgumentSourceAttribute.Default; + + if (!FindArgumentByName(sourceAttribute, request, jsonContent, parameterInfo.Name, out object value)) + { + if (parameterInfo.HasDefaultValue) + { + arguments[n] = parameterInfo.DefaultValue; + } else + throw new BadRequestException(); + } else { + try + { + if (value is JSONValue jsonValue) + { + if (!JSONMapper.DefaultMapper.Deserialize(jsonValue, parameterInfo.ParameterType, out arguments[n])) + throw new BadRequestException(); + } else + { + arguments[n] = Convert.ChangeType(value, parameterInfo.ParameterType); + } + } catch (FormatException) + { + throw new BadRequestException(); + } + } + } + } + + bool FindArgumentByName(ArgumentSourceAttribute sourceAttribute, HttpRequest request, JSONValue jsonContent, string parameterName, out object value) + { + parameterName = sourceAttribute.SourceName ?? parameterName; + + if (((sourceAttribute.Source & ArgumentSource.PARAMETER) == ArgumentSource.PARAMETER) && request.ContainsParameter(parameterName)) + { + value = request.GetParameter(parameterName); + return true; + } else if (((sourceAttribute.Source & ArgumentSource.CONTENT) == ArgumentSource.CONTENT) && (jsonContent is JSONObject jsonObject) && (jsonObject.ContainsKey(parameterName))) + { + value = jsonObject[parameterName]; + return true; + } + + value = null; + return false; + } + + + EndpointMethodCache GetOrCreateMethodCache(string route) + { + if (!methodCaches.TryGetValue(route, out EndpointMethodCache methodCache)) + { + methodCache = new EndpointMethodCache(); + methodCaches.Add(route, methodCache); + AddSimpleRoute(route, (request) => { + MethodInfo methodInfo = methodCache[Enum.Parse(request.Method)]; + return MapRequest(request, methodInfo); + }); + } + return methodCache; + } + + + + + class EndpointMethodCache + { + Dictionary cache = new Dictionary(); + public EndpointMethodCache() + { + } + + public MethodInfo this[HttpMethod httpMethod] + { + get => cache[httpMethod]; + set => cache[httpMethod] = value; + } + + public void Add(HttpMethod httpMethod, MethodInfo methodInfo) => cache.Add(httpMethod, methodInfo); + public void Remove(HttpMethod httpMethod, MethodInfo methodInfo) => cache.Remove(httpMethod); + + } + + } + +} \ No newline at end of file diff --git a/ln.http.api/attributes/ArgumentSource.cs b/ln.http.api/attributes/ArgumentSource.cs new file mode 100644 index 0000000..b0b3379 --- /dev/null +++ b/ln.http.api/attributes/ArgumentSource.cs @@ -0,0 +1,13 @@ + + +using System; + +namespace ln.http.api.attributes +{ + [Flags] + public enum ArgumentSource : int{ + AUTO = -1, + CONTENT = (1<<0), + PARAMETER = (1<<1) + } +} \ No newline at end of file diff --git a/ln.http.api/attributes/ControllerAttributes.cs b/ln.http.api/attributes/ControllerAttributes.cs new file mode 100644 index 0000000..acccc15 --- /dev/null +++ b/ln.http.api/attributes/ControllerAttributes.cs @@ -0,0 +1,90 @@ + +using System; + +namespace ln.http.api.attributes +{ + + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + public abstract class EndpointAttribute : Attribute + { + public string Route { get; set; } + public HttpMethod Method { get; set; } + + public EndpointAttribute(HttpMethod method, string route) + { + Method = method; + Route = route; + } + + } + + public class HEADAttribute : EndpointAttribute + { + public HEADAttribute(string route) : base(HttpMethod.HEAD, route){} + } + public class GETAttribute : EndpointAttribute + { + public GETAttribute(string route) : base(HttpMethod.GET, route){} + } + public class POSTAttribute : EndpointAttribute + { + public POSTAttribute(string route) : base(HttpMethod.POST, route){} + } + public class PUTAttribute : EndpointAttribute + { + public PUTAttribute(string route) : base(HttpMethod.PUT, route){} + } + public class PATCHAttribute : EndpointAttribute + { + public PATCHAttribute(string route) : base(HttpMethod.PATCH, route){} + } + public class DELETEAttribute : EndpointAttribute + { + public DELETEAttribute(string route) : base(HttpMethod.DELETE, route){} + } + public class CONNECTAttribute : EndpointAttribute + { + public CONNECTAttribute(string route) : base(HttpMethod.CONNECT, route){} + } + public class OPTIONSAttribute : EndpointAttribute + { + public OPTIONSAttribute(string route) : base(HttpMethod.OPTIONS, route){} + } + public class TRACEAttribute : EndpointAttribute + { + public TRACEAttribute(string route) : base(HttpMethod.TRACE, route){} + } + + + public abstract class ArgumentSourceAttribute : Attribute + { + public static readonly ArgumentSourceAttribute Default = new DefaultArgumentSourceAttribute(); + public string SourceName { get; set; } + public ArgumentSource Source { get; } + + public ArgumentSourceAttribute(ArgumentSource source) + { + Source = source; + } + + class DefaultArgumentSourceAttribute : ArgumentSourceAttribute + { + public DefaultArgumentSourceAttribute() :base(ArgumentSource.AUTO) {} + } + } + + [AttributeUsage(AttributeTargets.Parameter)] + public class FromParameterAttribute : ArgumentSourceAttribute + { + public FromParameterAttribute() :base(ArgumentSource.PARAMETER) {} + } + + [AttributeUsage(AttributeTargets.Parameter)] + public class FromContentAttribute : ArgumentSourceAttribute + { + public FromContentAttribute() :base(ArgumentSource.CONTENT) {} + } + + + +} \ No newline at end of file diff --git a/ln.http.api/ln.http.api.csproj b/ln.http.api/ln.http.api.csproj new file mode 100644 index 0000000..f17fa97 --- /dev/null +++ b/ln.http.api/ln.http.api.csproj @@ -0,0 +1,21 @@ + + + + netcoreapp3.1 + + true + 0.0.1-test + Harald Wolff-Thobaben + l--n.de + Framework to create REST like APIs + (c) 2020 Harald Wolff-Thobaben + + + + + + + + + +