finish implementing path parameters
This commit is contained in:
@@ -2,23 +2,46 @@
|
||||
using System.Reflection;
|
||||
|
||||
namespace SimpleHttpServer.Types;
|
||||
internal readonly struct EndpointInvocationInfo {
|
||||
internal record struct QueryParameterInfo(string Name, Type Type, bool IsOptional);
|
||||
internal record EndpointInvocationInfo {
|
||||
//internal record struct QueryParameterInfo(string Name, Type Type, bool isPathParam, bool Path_isCatchAll, bool Query_IsOptional) {
|
||||
// public static QueryParameterInfo CreatePathParam(string name, Type type) => new(name, type, false, name == "$*", false);
|
||||
// public static QueryParameterInfo CreateQueryParam(string name, Type type, bool isOptional) => new(name, type, false, false, isOptional);
|
||||
//}
|
||||
internal record struct PathParameterInfo(string Name, Type Type, int ArgPos, int SegmentStartPos, bool IsCatchAll) {
|
||||
public PathParameterInfo(string name, Type type, int argPos) : this(name, type, argPos, -1, name == "$*") { }
|
||||
}
|
||||
|
||||
internal record struct QueryParameterInfo(string Name, Type Type, int ArgPos, bool IsOptional);
|
||||
|
||||
internal readonly MethodInfo methodInfo;
|
||||
internal readonly List<QueryParameterInfo> queryParameters;
|
||||
internal readonly List<PathParameterInfo> pathParameters;
|
||||
internal readonly InternalEndpointCheckAttribute[] requiredChecks;
|
||||
/// <summary>
|
||||
/// a reference to the object in which this method is defined (or null if the class is static)
|
||||
/// </summary>
|
||||
internal readonly object? typeInstanceReference;
|
||||
|
||||
public EndpointInvocationInfo(MethodInfo methodInfo, List<QueryParameterInfo> queryParameters, InternalEndpointCheckAttribute[] requiredChecks, object? typeInstanceReference) {
|
||||
public EndpointInvocationInfo(MethodInfo methodInfo, List<PathParameterInfo> pathParameters, List<QueryParameterInfo> queryParameters, InternalEndpointCheckAttribute[] requiredChecks,
|
||||
object? typeInstanceReference) {
|
||||
|
||||
this.methodInfo = methodInfo ?? throw new ArgumentNullException(nameof(methodInfo));
|
||||
this.queryParameters = queryParameters ?? throw new ArgumentNullException(nameof(queryParameters));
|
||||
this.pathParameters = pathParameters ?? throw new ArgumentNullException(nameof(pathParameters));
|
||||
this.requiredChecks = requiredChecks;
|
||||
this.typeInstanceReference = typeInstanceReference;
|
||||
|
||||
if (pathParameters.Any()) {
|
||||
Assert(pathParameters.Count(x => x.IsCatchAll) <= 1); // at most one catchall parameter
|
||||
var argPoses = pathParameters.Select(x => x.ArgPos).Concat(queryParameters.Select(x => x.ArgPos)).ToArray();
|
||||
var argCnt = pathParameters.Count + queryParameters.Count;
|
||||
Assert(argPoses.Distinct().Count() == argCnt); // ArgPoses must be unique
|
||||
Assert(argPoses.Min() == HttpServer.expectedEndpointParameterPrefixCount); // ArgPoses must start from just after the prefix
|
||||
Assert(argPoses.Max() == HttpServer.expectedEndpointParameterPrefixCount + argCnt - 1); // ArgPoses must be contiguous
|
||||
|
||||
Assert(pathParameters.All(x => x.SegmentStartPos != -1));
|
||||
}
|
||||
}
|
||||
|
||||
public readonly bool CheckAll(HttpListenerRequest req) => requiredChecks.All(x => x.Check(req));
|
||||
public bool CheckAll(HttpListenerRequest req) => requiredChecks.All(x => x.Check(req));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace SimpleHttpServer.Types;
|
||||
internal class MultiKeyDictionary<K1, K2, V> where K1 : notnull where K2 : notnull {
|
||||
internal readonly Dictionary<K1, Dictionary<K2, V>> backingDict = new();
|
||||
public MultiKeyDictionary() { }
|
||||
|
||||
public void Add(K1 k1, K2 k2, V value) {
|
||||
if (!backingDict.TryGetValue(k1, out var d2))
|
||||
d2 = new();
|
||||
d2.Add(k2, value);
|
||||
backingDict[k1] = d2;
|
||||
}
|
||||
|
||||
public bool TryGetValue(K1 k1, K2 k2, [MaybeNullWhen(false)] out V value) {
|
||||
if (backingDict.TryGetValue(k1, out var d2) && d2.TryGetValue(k2, out value))
|
||||
return true;
|
||||
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,10 @@ public sealed class PathParameterAttribute : Attribute {
|
||||
throw new ArgumentException($"'{nameof(name)}' must not contain spaces.", nameof(name));
|
||||
}
|
||||
|
||||
if (!uint.TryParse(name[1..], out _) && name != "$*") {
|
||||
throw new ArgumentException($"'{nameof(name)}' must only consist of spaces or be exactly '$*'.", nameof(name));
|
||||
}
|
||||
|
||||
Name = name;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
using System.Data;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace SimpleHttpServer.Types;
|
||||
|
||||
internal class PathTree<T> where T : class {
|
||||
private readonly Node? rootNode = null;
|
||||
|
||||
public PathTree() : this(new()) { }
|
||||
public PathTree(Dictionary<string, T> dict) {
|
||||
if (dict == null || dict.Count == 0)
|
||||
return;
|
||||
|
||||
rootNode = new();
|
||||
var currNode = rootNode;
|
||||
var unpackedPaths = dict.Keys.Select(p => p.Split('/').ToArray()).ToArray();
|
||||
var unpackedLeafData = dict.Values.ToArray();
|
||||
for (int i = 0; i < unpackedPaths.Length; i++) {
|
||||
var path = unpackedPaths[i];
|
||||
var catchallidx = Array.IndexOf(path, "$*");
|
||||
if (catchallidx != -1 && catchallidx != path.Length - 1) {
|
||||
throw new Exception($"Found illegal catchall-wildcard in path: '{string.Join('/', path)}'");
|
||||
}
|
||||
|
||||
var leafdata = unpackedLeafData[i];
|
||||
rootNode.AddSuccessor(path, leafdata);
|
||||
}
|
||||
}
|
||||
|
||||
internal bool TryGetPath(string reqPath, [MaybeNullWhen(false)] out T? endpoint) {
|
||||
if (rootNode == null) {
|
||||
endpoint = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
// try to find path-match
|
||||
Node currNode = rootNode;
|
||||
Assert(reqPath[0] == '/');
|
||||
var splittedPath = reqPath[1..].Split("/");
|
||||
Node? lastCatchallNode = null;
|
||||
for (int i = 0; i < splittedPath.Length; i++) {
|
||||
|
||||
// keep track of the current best catchallNode
|
||||
if (currNode.catchAllNext != null) {
|
||||
lastCatchallNode = currNode.catchAllNext;
|
||||
}
|
||||
|
||||
var seg = splittedPath[i];
|
||||
if (currNode.next?.TryGetValue(seg, out var next) == true) { // look for an explicit path to follow greedily
|
||||
currNode = next;
|
||||
} else if (currNode.pathWildcardNext != null) { // otherwise look for a single-wildcard to follow
|
||||
currNode = currNode.pathWildcardNext;
|
||||
} else { // otherwise we are done, there is no valid path --> fall back to the most specific catchall
|
||||
endpoint = lastCatchallNode?.leafData;
|
||||
return lastCatchallNode != null;
|
||||
}
|
||||
}
|
||||
|
||||
// return found path
|
||||
endpoint = currNode.leafData;
|
||||
return true;
|
||||
}
|
||||
|
||||
private class Node {
|
||||
public T? leafData = null;
|
||||
public Dictionary<string, Node>? next = null;
|
||||
public Node? pathWildcardNext = null; // path wildcard
|
||||
public Node? catchAllNext = null; // trailing-catchall wildcard
|
||||
|
||||
public void AddSuccessor(string[] segments, T? newLeafData) {
|
||||
if (segments.Length == 0) { // actually add the data to this node
|
||||
Assert(leafData == null);
|
||||
leafData = newLeafData;
|
||||
return;
|
||||
}
|
||||
|
||||
var seg = segments[0];
|
||||
bool newIsWildcard = seg.Length > 1 && seg[0] == '$';
|
||||
if (newIsWildcard) {
|
||||
bool newIsCatchallWildcard = newIsWildcard && seg.Length == 2 && seg[1] == '*';
|
||||
if (newIsCatchallWildcard) { // this is a catchall wildcard
|
||||
Assert(catchAllNext == null);
|
||||
catchAllNext = new();
|
||||
catchAllNext.AddSuccessor(segments[1..], newLeafData);
|
||||
return;
|
||||
} else { // must be single wildcard otherwise
|
||||
Assert(pathWildcardNext == null);
|
||||
pathWildcardNext = new();
|
||||
pathWildcardNext.AddSuccessor(segments[1..], newLeafData);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// otherwise we want to add a new constant path successor
|
||||
if (next == null) {
|
||||
next = new();
|
||||
}
|
||||
|
||||
if (next.TryGetValue(seg, out var existingNode)) {
|
||||
existingNode.AddSuccessor(segments[1..], newLeafData);
|
||||
} else {
|
||||
var newNode = next[seg] = new();
|
||||
newNode.AddSuccessor(segments[1..], newLeafData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user