updating OM of routing, about 10% better perf
This commit is contained in:
parent
85225055b9
commit
179841743e
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
using Microsoft.AspNet.Abstractions;
|
using Microsoft.AspNet.Abstractions;
|
||||||
using Microsoft.AspNet.Routing.Owin;
|
using Microsoft.AspNet.Routing.Owin;
|
||||||
|
using Microsoft.AspNet.Routing.Template;
|
||||||
using Owin;
|
using Owin;
|
||||||
|
|
||||||
namespace RoutingSample
|
namespace RoutingSample
|
||||||
|
|
@ -25,7 +26,7 @@ namespace RoutingSample
|
||||||
var endpoint2 = new HttpContextRouteEndpoint(async (context) => await context.Response.WriteAsync("Hello, World!"));
|
var endpoint2 = new HttpContextRouteEndpoint(async (context) => await context.Response.WriteAsync("Hello, World!"));
|
||||||
|
|
||||||
routes.Add(new PrefixRoute(endpoint1, "api/store"));
|
routes.Add(new PrefixRoute(endpoint1, "api/store"));
|
||||||
routes.Add(new PrefixRoute(endpoint1, "api/checkout"));
|
routes.Add(new TemplateRoute(endpoint1, "api/checkout/{*extra}"));
|
||||||
routes.Add(new PrefixRoute(endpoint2, "hello/world"));
|
routes.Add(new PrefixRoute(endpoint2, "hello/world"));
|
||||||
routes.Add(new PrefixRoute(endpoint1, ""));
|
routes.Add(new PrefixRoute(endpoint1, ""));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Routing {
|
namespace Microsoft.AspNet.Routing {
|
||||||
using System;
|
using System;
|
||||||
using System.Reflection;
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -40,7 +39,7 @@ namespace Microsoft.AspNet.Routing {
|
||||||
internal static global::System.Resources.ResourceManager ResourceManager {
|
internal static global::System.Resources.ResourceManager ResourceManager {
|
||||||
get {
|
get {
|
||||||
if (object.ReferenceEquals(resourceMan, null)) {
|
if (object.ReferenceEquals(resourceMan, null)) {
|
||||||
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNet.Routing.Resources", IntrospectionExtensions.GetTypeInfo(typeof(Resources)).Assembly);
|
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNet.Routing.Resources", typeof(Resources).Assembly);
|
||||||
resourceMan = temp;
|
resourceMan = temp;
|
||||||
}
|
}
|
||||||
return resourceMan;
|
return resourceMan;
|
||||||
|
|
@ -116,7 +115,7 @@ namespace Microsoft.AspNet.Routing {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to There is an incomplete parameter in this path segment: '{0}'. Check that each '{{' character has a matching '}}' character..
|
/// Looks up a localized string similar to There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character..
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static string TemplateRoute_MismatchedParameter {
|
internal static string TemplateRoute_MismatchedParameter {
|
||||||
get {
|
get {
|
||||||
|
|
|
||||||
|
|
@ -136,7 +136,7 @@
|
||||||
<value>The route template cannot start with a '/' or '~' character and it cannot contain a '?' character.</value>
|
<value>The route template cannot start with a '/' or '~' character and it cannot contain a '?' character.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TemplateRoute_MismatchedParameter" xml:space="preserve">
|
<data name="TemplateRoute_MismatchedParameter" xml:space="preserve">
|
||||||
<value>There is an incomplete parameter in this path segment: '{0}'. Check that each '{{' character has a matching '}}' character.</value>
|
<value>There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TemplateRoute_RepeatedParameter" xml:space="preserve">
|
<data name="TemplateRoute_RepeatedParameter" xml:space="preserve">
|
||||||
<value>The route parameter name '{0}' appears more than one time in the route template.</value>
|
<value>The route parameter name '{0}' appears more than one time in the route template.</value>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,301 @@
|
||||||
|
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Diagnostics.Contracts;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Routing.Template
|
||||||
|
{
|
||||||
|
[DebuggerDisplay("{DebuggerToString()}")]
|
||||||
|
public class ParsedTemplate
|
||||||
|
{
|
||||||
|
private const string SeparatorString = "/";
|
||||||
|
private const char SeparatorChar = '/';
|
||||||
|
|
||||||
|
private static readonly char[] Delimiters = new char[] { SeparatorChar };
|
||||||
|
|
||||||
|
public ParsedTemplate(List<TemplateSegment> segments)
|
||||||
|
{
|
||||||
|
if (segments == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException("segments");
|
||||||
|
}
|
||||||
|
|
||||||
|
Segments = segments;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<TemplateSegment> Segments { get; private set; }
|
||||||
|
|
||||||
|
public IDictionary<string, object> Match(string requestPath, IDictionary<string, object> defaults)
|
||||||
|
{
|
||||||
|
var requestSegments = requestPath.Split(Delimiters);
|
||||||
|
|
||||||
|
if (defaults == null)
|
||||||
|
{
|
||||||
|
defaults = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
var values = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
for (int i = 0; i < requestSegments.Length; i++)
|
||||||
|
{
|
||||||
|
var routeSegment = Segments.Count > i ? Segments[i] : null;
|
||||||
|
var requestSegment = requestSegments[i];
|
||||||
|
|
||||||
|
if (routeSegment == null)
|
||||||
|
{
|
||||||
|
// If pathSegment is null, then we're out of route segments. All we can match is the empty
|
||||||
|
// string.
|
||||||
|
if (requestSegment.Length > 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (routeSegment.Parts.Count == 1)
|
||||||
|
{
|
||||||
|
// Optimize for the simple case - the segment is made up for a single part
|
||||||
|
var part = routeSegment.Parts[0];
|
||||||
|
if (part.IsLiteral)
|
||||||
|
{
|
||||||
|
if (!part.Text.Equals(requestSegment, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Contract.Assert(part.IsParameter);
|
||||||
|
|
||||||
|
if (part.IsCatchAll)
|
||||||
|
{
|
||||||
|
var captured = string.Join(SeparatorString, requestSegments, i, requestSegments.Length - i);
|
||||||
|
if (captured.Length > 0)
|
||||||
|
{
|
||||||
|
values.Add(part.Name, captured);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// It's ok for a catch-all to produce a null value
|
||||||
|
object defaultValue;
|
||||||
|
defaults.TryGetValue(part.Name, out defaultValue);
|
||||||
|
|
||||||
|
values.Add(part.Name, defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A catch-all has to be the last part, so we're done.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (requestSegment.Length > 0)
|
||||||
|
{
|
||||||
|
values.Add(part.Name, requestSegment);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
object defaultValue;
|
||||||
|
if (defaults.TryGetValue(part.Name, out defaultValue))
|
||||||
|
{
|
||||||
|
values.Add(part.Name, defaultValue);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// There's no default for this parameter
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!MatchComplexSegment(routeSegment, requestSegment, defaults, values))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = requestSegments.Length; i < Segments.Count; i++)
|
||||||
|
{
|
||||||
|
// We've matched the request path so far, but still have remaining route segments. These need
|
||||||
|
// to be all single-part parameter segments with default values or else they won't match.
|
||||||
|
var routeSegment = Segments[i];
|
||||||
|
if (routeSegment.Parts.Count > 1)
|
||||||
|
{
|
||||||
|
// If it has more than one part it must contain literals, so it can't match.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var part = routeSegment.Parts[0];
|
||||||
|
if (part.IsLiteral)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Contract.Assert(part.IsParameter);
|
||||||
|
|
||||||
|
// It's ok for a catch-all to produce a null value
|
||||||
|
object defaultValue;
|
||||||
|
if (defaults.TryGetValue(part.Name, out defaultValue) || part.IsCatchAll)
|
||||||
|
{
|
||||||
|
values.Add(part.Name, defaultValue);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// There's no default for this (non-catch-all) parameter so it can't match.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy all remaining default values to the route data
|
||||||
|
if (defaults != null)
|
||||||
|
{
|
||||||
|
foreach (var kvp in defaults)
|
||||||
|
{
|
||||||
|
if (!values.ContainsKey(kvp.Key))
|
||||||
|
{
|
||||||
|
values.Add(kvp.Key, kvp.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool MatchComplexSegment(TemplateSegment routeSegment, string requestSegment, IDictionary<string, object> defaults, Dictionary<string, object> values)
|
||||||
|
{
|
||||||
|
Contract.Assert(routeSegment != null);
|
||||||
|
Contract.Assert(routeSegment.Parts.Count > 1);
|
||||||
|
|
||||||
|
// Find last literal segment and get its last index in the string
|
||||||
|
int lastIndex = requestSegment.Length;
|
||||||
|
int indexOfLastSegmentUsed = routeSegment.Parts.Count - 1;
|
||||||
|
|
||||||
|
TemplatePart parameterNeedsValue = null; // Keeps track of a parameter segment that is pending a value
|
||||||
|
TemplatePart lastLiteral = null; // Keeps track of the left-most literal we've encountered
|
||||||
|
|
||||||
|
while (indexOfLastSegmentUsed >= 0)
|
||||||
|
{
|
||||||
|
int newLastIndex = lastIndex;
|
||||||
|
|
||||||
|
var part = routeSegment.Parts[indexOfLastSegmentUsed];
|
||||||
|
if (part.IsParameter)
|
||||||
|
{
|
||||||
|
// Hold on to the parameter so that we can fill it in when we locate the next literal
|
||||||
|
parameterNeedsValue = part;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Contract.Assert(part.IsLiteral);
|
||||||
|
lastLiteral = part;
|
||||||
|
|
||||||
|
int startIndex = lastIndex - 1;
|
||||||
|
// If we have a pending parameter subsegment, we must leave at least one character for that
|
||||||
|
if (parameterNeedsValue != null)
|
||||||
|
{
|
||||||
|
startIndex--;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startIndex < 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int indexOfLiteral = requestSegment.LastIndexOf(part.Text, startIndex, StringComparison.OrdinalIgnoreCase);
|
||||||
|
if (indexOfLiteral == -1)
|
||||||
|
{
|
||||||
|
// If we couldn't find this literal index, this segment cannot match
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the first subsegment is a literal, it must match at the right-most extent of the request URI.
|
||||||
|
// Without this check if your route had "/Foo/" we'd match the request URI "/somethingFoo/".
|
||||||
|
// This check is related to the check we do at the very end of this function.
|
||||||
|
if (indexOfLastSegmentUsed == (routeSegment.Parts.Count - 1))
|
||||||
|
{
|
||||||
|
if ((indexOfLiteral + part.Text.Length) != requestSegment.Length)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newLastIndex = indexOfLiteral;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((parameterNeedsValue != null) && (((lastLiteral != null) && (part.IsLiteral)) || (indexOfLastSegmentUsed == 0)))
|
||||||
|
{
|
||||||
|
// If we have a pending parameter that needs a value, grab that value
|
||||||
|
|
||||||
|
int parameterStartIndex;
|
||||||
|
int parameterTextLength;
|
||||||
|
|
||||||
|
if (lastLiteral == null)
|
||||||
|
{
|
||||||
|
if (indexOfLastSegmentUsed == 0)
|
||||||
|
{
|
||||||
|
parameterStartIndex = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
parameterStartIndex = newLastIndex;
|
||||||
|
Contract.Assert(false, "indexOfLastSegementUsed should always be 0 from the check above");
|
||||||
|
}
|
||||||
|
parameterTextLength = lastIndex;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// If we're getting a value for a parameter that is somewhere in the middle of the segment
|
||||||
|
if ((indexOfLastSegmentUsed == 0) && (part.IsParameter))
|
||||||
|
{
|
||||||
|
parameterStartIndex = 0;
|
||||||
|
parameterTextLength = lastIndex;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
parameterStartIndex = newLastIndex + lastLiteral.Text.Length;
|
||||||
|
parameterTextLength = lastIndex - parameterStartIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string parameterValueString = requestSegment.Substring(parameterStartIndex, parameterTextLength);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(parameterValueString))
|
||||||
|
{
|
||||||
|
// If we're here that means we have a segment that contains multiple sub-segments.
|
||||||
|
// For these segments all parameters must have non-empty values. If the parameter
|
||||||
|
// has an empty value it's not a match.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// If there's a value in the segment for this parameter, use the subsegment value
|
||||||
|
values.Add(parameterNeedsValue.Name, parameterValueString);
|
||||||
|
}
|
||||||
|
|
||||||
|
parameterNeedsValue = null;
|
||||||
|
lastLiteral = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastIndex = newLastIndex;
|
||||||
|
indexOfLastSegmentUsed--;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the last subsegment is a parameter, it's OK that we didn't parse all the way to the left extent of
|
||||||
|
// the string since the parameter will have consumed all the remaining text anyway. If the last subsegment
|
||||||
|
// is a literal then we *must* have consumed the entire text in that literal. Otherwise we end up matching
|
||||||
|
// the route "Foo" to the request URI "somethingFoo". Thus we have to check that we parsed the *entire*
|
||||||
|
// request URI in order for it to be a match.
|
||||||
|
// This check is related to the check we do earlier in this function for LiteralSubsegments.
|
||||||
|
return (lastIndex == 0) || routeSegment.Parts[0].IsParameter;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string DebuggerToString()
|
||||||
|
{
|
||||||
|
return string.Join(SeparatorString, Segments.Select(s => s.DebuggerToString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
|
|
||||||
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Routing.Template
|
|
||||||
{
|
|
||||||
// Represents a segment of a URI that is not a separator. It contains subsegments such as literals and parameters.
|
|
||||||
internal sealed class PathContentSegment : PathSegment
|
|
||||||
{
|
|
||||||
public PathContentSegment(IList<PathSubsegment> subsegments)
|
|
||||||
{
|
|
||||||
Subsegments = subsegments;
|
|
||||||
}
|
|
||||||
|
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Justification = "Not changing original algorithm.")]
|
|
||||||
public bool IsCatchAll
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
// TODO: Verify this is correct. Maybe add an assert.
|
|
||||||
// Performance sensitive
|
|
||||||
// Caching count is faster for IList<T>
|
|
||||||
int subsegmentCount = Subsegments.Count;
|
|
||||||
for (int i = 0; i < subsegmentCount; i++)
|
|
||||||
{
|
|
||||||
PathSubsegment seg = Subsegments[i];
|
|
||||||
PathParameterSubsegment paramterSubSegment = seg as PathParameterSubsegment;
|
|
||||||
if (paramterSubSegment != null && paramterSubSegment.IsCatchAll)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public IList<PathSubsegment> Subsegments { get; private set; }
|
|
||||||
|
|
||||||
#if ROUTE_DEBUGGING
|
|
||||||
public override string LiteralText
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
List<string> s = new List<string>();
|
|
||||||
foreach (PathSubsegment subsegment in Subsegments)
|
|
||||||
{
|
|
||||||
s.Add(subsegment.LiteralText);
|
|
||||||
}
|
|
||||||
return String.Join(String.Empty, s.ToArray());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
List<string> s = new List<string>();
|
|
||||||
foreach (PathSubsegment subsegment in Subsegments)
|
|
||||||
{
|
|
||||||
s.Add(subsegment.ToString());
|
|
||||||
}
|
|
||||||
return "[ " + String.Join(", ", s.ToArray()) + " ]";
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
|
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Routing.Template
|
|
||||||
{
|
|
||||||
// Represents a literal subsegment of a ContentPathSegment
|
|
||||||
internal sealed class PathLiteralSubsegment : PathSubsegment
|
|
||||||
{
|
|
||||||
public PathLiteralSubsegment(string literal)
|
|
||||||
{
|
|
||||||
Literal = literal;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Literal { get; private set; }
|
|
||||||
|
|
||||||
#if ROUTE_DEBUGGING
|
|
||||||
public override string LiteralText
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return Literal;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return "\"" + Literal + "\"";
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
|
|
||||||
|
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Routing.Template
|
|
||||||
{
|
|
||||||
// Represents a parameter subsegment of a ContentPathSegment
|
|
||||||
internal sealed class PathParameterSubsegment : PathSubsegment
|
|
||||||
{
|
|
||||||
public PathParameterSubsegment(string parameterName)
|
|
||||||
{
|
|
||||||
if (parameterName.StartsWith("*", StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
ParameterName = parameterName.Substring(1);
|
|
||||||
IsCatchAll = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ParameterName = parameterName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsCatchAll { get; private set; }
|
|
||||||
|
|
||||||
public string ParameterName { get; private set; }
|
|
||||||
|
|
||||||
#if ROUTE_DEBUGGING
|
|
||||||
public override string LiteralText
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return "{" + (IsCatchAll ? "*" : String.Empty) + ParameterName + "}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return "{" + (IsCatchAll ? "*" : String.Empty) + ParameterName + "}";
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
|
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Routing.Template
|
|
||||||
{
|
|
||||||
// Represents a segment of a URI such as a separator or content
|
|
||||||
public abstract class PathSegment
|
|
||||||
{
|
|
||||||
#if ROUTE_DEBUGGING
|
|
||||||
public abstract string LiteralText
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
|
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Routing.Template
|
|
||||||
{
|
|
||||||
// Represents a "/" separator in a URI
|
|
||||||
internal sealed class PathSeparatorSegment : PathSegment
|
|
||||||
{
|
|
||||||
#if ROUTE_DEBUGGING
|
|
||||||
public override string LiteralText
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return "/";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return "\"/\"";
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
|
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Routing.Template
|
|
||||||
{
|
|
||||||
// Represents a subsegment of a ContentPathSegment such as a parameter or a literal.
|
|
||||||
internal abstract class PathSubsegment
|
|
||||||
{
|
|
||||||
#if ROUTE_DEBUGGING
|
|
||||||
public abstract string LiteralText
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,381 +0,0 @@
|
||||||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics.Contracts;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Routing.Template
|
|
||||||
{
|
|
||||||
public class TemplateParsedRoute
|
|
||||||
{
|
|
||||||
public TemplateParsedRoute(IList<PathSegment> pathSegments)
|
|
||||||
{
|
|
||||||
Contract.Assert(pathSegments != null);
|
|
||||||
PathSegments = pathSegments;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal IList<PathSegment> PathSegments { get; private set; }
|
|
||||||
|
|
||||||
private static bool ForEachParameter(IList<PathSegment> pathSegments, Func<PathParameterSubsegment, bool> action)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < pathSegments.Count; i++)
|
|
||||||
{
|
|
||||||
PathSegment pathSegment = pathSegments[i];
|
|
||||||
|
|
||||||
if (pathSegment is PathSeparatorSegment)
|
|
||||||
{
|
|
||||||
// We only care about parameter subsegments, so skip this
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
PathContentSegment contentPathSegment = pathSegment as PathContentSegment;
|
|
||||||
if (contentPathSegment != null)
|
|
||||||
{
|
|
||||||
foreach (PathSubsegment subsegment in contentPathSegment.Subsegments)
|
|
||||||
{
|
|
||||||
PathLiteralSubsegment literalSubsegment = subsegment as PathLiteralSubsegment;
|
|
||||||
if (literalSubsegment != null)
|
|
||||||
{
|
|
||||||
// We only care about parameter subsegments, so skip this
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
PathParameterSubsegment parameterSubsegment = subsegment as PathParameterSubsegment;
|
|
||||||
if (parameterSubsegment != null)
|
|
||||||
{
|
|
||||||
if (!action(parameterSubsegment))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Contract.Assert(false, "Invalid path subsegment type");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Contract.Assert(false, "Invalid path segment type");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IDictionary<string, object> Match(string virtualPath, IDictionary<string, object> defaultValues)
|
|
||||||
{
|
|
||||||
IList<string> requestPathSegments = TemplateRouteParser.SplitUriToPathSegmentStrings(virtualPath);
|
|
||||||
|
|
||||||
if (defaultValues == null)
|
|
||||||
{
|
|
||||||
defaultValues = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
IDictionary<string, object> matchedValues = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
// This flag gets set once all the data in the URI has been parsed through, but
|
|
||||||
// the route we're trying to match against still has more parts. At this point
|
|
||||||
// we'll only continue matching separator characters and parameters that have
|
|
||||||
// default values.
|
|
||||||
bool ranOutOfStuffToParse = false;
|
|
||||||
|
|
||||||
// This value gets set once we start processing a catchall parameter (if there is one
|
|
||||||
// at all). Once we set this value we consume all remaining parts of the URI into its
|
|
||||||
// parameter value.
|
|
||||||
bool usedCatchAllParameter = false;
|
|
||||||
|
|
||||||
for (int i = 0; i < PathSegments.Count; i++)
|
|
||||||
{
|
|
||||||
PathSegment pathSegment = PathSegments[i];
|
|
||||||
|
|
||||||
if (requestPathSegments.Count <= i)
|
|
||||||
{
|
|
||||||
ranOutOfStuffToParse = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
string requestPathSegment = ranOutOfStuffToParse ? null : requestPathSegments[i];
|
|
||||||
|
|
||||||
if (pathSegment is PathSeparatorSegment)
|
|
||||||
{
|
|
||||||
if (ranOutOfStuffToParse)
|
|
||||||
{
|
|
||||||
// If we're trying to match a separator in the route but there's no more content, that's OK
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (!String.Equals(requestPathSegment, "/", StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
PathContentSegment contentPathSegment = pathSegment as PathContentSegment;
|
|
||||||
if (contentPathSegment != null)
|
|
||||||
{
|
|
||||||
if (contentPathSegment.IsCatchAll)
|
|
||||||
{
|
|
||||||
Contract.Assert(i == (PathSegments.Count - 1), "If we're processing a catch-all, we should be on the last route segment.");
|
|
||||||
MatchCatchAll(contentPathSegment, requestPathSegments.Skip(i), defaultValues, matchedValues);
|
|
||||||
usedCatchAllParameter = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (!MatchContentPathSegment(contentPathSegment, requestPathSegment, defaultValues, matchedValues))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Contract.Assert(false, "Invalid path segment type");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!usedCatchAllParameter)
|
|
||||||
{
|
|
||||||
if (PathSegments.Count < requestPathSegments.Count)
|
|
||||||
{
|
|
||||||
// If we've already gone through all the parts defined in the route but the URI
|
|
||||||
// still contains more content, check that the remaining content is all separators.
|
|
||||||
for (int i = PathSegments.Count; i < requestPathSegments.Count; i++)
|
|
||||||
{
|
|
||||||
if (!TemplateRouteParser.IsSeparator(requestPathSegments[i]))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy all remaining default values to the route data
|
|
||||||
if (defaultValues != null)
|
|
||||||
{
|
|
||||||
foreach (var defaultValue in defaultValues)
|
|
||||||
{
|
|
||||||
if (!matchedValues.ContainsKey(defaultValue.Key))
|
|
||||||
{
|
|
||||||
matchedValues.Add(defaultValue.Key, defaultValue.Value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return matchedValues;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void MatchCatchAll(PathContentSegment contentPathSegment, IEnumerable<string> remainingRequestSegments, IDictionary<string, object> defaultValues, IDictionary<string, object> matchedValues)
|
|
||||||
{
|
|
||||||
string remainingRequest = String.Join(String.Empty, remainingRequestSegments.ToArray());
|
|
||||||
|
|
||||||
PathParameterSubsegment catchAllSegment = contentPathSegment.Subsegments[0] as PathParameterSubsegment;
|
|
||||||
|
|
||||||
object catchAllValue;
|
|
||||||
|
|
||||||
if (remainingRequest.Length > 0)
|
|
||||||
{
|
|
||||||
catchAllValue = remainingRequest;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
defaultValues.TryGetValue(catchAllSegment.ParameterName, out catchAllValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
matchedValues.Add(catchAllSegment.ParameterName, catchAllValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool MatchContentPathSegment(PathContentSegment routeSegment, string requestPathSegment, IDictionary<string, object> defaultValues, IDictionary<string, object> matchedValues)
|
|
||||||
{
|
|
||||||
if (String.IsNullOrEmpty(requestPathSegment))
|
|
||||||
{
|
|
||||||
// If there's no data to parse, we must have exactly one parameter segment and no other segments - otherwise no match
|
|
||||||
|
|
||||||
if (routeSegment.Subsegments.Count > 1)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
PathParameterSubsegment parameterSubsegment = routeSegment.Subsegments[0] as PathParameterSubsegment;
|
|
||||||
if (parameterSubsegment == null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We must have a default value since there's no value in the request URI
|
|
||||||
object parameterValue;
|
|
||||||
if (defaultValues.TryGetValue(parameterSubsegment.ParameterName, out parameterValue))
|
|
||||||
{
|
|
||||||
// If there's a default value for this parameter, use that default value
|
|
||||||
matchedValues.Add(parameterSubsegment.ParameterName, parameterValue);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// If there's no default value, this segment doesn't match
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Optimize for the common case where there is only one subsegment in the segment - either a parameter or a literal
|
|
||||||
if (routeSegment.Subsegments.Count == 1)
|
|
||||||
{
|
|
||||||
return MatchSingleContentPathSegment(routeSegment.Subsegments[0], requestPathSegment, matchedValues);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find last literal segment and get its last index in the string
|
|
||||||
|
|
||||||
int lastIndex = requestPathSegment.Length;
|
|
||||||
int indexOfLastSegmentUsed = routeSegment.Subsegments.Count - 1;
|
|
||||||
|
|
||||||
PathParameterSubsegment parameterNeedsValue = null; // Keeps track of a parameter segment that is pending a value
|
|
||||||
PathLiteralSubsegment lastLiteral = null; // Keeps track of the left-most literal we've encountered
|
|
||||||
|
|
||||||
while (indexOfLastSegmentUsed >= 0)
|
|
||||||
{
|
|
||||||
int newLastIndex = lastIndex;
|
|
||||||
|
|
||||||
PathParameterSubsegment parameterSubsegment = routeSegment.Subsegments[indexOfLastSegmentUsed] as PathParameterSubsegment;
|
|
||||||
if (parameterSubsegment != null)
|
|
||||||
{
|
|
||||||
// Hold on to the parameter so that we can fill it in when we locate the next literal
|
|
||||||
parameterNeedsValue = parameterSubsegment;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
PathLiteralSubsegment literalSubsegment = routeSegment.Subsegments[indexOfLastSegmentUsed] as PathLiteralSubsegment;
|
|
||||||
if (literalSubsegment != null)
|
|
||||||
{
|
|
||||||
lastLiteral = literalSubsegment;
|
|
||||||
|
|
||||||
int startIndex = lastIndex - 1;
|
|
||||||
// If we have a pending parameter subsegment, we must leave at least one character for that
|
|
||||||
if (parameterNeedsValue != null)
|
|
||||||
{
|
|
||||||
startIndex--;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (startIndex < 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
int indexOfLiteral = requestPathSegment.LastIndexOf(literalSubsegment.Literal, startIndex, StringComparison.OrdinalIgnoreCase);
|
|
||||||
if (indexOfLiteral == -1)
|
|
||||||
{
|
|
||||||
// If we couldn't find this literal index, this segment cannot match
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the first subsegment is a literal, it must match at the right-most extent of the request URI.
|
|
||||||
// Without this check if your route had "/Foo/" we'd match the request URI "/somethingFoo/".
|
|
||||||
// This check is related to the check we do at the very end of this function.
|
|
||||||
if (indexOfLastSegmentUsed == (routeSegment.Subsegments.Count - 1))
|
|
||||||
{
|
|
||||||
if ((indexOfLiteral + literalSubsegment.Literal.Length) != requestPathSegment.Length)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
newLastIndex = indexOfLiteral;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Contract.Assert(false, "Invalid path segment type");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((parameterNeedsValue != null) && (((lastLiteral != null) && (parameterSubsegment == null)) || (indexOfLastSegmentUsed == 0)))
|
|
||||||
{
|
|
||||||
// If we have a pending parameter that needs a value, grab that value
|
|
||||||
|
|
||||||
int parameterStartIndex;
|
|
||||||
int parameterTextLength;
|
|
||||||
|
|
||||||
if (lastLiteral == null)
|
|
||||||
{
|
|
||||||
if (indexOfLastSegmentUsed == 0)
|
|
||||||
{
|
|
||||||
parameterStartIndex = 0;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
parameterStartIndex = newLastIndex;
|
|
||||||
Contract.Assert(false, "indexOfLastSegementUsed should always be 0 from the check above");
|
|
||||||
}
|
|
||||||
parameterTextLength = lastIndex;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// If we're getting a value for a parameter that is somewhere in the middle of the segment
|
|
||||||
if ((indexOfLastSegmentUsed == 0) && (parameterSubsegment != null))
|
|
||||||
{
|
|
||||||
parameterStartIndex = 0;
|
|
||||||
parameterTextLength = lastIndex;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
parameterStartIndex = newLastIndex + lastLiteral.Literal.Length;
|
|
||||||
parameterTextLength = lastIndex - parameterStartIndex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
string parameterValueString = requestPathSegment.Substring(parameterStartIndex, parameterTextLength);
|
|
||||||
|
|
||||||
if (String.IsNullOrEmpty(parameterValueString))
|
|
||||||
{
|
|
||||||
// If we're here that means we have a segment that contains multiple sub-segments.
|
|
||||||
// For these segments all parameters must have non-empty values. If the parameter
|
|
||||||
// has an empty value it's not a match.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// If there's a value in the segment for this parameter, use the subsegment value
|
|
||||||
matchedValues.Add(parameterNeedsValue.ParameterName, parameterValueString);
|
|
||||||
}
|
|
||||||
|
|
||||||
parameterNeedsValue = null;
|
|
||||||
lastLiteral = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastIndex = newLastIndex;
|
|
||||||
indexOfLastSegmentUsed--;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the last subsegment is a parameter, it's OK that we didn't parse all the way to the left extent of
|
|
||||||
// the string since the parameter will have consumed all the remaining text anyway. If the last subsegment
|
|
||||||
// is a literal then we *must* have consumed the entire text in that literal. Otherwise we end up matching
|
|
||||||
// the route "Foo" to the request URI "somethingFoo". Thus we have to check that we parsed the *entire*
|
|
||||||
// request URI in order for it to be a match.
|
|
||||||
// This check is related to the check we do earlier in this function for LiteralSubsegments.
|
|
||||||
return (lastIndex == 0) || (routeSegment.Subsegments[0] is PathParameterSubsegment);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool MatchSingleContentPathSegment(PathSubsegment pathSubsegment, string requestPathSegment, IDictionary<string, object> matchedValues)
|
|
||||||
{
|
|
||||||
PathParameterSubsegment parameterSubsegment = pathSubsegment as PathParameterSubsegment;
|
|
||||||
if (parameterSubsegment == null)
|
|
||||||
{
|
|
||||||
// Handle a single literal segment
|
|
||||||
PathLiteralSubsegment literalSubsegment = pathSubsegment as PathLiteralSubsegment;
|
|
||||||
Contract.Assert(literalSubsegment != null, "Invalid path segment type");
|
|
||||||
return literalSubsegment.Literal.Equals(requestPathSegment, StringComparison.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Handle a single parameter segment
|
|
||||||
matchedValues.Add(parameterSubsegment.ParameterName, requestPathSegment);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,404 @@
|
||||||
|
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Diagnostics.Contracts;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Routing.Template
|
||||||
|
{
|
||||||
|
public static class TemplateParser
|
||||||
|
{
|
||||||
|
private const char Separator = '/';
|
||||||
|
private const char OpenBrace = '{';
|
||||||
|
private const char CloseBrace = '}';
|
||||||
|
|
||||||
|
public static ParsedTemplate Parse(string routeTemplate)
|
||||||
|
{
|
||||||
|
if (routeTemplate == null)
|
||||||
|
{
|
||||||
|
routeTemplate = String.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsInvalidRouteTemplate(routeTemplate))
|
||||||
|
{
|
||||||
|
throw new ArgumentException(Resources.TemplateRoute_InvalidRouteTemplate, "routeTemplate");
|
||||||
|
}
|
||||||
|
|
||||||
|
var context = new TemplateParserContext(routeTemplate);
|
||||||
|
var segments = new List<TemplateSegment>();
|
||||||
|
|
||||||
|
while (context.Next())
|
||||||
|
{
|
||||||
|
if (context.Current == Separator)
|
||||||
|
{
|
||||||
|
// If we get here is means that there's a consecutive '/' character. Templates don't start with a '/' and
|
||||||
|
// parsing a segment consumes the separator.
|
||||||
|
throw new ArgumentException(Resources.TemplateRoute_CannotHaveConsecutiveSeparators, "routeTemplate");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!ParseSegment(context, segments))
|
||||||
|
{
|
||||||
|
throw new ArgumentException(context.Error, "routeTemplate");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsAllValid(context, segments))
|
||||||
|
{
|
||||||
|
return new ParsedTemplate(segments);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new ArgumentException(context.Error, "routeTemplate");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ParseSegment(TemplateParserContext context, List<TemplateSegment> segments)
|
||||||
|
{
|
||||||
|
Contract.Assert(context != null);
|
||||||
|
Contract.Assert(segments != null);
|
||||||
|
|
||||||
|
var segment = new TemplateSegment();
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
if (context.Current == OpenBrace)
|
||||||
|
{
|
||||||
|
if (!context.Next())
|
||||||
|
{
|
||||||
|
// This is a dangling open-brace, which is not allowed
|
||||||
|
context.Error = Resources.TemplateRoute_MismatchedParameter;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.Current == OpenBrace)
|
||||||
|
{
|
||||||
|
// This is an 'escaped' brace in a literal, like "{{foo"
|
||||||
|
context.Back();
|
||||||
|
if (!ParseLiteral(context, segment))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// This is the inside of a parameter
|
||||||
|
if (!ParseParameter(context, segment))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (context.Current == Separator)
|
||||||
|
{
|
||||||
|
// We've reached the end of the segment
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!ParseLiteral(context, segment))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!context.Next())
|
||||||
|
{
|
||||||
|
// We've reached the end of the string
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsSegmentValid(context, segment))
|
||||||
|
{
|
||||||
|
segments.Add(segment);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ParseParameter(TemplateParserContext context, TemplateSegment segment)
|
||||||
|
{
|
||||||
|
context.Mark();
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
if (context.Current == Separator)
|
||||||
|
{
|
||||||
|
// This is a dangling open-brace, which is not allowed
|
||||||
|
context.Error = Resources.TemplateRoute_MismatchedParameter;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else if (context.Current == OpenBrace)
|
||||||
|
{
|
||||||
|
// If we see a '{' while parsing a parameter name it's invalid. We'll just accept it for now
|
||||||
|
// and let the validation code for the name find it.
|
||||||
|
}
|
||||||
|
else if (context.Current == CloseBrace)
|
||||||
|
{
|
||||||
|
if (!context.Next())
|
||||||
|
{
|
||||||
|
// This is the end of the string - and we have a valid parameter
|
||||||
|
context.Back();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.Current == CloseBrace)
|
||||||
|
{
|
||||||
|
// This is an 'escaped' brace in a parameter name, which is not allowed. We'll just accept it for now
|
||||||
|
// and let the validation code for the name find it.
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// This is the end of the parameter
|
||||||
|
context.Back();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!context.Next())
|
||||||
|
{
|
||||||
|
// This is a dangling open-brace, which is not allowed
|
||||||
|
context.Error = Resources.TemplateRoute_MismatchedParameter;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var rawName = context.Capture();
|
||||||
|
|
||||||
|
var isCatchAll = rawName.StartsWith("*", StringComparison.Ordinal);
|
||||||
|
var parameterName = isCatchAll ? rawName.Substring(1) : rawName;
|
||||||
|
if (IsValidParameterName(context, parameterName))
|
||||||
|
{
|
||||||
|
segment.Parts.Add(TemplatePart.CreateParameter(parameterName, isCatchAll));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ParseLiteral(TemplateParserContext context, TemplateSegment segment)
|
||||||
|
{
|
||||||
|
context.Mark();
|
||||||
|
|
||||||
|
string encoded;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
if (context.Current == Separator)
|
||||||
|
{
|
||||||
|
encoded = context.Capture();
|
||||||
|
context.Back();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else if (context.Current == OpenBrace)
|
||||||
|
{
|
||||||
|
if (!context.Next())
|
||||||
|
{
|
||||||
|
// This is a dangling open-brace, which is not allowed
|
||||||
|
context.Error = Resources.TemplateRoute_MismatchedParameter;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.Current == OpenBrace)
|
||||||
|
{
|
||||||
|
// This is an 'escaped' brace in a literal, like "{{foo" - keep going.
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// We've just seen the start of a parameter, so back up and return
|
||||||
|
context.Back();
|
||||||
|
encoded = context.Capture();
|
||||||
|
context.Back();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (context.Current == CloseBrace)
|
||||||
|
{
|
||||||
|
if (!context.Next())
|
||||||
|
{
|
||||||
|
// This is a dangling close-brace, which is not allowed
|
||||||
|
context.Error = Resources.TemplateRoute_MismatchedParameter;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.Current == CloseBrace)
|
||||||
|
{
|
||||||
|
// This is an 'escaped' brace in a literal, like "{{foo" - keep going.
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// This is an unbalanced close-brace, which is not allowed
|
||||||
|
context.Error = Resources.TemplateRoute_MismatchedParameter;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!context.Next())
|
||||||
|
{
|
||||||
|
encoded = context.Capture();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var decoded = encoded.Replace("}}", "}").Replace("{{", "}");
|
||||||
|
segment.Parts.Add(TemplatePart.CreateLiteral(decoded));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsAllValid(TemplateParserContext context, List<TemplateSegment> segments)
|
||||||
|
{
|
||||||
|
// A catch-all parameter must be the last part of the last segment
|
||||||
|
for (int i = 0; i < segments.Count; i++)
|
||||||
|
{
|
||||||
|
var segment = segments[i];
|
||||||
|
for (int j = 0; j < segment.Parts.Count; j++)
|
||||||
|
{
|
||||||
|
var part = segment.Parts[j];
|
||||||
|
if (part.IsParameter && part.IsCatchAll && (i != segments.Count - 1 || j != segment.Parts.Count - 1))
|
||||||
|
{
|
||||||
|
context.Error = Resources.TemplateRoute_CatchAllMustBeLast;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsSegmentValid(TemplateParserContext context, TemplateSegment segment)
|
||||||
|
{
|
||||||
|
// If a segment has multiple parts, then it can't contain a catch all.
|
||||||
|
for (int i = 0; i < segment.Parts.Count; i++)
|
||||||
|
{
|
||||||
|
var part = segment.Parts[i];
|
||||||
|
if (part.IsParameter && part.IsCatchAll && segment.Parts.Count > 1)
|
||||||
|
{
|
||||||
|
context.Error = Resources.TemplateRoute_CannotHaveCatchAllInMultiSegment;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A segment cannot containt two consecutive parameters
|
||||||
|
var isLastSegmentParameter = false;
|
||||||
|
for (int i = 0; i < segment.Parts.Count; i++)
|
||||||
|
{
|
||||||
|
var part = segment.Parts[i];
|
||||||
|
if (part.IsParameter && isLastSegmentParameter)
|
||||||
|
{
|
||||||
|
context.Error = Resources.TemplateRoute_CannotHaveConsecutiveParameters;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
isLastSegmentParameter = part.IsParameter;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsValidParameterName(TemplateParserContext context, string parameterName)
|
||||||
|
{
|
||||||
|
if (parameterName.Length == 0)
|
||||||
|
{
|
||||||
|
context.Error = String.Format(CultureInfo.CurrentCulture, Resources.TemplateRoute_InvalidParameterName, parameterName);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < parameterName.Length; i++)
|
||||||
|
{
|
||||||
|
var c = parameterName[i];
|
||||||
|
if (c == '/' || c == '{' || c == '}')
|
||||||
|
{
|
||||||
|
context.Error = String.Format(CultureInfo.CurrentCulture, Resources.TemplateRoute_InvalidParameterName, parameterName);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!context.ParameterNames.Add(parameterName))
|
||||||
|
{
|
||||||
|
context.Error = String.Format(CultureInfo.CurrentCulture, Resources.TemplateRoute_RepeatedParameter, parameterName);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsInvalidRouteTemplate(string routeTemplate)
|
||||||
|
{
|
||||||
|
return routeTemplate.StartsWith("~", StringComparison.Ordinal) ||
|
||||||
|
routeTemplate.StartsWith("/", StringComparison.Ordinal) ||
|
||||||
|
(routeTemplate.IndexOf('?') != -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private class TemplateParserContext
|
||||||
|
{
|
||||||
|
private readonly string _template;
|
||||||
|
private int _index;
|
||||||
|
private int? _mark;
|
||||||
|
|
||||||
|
private HashSet<string> _parameterNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
public TemplateParserContext(string template)
|
||||||
|
{
|
||||||
|
Contract.Assert(template != null);
|
||||||
|
_template = template;
|
||||||
|
|
||||||
|
_index = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public char Current
|
||||||
|
{
|
||||||
|
get { return (_index < _template.Length && _index >= 0) ? _template[_index] : (char)0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Error
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HashSet<string> ParameterNames
|
||||||
|
{
|
||||||
|
get { return _parameterNames; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Back()
|
||||||
|
{
|
||||||
|
return --_index >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Next()
|
||||||
|
{
|
||||||
|
return ++_index < _template.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Mark()
|
||||||
|
{
|
||||||
|
_mark = _index;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Capture()
|
||||||
|
{
|
||||||
|
if (_mark.HasValue)
|
||||||
|
{
|
||||||
|
var value = _template.Substring(_mark.Value, _index - _mark.Value);
|
||||||
|
_mark = null;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Routing.Template
|
||||||
|
{
|
||||||
|
[DebuggerDisplay("{DebuggerToString()}")]
|
||||||
|
public class TemplatePart
|
||||||
|
{
|
||||||
|
public static TemplatePart CreateLiteral(string text)
|
||||||
|
{
|
||||||
|
return new TemplatePart()
|
||||||
|
{
|
||||||
|
IsLiteral = true,
|
||||||
|
Text = text,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TemplatePart CreateParameter(string name, bool isCatchAll)
|
||||||
|
{
|
||||||
|
return new TemplatePart()
|
||||||
|
{
|
||||||
|
IsParameter = true,
|
||||||
|
Name = name,
|
||||||
|
IsCatchAll = isCatchAll,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsCatchAll { get; private set; }
|
||||||
|
public bool IsLiteral { get; private set; }
|
||||||
|
public bool IsParameter { get; private set; }
|
||||||
|
|
||||||
|
public string Name { get; private set; }
|
||||||
|
public string Text { get; private set; }
|
||||||
|
|
||||||
|
internal string DebuggerToString()
|
||||||
|
{
|
||||||
|
if (IsParameter)
|
||||||
|
{
|
||||||
|
return "{" + (IsCatchAll ? "*" : string.Empty) + Name + "}";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return Text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,7 +9,7 @@ namespace Microsoft.AspNet.Routing.Template
|
||||||
{
|
{
|
||||||
private readonly IDictionary<string, object> _defaults;
|
private readonly IDictionary<string, object> _defaults;
|
||||||
private readonly IRouteEndpoint _endpoint;
|
private readonly IRouteEndpoint _endpoint;
|
||||||
private readonly TemplateParsedRoute _parsedRoute;
|
private readonly ParsedTemplate _parsedRoute;
|
||||||
private readonly string _routeTemplate;
|
private readonly string _routeTemplate;
|
||||||
|
|
||||||
public TemplateRoute(IRouteEndpoint endpoint, string routeTemplate)
|
public TemplateRoute(IRouteEndpoint endpoint, string routeTemplate)
|
||||||
|
|
@ -29,7 +29,7 @@ namespace Microsoft.AspNet.Routing.Template
|
||||||
_defaults = defaults ?? new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
_defaults = defaults ?? new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
// The parser will throw for invalid routes.
|
// The parser will throw for invalid routes.
|
||||||
_parsedRoute = TemplateRouteParser.Parse(RouteTemplate);
|
_parsedRoute = TemplateParser.Parse(RouteTemplate);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IDictionary<string, object> Defaults
|
public IDictionary<string, object> Defaults
|
||||||
|
|
|
||||||
|
|
@ -1,373 +0,0 @@
|
||||||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.Diagnostics.Contracts;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Routing.Template
|
|
||||||
{
|
|
||||||
public static class TemplateRouteParser
|
|
||||||
{
|
|
||||||
private static string GetLiteral(string segmentLiteral)
|
|
||||||
{
|
|
||||||
// Scan for errant single { and } and convert double {{ to { and double }} to }
|
|
||||||
|
|
||||||
// First we eliminate all escaped braces and then check if any other braces are remaining
|
|
||||||
string newLiteral = segmentLiteral.Replace("{{", String.Empty).Replace("}}", String.Empty);
|
|
||||||
if (newLiteral.Contains("{") || newLiteral.Contains("}"))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it's a valid format, we unescape the braces
|
|
||||||
return segmentLiteral.Replace("{{", "{").Replace("}}", "}");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int IndexOfFirstOpenParameter(string segment, int startIndex)
|
|
||||||
{
|
|
||||||
// Find the first unescaped open brace
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
startIndex = segment.IndexOf('{', startIndex);
|
|
||||||
if (startIndex == -1)
|
|
||||||
{
|
|
||||||
// If there are no more open braces, stop
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if ((startIndex + 1 == segment.Length) ||
|
|
||||||
((startIndex + 1 < segment.Length) && (segment[startIndex + 1] != '{')))
|
|
||||||
{
|
|
||||||
// If we found an open brace that is followed by a non-open brace, it's
|
|
||||||
// a parameter delimiter.
|
|
||||||
// It's also a delimiter if the open brace is the last character - though
|
|
||||||
// it ends up being being called out as invalid later on.
|
|
||||||
return startIndex;
|
|
||||||
}
|
|
||||||
// Increment by two since we want to skip both the open brace that
|
|
||||||
// we're on as well as the subsequent character since we know for
|
|
||||||
// sure that it is part of an escape sequence.
|
|
||||||
startIndex += 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static bool IsSeparator(string s)
|
|
||||||
{
|
|
||||||
return String.Equals(s, "/", StringComparison.Ordinal);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsValidParameterName(string parameterName)
|
|
||||||
{
|
|
||||||
if (parameterName.Length == 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < parameterName.Length; i++)
|
|
||||||
{
|
|
||||||
char c = parameterName[i];
|
|
||||||
if (c == '/' || c == '{' || c == '}')
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static bool IsInvalidRouteTemplate(string routeTemplate)
|
|
||||||
{
|
|
||||||
return routeTemplate.StartsWith("~", StringComparison.Ordinal) ||
|
|
||||||
routeTemplate.StartsWith("/", StringComparison.Ordinal) ||
|
|
||||||
(routeTemplate.IndexOf('?') != -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static TemplateParsedRoute Parse(string routeTemplate)
|
|
||||||
{
|
|
||||||
if (routeTemplate == null)
|
|
||||||
{
|
|
||||||
routeTemplate = String.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IsInvalidRouteTemplate(routeTemplate))
|
|
||||||
{
|
|
||||||
throw new ArgumentException(Resources.TemplateRoute_InvalidRouteTemplate, "routeTemplate");
|
|
||||||
}
|
|
||||||
|
|
||||||
IList<string> uriParts = SplitUriToPathSegmentStrings(routeTemplate);
|
|
||||||
Exception ex = ValidateUriParts(uriParts);
|
|
||||||
if (ex != null)
|
|
||||||
{
|
|
||||||
throw ex;
|
|
||||||
}
|
|
||||||
|
|
||||||
IList<PathSegment> pathSegments = SplitUriToPathSegments(uriParts);
|
|
||||||
|
|
||||||
Contract.Assert(uriParts.Count == pathSegments.Count, "The number of string segments should be the same as the number of path segments");
|
|
||||||
|
|
||||||
return new TemplateParsedRoute(pathSegments);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly",
|
|
||||||
Justification = "The exceptions are just constructed here, but they are thrown from a method that does have those parameter names.")]
|
|
||||||
private static IList<PathSubsegment> ParseUriSegment(string segment, out Exception exception)
|
|
||||||
{
|
|
||||||
int startIndex = 0;
|
|
||||||
|
|
||||||
List<PathSubsegment> pathSubsegments = new List<PathSubsegment>();
|
|
||||||
|
|
||||||
while (startIndex < segment.Length)
|
|
||||||
{
|
|
||||||
int nextParameterStart = IndexOfFirstOpenParameter(segment, startIndex);
|
|
||||||
if (nextParameterStart == -1)
|
|
||||||
{
|
|
||||||
// If there are no more parameters in the segment, capture the remainder as a literal and stop
|
|
||||||
string lastLiteralPart = GetLiteral(segment.Substring(startIndex));
|
|
||||||
if (lastLiteralPart == null)
|
|
||||||
{
|
|
||||||
exception = new ArgumentException(
|
|
||||||
String.Format(CultureInfo.CurrentCulture, Resources.TemplateRoute_MismatchedParameter, segment),
|
|
||||||
"routeTemplate");
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastLiteralPart.Length > 0)
|
|
||||||
{
|
|
||||||
pathSubsegments.Add(new PathLiteralSubsegment(lastLiteralPart));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
int nextParameterEnd = segment.IndexOf('}', nextParameterStart + 1);
|
|
||||||
if (nextParameterEnd == -1)
|
|
||||||
{
|
|
||||||
exception = new ArgumentException(
|
|
||||||
String.Format(CultureInfo.CurrentCulture, Resources.TemplateRoute_MismatchedParameter, segment),
|
|
||||||
"routeTemplate");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
string literalPart = GetLiteral(segment.Substring(startIndex, nextParameterStart - startIndex));
|
|
||||||
if (literalPart == null)
|
|
||||||
{
|
|
||||||
exception = new ArgumentException(
|
|
||||||
String.Format(CultureInfo.CurrentCulture, Resources.TemplateRoute_MismatchedParameter, segment),
|
|
||||||
"routeTemplate");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (literalPart.Length > 0)
|
|
||||||
{
|
|
||||||
pathSubsegments.Add(new PathLiteralSubsegment(literalPart));
|
|
||||||
}
|
|
||||||
|
|
||||||
string parameterName = segment.Substring(nextParameterStart + 1, nextParameterEnd - nextParameterStart - 1);
|
|
||||||
pathSubsegments.Add(new PathParameterSubsegment(parameterName));
|
|
||||||
|
|
||||||
startIndex = nextParameterEnd + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
exception = null;
|
|
||||||
return pathSubsegments;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IList<PathSegment> SplitUriToPathSegments(IList<string> uriParts)
|
|
||||||
{
|
|
||||||
List<PathSegment> pathSegments = new List<PathSegment>();
|
|
||||||
|
|
||||||
foreach (string pathSegment in uriParts)
|
|
||||||
{
|
|
||||||
bool isCurrentPartSeparator = IsSeparator(pathSegment);
|
|
||||||
if (isCurrentPartSeparator)
|
|
||||||
{
|
|
||||||
pathSegments.Add(new PathSeparatorSegment());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Exception exception;
|
|
||||||
IList<PathSubsegment> subsegments = ParseUriSegment(pathSegment, out exception);
|
|
||||||
Contract.Assert(exception == null, "This only gets called after the path has been validated, so there should never be an exception here");
|
|
||||||
pathSegments.Add(new PathContentSegment(subsegments));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return pathSegments;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static IList<string> SplitUriToPathSegmentStrings(string uri)
|
|
||||||
{
|
|
||||||
List<string> parts = new List<string>();
|
|
||||||
|
|
||||||
if (String.IsNullOrEmpty(uri))
|
|
||||||
{
|
|
||||||
return parts;
|
|
||||||
}
|
|
||||||
|
|
||||||
int currentIndex = 0;
|
|
||||||
|
|
||||||
// Split the incoming URI into individual parts
|
|
||||||
while (currentIndex < uri.Length)
|
|
||||||
{
|
|
||||||
int indexOfNextSeparator = uri.IndexOf('/', currentIndex);
|
|
||||||
if (indexOfNextSeparator == -1)
|
|
||||||
{
|
|
||||||
// If there are no more separators, the rest of the string is the last part
|
|
||||||
string finalPart = uri.Substring(currentIndex);
|
|
||||||
if (finalPart.Length > 0)
|
|
||||||
{
|
|
||||||
parts.Add(finalPart);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
string nextPart = uri.Substring(currentIndex, indexOfNextSeparator - currentIndex);
|
|
||||||
if (nextPart.Length > 0)
|
|
||||||
{
|
|
||||||
parts.Add(nextPart);
|
|
||||||
}
|
|
||||||
|
|
||||||
Contract.Assert(uri[indexOfNextSeparator] == '/', "The separator char itself should always be a '/'.");
|
|
||||||
parts.Add("/");
|
|
||||||
currentIndex = indexOfNextSeparator + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return parts;
|
|
||||||
}
|
|
||||||
|
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Justification = "Not changing original algorithm")]
|
|
||||||
[SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly",
|
|
||||||
Justification = "The exceptions are just constructed here, but they are thrown from a method that does have those parameter names.")]
|
|
||||||
private static Exception ValidateUriParts(IList<string> pathSegments)
|
|
||||||
{
|
|
||||||
Contract.Assert(pathSegments != null, "The value should always come from SplitUri(), and that function should never return null.");
|
|
||||||
|
|
||||||
HashSet<string> usedParameterNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
bool? isPreviousPartSeparator = null;
|
|
||||||
|
|
||||||
bool foundCatchAllParameter = false;
|
|
||||||
|
|
||||||
foreach (string pathSegment in pathSegments)
|
|
||||||
{
|
|
||||||
if (foundCatchAllParameter)
|
|
||||||
{
|
|
||||||
// If we ever start an iteration of the loop and we've already found a
|
|
||||||
// catchall parameter then we have an invalid URI format.
|
|
||||||
return new ArgumentException(Resources.TemplateRoute_CatchAllMustBeLast, "routeTemplate");
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isCurrentPartSeparator;
|
|
||||||
if (isPreviousPartSeparator == null)
|
|
||||||
{
|
|
||||||
// Prime the loop with the first value
|
|
||||||
isPreviousPartSeparator = IsSeparator(pathSegment);
|
|
||||||
isCurrentPartSeparator = isPreviousPartSeparator.Value;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
isCurrentPartSeparator = IsSeparator(pathSegment);
|
|
||||||
|
|
||||||
// If both the previous part and the current part are separators, it's invalid
|
|
||||||
if (isCurrentPartSeparator && isPreviousPartSeparator.Value)
|
|
||||||
{
|
|
||||||
return new ArgumentException(Resources.TemplateRoute_CannotHaveConsecutiveSeparators, "routeTemplate");
|
|
||||||
}
|
|
||||||
|
|
||||||
Contract.Assert(isCurrentPartSeparator != isPreviousPartSeparator.Value, "This assert should only happen if both the current and previous parts are non-separators. This should never happen because consecutive non-separators are always parsed as a single part.");
|
|
||||||
isPreviousPartSeparator = isCurrentPartSeparator;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it's not a separator, parse the segment for parameters and validate it
|
|
||||||
if (!isCurrentPartSeparator)
|
|
||||||
{
|
|
||||||
Exception exception;
|
|
||||||
IList<PathSubsegment> subsegments = ParseUriSegment(pathSegment, out exception);
|
|
||||||
if (exception != null)
|
|
||||||
{
|
|
||||||
return exception;
|
|
||||||
}
|
|
||||||
|
|
||||||
exception = ValidateUriSegment(subsegments, usedParameterNames);
|
|
||||||
if (exception != null)
|
|
||||||
{
|
|
||||||
return exception;
|
|
||||||
}
|
|
||||||
|
|
||||||
foundCatchAllParameter = subsegments.Any<PathSubsegment>(seg => (seg is PathParameterSubsegment) && ((PathParameterSubsegment)seg).IsCatchAll);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
[SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly",
|
|
||||||
Justification = "The exceptions are just constructed here, but they are thrown from a method that does have those parameter names.")]
|
|
||||||
private static Exception ValidateUriSegment(IList<PathSubsegment> pathSubsegments, HashSet<string> usedParameterNames)
|
|
||||||
{
|
|
||||||
bool segmentContainsCatchAll = false;
|
|
||||||
|
|
||||||
Type previousSegmentType = null;
|
|
||||||
|
|
||||||
foreach (PathSubsegment subsegment in pathSubsegments)
|
|
||||||
{
|
|
||||||
if (previousSegmentType != null)
|
|
||||||
{
|
|
||||||
if (previousSegmentType == subsegment.GetType())
|
|
||||||
{
|
|
||||||
return new ArgumentException(Resources.TemplateRoute_CannotHaveConsecutiveParameters, "routeTemplate");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
previousSegmentType = subsegment.GetType();
|
|
||||||
|
|
||||||
PathLiteralSubsegment literalSubsegment = subsegment as PathLiteralSubsegment;
|
|
||||||
if (literalSubsegment != null)
|
|
||||||
{
|
|
||||||
// Nothing to validate for literals - everything is valid
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
PathParameterSubsegment parameterSubsegment = subsegment as PathParameterSubsegment;
|
|
||||||
if (parameterSubsegment != null)
|
|
||||||
{
|
|
||||||
string parameterName = parameterSubsegment.ParameterName;
|
|
||||||
|
|
||||||
if (parameterSubsegment.IsCatchAll)
|
|
||||||
{
|
|
||||||
segmentContainsCatchAll = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for valid characters in the parameter name
|
|
||||||
if (!IsValidParameterName(parameterName))
|
|
||||||
{
|
|
||||||
return new ArgumentException(
|
|
||||||
String.Format(CultureInfo.CurrentCulture, Resources.TemplateRoute_InvalidParameterName, parameterName),
|
|
||||||
"routeTemplate");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (usedParameterNames.Contains(parameterName))
|
|
||||||
{
|
|
||||||
return new ArgumentException(
|
|
||||||
String.Format(CultureInfo.CurrentCulture, Resources.TemplateRoute_RepeatedParameter, parameterName),
|
|
||||||
"routeTemplate");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
usedParameterNames.Add(parameterName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Contract.Assert(false, "Invalid path subsegment type");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (segmentContainsCatchAll && (pathSubsegments.Count != 1))
|
|
||||||
{
|
|
||||||
return new ArgumentException(Resources.TemplateRoute_CannotHaveCatchAllInMultiSegment, "routeTemplate");
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Routing.Template
|
||||||
|
{
|
||||||
|
[DebuggerDisplay("{DebuggerToString()}")]
|
||||||
|
public class TemplateSegment
|
||||||
|
{
|
||||||
|
private readonly List<TemplatePart> _parts = new List<TemplatePart>();
|
||||||
|
|
||||||
|
public List<TemplatePart> Parts
|
||||||
|
{
|
||||||
|
get { return _parts; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal string DebuggerToString()
|
||||||
|
{
|
||||||
|
return string.Join(string.Empty, Parts.Select(p => p.DebuggerToString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,400 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Extensions;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Routing.Template.Tests
|
||||||
|
{
|
||||||
|
public class TemplateRouteParserTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Parse_SingleLiteral()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var template = "cool";
|
||||||
|
|
||||||
|
var expected = new ParsedTemplate(new List<TemplateSegment>());
|
||||||
|
expected.Segments.Add(new TemplateSegment());
|
||||||
|
expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool"));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var actual = TemplateParser.Parse(template);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal<ParsedTemplate>(expected, actual, new TemplateParsedRouteEqualityComparer());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Parse_SingleParameter()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var template = "{p}";
|
||||||
|
|
||||||
|
var expected = new ParsedTemplate(new List<TemplateSegment>());
|
||||||
|
expected.Segments.Add(new TemplateSegment());
|
||||||
|
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p", false));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var actual = TemplateParser.Parse(template);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal<ParsedTemplate>(expected, actual, new TemplateParsedRouteEqualityComparer());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Parse_MultipleLiterals()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var template = "cool/awesome/super";
|
||||||
|
|
||||||
|
var expected = new ParsedTemplate(new List<TemplateSegment>());
|
||||||
|
expected.Segments.Add(new TemplateSegment());
|
||||||
|
expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool"));
|
||||||
|
expected.Segments.Add(new TemplateSegment());
|
||||||
|
expected.Segments[1].Parts.Add(TemplatePart.CreateLiteral("awesome"));
|
||||||
|
expected.Segments.Add(new TemplateSegment());
|
||||||
|
expected.Segments[2].Parts.Add(TemplatePart.CreateLiteral("super"));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var actual = TemplateParser.Parse(template);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal<ParsedTemplate>(expected, actual, new TemplateParsedRouteEqualityComparer());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Parse_MultipleParameters()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var template = "{p1}/{p2}/{*p3}";
|
||||||
|
|
||||||
|
var expected = new ParsedTemplate(new List<TemplateSegment>());
|
||||||
|
expected.Segments.Add(new TemplateSegment());
|
||||||
|
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", false));
|
||||||
|
expected.Segments.Add(new TemplateSegment());
|
||||||
|
expected.Segments[1].Parts.Add(TemplatePart.CreateParameter("p2", false));
|
||||||
|
expected.Segments.Add(new TemplateSegment());
|
||||||
|
expected.Segments[2].Parts.Add(TemplatePart.CreateParameter("p3", true));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var actual = TemplateParser.Parse(template);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal<ParsedTemplate>(expected, actual, new TemplateParsedRouteEqualityComparer());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Parse_ComplexSegment_LP()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var template = "cool-{p1}";
|
||||||
|
|
||||||
|
var expected = new ParsedTemplate(new List<TemplateSegment>());
|
||||||
|
expected.Segments.Add(new TemplateSegment());
|
||||||
|
expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool-"));
|
||||||
|
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", false));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var actual = TemplateParser.Parse(template);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal<ParsedTemplate>(expected, actual, new TemplateParsedRouteEqualityComparer());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Parse_ComplexSegment_PL()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var template = "{p1}-cool";
|
||||||
|
|
||||||
|
var expected = new ParsedTemplate(new List<TemplateSegment>());
|
||||||
|
expected.Segments.Add(new TemplateSegment());
|
||||||
|
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", false));
|
||||||
|
expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool-"));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var actual = TemplateParser.Parse(template);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal<ParsedTemplate>(expected, actual, new TemplateParsedRouteEqualityComparer());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Parse_ComplexSegment_PLP()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var template = "{p1}-cool-{p2}";
|
||||||
|
|
||||||
|
var expected = new ParsedTemplate(new List<TemplateSegment>());
|
||||||
|
expected.Segments.Add(new TemplateSegment());
|
||||||
|
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", false));
|
||||||
|
expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool-"));
|
||||||
|
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2", false));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var actual = TemplateParser.Parse(template);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal<ParsedTemplate>(expected, actual, new TemplateParsedRouteEqualityComparer());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Parse_ComplexSegment_LPL()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var template = "cool-{p1}-awesome";
|
||||||
|
|
||||||
|
var expected = new ParsedTemplate(new List<TemplateSegment>());
|
||||||
|
expected.Segments.Add(new TemplateSegment());
|
||||||
|
expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool-"));
|
||||||
|
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", false));
|
||||||
|
expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("-awesome"));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var actual = TemplateParser.Parse(template);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal<ParsedTemplate>(expected, actual, new TemplateParsedRouteEqualityComparer());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_WithRepeatedParameter()
|
||||||
|
{
|
||||||
|
var ex = Assert.Throws<ArgumentException>(
|
||||||
|
() => TemplateParser.Parse("{Controller}.mvc/{id}/{controller}"),
|
||||||
|
"The route parameter name 'controller' appears more than one time in the route template." + Environment.NewLine + "Parameter name: routeTemplate");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("123{a}abc{")]
|
||||||
|
[InlineData("123{a}abc}")]
|
||||||
|
[InlineData("xyz}123{a}abc}")]
|
||||||
|
[InlineData("{{p1}")]
|
||||||
|
[InlineData("{p1}}")]
|
||||||
|
[InlineData("p1}}p2{")]
|
||||||
|
public void InvalidTemplate_WithMismatchedBraces(string template)
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>(
|
||||||
|
() => TemplateParser.Parse(template),
|
||||||
|
@"There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character." + Environment.NewLine +
|
||||||
|
"Parameter name: routeTemplate");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_CannotHaveCatchAllInMultiSegment()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>(
|
||||||
|
() => TemplateParser.Parse("123{a}abc{*moo}"),
|
||||||
|
"A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter." + Environment.NewLine +
|
||||||
|
"Parameter name: routeTemplate");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_CannotHaveMoreThanOneCatchAll()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>(
|
||||||
|
() => TemplateParser.Parse("{*p1}/{*p2}"),
|
||||||
|
"A catch-all parameter can only appear as the last segment of the route template." + Environment.NewLine +
|
||||||
|
"Parameter name: routeTemplate");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_CannotHaveMoreThanOneCatchAllInMultiSegment()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>(
|
||||||
|
() => TemplateParser.Parse("{*p1}abc{*p2}"),
|
||||||
|
"A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter." + Environment.NewLine +
|
||||||
|
"Parameter name: routeTemplate");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_CannotHaveCatchAllWithNoName()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>(
|
||||||
|
() => TemplateParser.Parse("foo/{*}"),
|
||||||
|
@"The route parameter name '' is invalid. Route parameter names must be non-empty and cannot contain these characters: ""{"", ""}"", ""/"", ""?""" + Environment.NewLine +
|
||||||
|
"Parameter name: routeTemplate");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_CannotHaveConsecutiveOpenBrace()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>(
|
||||||
|
() => TemplateParser.Parse("foo/{{p1}"),
|
||||||
|
"There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character." + Environment.NewLine +
|
||||||
|
"Parameter name: routeTemplate");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_CannotHaveConsecutiveCloseBrace()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>(
|
||||||
|
() => TemplateParser.Parse("foo/{p1}}"),
|
||||||
|
"There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character." + Environment.NewLine +
|
||||||
|
"Parameter name: routeTemplate");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_SameParameterTwiceThrows()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>(
|
||||||
|
() => TemplateParser.Parse("{aaa}/{AAA}"),
|
||||||
|
"The route parameter name 'AAA' appears more than one time in the route template." + Environment.NewLine +
|
||||||
|
"Parameter name: routeTemplate");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_SameParameterTwiceAndOneCatchAllThrows()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>(
|
||||||
|
() => TemplateParser.Parse("{aaa}/{*AAA}"),
|
||||||
|
"The route parameter name 'AAA' appears more than one time in the route template." + Environment.NewLine +
|
||||||
|
"Parameter name: routeTemplate");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_InvalidParameterNameWithCloseBracketThrows()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>(
|
||||||
|
() => TemplateParser.Parse("{a}/{aa}a}/{z}"),
|
||||||
|
"There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character." + Environment.NewLine +
|
||||||
|
"Parameter name: routeTemplate");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_InvalidParameterNameWithOpenBracketThrows()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>(
|
||||||
|
() => TemplateParser.Parse("{a}/{a{aa}/{z}"),
|
||||||
|
@"The route parameter name 'a{aa' is invalid. Route parameter names must be non-empty and cannot contain these characters: ""{"", ""}"", ""/"", ""?""" + Environment.NewLine +
|
||||||
|
"Parameter name: routeTemplate");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_InvalidParameterNameWithEmptyNameThrows()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>(
|
||||||
|
() => TemplateParser.Parse("{a}/{}/{z}"),
|
||||||
|
@"The route parameter name '' is invalid. Route parameter names must be non-empty and cannot contain these characters: ""{"", ""}"", ""/"", ""?""" + Environment.NewLine +
|
||||||
|
"Parameter name: routeTemplate");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_InvalidParameterNameWithQuestionThrows()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>(
|
||||||
|
() => TemplateParser.Parse("{Controller}.mvc/{?}"),
|
||||||
|
"The route template cannot start with a '/' or '~' character and it cannot contain a '?' character." + Environment.NewLine +
|
||||||
|
"Parameter name: routeTemplate");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_ConsecutiveSeparatorsSlashSlashThrows()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>(
|
||||||
|
() => TemplateParser.Parse("{a}//{z}"),
|
||||||
|
"The route template separator character '/' cannot appear consecutively. It must be separated by either a parameter or a literal value." + Environment.NewLine +
|
||||||
|
"Parameter name: routeTemplate");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_WithCatchAllNotAtTheEndThrows()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>(
|
||||||
|
() => TemplateParser.Parse("foo/{p1}/{*p2}/{p3}"),
|
||||||
|
"A catch-all parameter can only appear as the last segment of the route template." + Environment.NewLine +
|
||||||
|
"Parameter name: routeTemplate");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_RepeatedParametersThrows()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>(
|
||||||
|
() => TemplateParser.Parse("foo/aa{p1}{p2}"),
|
||||||
|
"A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string." + Environment.NewLine +
|
||||||
|
"Parameter name: routeTemplate");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_CannotStartWithSlash()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>(
|
||||||
|
() => TemplateParser.Parse("/foo"),
|
||||||
|
"The route template cannot start with a '/' or '~' character and it cannot contain a '?' character." + Environment.NewLine +
|
||||||
|
"Parameter name: routeTemplate");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_CannotStartWithTilde()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>(
|
||||||
|
() => TemplateParser.Parse("~foo"),
|
||||||
|
"The route template cannot start with a '/' or '~' character and it cannot contain a '?' character." + Environment.NewLine +
|
||||||
|
"Parameter name: routeTemplate");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_CannotContainQuestionMark()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>(
|
||||||
|
() => TemplateParser.Parse("foor?bar"),
|
||||||
|
"The route template cannot start with a '/' or '~' character and it cannot contain a '?' character." + Environment.NewLine +
|
||||||
|
"Parameter name: routeTemplate");
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TemplateParsedRouteEqualityComparer : IEqualityComparer<ParsedTemplate>
|
||||||
|
{
|
||||||
|
public bool Equals(ParsedTemplate x, ParsedTemplate y)
|
||||||
|
{
|
||||||
|
if (x == null && y == null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (x == null || y == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (x.Segments.Count != y.Segments.Count)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < x.Segments.Count; i++)
|
||||||
|
{
|
||||||
|
if (x.Segments[i].Parts.Count != y.Segments[i].Parts.Count)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int j = 0; j < x.Segments[i].Parts.Count; j++)
|
||||||
|
{
|
||||||
|
var xPart = x.Segments[i].Parts[j];
|
||||||
|
var yPart = y.Segments[i].Parts[j];
|
||||||
|
|
||||||
|
if (xPart.IsLiteral != yPart.IsLiteral ||
|
||||||
|
xPart.IsParameter != yPart.IsParameter ||
|
||||||
|
xPart.IsCatchAll != yPart.IsCatchAll ||
|
||||||
|
!String.Equals(xPart.Name, yPart.Name, StringComparison.Ordinal) ||
|
||||||
|
!String.Equals(xPart.Name, yPart.Name, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetHashCode(ParsedTemplate obj)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,198 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Xunit;
|
|
||||||
using Xunit.Extensions;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Routing.Template.Tests
|
|
||||||
{
|
|
||||||
public class TemplateRouteParserTests
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public void InvalidTemplate_WithRepeatedParameter()
|
|
||||||
{
|
|
||||||
var ex = Assert.Throws<ArgumentException>(
|
|
||||||
() => TemplateRouteParser.Parse("{Controller}.mvc/{id}/{controller}"),
|
|
||||||
"The route parameter name 'controller' appears more than one time in the route template." + Environment.NewLine + "Parameter name: routeTemplate");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData("123{a}abc{")]
|
|
||||||
[InlineData("123{a}abc}")]
|
|
||||||
[InlineData("xyz}123{a}abc}")]
|
|
||||||
[InlineData("{{p1}")]
|
|
||||||
[InlineData("{p1}}")]
|
|
||||||
[InlineData("p1}}p2{")]
|
|
||||||
public void InvalidTemplate_WithMismatchedBraces(string template)
|
|
||||||
{
|
|
||||||
Assert.Throws<ArgumentException>(
|
|
||||||
() => TemplateRouteParser.Parse(template),
|
|
||||||
@"There is an incomplete parameter in this path segment: '" + template + @"'. Check that each '{' character has a matching '}' character." + Environment.NewLine +
|
|
||||||
"Parameter name: routeTemplate");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void InvalidTemplate_CannotHaveCatchAllInMultiSegment()
|
|
||||||
{
|
|
||||||
Assert.Throws<ArgumentException>(
|
|
||||||
() => TemplateRouteParser.Parse("123{a}abc{*moo}"),
|
|
||||||
"A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter." + Environment.NewLine +
|
|
||||||
"Parameter name: routeTemplate");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void InvalidTemplate_CannotHaveMoreThanOneCatchAll()
|
|
||||||
{
|
|
||||||
Assert.Throws<ArgumentException>(
|
|
||||||
() => TemplateRouteParser.Parse("{*p1}/{*p2}"),
|
|
||||||
"A catch-all parameter can only appear as the last segment of the route template." + Environment.NewLine +
|
|
||||||
"Parameter name: routeTemplate");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void InvalidTemplate_CannotHaveMoreThanOneCatchAllInMultiSegment()
|
|
||||||
{
|
|
||||||
Assert.Throws<ArgumentException>(
|
|
||||||
() => TemplateRouteParser.Parse("{*p1}abc{*p2}"),
|
|
||||||
"A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter." + Environment.NewLine +
|
|
||||||
"Parameter name: routeTemplate");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void InvalidTemplate_CannotHaveCatchAllWithNoName()
|
|
||||||
{
|
|
||||||
Assert.Throws<ArgumentException>(
|
|
||||||
() => TemplateRouteParser.Parse("foo/{*}"),
|
|
||||||
@"The route parameter name '' is invalid. Route parameter names must be non-empty and cannot contain these characters: ""{"", ""}"", ""/"", ""?""" + Environment.NewLine +
|
|
||||||
"Parameter name: routeTemplate");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void InvalidTemplate_CannotHaveConsecutiveOpenBrace()
|
|
||||||
{
|
|
||||||
Assert.Throws<ArgumentException>(
|
|
||||||
() => TemplateRouteParser.Parse("foo/{{p1}"),
|
|
||||||
"There is an incomplete parameter in this path segment: '{{p1}'. Check that each '{' character has a matching '}' character." + Environment.NewLine +
|
|
||||||
"Parameter name: routeTemplate");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void InvalidTemplate_CannotHaveConsecutiveCloseBrace()
|
|
||||||
{
|
|
||||||
Assert.Throws<ArgumentException>(
|
|
||||||
() => TemplateRouteParser.Parse("foo/{p1}}"),
|
|
||||||
"There is an incomplete parameter in this path segment: '{p1}}'. Check that each '{' character has a matching '}' character." + Environment.NewLine +
|
|
||||||
"Parameter name: routeTemplate");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void InvalidTemplate_SameParameterTwiceThrows()
|
|
||||||
{
|
|
||||||
Assert.Throws<ArgumentException>(
|
|
||||||
() => TemplateRouteParser.Parse("{aaa}/{AAA}"),
|
|
||||||
"The route parameter name 'AAA' appears more than one time in the route template." + Environment.NewLine +
|
|
||||||
"Parameter name: routeTemplate");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void InvalidTemplate_SameParameterTwiceAndOneCatchAllThrows()
|
|
||||||
{
|
|
||||||
Assert.Throws<ArgumentException>(
|
|
||||||
() => TemplateRouteParser.Parse("{aaa}/{*AAA}"),
|
|
||||||
"The route parameter name 'AAA' appears more than one time in the route template." + Environment.NewLine +
|
|
||||||
"Parameter name: routeTemplate");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void InvalidTemplate_InvalidParameterNameWithCloseBracketThrows()
|
|
||||||
{
|
|
||||||
Assert.Throws<ArgumentException>(
|
|
||||||
() => TemplateRouteParser.Parse("{a}/{aa}a}/{z}"),
|
|
||||||
"There is an incomplete parameter in this path segment: '{aa}a}'. Check that each '{' character has a matching '}' character." + Environment.NewLine +
|
|
||||||
"Parameter name: routeTemplate");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void InvalidTemplate_InvalidParameterNameWithOpenBracketThrows()
|
|
||||||
{
|
|
||||||
Assert.Throws<ArgumentException>(
|
|
||||||
() => TemplateRouteParser.Parse("{a}/{a{aa}/{z}"),
|
|
||||||
@"The route parameter name 'a{aa' is invalid. Route parameter names must be non-empty and cannot contain these characters: ""{"", ""}"", ""/"", ""?""" + Environment.NewLine +
|
|
||||||
"Parameter name: routeTemplate");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void InvalidTemplate_InvalidParameterNameWithEmptyNameThrows()
|
|
||||||
{
|
|
||||||
Assert.Throws<ArgumentException>(
|
|
||||||
() => TemplateRouteParser.Parse("{a}/{}/{z}"),
|
|
||||||
@"The route parameter name '' is invalid. Route parameter names must be non-empty and cannot contain these characters: ""{"", ""}"", ""/"", ""?""" + Environment.NewLine +
|
|
||||||
"Parameter name: routeTemplate");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void InvalidTemplate_InvalidParameterNameWithQuestionThrows()
|
|
||||||
{
|
|
||||||
Assert.Throws<ArgumentException>(
|
|
||||||
() => TemplateRouteParser.Parse("{Controller}.mvc/{?}"),
|
|
||||||
"The route template cannot start with a '/' or '~' character and it cannot contain a '?' character." + Environment.NewLine +
|
|
||||||
"Parameter name: routeTemplate");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void InvalidTemplate_ConsecutiveSeparatorsSlashSlashThrows()
|
|
||||||
{
|
|
||||||
Assert.Throws<ArgumentException>(
|
|
||||||
() => TemplateRouteParser.Parse("{a}//{z}"),
|
|
||||||
"The route template separator character '/' cannot appear consecutively. It must be separated by either a parameter or a literal value." + Environment.NewLine +
|
|
||||||
"Parameter name: routeTemplate");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void InvalidTemplate_WithCatchAllNotAtTheEndThrows()
|
|
||||||
{
|
|
||||||
Assert.Throws<ArgumentException>(
|
|
||||||
() => TemplateRouteParser.Parse("foo/{p1}/{*p2}/{p3}"),
|
|
||||||
"A catch-all parameter can only appear as the last segment of the route template." + Environment.NewLine +
|
|
||||||
"Parameter name: routeTemplate");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void InvalidTemplate_RepeatedParametersThrows()
|
|
||||||
{
|
|
||||||
Assert.Throws<ArgumentException>(
|
|
||||||
() => TemplateRouteParser.Parse("foo/aa{p1}{p2}"),
|
|
||||||
"A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string." + Environment.NewLine +
|
|
||||||
"Parameter name: routeTemplate");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void InvalidTemplate_CannotStartWithSlash()
|
|
||||||
{
|
|
||||||
Assert.Throws<ArgumentException>(
|
|
||||||
() => TemplateRouteParser.Parse("/foo"),
|
|
||||||
"The route template cannot start with a '/' or '~' character and it cannot contain a '?' character." + Environment.NewLine +
|
|
||||||
"Parameter name: routeTemplate");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void InvalidTemplate_CannotStartWithTilde()
|
|
||||||
{
|
|
||||||
Assert.Throws<ArgumentException>(
|
|
||||||
() => TemplateRouteParser.Parse("~foo"),
|
|
||||||
"The route template cannot start with a '/' or '~' character and it cannot contain a '?' character." + Environment.NewLine +
|
|
||||||
"Parameter name: routeTemplate");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void InvalidTemplate_CannotContainQuestionMark()
|
|
||||||
{
|
|
||||||
Assert.Throws<ArgumentException>(
|
|
||||||
() => TemplateRouteParser.Parse("foor?bar"),
|
|
||||||
"The route template cannot start with a '/' or '~' character and it cannot contain a '?' character." + Environment.NewLine +
|
|
||||||
"Parameter name: routeTemplate");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -20,12 +20,13 @@ namespace Microsoft.AspNet.Routing.Template.Tests
|
||||||
TemplateRoute r = CreateRoute("{controller}/{action}/{id}", null);
|
TemplateRoute r = CreateRoute("{controller}/{action}/{id}", null);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var rd = r.Match(new RouteContext(context));
|
var match = r.Match(new RouteContext(context));
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal("Bank", rd.Values["controller"]);
|
Assert.NotNull(match);
|
||||||
Assert.Equal("DoAction", rd.Values["action"]);
|
Assert.Equal("Bank", match.Values["controller"]);
|
||||||
Assert.Equal("123", rd.Values["id"]);
|
Assert.Equal("DoAction", match.Values["action"]);
|
||||||
|
Assert.Equal("123", match.Values["id"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -736,7 +737,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Assert.NotNull(match);
|
Assert.NotNull(match);
|
||||||
Assert.Equal<int>(match.Values.Count, expectedValues.Count);
|
Assert.Equal<int>(expectedValues.Count, match.Values.Count);
|
||||||
foreach (string key in match.Values.Keys)
|
foreach (string key in match.Values.Keys)
|
||||||
{
|
{
|
||||||
Assert.Equal(expectedValues[key], match.Values[key]);
|
Assert.Equal(expectedValues[key], match.Values[key]);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue