Files
Stationeers-RemoteControl/Vendor/EmbedIO-3.5.2/Routing/RouteMatcher.cs

183 lines
7.7 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text.RegularExpressions;
using EmbedIO.Utilities;
using Swan;
namespace EmbedIO.Routing
{
/// <summary>
/// Matches URL paths against a route.
/// </summary>
public sealed class RouteMatcher : IEquatable<RouteMatcher>
{
private static readonly object SyncRoot = new object();
private static readonly Dictionary<(bool, string), RouteMatcher> Cache = new Dictionary<(bool, string), RouteMatcher>();
private readonly Regex _regex;
private RouteMatcher(bool isBaseRoute, string route, string pattern, IReadOnlyList<string> parameterNames)
{
IsBaseRoute = isBaseRoute;
Route = route;
ParameterNames = parameterNames;
_regex = new Regex(pattern, RegexOptions.Compiled | RegexOptions.CultureInvariant);
}
/// <summary>
/// Gets a value indicating whether the <see cref="Route"/> property
/// is a base route.
/// </summary>
public bool IsBaseRoute { get; }
/// <summary>
/// Gets the route this instance matches URL paths against.
/// </summary>
public string Route { get; }
/// <summary>
/// Gets the names of the route's parameters.
/// </summary>
public IReadOnlyList<string> ParameterNames { get; }
/// <summary>
/// Constructs an instance of <see cref="RouteMatcher"/> by parsing the specified route.
/// <para>If the same route was previously parsed and the <see cref="ClearCache"/> method has not been called since,
/// this method obtains an instance from a static cache.</para>
/// </summary>
/// <param name="route">The route to parse.</param>
/// <param name="isBaseRoute"><see langword="true"/> if the route to parse
/// is a base route; otherwise, <see langword="false"/>.</param>
/// <returns>A newly-constructed instance of <see cref="RouteMatcher"/>
/// that will match URL paths against <paramref name="route"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="route"/> is <see langword="null"/>.</exception>
/// <exception cref="FormatException"><paramref name="route"/> is not a valid route.</exception>
/// <seealso cref="TryParse"/>
/// <seealso cref="ClearCache"/>
public static RouteMatcher Parse(string route, bool isBaseRoute)
{
var exception = TryParseInternal(route, isBaseRoute, out var result);
if (exception != null)
throw exception;
return result!;
}
/// <summary>
/// <para>Attempts to obtain an instance of <see cref="RouteMatcher" /> by parsing the specified route.</para>
/// <para>If the same route was previously parsed and the <see cref="ClearCache"/> method has not been called since,
/// this method obtains an instance from a static cache.</para>
/// </summary>
/// <param name="route">The route to parse.</param>
/// <param name="isBaseRoute"><see langword="true"/> if the route to parse
/// is a base route; otherwise, <see langword="false"/>.</param>
/// <param name="result">When this method returns <see langword="true"/>, a newly-constructed instance of <see cref="RouteMatcher" />
/// that will match URL paths against <paramref name="route"/>; otherwise, <see langword="null"/>.
/// This parameter is passed uninitialized.</param>
/// <returns><see langword="true"/> if parsing was successful; otherwise, <see langword="false"/>.</returns>
/// <seealso cref="Parse"/>
/// <seealso cref="ClearCache"/>
public static bool TryParse(string route, bool isBaseRoute, out RouteMatcher? result)
=> TryParseInternal(route, isBaseRoute, out result) == null;
/// <summary>
/// Clears <see cref="RouteMatcher"/>'s internal instance cache.
/// </summary>
/// <seealso cref="Parse"/>
/// <seealso cref="TryParse"/>
public static void ClearCache()
{
lock (SyncRoot)
{
Cache.Clear();
}
}
/// <summary>
/// Returns a hash code for this instance.
/// </summary>
/// <returns>A hash code for this instance, suitable for use in hashing algorithms
/// and data structures like a hash table.</returns>
public override int GetHashCode() => CompositeHashCode.Using(Route, IsBaseRoute);
/// <summary>
/// Determines whether the specified <see cref="object"/> is equal to this instance.
/// </summary>
/// <param name="obj">The <see cref="object"/> to compare with this instance.</param>
/// <returns><see langword="true"/> if <paramref name="obj"/> is equal to this instance;
/// otherwise, <see langword="false"/>.</returns>
public override bool Equals(object? obj) => obj is RouteMatcher other && Equals(other);
/// <summary>
/// Indicates whether this instance is equal to another instance of <see cref="RouteMatcher"/>.
/// </summary>
/// <param name="other">A <see cref="RouteMatcher"/> to compare with this instance.</param>
/// <returns><see langword="true"/> if this instance is equal to <paramref name="other"/>;
/// otherwise, <see langword="false"/>.</returns>
public bool Equals(RouteMatcher? other)
=> other != null
&& other.Route == Route
&& other.IsBaseRoute == IsBaseRoute;
/// <summary>
/// Matches the specified URL path against <see cref="Route"/>
/// and extracts values for the route's parameters.
/// </summary>
/// <param name="path">The URL path to match.</param>
/// <returns>If the match is successful, a <see cref="RouteMatch"/> object;
/// otherwise, <see langword="null"/>.</returns>
public RouteMatch? Match(string path)
{
if (path == null)
return null;
// Optimize for parameterless base routes
if (IsBaseRoute)
{
if (Route.Length == 1)
return RouteMatch.UnsafeFromRoot(path);
if (ParameterNames.Count == 0)
return RouteMatch.UnsafeFromBasePath(Route, path);
}
var match = _regex.Match(path);
if (!match.Success)
return null;
return new RouteMatch(
path,
ParameterNames,
match.Groups.Cast<Group>().Skip(1).Select(g => WebUtility.UrlDecode(g.Value)).ToArray(),
IsBaseRoute ? "/" + path.Substring(match.Groups[0].Length) : null);
}
private static Exception? TryParseInternal(string route, bool isBaseRoute, out RouteMatcher? result)
{
lock (SyncRoot)
{
string? pattern = null;
var parameterNames = new List<string>();
var exception = Routing.Route.ParseInternal(route, isBaseRoute, (_, n, p) => {
parameterNames.AddRange(n);
pattern = p;
});
if (exception != null)
{
result = null;
return exception;
}
route = UrlPath.UnsafeNormalize(route, isBaseRoute);
if (Cache.TryGetValue((isBaseRoute, route), out result))
return null;
result = new RouteMatcher(isBaseRoute, route, pattern!, parameterNames);
Cache.Add((isBaseRoute, route), result);
return null;
}
}
}
}