Initial Commit

master
Harald Wolff 2020-12-05 23:07:18 +01:00
commit 446247a549
10 changed files with 492 additions and 0 deletions

46
.gitignore vendored 100644
View File

@ -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

View File

@ -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";
}
}

View File

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\ln.http.api\ln.http.api.csproj" />
<!--PackageReference Include="ln.http" Version="0.2.1-test" /-->
<ProjectReference Include="..\..\ln.http\ln.http\ln.http.csproj" />
</ItemGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
</Project>

48
ln.http.api.sln 100644
View File

@ -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

View File

@ -0,0 +1,10 @@
namespace ln.http.api
{
public enum HttpMethod {
HEAD, GET, POST, PUT, PATCH, DELETE, CONNECT, OPTIONS, TRACE
}
}

View File

@ -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;
}
}
}

View File

@ -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<string,EndpointMethodCache> methodCaches = new Dictionary<string, EndpointMethodCache>();
public WebApiController()
{
Initialize();
}
void Initialize()
{
foreach (MethodInfo methodInfo in GetType().GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
{
foreach (EndpointAttribute endpointAttribute in methodInfo.GetCustomAttributes<EndpointAttribute>())
{
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<arguments.Length;n++)
{
ParameterInfo parameterInfo = parameterInfos[n];
ArgumentSourceAttribute sourceAttribute = parameterInfo.GetCustomAttribute<ArgumentSourceAttribute>() ?? 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<HttpMethod>(request.Method)];
return MapRequest(request, methodInfo);
});
}
return methodCache;
}
class EndpointMethodCache
{
Dictionary<HttpMethod,MethodInfo> cache = new Dictionary<HttpMethod, MethodInfo>();
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);
}
}
}

View File

@ -0,0 +1,13 @@
using System;
namespace ln.http.api.attributes
{
[Flags]
public enum ArgumentSource : int{
AUTO = -1,
CONTENT = (1<<0),
PARAMETER = (1<<1)
}
}

View File

@ -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) {}
}
}

View File

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Version>0.0.1-test</Version>
<Authors>Harald Wolff-Thobaben</Authors>
<Company>l--n.de</Company>
<Description>Framework to create REST like APIs</Description>
<Copyright>(c) 2020 Harald Wolff-Thobaben</Copyright>
<PackageTags></PackageTags>
</PropertyGroup>
<ItemGroup>
<!--PackageReference Include="ln.http" Version="0.2.1-test" /-->
<PackageReference Include="ln.json" Version="1.0.0" />
<ProjectReference Include="..\..\ln.http\ln.http\ln.http.csproj" />
</ItemGroup>
</Project>