Merge pull request #609 from aspnet/release/2.2
Introduce RoutePattern (#585)
This commit is contained in:
commit
650178b09d
|
|
@ -0,0 +1,248 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Routing.Patterns
|
||||||
|
{
|
||||||
|
internal static class RouteParameterParser
|
||||||
|
{
|
||||||
|
// This code parses the inside of the route parameter
|
||||||
|
//
|
||||||
|
// Ex: {hello} - this method is responsible for parsing 'hello'
|
||||||
|
// The factoring between this class and RoutePatternParser is due to legacy.
|
||||||
|
public static RoutePatternParameterPart ParseRouteParameter(string parameter)
|
||||||
|
{
|
||||||
|
if (parameter == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(parameter));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parameter.Length == 0)
|
||||||
|
{
|
||||||
|
return new RoutePatternParameterPart(string.Empty, null, RoutePatternParameterKind.Standard, Array.Empty<RoutePatternConstraintReference>());
|
||||||
|
}
|
||||||
|
|
||||||
|
var startIndex = 0;
|
||||||
|
var endIndex = parameter.Length - 1;
|
||||||
|
|
||||||
|
var parameterKind = RoutePatternParameterKind.Standard;
|
||||||
|
if (parameter[0] == '*')
|
||||||
|
{
|
||||||
|
parameterKind = RoutePatternParameterKind.CatchAll;
|
||||||
|
startIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parameter[endIndex] == '?')
|
||||||
|
{
|
||||||
|
parameterKind = RoutePatternParameterKind.Optional;
|
||||||
|
endIndex--;
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentIndex = startIndex;
|
||||||
|
|
||||||
|
// Parse parameter name
|
||||||
|
var parameterName = string.Empty;
|
||||||
|
|
||||||
|
while (currentIndex <= endIndex)
|
||||||
|
{
|
||||||
|
var currentChar = parameter[currentIndex];
|
||||||
|
|
||||||
|
if ((currentChar == ':' || currentChar == '=') && startIndex != currentIndex)
|
||||||
|
{
|
||||||
|
// Parameter names are allowed to start with delimiters used to denote constraints or default values.
|
||||||
|
// i.e. "=foo" or ":bar" would be treated as parameter names rather than default value or constraint
|
||||||
|
// specifications.
|
||||||
|
parameterName = parameter.Substring(startIndex, currentIndex - startIndex);
|
||||||
|
|
||||||
|
// Roll the index back and move to the constraint parsing stage.
|
||||||
|
currentIndex--;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else if (currentIndex == endIndex)
|
||||||
|
{
|
||||||
|
parameterName = parameter.Substring(startIndex, currentIndex - startIndex + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
var parseResults = ParseConstraints(parameter, parameterName, currentIndex, endIndex);
|
||||||
|
currentIndex = parseResults.CurrentIndex;
|
||||||
|
|
||||||
|
string defaultValue = null;
|
||||||
|
if (currentIndex <= endIndex &&
|
||||||
|
parameter[currentIndex] == '=')
|
||||||
|
{
|
||||||
|
defaultValue = parameter.Substring(currentIndex + 1, endIndex - currentIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RoutePatternParameterPart(parameterName, defaultValue, parameterKind, parseResults.Constraints.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ConstraintParseResults ParseConstraints(
|
||||||
|
string text,
|
||||||
|
string parameterName,
|
||||||
|
int currentIndex,
|
||||||
|
int endIndex)
|
||||||
|
{
|
||||||
|
var constraints = new List<RoutePatternConstraintReference>();
|
||||||
|
var state = ParseState.Start;
|
||||||
|
var startIndex = currentIndex;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
var currentChar = currentIndex > endIndex ? null : (char?)text[currentIndex];
|
||||||
|
switch (state)
|
||||||
|
{
|
||||||
|
case ParseState.Start:
|
||||||
|
switch (currentChar)
|
||||||
|
{
|
||||||
|
case null:
|
||||||
|
state = ParseState.End;
|
||||||
|
break;
|
||||||
|
case ':':
|
||||||
|
state = ParseState.ParsingName;
|
||||||
|
startIndex = currentIndex + 1;
|
||||||
|
break;
|
||||||
|
case '(':
|
||||||
|
state = ParseState.InsideParenthesis;
|
||||||
|
break;
|
||||||
|
case '=':
|
||||||
|
state = ParseState.End;
|
||||||
|
currentIndex--;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ParseState.InsideParenthesis:
|
||||||
|
switch (currentChar)
|
||||||
|
{
|
||||||
|
case null:
|
||||||
|
state = ParseState.End;
|
||||||
|
var constraintText = text.Substring(startIndex, currentIndex - startIndex);
|
||||||
|
constraints.Add(RoutePatternFactory.Constraint(parameterName, constraintText));
|
||||||
|
break;
|
||||||
|
case ')':
|
||||||
|
// Only consume a ')' token if
|
||||||
|
// (a) it is the last token
|
||||||
|
// (b) the next character is the start of the new constraint ':'
|
||||||
|
// (c) the next character is the start of the default value.
|
||||||
|
|
||||||
|
var nextChar = currentIndex + 1 > endIndex ? null : (char?)text[currentIndex + 1];
|
||||||
|
switch (nextChar)
|
||||||
|
{
|
||||||
|
case null:
|
||||||
|
state = ParseState.End;
|
||||||
|
constraintText = text.Substring(startIndex, currentIndex - startIndex + 1);
|
||||||
|
constraints.Add(RoutePatternFactory.Constraint(parameterName, constraintText));
|
||||||
|
break;
|
||||||
|
case ':':
|
||||||
|
state = ParseState.Start;
|
||||||
|
constraintText = text.Substring(startIndex, currentIndex - startIndex + 1);
|
||||||
|
constraints.Add(RoutePatternFactory.Constraint(parameterName, constraintText));
|
||||||
|
startIndex = currentIndex + 1;
|
||||||
|
break;
|
||||||
|
case '=':
|
||||||
|
state = ParseState.End;
|
||||||
|
constraintText = text.Substring(startIndex, currentIndex - startIndex + 1);
|
||||||
|
constraints.Add(RoutePatternFactory.Constraint(parameterName, constraintText));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ':':
|
||||||
|
case '=':
|
||||||
|
// In the original implementation, the Regex would've backtracked if it encountered an
|
||||||
|
// unbalanced opening bracket followed by (not necessarily immediatiely) a delimiter.
|
||||||
|
// Simply verifying that the parantheses will eventually be closed should suffice to
|
||||||
|
// determine if the terminator needs to be consumed as part of the current constraint
|
||||||
|
// specification.
|
||||||
|
var indexOfClosingParantheses = text.IndexOf(')', currentIndex + 1);
|
||||||
|
if (indexOfClosingParantheses == -1)
|
||||||
|
{
|
||||||
|
constraintText = text.Substring(startIndex, currentIndex - startIndex);
|
||||||
|
constraints.Add(RoutePatternFactory.Constraint(parameterName, constraintText));
|
||||||
|
|
||||||
|
if (currentChar == ':')
|
||||||
|
{
|
||||||
|
state = ParseState.ParsingName;
|
||||||
|
startIndex = currentIndex + 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
state = ParseState.End;
|
||||||
|
currentIndex--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
currentIndex = indexOfClosingParantheses;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ParseState.ParsingName:
|
||||||
|
switch (currentChar)
|
||||||
|
{
|
||||||
|
case null:
|
||||||
|
state = ParseState.End;
|
||||||
|
var constraintText = text.Substring(startIndex, currentIndex - startIndex);
|
||||||
|
if (constraintText.Length > 0)
|
||||||
|
{
|
||||||
|
constraints.Add(RoutePatternFactory.Constraint(parameterName, constraintText));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ':':
|
||||||
|
constraintText = text.Substring(startIndex, currentIndex - startIndex);
|
||||||
|
if (constraintText.Length > 0)
|
||||||
|
{
|
||||||
|
constraints.Add(RoutePatternFactory.Constraint(parameterName, constraintText));
|
||||||
|
}
|
||||||
|
startIndex = currentIndex + 1;
|
||||||
|
break;
|
||||||
|
case '(':
|
||||||
|
state = ParseState.InsideParenthesis;
|
||||||
|
break;
|
||||||
|
case '=':
|
||||||
|
state = ParseState.End;
|
||||||
|
constraintText = text.Substring(startIndex, currentIndex - startIndex);
|
||||||
|
if (constraintText.Length > 0)
|
||||||
|
{
|
||||||
|
constraints.Add(RoutePatternFactory.Constraint(parameterName, constraintText));
|
||||||
|
}
|
||||||
|
currentIndex--;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentIndex++;
|
||||||
|
|
||||||
|
} while (state != ParseState.End);
|
||||||
|
|
||||||
|
return new ConstraintParseResults(currentIndex, constraints);
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum ParseState
|
||||||
|
{
|
||||||
|
Start,
|
||||||
|
ParsingName,
|
||||||
|
InsideParenthesis,
|
||||||
|
End
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly struct ConstraintParseResults
|
||||||
|
{
|
||||||
|
public readonly int CurrentIndex;
|
||||||
|
|
||||||
|
public readonly IReadOnlyList<RoutePatternConstraintReference> Constraints;
|
||||||
|
|
||||||
|
public ConstraintParseResults(int currentIndex, IReadOnlyList<RoutePatternConstraintReference> constraints)
|
||||||
|
{
|
||||||
|
CurrentIndex = currentIndex;
|
||||||
|
Constraints = constraints;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Routing.Patterns
|
||||||
|
{
|
||||||
|
[DebuggerDisplay("{DebuggerToString()}")]
|
||||||
|
public sealed class RoutePattern
|
||||||
|
{
|
||||||
|
private const string SeparatorString = "/";
|
||||||
|
|
||||||
|
internal RoutePattern(
|
||||||
|
string rawText,
|
||||||
|
Dictionary<string, object> defaults,
|
||||||
|
Dictionary<string, IReadOnlyList<RoutePatternConstraintReference>> constraints,
|
||||||
|
RoutePatternParameterPart[] parameters,
|
||||||
|
RoutePatternPathSegment[] pathSegments)
|
||||||
|
{
|
||||||
|
Debug.Assert(defaults != null);
|
||||||
|
Debug.Assert(constraints != null);
|
||||||
|
Debug.Assert(parameters != null);
|
||||||
|
Debug.Assert(pathSegments != null);
|
||||||
|
|
||||||
|
RawText = rawText;
|
||||||
|
Defaults = defaults;
|
||||||
|
Constraints = constraints;
|
||||||
|
Parameters = parameters;
|
||||||
|
PathSegments = pathSegments;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyDictionary<string, object> Defaults { get; }
|
||||||
|
|
||||||
|
public IReadOnlyDictionary<string, IReadOnlyList<RoutePatternConstraintReference>> Constraints { get; }
|
||||||
|
|
||||||
|
public string RawText { get; }
|
||||||
|
|
||||||
|
public IReadOnlyList<RoutePatternParameterPart> Parameters { get; }
|
||||||
|
|
||||||
|
public IReadOnlyList<RoutePatternPathSegment> PathSegments { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the parameter matching the given name.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">The name of the parameter to match.</param>
|
||||||
|
/// <returns>The matching parameter or <c>null</c> if no parameter matches the given name.</returns>
|
||||||
|
public RoutePatternParameterPart GetParameter(string name)
|
||||||
|
{
|
||||||
|
if (name == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < Parameters.Count; i++)
|
||||||
|
{
|
||||||
|
var parameter = Parameters[i];
|
||||||
|
if (string.Equals(parameter.Name, name, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return parameter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string DebuggerToString()
|
||||||
|
{
|
||||||
|
return RawText ?? string.Join(SeparatorString, PathSegments.Select(s => s.DebuggerToString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Routing.Patterns
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The parsed representation of a constraint in a <see cref="RoutePattern"/> parameter.
|
||||||
|
/// </summary>
|
||||||
|
[DebuggerDisplay("{DebuggerToString()}")]
|
||||||
|
public sealed class RoutePatternConstraintReference
|
||||||
|
{
|
||||||
|
internal RoutePatternConstraintReference(string parameterName, string content)
|
||||||
|
{
|
||||||
|
ParameterName = parameterName;
|
||||||
|
Content = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal RoutePatternConstraintReference(string parameterName, IRouteConstraint constraint)
|
||||||
|
{
|
||||||
|
ParameterName = parameterName;
|
||||||
|
Constraint = constraint;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the constraint text.
|
||||||
|
/// </summary>
|
||||||
|
public string Content { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a pre-existing <see cref="IRouteConstraint"/> that was used to construct this reference.
|
||||||
|
/// </summary>
|
||||||
|
public IRouteConstraint Constraint { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the parameter name associated with the constraint.
|
||||||
|
/// </summary>
|
||||||
|
public string ParameterName { get; }
|
||||||
|
|
||||||
|
private string DebuggerToString()
|
||||||
|
{
|
||||||
|
return Content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Runtime.Serialization;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Routing.Patterns
|
||||||
|
{
|
||||||
|
[Serializable]
|
||||||
|
public sealed class RoutePatternException : Exception
|
||||||
|
{
|
||||||
|
private RoutePatternException(SerializationInfo info, StreamingContext context)
|
||||||
|
: base(info, context)
|
||||||
|
{
|
||||||
|
Pattern = (string)info.GetValue(nameof(Pattern), typeof(string));
|
||||||
|
}
|
||||||
|
|
||||||
|
public RoutePatternException(string pattern, string message)
|
||||||
|
: base(message)
|
||||||
|
{
|
||||||
|
if (pattern == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(pattern));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
Pattern = pattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Pattern { get; }
|
||||||
|
|
||||||
|
public override void GetObjectData(SerializationInfo info, StreamingContext context)
|
||||||
|
{
|
||||||
|
info.AddValue(nameof(Pattern), Pattern);
|
||||||
|
base.GetObjectData(info, context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,504 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Microsoft.AspNetCore.Routing.Constraints;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Routing.Patterns
|
||||||
|
{
|
||||||
|
public static class RoutePatternFactory
|
||||||
|
{
|
||||||
|
public static RoutePattern Parse(string pattern)
|
||||||
|
{
|
||||||
|
if (pattern == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(pattern));
|
||||||
|
}
|
||||||
|
|
||||||
|
return RoutePatternParser.Parse(pattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RoutePattern Parse(string pattern, object defaults, object constraints)
|
||||||
|
{
|
||||||
|
if (pattern == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(pattern));
|
||||||
|
}
|
||||||
|
|
||||||
|
var original = RoutePatternParser.Parse(pattern);
|
||||||
|
return Pattern(original.RawText, defaults, constraints, original.PathSegments);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RoutePattern Pattern(IEnumerable<RoutePatternPathSegment> segments)
|
||||||
|
{
|
||||||
|
if (segments == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(segments));
|
||||||
|
}
|
||||||
|
|
||||||
|
return PatternCore(null, null, null, segments);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RoutePattern Pattern(string rawText, IEnumerable<RoutePatternPathSegment> segments)
|
||||||
|
{
|
||||||
|
if (segments == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(segments));
|
||||||
|
}
|
||||||
|
|
||||||
|
return PatternCore(rawText, null, null, segments);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RoutePattern Pattern(
|
||||||
|
object defaults,
|
||||||
|
object constraints,
|
||||||
|
IEnumerable<RoutePatternPathSegment> segments)
|
||||||
|
{
|
||||||
|
if (segments == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(segments));
|
||||||
|
}
|
||||||
|
|
||||||
|
return PatternCore(null, new RouteValueDictionary(defaults), new RouteValueDictionary(constraints), segments);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RoutePattern Pattern(
|
||||||
|
string rawText,
|
||||||
|
object defaults,
|
||||||
|
object constraints,
|
||||||
|
IEnumerable<RoutePatternPathSegment> segments)
|
||||||
|
{
|
||||||
|
if (segments == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(segments));
|
||||||
|
}
|
||||||
|
|
||||||
|
return PatternCore(rawText, new RouteValueDictionary(defaults), new RouteValueDictionary(constraints), segments);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RoutePattern Pattern(params RoutePatternPathSegment[] segments)
|
||||||
|
{
|
||||||
|
if (segments == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(segments));
|
||||||
|
}
|
||||||
|
|
||||||
|
return PatternCore(null, null, null, segments);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RoutePattern Pattern(string rawText, params RoutePatternPathSegment[] segments)
|
||||||
|
{
|
||||||
|
if (segments == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(segments));
|
||||||
|
}
|
||||||
|
|
||||||
|
return PatternCore(rawText, null, null, segments);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RoutePattern Pattern(
|
||||||
|
object defaults,
|
||||||
|
object constraints,
|
||||||
|
params RoutePatternPathSegment[] segments)
|
||||||
|
{
|
||||||
|
if (segments == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(segments));
|
||||||
|
}
|
||||||
|
|
||||||
|
return PatternCore(null, new RouteValueDictionary(defaults), new RouteValueDictionary(constraints), segments);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RoutePattern Pattern(
|
||||||
|
string rawText,
|
||||||
|
object defaults,
|
||||||
|
object constraints,
|
||||||
|
params RoutePatternPathSegment[] segments)
|
||||||
|
{
|
||||||
|
if (segments == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(segments));
|
||||||
|
}
|
||||||
|
|
||||||
|
return PatternCore(rawText, new RouteValueDictionary(defaults), new RouteValueDictionary(constraints), segments);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RoutePattern PatternCore(
|
||||||
|
string rawText,
|
||||||
|
IDictionary<string, object> defaults,
|
||||||
|
IDictionary<string, object> constraints,
|
||||||
|
IEnumerable<RoutePatternPathSegment> segments)
|
||||||
|
{
|
||||||
|
// We want to merge the segment data with the 'out of line' defaults and constraints.
|
||||||
|
//
|
||||||
|
// This means that for parameters that have 'out of line' defaults we will modify
|
||||||
|
// the parameter to contain the default (same story for constraints).
|
||||||
|
//
|
||||||
|
// We also maintain a collection of defaults and constraints that will also
|
||||||
|
// contain the values that don't match a parameter.
|
||||||
|
//
|
||||||
|
// It's important that these two views of the data are consistent. We don't want
|
||||||
|
// values specified out of line to have a different behavior.
|
||||||
|
|
||||||
|
var updatedDefaults = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
if (defaults != null)
|
||||||
|
{
|
||||||
|
foreach (var kvp in defaults)
|
||||||
|
{
|
||||||
|
updatedDefaults.Add(kvp.Key, kvp.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var updatedConstraints = new Dictionary<string, List<RoutePatternConstraintReference>>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
if (constraints != null)
|
||||||
|
{
|
||||||
|
foreach (var kvp in constraints)
|
||||||
|
{
|
||||||
|
updatedConstraints.Add(kvp.Key, new List<RoutePatternConstraintReference>()
|
||||||
|
{
|
||||||
|
Constraint(kvp.Key, kvp.Value),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var parameters = new List<RoutePatternParameterPart>();
|
||||||
|
var updatedSegments = segments.ToArray();
|
||||||
|
for (var i = 0; i < updatedSegments.Length; i++)
|
||||||
|
{
|
||||||
|
var segment = VisitSegment(updatedSegments[i]);
|
||||||
|
updatedSegments[i] = segment;
|
||||||
|
|
||||||
|
for (var j = 0; j < segment.Parts.Count; j++)
|
||||||
|
{
|
||||||
|
if (segment.Parts[j] is RoutePatternParameterPart parameter)
|
||||||
|
{
|
||||||
|
parameters.Add(parameter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RoutePattern(
|
||||||
|
rawText,
|
||||||
|
updatedDefaults,
|
||||||
|
updatedConstraints.ToDictionary(kvp => kvp.Key, kvp => (IReadOnlyList<RoutePatternConstraintReference>)kvp.Value.ToArray()),
|
||||||
|
parameters.ToArray(),
|
||||||
|
updatedSegments.ToArray());
|
||||||
|
|
||||||
|
RoutePatternPathSegment VisitSegment(RoutePatternPathSegment segment)
|
||||||
|
{
|
||||||
|
var updatedParts = new RoutePatternPart[segment.Parts.Count];
|
||||||
|
for (var i = 0; i < segment.Parts.Count; i++)
|
||||||
|
{
|
||||||
|
var part = segment.Parts[i];
|
||||||
|
updatedParts[i] = VisitPart(part);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SegmentCore(updatedParts);
|
||||||
|
}
|
||||||
|
|
||||||
|
RoutePatternPart VisitPart(RoutePatternPart part)
|
||||||
|
{
|
||||||
|
if (!part.IsParameter)
|
||||||
|
{
|
||||||
|
return part;
|
||||||
|
}
|
||||||
|
|
||||||
|
var parameter = (RoutePatternParameterPart)part;
|
||||||
|
var @default = parameter.Default;
|
||||||
|
|
||||||
|
if (updatedDefaults.TryGetValue(parameter.Name, out var newDefault))
|
||||||
|
{
|
||||||
|
if (parameter.Default != null)
|
||||||
|
{
|
||||||
|
var message = Resources.FormatTemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly(parameter.Name);
|
||||||
|
throw new InvalidOperationException(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parameter.IsOptional)
|
||||||
|
{
|
||||||
|
var message = Resources.TemplateRoute_OptionalCannotHaveDefaultValue;
|
||||||
|
throw new InvalidOperationException(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@default = newDefault;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parameter.Default != null)
|
||||||
|
{
|
||||||
|
updatedDefaults.Add(parameter.Name, parameter.Default);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!updatedConstraints.TryGetValue(parameter.Name, out var parameterConstraints) &&
|
||||||
|
parameter.Constraints.Count > 0)
|
||||||
|
{
|
||||||
|
parameterConstraints = new List<RoutePatternConstraintReference>();
|
||||||
|
updatedConstraints.Add(parameter.Name, parameterConstraints);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parameter.Constraints.Count > 0)
|
||||||
|
{
|
||||||
|
parameterConstraints.AddRange(parameter.Constraints);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ParameterPartCore(
|
||||||
|
parameter.Name,
|
||||||
|
@default,
|
||||||
|
parameter.ParameterKind,
|
||||||
|
(IEnumerable<RoutePatternConstraintReference>)parameterConstraints ?? Array.Empty<RoutePatternConstraintReference>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RoutePatternPathSegment Segment(IEnumerable<RoutePatternPart> parts)
|
||||||
|
{
|
||||||
|
if (parts == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(parts));
|
||||||
|
}
|
||||||
|
|
||||||
|
return SegmentCore(parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RoutePatternPathSegment Segment(params RoutePatternPart[] parts)
|
||||||
|
{
|
||||||
|
if (parts == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(parts));
|
||||||
|
}
|
||||||
|
|
||||||
|
return SegmentCore(parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RoutePatternPathSegment SegmentCore(IEnumerable<RoutePatternPart> parts)
|
||||||
|
{
|
||||||
|
return new RoutePatternPathSegment(parts.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RoutePatternLiteralPart LiteralPart(string content)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(content))
|
||||||
|
{
|
||||||
|
throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(content));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.IndexOf('?') >= 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentException(Resources.FormatTemplateRoute_InvalidLiteral(content));
|
||||||
|
}
|
||||||
|
|
||||||
|
return LiteralPartCore(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RoutePatternLiteralPart LiteralPartCore(string content)
|
||||||
|
{
|
||||||
|
return new RoutePatternLiteralPart(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RoutePatternSeparatorPart SeparatorPart(string content)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(content))
|
||||||
|
{
|
||||||
|
throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(content));
|
||||||
|
}
|
||||||
|
|
||||||
|
return SeparatorPartCore(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RoutePatternSeparatorPart SeparatorPartCore(string content)
|
||||||
|
{
|
||||||
|
return new RoutePatternSeparatorPart(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RoutePatternParameterPart ParameterPart(string parameterName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(parameterName))
|
||||||
|
{
|
||||||
|
throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(parameterName));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parameterName.IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentException(Resources.FormatTemplateRoute_InvalidParameterName(parameterName));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ParameterPartCore(
|
||||||
|
parameterName: parameterName,
|
||||||
|
@default: null,
|
||||||
|
parameterKind: RoutePatternParameterKind.Standard,
|
||||||
|
constraints: Array.Empty<RoutePatternConstraintReference>());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RoutePatternParameterPart ParameterPart(string parameterName, object @default)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(parameterName))
|
||||||
|
{
|
||||||
|
throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(parameterName));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parameterName.IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentException(Resources.FormatTemplateRoute_InvalidParameterName(parameterName));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ParameterPartCore(
|
||||||
|
parameterName: parameterName,
|
||||||
|
@default: @default,
|
||||||
|
parameterKind: RoutePatternParameterKind.Standard,
|
||||||
|
constraints: Array.Empty<RoutePatternConstraintReference>());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RoutePatternParameterPart ParameterPart(
|
||||||
|
string parameterName,
|
||||||
|
object @default,
|
||||||
|
RoutePatternParameterKind parameterKind)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(parameterName))
|
||||||
|
{
|
||||||
|
throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(parameterName));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parameterName.IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentException(Resources.FormatTemplateRoute_InvalidParameterName(parameterName));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (@default != null && parameterKind == RoutePatternParameterKind.Optional)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(Resources.TemplateRoute_OptionalCannotHaveDefaultValue, nameof(parameterKind));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ParameterPartCore(
|
||||||
|
parameterName: parameterName,
|
||||||
|
@default: @default,
|
||||||
|
parameterKind: parameterKind,
|
||||||
|
constraints: Array.Empty<RoutePatternConstraintReference>());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RoutePatternParameterPart ParameterPart(
|
||||||
|
string parameterName,
|
||||||
|
object @default,
|
||||||
|
RoutePatternParameterKind parameterKind,
|
||||||
|
IEnumerable<RoutePatternConstraintReference> constraints)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(parameterName))
|
||||||
|
{
|
||||||
|
throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(parameterName));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parameterName.IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentException(Resources.FormatTemplateRoute_InvalidParameterName(parameterName));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (@default != null && parameterKind == RoutePatternParameterKind.Optional)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(Resources.TemplateRoute_OptionalCannotHaveDefaultValue, nameof(parameterKind));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (constraints == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(constraints));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ParameterPartCore(
|
||||||
|
parameterName: parameterName,
|
||||||
|
@default: @default,
|
||||||
|
parameterKind: parameterKind,
|
||||||
|
constraints: constraints);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RoutePatternParameterPart ParameterPart(
|
||||||
|
string parameterName,
|
||||||
|
object @default,
|
||||||
|
RoutePatternParameterKind parameterKind,
|
||||||
|
params RoutePatternConstraintReference[] constraints)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(parameterName))
|
||||||
|
{
|
||||||
|
throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(parameterName));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parameterName.IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentException(Resources.FormatTemplateRoute_InvalidParameterName(parameterName));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (@default != null && parameterKind == RoutePatternParameterKind.Optional)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(Resources.TemplateRoute_OptionalCannotHaveDefaultValue, nameof(parameterKind));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (constraints == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(constraints));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ParameterPartCore(
|
||||||
|
parameterName: parameterName,
|
||||||
|
@default: @default,
|
||||||
|
parameterKind: parameterKind,
|
||||||
|
constraints: constraints);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RoutePatternParameterPart ParameterPartCore(
|
||||||
|
string parameterName,
|
||||||
|
object @default,
|
||||||
|
RoutePatternParameterKind parameterKind,
|
||||||
|
IEnumerable<RoutePatternConstraintReference> constraints)
|
||||||
|
{
|
||||||
|
return new RoutePatternParameterPart(parameterName, @default, parameterKind, constraints.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RoutePatternConstraintReference Constraint(string parameterName, object constraint)
|
||||||
|
{
|
||||||
|
// Similar to RouteConstraintBuilder
|
||||||
|
if (constraint is IRouteConstraint routeConstraint)
|
||||||
|
{
|
||||||
|
return ConstraintCore(parameterName, routeConstraint);
|
||||||
|
}
|
||||||
|
else if (constraint is string content)
|
||||||
|
{
|
||||||
|
return ConstraintCore(parameterName, new RegexRouteConstraint("^(" + content + ")$"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(Resources.FormatConstraintMustBeStringOrConstraint(
|
||||||
|
parameterName,
|
||||||
|
constraint,
|
||||||
|
typeof(IRouteConstraint)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RoutePatternConstraintReference Constraint(string parameterName, IRouteConstraint constraint)
|
||||||
|
{
|
||||||
|
if (constraint == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(constraint));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ConstraintCore(parameterName, constraint);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RoutePatternConstraintReference Constraint(string parameterName, string constraint)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(constraint))
|
||||||
|
{
|
||||||
|
throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(constraint));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ConstraintCore(parameterName, constraint);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static RoutePatternConstraintReference ConstraintCore(string parameterName, IRouteConstraint constraint)
|
||||||
|
{
|
||||||
|
return new RoutePatternConstraintReference(parameterName, constraint);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RoutePatternConstraintReference ConstraintCore(string parameterName, string constraint)
|
||||||
|
{
|
||||||
|
return new RoutePatternConstraintReference(parameterName, constraint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Routing.Patterns
|
||||||
|
{
|
||||||
|
[DebuggerDisplay("{DebuggerToString()}")]
|
||||||
|
public sealed class RoutePatternLiteralPart : RoutePatternPart
|
||||||
|
{
|
||||||
|
internal RoutePatternLiteralPart(string content)
|
||||||
|
: base(RoutePatternPartKind.Literal)
|
||||||
|
{
|
||||||
|
Debug.Assert(!string.IsNullOrEmpty(content));
|
||||||
|
Content = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Content { get; }
|
||||||
|
|
||||||
|
internal override string DebuggerToString()
|
||||||
|
{
|
||||||
|
return Content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,511 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using Microsoft.AspNetCore.Routing.Internal;
|
||||||
|
using Microsoft.AspNetCore.Routing.Patterns;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Routing
|
||||||
|
{
|
||||||
|
public class RoutePatternMatcher
|
||||||
|
{
|
||||||
|
private const string SeparatorString = "/";
|
||||||
|
private const char SeparatorChar = '/';
|
||||||
|
|
||||||
|
// Perf: This is a cache to avoid looking things up in 'Defaults' each request.
|
||||||
|
private readonly bool[] _hasDefaultValue;
|
||||||
|
private readonly object[] _defaultValues;
|
||||||
|
|
||||||
|
private static readonly char[] Delimiters = new char[] { SeparatorChar };
|
||||||
|
|
||||||
|
public RoutePatternMatcher(
|
||||||
|
RoutePattern pattern,
|
||||||
|
RouteValueDictionary defaults)
|
||||||
|
{
|
||||||
|
if (pattern == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(pattern));
|
||||||
|
}
|
||||||
|
|
||||||
|
RoutePattern = pattern;
|
||||||
|
Defaults = defaults ?? new RouteValueDictionary();
|
||||||
|
|
||||||
|
// Perf: cache the default value for each parameter (other than complex segments).
|
||||||
|
_hasDefaultValue = new bool[RoutePattern.PathSegments.Count];
|
||||||
|
_defaultValues = new object[RoutePattern.PathSegments.Count];
|
||||||
|
|
||||||
|
for (var i = 0; i < RoutePattern.PathSegments.Count; i++)
|
||||||
|
{
|
||||||
|
var segment = RoutePattern.PathSegments[i];
|
||||||
|
if (!segment.IsSimple)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var part = segment.Parts[0];
|
||||||
|
if (!part.IsParameter)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var parameter = (RoutePatternParameterPart)part;
|
||||||
|
if (Defaults.TryGetValue(parameter.Name, out var value))
|
||||||
|
{
|
||||||
|
_hasDefaultValue[i] = true;
|
||||||
|
_defaultValues[i] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public RouteValueDictionary Defaults { get; }
|
||||||
|
|
||||||
|
public RoutePattern RoutePattern { get; }
|
||||||
|
|
||||||
|
public bool TryMatch(PathString path, RouteValueDictionary values)
|
||||||
|
{
|
||||||
|
if (values == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(values));
|
||||||
|
}
|
||||||
|
|
||||||
|
var i = 0;
|
||||||
|
var pathTokenizer = new PathTokenizer(path);
|
||||||
|
|
||||||
|
// Perf: We do a traversal of the request-segments + route-segments twice.
|
||||||
|
//
|
||||||
|
// For most segment-types, we only really need to any work on one of the two passes.
|
||||||
|
//
|
||||||
|
// On the first pass, we're just looking to see if there's anything that would disqualify us from matching.
|
||||||
|
// The most common case would be a literal segment that doesn't match.
|
||||||
|
//
|
||||||
|
// On the second pass, we're almost certainly going to match the URL, so go ahead and allocate the 'values'
|
||||||
|
// and start capturing strings.
|
||||||
|
foreach (var stringSegment in pathTokenizer)
|
||||||
|
{
|
||||||
|
if (stringSegment.Length == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pathSegment = i >= RoutePattern.PathSegments.Count ? null : RoutePattern.PathSegments[i];
|
||||||
|
if (pathSegment == null && stringSegment.Length > 0)
|
||||||
|
{
|
||||||
|
// If pathSegment is null, then we're out of route segments. All we can match is the empty
|
||||||
|
// string.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else if (pathSegment.IsSimple && pathSegment.Parts[0] is RoutePatternParameterPart parameter && parameter.IsCatchAll)
|
||||||
|
{
|
||||||
|
// Nothing to validate for a catch-all - it can match any string, including the empty string.
|
||||||
|
//
|
||||||
|
// Also, a catch-all has to be the last part, so we're done.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!TryMatchLiterals(i++, stringSegment, pathSegment))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (; i < RoutePattern.PathSegments.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 pathSegment = RoutePattern.PathSegments[i];
|
||||||
|
Debug.Assert(pathSegment != null);
|
||||||
|
|
||||||
|
if (!pathSegment.IsSimple)
|
||||||
|
{
|
||||||
|
// If the segment is a complex segment, it MUST contain literals, and we've parsed the full
|
||||||
|
// path so far, so it can't match.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var part = pathSegment.Parts[0];
|
||||||
|
if (part.IsLiteral || part.IsSeparator)
|
||||||
|
{
|
||||||
|
// If the segment is a simple literal - which need the URL to provide a value, so we don't match.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var parameter = (RoutePatternParameterPart)part;
|
||||||
|
if (parameter.IsCatchAll)
|
||||||
|
{
|
||||||
|
// Nothing to validate for a catch-all - it can match any string, including the empty string.
|
||||||
|
//
|
||||||
|
// Also, a catch-all has to be the last part, so we're done.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we get here, this is a simple segment with a parameter. We need it to be optional, or for the
|
||||||
|
// defaults to have a value.
|
||||||
|
if (!_hasDefaultValue[i] && !parameter.IsOptional)
|
||||||
|
{
|
||||||
|
// There's no default for this (non-optional) parameter so it can't match.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this point we've very likely got a match, so start capturing values for real.
|
||||||
|
i = 0;
|
||||||
|
foreach (var requestSegment in pathTokenizer)
|
||||||
|
{
|
||||||
|
var pathSegment = RoutePattern.PathSegments[i++];
|
||||||
|
if (SavePathSegmentsAsValues(i, values, requestSegment, pathSegment))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!pathSegment.IsSimple)
|
||||||
|
{
|
||||||
|
if (!MatchComplexSegment(pathSegment, requestSegment.ToString(), Defaults, values))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (; i < RoutePattern.PathSegments.Count; i++)
|
||||||
|
{
|
||||||
|
// We've matched the request path so far, but still have remaining route segments. We already know these
|
||||||
|
// are simple parameters that either have a default, or don't need to produce a value.
|
||||||
|
var pathSegment = RoutePattern.PathSegments[i];
|
||||||
|
Debug.Assert(pathSegment != null);
|
||||||
|
Debug.Assert(pathSegment.IsSimple);
|
||||||
|
|
||||||
|
var part = pathSegment.Parts[0];
|
||||||
|
Debug.Assert(part.IsParameter);
|
||||||
|
|
||||||
|
// It's ok for a catch-all to produce a null value
|
||||||
|
if (part is RoutePatternParameterPart parameter && (parameter.IsCatchAll || _hasDefaultValue[i]))
|
||||||
|
{
|
||||||
|
// Don't replace an existing value with a null.
|
||||||
|
var defaultValue = _defaultValues[i];
|
||||||
|
if (defaultValue != null || !values.ContainsKey(parameter.Name))
|
||||||
|
{
|
||||||
|
values[parameter.Name] = defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy all remaining default values to the route data
|
||||||
|
foreach (var kvp in Defaults)
|
||||||
|
{
|
||||||
|
if (!values.ContainsKey(kvp.Key))
|
||||||
|
{
|
||||||
|
values.Add(kvp.Key, kvp.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryMatchLiterals(int index, StringSegment stringSegment, RoutePatternPathSegment pathSegment)
|
||||||
|
{
|
||||||
|
if (pathSegment.IsSimple && !pathSegment.Parts[0].IsParameter)
|
||||||
|
{
|
||||||
|
// This is a literal segment, so we need to match the text, or the route isn't a match.
|
||||||
|
if (pathSegment.Parts[0].IsLiteral)
|
||||||
|
{
|
||||||
|
var part = (RoutePatternLiteralPart)pathSegment.Parts[0];
|
||||||
|
|
||||||
|
if (!stringSegment.Equals(part.Content, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var part = (RoutePatternSeparatorPart)pathSegment.Parts[0];
|
||||||
|
|
||||||
|
if (!stringSegment.Equals(part.Content, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (pathSegment.IsSimple && pathSegment.Parts[0].IsParameter)
|
||||||
|
{
|
||||||
|
// For a parameter, validate that it's a has some length, or we have a default, or it's optional.
|
||||||
|
var part = (RoutePatternParameterPart)pathSegment.Parts[0];
|
||||||
|
if (stringSegment.Length == 0 &&
|
||||||
|
!_hasDefaultValue[index] &&
|
||||||
|
!part.IsOptional)
|
||||||
|
{
|
||||||
|
// There's no value for this parameter, the route can't match.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Debug.Assert(!pathSegment.IsSimple);
|
||||||
|
// Don't attempt to validate a complex segment at this point other than being non-emtpy,
|
||||||
|
// do it in the second pass.
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool SavePathSegmentsAsValues(int index, RouteValueDictionary values, StringSegment requestSegment, RoutePatternPathSegment pathSegment)
|
||||||
|
{
|
||||||
|
if (pathSegment.IsSimple && pathSegment.Parts[0] is RoutePatternParameterPart parameter && parameter.IsCatchAll)
|
||||||
|
{
|
||||||
|
// A catch-all captures til the end of the string.
|
||||||
|
var captured = requestSegment.Buffer.Substring(requestSegment.Offset);
|
||||||
|
if (captured.Length > 0)
|
||||||
|
{
|
||||||
|
values[parameter.Name] = captured;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// It's ok for a catch-all to produce a null value, so we don't check _hasDefaultValue.
|
||||||
|
values[parameter.Name] = _defaultValues[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
// A catch-all has to be the last part, so we're done.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (pathSegment.IsSimple && pathSegment.Parts[0].IsParameter)
|
||||||
|
{
|
||||||
|
// A simple parameter captures the whole segment, or a default value if nothing was
|
||||||
|
// provided.
|
||||||
|
parameter = (RoutePatternParameterPart)pathSegment.Parts[0];
|
||||||
|
if (requestSegment.Length > 0)
|
||||||
|
{
|
||||||
|
values[parameter.Name] = requestSegment.ToString();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (_hasDefaultValue[index])
|
||||||
|
{
|
||||||
|
values[parameter.Name] = _defaultValues[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool MatchComplexSegment(
|
||||||
|
RoutePatternPathSegment routeSegment,
|
||||||
|
string requestSegment,
|
||||||
|
IReadOnlyDictionary<string, object> defaults,
|
||||||
|
RouteValueDictionary values)
|
||||||
|
{
|
||||||
|
var indexOfLastSegment = routeSegment.Parts.Count - 1;
|
||||||
|
|
||||||
|
// We match the request to the template starting at the rightmost parameter
|
||||||
|
// If the last segment of template is optional, then request can match the
|
||||||
|
// template with or without the last parameter. So we start with regular matching,
|
||||||
|
// but if it doesn't match, we start with next to last parameter. Example:
|
||||||
|
// Template: {p1}/{p2}.{p3?}. If the request is one/two.three it will match right away
|
||||||
|
// giving p3 value of three. But if the request is one/two, we start matching from the
|
||||||
|
// rightmost giving p3 the value of two, then we end up not matching the segment.
|
||||||
|
// In this case we start again from p2 to match the request and we succeed giving
|
||||||
|
// the value two to p2
|
||||||
|
if (routeSegment.Parts[indexOfLastSegment] is RoutePatternParameterPart parameter && parameter.IsOptional &&
|
||||||
|
routeSegment.Parts[indexOfLastSegment - 1].IsSeparator)
|
||||||
|
{
|
||||||
|
if (MatchComplexSegmentCore(routeSegment, requestSegment, Defaults, values, indexOfLastSegment))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var separator = (RoutePatternSeparatorPart)routeSegment.Parts[indexOfLastSegment - 1];
|
||||||
|
if (requestSegment.EndsWith(
|
||||||
|
separator.Content,
|
||||||
|
StringComparison.OrdinalIgnoreCase))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return MatchComplexSegmentCore(
|
||||||
|
routeSegment,
|
||||||
|
requestSegment,
|
||||||
|
Defaults,
|
||||||
|
values,
|
||||||
|
indexOfLastSegment - 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return MatchComplexSegmentCore(routeSegment, requestSegment, Defaults, values, indexOfLastSegment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool MatchComplexSegmentCore(
|
||||||
|
RoutePatternPathSegment routeSegment,
|
||||||
|
string requestSegment,
|
||||||
|
IReadOnlyDictionary<string, object> defaults,
|
||||||
|
RouteValueDictionary values,
|
||||||
|
int indexOfLastSegmentUsed)
|
||||||
|
{
|
||||||
|
Debug.Assert(routeSegment != null);
|
||||||
|
Debug.Assert(routeSegment.Parts.Count > 1);
|
||||||
|
|
||||||
|
// Find last literal segment and get its last index in the string
|
||||||
|
var lastIndex = requestSegment.Length;
|
||||||
|
|
||||||
|
RoutePatternParameterPart parameterNeedsValue = null; // Keeps track of a parameter segment that is pending a value
|
||||||
|
RoutePatternPart lastLiteral = null; // Keeps track of the left-most literal we've encountered
|
||||||
|
|
||||||
|
var outValues = new RouteValueDictionary();
|
||||||
|
|
||||||
|
while (indexOfLastSegmentUsed >= 0)
|
||||||
|
{
|
||||||
|
var 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 = (RoutePatternParameterPart)part;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Debug.Assert(part.IsLiteral || part.IsSeparator);
|
||||||
|
lastLiteral = part;
|
||||||
|
|
||||||
|
var 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;
|
||||||
|
if (part.IsLiteral)
|
||||||
|
{
|
||||||
|
var literal = (RoutePatternLiteralPart)part;
|
||||||
|
indexOfLiteral = requestSegment.LastIndexOf(
|
||||||
|
literal.Content,
|
||||||
|
startIndex,
|
||||||
|
StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var literal = (RoutePatternSeparatorPart)part;
|
||||||
|
indexOfLiteral = requestSegment.LastIndexOf(
|
||||||
|
literal.Content,
|
||||||
|
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 (part is RoutePatternLiteralPart literal && ((indexOfLiteral + literal.Content.Length) != requestSegment.Length))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else if (part is RoutePatternSeparatorPart separator && ((indexOfLiteral + separator.Content.Length) != requestSegment.Length))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newLastIndex = indexOfLiteral;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((parameterNeedsValue != null) &&
|
||||||
|
(((lastLiteral != null) && !part.IsParameter) || (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;
|
||||||
|
Debug.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
|
||||||
|
{
|
||||||
|
if (lastLiteral.IsLiteral)
|
||||||
|
{
|
||||||
|
var literal = (RoutePatternLiteralPart)lastLiteral;
|
||||||
|
parameterStartIndex = newLastIndex + literal.Content.Length;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var separator = (RoutePatternSeparatorPart)lastLiteral;
|
||||||
|
parameterStartIndex = newLastIndex + separator.Content.Length;
|
||||||
|
}
|
||||||
|
parameterTextLength = lastIndex - parameterStartIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var 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
|
||||||
|
outValues.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.
|
||||||
|
if (lastIndex == 0 || routeSegment.Parts[0].IsParameter)
|
||||||
|
{
|
||||||
|
foreach (var item in outValues)
|
||||||
|
{
|
||||||
|
values.Add(item.Key, item.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Routing.Patterns
|
||||||
|
{
|
||||||
|
public enum RoutePatternParameterKind
|
||||||
|
{
|
||||||
|
Standard,
|
||||||
|
Optional,
|
||||||
|
CatchAll,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Routing.Patterns
|
||||||
|
{
|
||||||
|
[DebuggerDisplay("{DebuggerToString()}")]
|
||||||
|
public class RoutePatternParameterPart : RoutePatternPart
|
||||||
|
{
|
||||||
|
internal RoutePatternParameterPart(
|
||||||
|
string parameterName,
|
||||||
|
object @default,
|
||||||
|
RoutePatternParameterKind parameterKind,
|
||||||
|
RoutePatternConstraintReference[] constraints)
|
||||||
|
: base(RoutePatternPartKind.Parameter)
|
||||||
|
{
|
||||||
|
// See #475 - this code should have some asserts, but it can't because of the design of RouteParameterParser.
|
||||||
|
|
||||||
|
Name = parameterName;
|
||||||
|
Default = @default;
|
||||||
|
ParameterKind = parameterKind;
|
||||||
|
Constraints = constraints;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<RoutePatternConstraintReference> Constraints { get; }
|
||||||
|
|
||||||
|
public object Default { get; }
|
||||||
|
|
||||||
|
public bool IsCatchAll => ParameterKind == RoutePatternParameterKind.CatchAll;
|
||||||
|
|
||||||
|
public bool IsOptional => ParameterKind == RoutePatternParameterKind.Optional;
|
||||||
|
|
||||||
|
public RoutePatternParameterKind ParameterKind { get; }
|
||||||
|
|
||||||
|
public string Name { get; }
|
||||||
|
|
||||||
|
internal override string DebuggerToString()
|
||||||
|
{
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
builder.Append("{");
|
||||||
|
|
||||||
|
if (IsCatchAll)
|
||||||
|
{
|
||||||
|
builder.Append("*");
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Append(Name);
|
||||||
|
|
||||||
|
foreach (var constraint in Constraints)
|
||||||
|
{
|
||||||
|
builder.Append(":");
|
||||||
|
builder.Append(constraint.Constraint);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Default != null)
|
||||||
|
{
|
||||||
|
builder.Append("=");
|
||||||
|
builder.Append(Default);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsOptional)
|
||||||
|
{
|
||||||
|
builder.Append("?");
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Append("}");
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,577 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Routing.Patterns
|
||||||
|
{
|
||||||
|
internal static class RoutePatternParser
|
||||||
|
{
|
||||||
|
private const char Separator = '/';
|
||||||
|
private const char OpenBrace = '{';
|
||||||
|
private const char CloseBrace = '}';
|
||||||
|
private const char EqualsSign = '=';
|
||||||
|
private const char QuestionMark = '?';
|
||||||
|
private const char Asterisk = '*';
|
||||||
|
private const string PeriodString = ".";
|
||||||
|
|
||||||
|
internal static readonly char[] InvalidParameterNameChars = new char[]
|
||||||
|
{
|
||||||
|
Separator,
|
||||||
|
OpenBrace,
|
||||||
|
CloseBrace,
|
||||||
|
QuestionMark,
|
||||||
|
Asterisk
|
||||||
|
};
|
||||||
|
|
||||||
|
public static RoutePattern Parse(string pattern)
|
||||||
|
{
|
||||||
|
if (pattern == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(pattern));
|
||||||
|
}
|
||||||
|
|
||||||
|
var trimmedPattern = TrimPrefix(pattern);
|
||||||
|
|
||||||
|
var context = new Context(trimmedPattern);
|
||||||
|
var segments = new List<RoutePatternPathSegment>();
|
||||||
|
|
||||||
|
while (context.MoveNext())
|
||||||
|
{
|
||||||
|
var i = context.Index;
|
||||||
|
|
||||||
|
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 RoutePatternException(pattern, Resources.TemplateRoute_CannotHaveConsecutiveSeparators);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ParseSegment(context, segments))
|
||||||
|
{
|
||||||
|
throw new RoutePatternException(pattern, context.Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A successful parse should always result in us being at the end or at a separator.
|
||||||
|
Debug.Assert(context.AtEnd() || context.Current == Separator);
|
||||||
|
|
||||||
|
if (context.Index <= i)
|
||||||
|
{
|
||||||
|
// This shouldn't happen, but we want to crash if it does.
|
||||||
|
var message = "Infinite loop detected in the parser. Please open an issue.";
|
||||||
|
throw new InvalidProgramException(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsAllValid(context, segments))
|
||||||
|
{
|
||||||
|
return RoutePatternFactory.Pattern(pattern, segments);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new RoutePatternException(pattern, context.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ParseSegment(Context context, List<RoutePatternPathSegment> segments)
|
||||||
|
{
|
||||||
|
Debug.Assert(context != null);
|
||||||
|
Debug.Assert(segments != null);
|
||||||
|
|
||||||
|
var parts = new List<RoutePatternPart>();
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var i = context.Index;
|
||||||
|
|
||||||
|
if (context.Current == OpenBrace)
|
||||||
|
{
|
||||||
|
if (!context.MoveNext())
|
||||||
|
{
|
||||||
|
// 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, parts))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// This is a parameter
|
||||||
|
context.Back();
|
||||||
|
if (!ParseParameter(context, parts))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!ParseLiteral(context, parts))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.Current == Separator || context.AtEnd())
|
||||||
|
{
|
||||||
|
// We've reached the end of the segment
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.Index <= i)
|
||||||
|
{
|
||||||
|
// This shouldn't happen, but we want to crash if it does.
|
||||||
|
var message = "Infinite loop detected in the parser. Please open an issue.";
|
||||||
|
throw new InvalidProgramException(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsSegmentValid(context, parts))
|
||||||
|
{
|
||||||
|
segments.Add(new RoutePatternPathSegment(parts.ToArray()));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ParseParameter(Context context, List<RoutePatternPart> parts)
|
||||||
|
{
|
||||||
|
Debug.Assert(context.Current == OpenBrace);
|
||||||
|
context.Mark();
|
||||||
|
|
||||||
|
context.MoveNext();
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
if (context.Current == OpenBrace)
|
||||||
|
{
|
||||||
|
// This is an open brace inside of a parameter, it has to be escaped
|
||||||
|
if (context.MoveNext())
|
||||||
|
{
|
||||||
|
if (context.Current != OpenBrace)
|
||||||
|
{
|
||||||
|
// If we see something like "{p1:regex(^\d{3", we will come here.
|
||||||
|
context.Error = Resources.TemplateRoute_UnescapedBrace;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// This is a dangling open-brace, which is not allowed
|
||||||
|
// Example: "{p1:regex(^\d{"
|
||||||
|
context.Error = Resources.TemplateRoute_MismatchedParameter;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (context.Current == CloseBrace)
|
||||||
|
{
|
||||||
|
// When we encounter Closed brace here, it either means end of the parameter or it is a closed
|
||||||
|
// brace in the parameter, in that case it needs to be escaped.
|
||||||
|
// Example: {p1:regex(([}}])\w+}. First pair is escaped one and last marks end of the parameter
|
||||||
|
if (!context.MoveNext())
|
||||||
|
{
|
||||||
|
// This is the end of the string -and we have a valid parameter
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.Current == CloseBrace)
|
||||||
|
{
|
||||||
|
// This is an 'escaped' brace in a parameter name
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// This is the end of the parameter
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!context.MoveNext())
|
||||||
|
{
|
||||||
|
// This is a dangling open-brace, which is not allowed
|
||||||
|
context.Error = Resources.TemplateRoute_MismatchedParameter;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var text = context.Capture();
|
||||||
|
if (text == "{}")
|
||||||
|
{
|
||||||
|
context.Error = Resources.FormatTemplateRoute_InvalidParameterName(string.Empty);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var inside = text.Substring(1, text.Length - 2);
|
||||||
|
var decoded = inside.Replace("}}", "}").Replace("{{", "{");
|
||||||
|
|
||||||
|
// At this point, we need to parse the raw name for inline constraint,
|
||||||
|
// default values and optional parameters.
|
||||||
|
var templatePart = RouteParameterParser.ParseRouteParameter(decoded);
|
||||||
|
|
||||||
|
// See #475 - this is here because InlineRouteParameterParser can't return errors
|
||||||
|
if (decoded.StartsWith("*", StringComparison.Ordinal) && decoded.EndsWith("?", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
context.Error = Resources.TemplateRoute_CatchAllCannotBeOptional;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (templatePart.IsOptional && templatePart.Default != null)
|
||||||
|
{
|
||||||
|
// Cannot be optional and have a default value.
|
||||||
|
// The only way to declare an optional parameter is to have a ? at the end,
|
||||||
|
// hence we cannot have both default value and optional parameter within the template.
|
||||||
|
// A workaround is to add it as a separate entry in the defaults argument.
|
||||||
|
context.Error = Resources.TemplateRoute_OptionalCannotHaveDefaultValue;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var parameterName = templatePart.Name;
|
||||||
|
if (IsValidParameterName(context, parameterName))
|
||||||
|
{
|
||||||
|
parts.Add(templatePart);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ParseLiteral(Context context, List<RoutePatternPart> parts)
|
||||||
|
{
|
||||||
|
context.Mark();
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
if (context.Current == Separator)
|
||||||
|
{
|
||||||
|
// End of the segment
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else if (context.Current == OpenBrace)
|
||||||
|
{
|
||||||
|
if (!context.MoveNext())
|
||||||
|
{
|
||||||
|
// 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.
|
||||||
|
context.Back();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (context.Current == CloseBrace)
|
||||||
|
{
|
||||||
|
if (!context.MoveNext())
|
||||||
|
{
|
||||||
|
// 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.MoveNext())
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var encoded = context.Capture();
|
||||||
|
var decoded = encoded.Replace("}}", "}").Replace("{{", "{");
|
||||||
|
if (IsValidLiteral(context, decoded))
|
||||||
|
{
|
||||||
|
parts.Add(RoutePatternFactory.LiteralPart(decoded));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsAllValid(Context context, List<RoutePatternPathSegment> segments)
|
||||||
|
{
|
||||||
|
// A catch-all parameter must be the last part of the last segment
|
||||||
|
for (var i = 0; i < segments.Count; i++)
|
||||||
|
{
|
||||||
|
var segment = segments[i];
|
||||||
|
for (var j = 0; j < segment.Parts.Count; j++)
|
||||||
|
{
|
||||||
|
var part = segment.Parts[j];
|
||||||
|
if (part is RoutePatternParameterPart parameter
|
||||||
|
&& parameter.IsCatchAll &&
|
||||||
|
(i != segments.Count - 1 || j != segment.Parts.Count - 1))
|
||||||
|
{
|
||||||
|
context.Error = Resources.TemplateRoute_CatchAllMustBeLast;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsSegmentValid(Context context, List<RoutePatternPart> parts)
|
||||||
|
{
|
||||||
|
// If a segment has multiple parts, then it can't contain a catch all.
|
||||||
|
for (var i = 0; i < parts.Count; i++)
|
||||||
|
{
|
||||||
|
var part = parts[i];
|
||||||
|
if (part is RoutePatternParameterPart parameter && parameter.IsCatchAll && parts.Count > 1)
|
||||||
|
{
|
||||||
|
context.Error = Resources.TemplateRoute_CannotHaveCatchAllInMultiSegment;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if a segment has multiple parts, then only the last one parameter can be optional
|
||||||
|
// if it is following a optional seperator.
|
||||||
|
for (var i = 0; i < parts.Count; i++)
|
||||||
|
{
|
||||||
|
var part = parts[i];
|
||||||
|
|
||||||
|
if (part is RoutePatternParameterPart parameter && parameter.IsOptional && parts.Count > 1)
|
||||||
|
{
|
||||||
|
// This optional parameter is the last part in the segment
|
||||||
|
if (i == parts.Count - 1)
|
||||||
|
{
|
||||||
|
var previousPart = parts[i - 1];
|
||||||
|
|
||||||
|
if (!previousPart.IsLiteral && !previousPart.IsSeparator)
|
||||||
|
{
|
||||||
|
// The optional parameter is preceded by something that is not a literal or separator
|
||||||
|
// Example of error message:
|
||||||
|
// "In the segment '{RouteValue}{param?}', the optional parameter 'param' is preceded
|
||||||
|
// by an invalid segment '{RouteValue}'. Only a period (.) can precede an optional parameter.
|
||||||
|
context.Error = string.Format(
|
||||||
|
Resources.TemplateRoute_OptionalParameterCanbBePrecededByPeriod,
|
||||||
|
RoutePatternPathSegment.DebuggerToString(parts),
|
||||||
|
parameter.Name,
|
||||||
|
parts[i - 1].DebuggerToString());
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else if (previousPart is RoutePatternLiteralPart literal && literal.Content != PeriodString)
|
||||||
|
{
|
||||||
|
// The optional parameter is preceded by a literal other than period.
|
||||||
|
// Example of error message:
|
||||||
|
// "In the segment '{RouteValue}-{param?}', the optional parameter 'param' is preceded
|
||||||
|
// by an invalid segment '-'. Only a period (.) can precede an optional parameter.
|
||||||
|
context.Error = string.Format(
|
||||||
|
Resources.TemplateRoute_OptionalParameterCanbBePrecededByPeriod,
|
||||||
|
RoutePatternPathSegment.DebuggerToString(parts),
|
||||||
|
parameter.Name,
|
||||||
|
parts[i - 1].DebuggerToString());
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
parts[i - 1] = RoutePatternFactory.SeparatorPart(((RoutePatternLiteralPart)previousPart).Content);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// This optional parameter is not the last one in the segment
|
||||||
|
// Example:
|
||||||
|
// An optional parameter must be at the end of the segment. In the segment '{RouteValue?})',
|
||||||
|
// optional parameter 'RouteValue' is followed by ')'
|
||||||
|
context.Error = string.Format(
|
||||||
|
Resources.TemplateRoute_OptionalParameterHasTobeTheLast,
|
||||||
|
RoutePatternPathSegment.DebuggerToString(parts),
|
||||||
|
parameter.Name,
|
||||||
|
parts[i + 1].DebuggerToString());
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A segment cannot contain two consecutive parameters
|
||||||
|
var isLastSegmentParameter = false;
|
||||||
|
for (var i = 0; i < parts.Count; i++)
|
||||||
|
{
|
||||||
|
var part = parts[i];
|
||||||
|
if (part.IsParameter && isLastSegmentParameter)
|
||||||
|
{
|
||||||
|
context.Error = Resources.TemplateRoute_CannotHaveConsecutiveParameters;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
isLastSegmentParameter = part.IsParameter;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsValidParameterName(Context context, string parameterName)
|
||||||
|
{
|
||||||
|
if (parameterName.Length == 0 || parameterName.IndexOfAny(InvalidParameterNameChars) >= 0)
|
||||||
|
{
|
||||||
|
context.Error = Resources.FormatTemplateRoute_InvalidParameterName(parameterName);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!context.ParameterNames.Add(parameterName))
|
||||||
|
{
|
||||||
|
context.Error = Resources.FormatTemplateRoute_RepeatedParameter(parameterName);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsValidLiteral(Context context, string literal)
|
||||||
|
{
|
||||||
|
Debug.Assert(context != null);
|
||||||
|
Debug.Assert(literal != null);
|
||||||
|
|
||||||
|
if (literal.IndexOf(QuestionMark) != -1)
|
||||||
|
{
|
||||||
|
context.Error = Resources.FormatTemplateRoute_InvalidLiteral(literal);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string TrimPrefix(string routePattern)
|
||||||
|
{
|
||||||
|
if (routePattern.StartsWith("~/", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return routePattern.Substring(2);
|
||||||
|
}
|
||||||
|
else if (routePattern.StartsWith("/", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return routePattern.Substring(1);
|
||||||
|
}
|
||||||
|
else if (routePattern.StartsWith("~", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
throw new RoutePatternException(routePattern, Resources.TemplateRoute_InvalidRouteTemplate);
|
||||||
|
}
|
||||||
|
return routePattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DebuggerDisplay("{DebuggerToString()}")]
|
||||||
|
private class Context
|
||||||
|
{
|
||||||
|
private readonly string _template;
|
||||||
|
private int _index;
|
||||||
|
private int? _mark;
|
||||||
|
|
||||||
|
private readonly HashSet<string> _parameterNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
public Context(string template)
|
||||||
|
{
|
||||||
|
Debug.Assert(template != null);
|
||||||
|
_template = template;
|
||||||
|
|
||||||
|
_index = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public char Current
|
||||||
|
{
|
||||||
|
get { return (_index < _template.Length && _index >= 0) ? _template[_index] : (char)0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Index => _index;
|
||||||
|
|
||||||
|
public string Error
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HashSet<string> ParameterNames
|
||||||
|
{
|
||||||
|
get { return _parameterNames; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Back()
|
||||||
|
{
|
||||||
|
return --_index >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool AtEnd()
|
||||||
|
{
|
||||||
|
return _index >= _template.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool MoveNext()
|
||||||
|
{
|
||||||
|
return ++_index < _template.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Mark()
|
||||||
|
{
|
||||||
|
Debug.Assert(_index >= 0);
|
||||||
|
|
||||||
|
// Index is always the index of the character *past* Current - we want to 'mark' Current.
|
||||||
|
_mark = _index;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Capture()
|
||||||
|
{
|
||||||
|
if (_mark.HasValue)
|
||||||
|
{
|
||||||
|
var value = _template.Substring(_mark.Value, _index - _mark.Value);
|
||||||
|
_mark = null;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string DebuggerToString()
|
||||||
|
{
|
||||||
|
if (_index == -1)
|
||||||
|
{
|
||||||
|
return _template;
|
||||||
|
}
|
||||||
|
else if (_mark.HasValue)
|
||||||
|
{
|
||||||
|
return _template.Substring(0, _mark.Value) +
|
||||||
|
"|" +
|
||||||
|
_template.Substring(_mark.Value, _index - _mark.Value) +
|
||||||
|
"|" +
|
||||||
|
_template.Substring(_index);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return _template.Substring(0, _index) + "|" + _template.Substring(_index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Routing.Patterns
|
||||||
|
{
|
||||||
|
public abstract class RoutePatternPart
|
||||||
|
{
|
||||||
|
// This class is **not** an extensibility point - every part of the routing system
|
||||||
|
// needs to be aware of what kind of parts we support.
|
||||||
|
//
|
||||||
|
// It is abstract so we can add semantics later inside the library.
|
||||||
|
private protected RoutePatternPart(RoutePatternPartKind partKind)
|
||||||
|
{
|
||||||
|
PartKind = partKind;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RoutePatternPartKind PartKind { get; }
|
||||||
|
|
||||||
|
public bool IsLiteral => PartKind == RoutePatternPartKind.Literal;
|
||||||
|
|
||||||
|
public bool IsParameter => PartKind == RoutePatternPartKind.Parameter;
|
||||||
|
|
||||||
|
public bool IsSeparator => PartKind == RoutePatternPartKind.Separator;
|
||||||
|
|
||||||
|
internal abstract string DebuggerToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Routing.Patterns
|
||||||
|
{
|
||||||
|
public enum RoutePatternPartKind
|
||||||
|
{
|
||||||
|
Literal,
|
||||||
|
Parameter,
|
||||||
|
Separator,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Routing.Patterns
|
||||||
|
{
|
||||||
|
[DebuggerDisplay("{DebuggerToString()}")]
|
||||||
|
public sealed class RoutePatternPathSegment
|
||||||
|
{
|
||||||
|
internal RoutePatternPathSegment(RoutePatternPart[] parts)
|
||||||
|
{
|
||||||
|
Parts = parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsSimple => Parts.Count == 1;
|
||||||
|
|
||||||
|
public IReadOnlyList<RoutePatternPart> Parts { get; }
|
||||||
|
|
||||||
|
internal string DebuggerToString()
|
||||||
|
{
|
||||||
|
return DebuggerToString(Parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string DebuggerToString(IReadOnlyList<RoutePatternPart> parts)
|
||||||
|
{
|
||||||
|
return string.Join(string.Empty, parts.Select(p => p.DebuggerToString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Routing.Patterns
|
||||||
|
{
|
||||||
|
[DebuggerDisplay("{DebuggerToString()}")]
|
||||||
|
public sealed class RoutePatternSeparatorPart : RoutePatternPart
|
||||||
|
{
|
||||||
|
internal RoutePatternSeparatorPart(string content)
|
||||||
|
: base(RoutePatternPartKind.Separator)
|
||||||
|
{
|
||||||
|
Debug.Assert(!string.IsNullOrEmpty(content));
|
||||||
|
|
||||||
|
Content = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Content { get; }
|
||||||
|
|
||||||
|
internal override string DebuggerToString()
|
||||||
|
{
|
||||||
|
return Content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -416,6 +416,48 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
internal static string FormatAmbiguousEndpoints(object p0, object p1)
|
internal static string FormatAmbiguousEndpoints(object p0, object p1)
|
||||||
=> string.Format(CultureInfo.CurrentCulture, GetString("AmbiguousEndpoints"), p0, p1);
|
=> string.Format(CultureInfo.CurrentCulture, GetString("AmbiguousEndpoints"), p0, p1);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Value cannot be null or empty.
|
||||||
|
/// </summary>
|
||||||
|
internal static string Argument_NullOrEmpty
|
||||||
|
{
|
||||||
|
get => GetString("Argument_NullOrEmpty");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Value cannot be null or empty.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatArgument_NullOrEmpty()
|
||||||
|
=> GetString("Argument_NullOrEmpty");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The collection cannot be empty.
|
||||||
|
/// </summary>
|
||||||
|
internal static string RoutePatternBuilder_CollectionCannotBeEmpty
|
||||||
|
{
|
||||||
|
get => GetString("RoutePatternBuilder_CollectionCannotBeEmpty");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The collection cannot be empty.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatRoutePatternBuilder_CollectionCannotBeEmpty()
|
||||||
|
=> GetString("RoutePatternBuilder_CollectionCannotBeEmpty");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The constraint entry '{0}' - '{1}' must have a string value or be of a type which implements '{2}'.
|
||||||
|
/// </summary>
|
||||||
|
internal static string ConstraintMustBeStringOrConstraint
|
||||||
|
{
|
||||||
|
get => GetString("ConstraintMustBeStringOrConstraint");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The constraint entry '{0}' - '{1}' must have a string value or be of a type which implements '{2}'.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatConstraintMustBeStringOrConstraint(object p0, object p1, object p2)
|
||||||
|
=> string.Format(CultureInfo.CurrentCulture, GetString("ConstraintMustBeStringOrConstraint"), p0, p1, p2);
|
||||||
|
|
||||||
private static string GetString(string name, params string[] formatterNames)
|
private static string GetString(string name, params string[] formatterNames)
|
||||||
{
|
{
|
||||||
var value = _resourceManager.GetString(name);
|
var value = _resourceManager.GetString(name);
|
||||||
|
|
|
||||||
|
|
@ -204,4 +204,13 @@
|
||||||
<data name="AmbiguousEndpoints" xml:space="preserve">
|
<data name="AmbiguousEndpoints" xml:space="preserve">
|
||||||
<value>The request matched multiple endpoints. Matches: {0}{0}{1}</value>
|
<value>The request matched multiple endpoints. Matches: {0}{0}{1}</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Argument_NullOrEmpty" xml:space="preserve">
|
||||||
|
<value>Value cannot be null or empty.</value>
|
||||||
|
</data>
|
||||||
|
<data name="RoutePatternBuilder_CollectionCannotBeEmpty" xml:space="preserve">
|
||||||
|
<value>The collection cannot be empty.</value>
|
||||||
|
</data>
|
||||||
|
<data name="ConstraintMustBeStringOrConstraint" xml:space="preserve">
|
||||||
|
<value>The constraint entry '{0}' - '{1}' must have a string value or be of a type which implements '{2}'.</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|
@ -161,7 +161,7 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
queryString = queryString.ToLowerInvariant();
|
queryString = queryString.ToLowerInvariant();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_options.AppendTrailingSlash && !urlWithoutQueryString.EndsWith("/"))
|
if (_options.AppendTrailingSlash && !urlWithoutQueryString.EndsWith("/", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
urlWithoutQueryString += "/";
|
urlWithoutQueryString += "/";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using Microsoft.AspNetCore.Routing.Patterns;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing.Template
|
namespace Microsoft.AspNetCore.Routing.Template
|
||||||
{
|
{
|
||||||
|
|
@ -24,6 +25,16 @@ namespace Microsoft.AspNetCore.Routing.Template
|
||||||
Constraint = constraint;
|
Constraint = constraint;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public InlineConstraint(RoutePatternConstraintReference other)
|
||||||
|
{
|
||||||
|
if (other == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(other));
|
||||||
|
}
|
||||||
|
|
||||||
|
Constraint = other.Content;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the constraint text.
|
/// Gets the constraint text.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Microsoft.AspNetCore.Routing.Patterns;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing.Template
|
namespace Microsoft.AspNetCore.Routing.Template
|
||||||
{
|
{
|
||||||
|
|
@ -13,6 +14,25 @@ namespace Microsoft.AspNetCore.Routing.Template
|
||||||
{
|
{
|
||||||
private const string SeparatorString = "/";
|
private const string SeparatorString = "/";
|
||||||
|
|
||||||
|
public RouteTemplate(RoutePattern other)
|
||||||
|
{
|
||||||
|
TemplateText = other.RawText;
|
||||||
|
Segments = new List<TemplateSegment>(other.PathSegments.Select(p => new TemplateSegment(p)));
|
||||||
|
Parameters = new List<TemplatePart>();
|
||||||
|
for (var i = 0; i < Segments.Count; i++)
|
||||||
|
{
|
||||||
|
var segment = Segments[i];
|
||||||
|
for (var j = 0; j < segment.Parts.Count; j++)
|
||||||
|
{
|
||||||
|
var part = segment.Parts[j];
|
||||||
|
if (part.IsParameter)
|
||||||
|
{
|
||||||
|
Parameters.Add(part);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public RouteTemplate(string template, List<TemplateSegment> segments)
|
public RouteTemplate(string template, List<TemplateSegment> segments)
|
||||||
{
|
{
|
||||||
if (segments == null)
|
if (segments == null)
|
||||||
|
|
@ -78,5 +98,16 @@ namespace Microsoft.AspNetCore.Routing.Template
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts the <see cref="RouteTemplate"/> to the equivalent
|
||||||
|
/// <see cref="RoutePattern"/>
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A <see cref="RoutePattern"/>.</returns>
|
||||||
|
public RoutePattern ToRoutePattern()
|
||||||
|
{
|
||||||
|
var segments = Segments.Select(s => s.ToRoutePatternPathSegment());
|
||||||
|
return RoutePatternFactory.Pattern(TemplateText, segments);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,8 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using Microsoft.AspNetCore.Routing;
|
||||||
using System.Diagnostics;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Routing.Internal;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing.Template
|
namespace Microsoft.AspNetCore.Routing.Template
|
||||||
{
|
{
|
||||||
|
|
@ -19,6 +17,7 @@ namespace Microsoft.AspNetCore.Routing.Template
|
||||||
private readonly object[] _defaultValues;
|
private readonly object[] _defaultValues;
|
||||||
|
|
||||||
private static readonly char[] Delimiters = new char[] { SeparatorChar };
|
private static readonly char[] Delimiters = new char[] { SeparatorChar };
|
||||||
|
private RoutePatternMatcher _routePatternMatcher;
|
||||||
|
|
||||||
public TemplateMatcher(
|
public TemplateMatcher(
|
||||||
RouteTemplate template,
|
RouteTemplate template,
|
||||||
|
|
@ -57,6 +56,9 @@ namespace Microsoft.AspNetCore.Routing.Template
|
||||||
_defaultValues[i] = value;
|
_defaultValues[i] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var routePattern = Template.ToRoutePattern();
|
||||||
|
_routePatternMatcher = new RoutePatternMatcher(routePattern, Defaults);
|
||||||
}
|
}
|
||||||
|
|
||||||
public RouteValueDictionary Defaults { get; }
|
public RouteValueDictionary Defaults { get; }
|
||||||
|
|
@ -70,387 +72,7 @@ namespace Microsoft.AspNetCore.Routing.Template
|
||||||
throw new ArgumentNullException(nameof(values));
|
throw new ArgumentNullException(nameof(values));
|
||||||
}
|
}
|
||||||
|
|
||||||
var i = 0;
|
return _routePatternMatcher.TryMatch(path, values);
|
||||||
var pathTokenizer = new PathTokenizer(path);
|
|
||||||
|
|
||||||
// Perf: We do a traversal of the request-segments + route-segments twice.
|
|
||||||
//
|
|
||||||
// For most segment-types, we only really need to any work on one of the two passes.
|
|
||||||
//
|
|
||||||
// On the first pass, we're just looking to see if there's anything that would disqualify us from matching.
|
|
||||||
// The most common case would be a literal segment that doesn't match.
|
|
||||||
//
|
|
||||||
// On the second pass, we're almost certainly going to match the URL, so go ahead and allocate the 'values'
|
|
||||||
// and start capturing strings.
|
|
||||||
foreach (var pathSegment in pathTokenizer)
|
|
||||||
{
|
|
||||||
if (pathSegment.Length == 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var routeSegment = Template.GetSegment(i++);
|
|
||||||
if (routeSegment == null && pathSegment.Length > 0)
|
|
||||||
{
|
|
||||||
// If routeSegment is null, then we're out of route segments. All we can match is the empty
|
|
||||||
// string.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
else if (routeSegment.IsSimple && routeSegment.Parts[0].IsLiteral)
|
|
||||||
{
|
|
||||||
// This is a literal segment, so we need to match the text, or the route isn't a match.
|
|
||||||
var part = routeSegment.Parts[0];
|
|
||||||
if (!pathSegment.Equals(part.Text, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (routeSegment.IsSimple && routeSegment.Parts[0].IsCatchAll)
|
|
||||||
{
|
|
||||||
// Nothing to validate for a catch-all - it can match any string, including the empty string.
|
|
||||||
//
|
|
||||||
// Also, a catch-all has to be the last part, so we're done.
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
else if (routeSegment.IsSimple && routeSegment.Parts[0].IsParameter)
|
|
||||||
{
|
|
||||||
// For a parameter, validate that it's a has some length, or we have a default, or it's optional.
|
|
||||||
var part = routeSegment.Parts[0];
|
|
||||||
if (pathSegment.Length == 0 &&
|
|
||||||
!_hasDefaultValue[i] &&
|
|
||||||
!part.IsOptional)
|
|
||||||
{
|
|
||||||
// There's no value for this parameter, the route can't match.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Debug.Assert(!routeSegment.IsSimple);
|
|
||||||
|
|
||||||
// Don't attempt to validate a complex segment at this point other than being non-emtpy,
|
|
||||||
// do it in the second pass.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (; i < Template.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 = Template.GetSegment(i);
|
|
||||||
Debug.Assert(routeSegment != null);
|
|
||||||
|
|
||||||
if (!routeSegment.IsSimple)
|
|
||||||
{
|
|
||||||
// If the segment is a complex segment, it MUST contain literals, and we've parsed the full
|
|
||||||
// path so far, so it can't match.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var part = routeSegment.Parts[0];
|
|
||||||
if (part.IsLiteral)
|
|
||||||
{
|
|
||||||
// If the segment is a simple literal - which need the URL to provide a value, so we don't match.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (part.IsCatchAll)
|
|
||||||
{
|
|
||||||
// Nothing to validate for a catch-all - it can match any string, including the empty string.
|
|
||||||
//
|
|
||||||
// Also, a catch-all has to be the last part, so we're done.
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we get here, this is a simple segment with a parameter. We need it to be optional, or for the
|
|
||||||
// defaults to have a value.
|
|
||||||
Debug.Assert(routeSegment.IsSimple && part.IsParameter);
|
|
||||||
if (!_hasDefaultValue[i] && !part.IsOptional)
|
|
||||||
{
|
|
||||||
// There's no default for this (non-optional) parameter so it can't match.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// At this point we've very likely got a match, so start capturing values for real.
|
|
||||||
|
|
||||||
i = 0;
|
|
||||||
foreach (var requestSegment in pathTokenizer)
|
|
||||||
{
|
|
||||||
var routeSegment = Template.GetSegment(i++);
|
|
||||||
|
|
||||||
if (routeSegment.IsSimple && routeSegment.Parts[0].IsCatchAll)
|
|
||||||
{
|
|
||||||
// A catch-all captures til the end of the string.
|
|
||||||
var part = routeSegment.Parts[0];
|
|
||||||
var captured = requestSegment.Buffer.Substring(requestSegment.Offset);
|
|
||||||
if (captured.Length > 0)
|
|
||||||
{
|
|
||||||
values[part.Name] = captured;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// It's ok for a catch-all to produce a null value, so we don't check _hasDefaultValue.
|
|
||||||
values[part.Name] = _defaultValues[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
// A catch-all has to be the last part, so we're done.
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
else if (routeSegment.IsSimple && routeSegment.Parts[0].IsParameter)
|
|
||||||
{
|
|
||||||
// A simple parameter captures the whole segment, or a default value if nothing was
|
|
||||||
// provided.
|
|
||||||
var part = routeSegment.Parts[0];
|
|
||||||
if (requestSegment.Length > 0)
|
|
||||||
{
|
|
||||||
values[part.Name] = requestSegment.ToString();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (_hasDefaultValue[i])
|
|
||||||
{
|
|
||||||
values[part.Name] = _defaultValues[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (!routeSegment.IsSimple)
|
|
||||||
{
|
|
||||||
if (!MatchComplexSegment(routeSegment, requestSegment.ToString(), Defaults, values))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (; i < Template.Segments.Count; i++)
|
|
||||||
{
|
|
||||||
// We've matched the request path so far, but still have remaining route segments. We already know these
|
|
||||||
// are simple parameters that either have a default, or don't need to produce a value.
|
|
||||||
var routeSegment = Template.GetSegment(i);
|
|
||||||
Debug.Assert(routeSegment != null);
|
|
||||||
Debug.Assert(routeSegment.IsSimple);
|
|
||||||
|
|
||||||
var part = routeSegment.Parts[0];
|
|
||||||
Debug.Assert(part.IsParameter);
|
|
||||||
|
|
||||||
// It's ok for a catch-all to produce a null value
|
|
||||||
if (_hasDefaultValue[i] || part.IsCatchAll)
|
|
||||||
{
|
|
||||||
// Don't replace an existing value with a null.
|
|
||||||
var defaultValue = _defaultValues[i];
|
|
||||||
if (defaultValue != null || !values.ContainsKey(part.Name))
|
|
||||||
{
|
|
||||||
values[part.Name] = defaultValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy all remaining default values to the route data
|
|
||||||
foreach (var kvp in Defaults)
|
|
||||||
{
|
|
||||||
if (!values.ContainsKey(kvp.Key))
|
|
||||||
{
|
|
||||||
values.Add(kvp.Key, kvp.Value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool MatchComplexSegment(
|
|
||||||
TemplateSegment routeSegment,
|
|
||||||
string requestSegment,
|
|
||||||
IReadOnlyDictionary<string, object> defaults,
|
|
||||||
RouteValueDictionary values)
|
|
||||||
{
|
|
||||||
var indexOfLastSegment = routeSegment.Parts.Count - 1;
|
|
||||||
|
|
||||||
// We match the request to the template starting at the rightmost parameter
|
|
||||||
// If the last segment of template is optional, then request can match the
|
|
||||||
// template with or without the last parameter. So we start with regular matching,
|
|
||||||
// but if it doesn't match, we start with next to last parameter. Example:
|
|
||||||
// Template: {p1}/{p2}.{p3?}. If the request is one/two.three it will match right away
|
|
||||||
// giving p3 value of three. But if the request is one/two, we start matching from the
|
|
||||||
// rightmost giving p3 the value of two, then we end up not matching the segment.
|
|
||||||
// In this case we start again from p2 to match the request and we succeed giving
|
|
||||||
// the value two to p2
|
|
||||||
if (routeSegment.Parts[indexOfLastSegment].IsOptional &&
|
|
||||||
routeSegment.Parts[indexOfLastSegment - 1].IsOptionalSeperator)
|
|
||||||
{
|
|
||||||
if (MatchComplexSegmentCore(routeSegment, requestSegment, Defaults, values, indexOfLastSegment))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (requestSegment.EndsWith(
|
|
||||||
routeSegment.Parts[indexOfLastSegment - 1].Text,
|
|
||||||
StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return MatchComplexSegmentCore(
|
|
||||||
routeSegment,
|
|
||||||
requestSegment,
|
|
||||||
Defaults,
|
|
||||||
values,
|
|
||||||
indexOfLastSegment - 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return MatchComplexSegmentCore(routeSegment, requestSegment, Defaults, values, indexOfLastSegment);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool MatchComplexSegmentCore(
|
|
||||||
TemplateSegment routeSegment,
|
|
||||||
string requestSegment,
|
|
||||||
IReadOnlyDictionary<string, object> defaults,
|
|
||||||
RouteValueDictionary values,
|
|
||||||
int indexOfLastSegmentUsed)
|
|
||||||
{
|
|
||||||
Debug.Assert(routeSegment != null);
|
|
||||||
Debug.Assert(routeSegment.Parts.Count > 1);
|
|
||||||
|
|
||||||
// Find last literal segment and get its last index in the string
|
|
||||||
var lastIndex = requestSegment.Length;
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
var outValues = new RouteValueDictionary();
|
|
||||||
|
|
||||||
while (indexOfLastSegmentUsed >= 0)
|
|
||||||
{
|
|
||||||
var 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
|
|
||||||
{
|
|
||||||
Debug.Assert(part.IsLiteral);
|
|
||||||
lastLiteral = part;
|
|
||||||
|
|
||||||
var 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
var 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;
|
|
||||||
Debug.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var 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
|
|
||||||
outValues.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.
|
|
||||||
if (lastIndex == 0 || routeSegment.Parts[0].IsParameter)
|
|
||||||
{
|
|
||||||
foreach (var item in outValues)
|
|
||||||
{
|
|
||||||
values.Add(item.Key, item.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,539 +1,29 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using Microsoft.AspNetCore.Routing.Patterns;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Globalization;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing.Template
|
namespace Microsoft.AspNetCore.Routing.Template
|
||||||
{
|
{
|
||||||
public static class TemplateParser
|
public static class TemplateParser
|
||||||
{
|
{
|
||||||
private const char Separator = '/';
|
|
||||||
private const char OpenBrace = '{';
|
|
||||||
private const char CloseBrace = '}';
|
|
||||||
private const char EqualsSign = '=';
|
|
||||||
private const char QuestionMark = '?';
|
|
||||||
private const char Asterisk = '*';
|
|
||||||
private const string PeriodString = ".";
|
|
||||||
|
|
||||||
public static RouteTemplate Parse(string routeTemplate)
|
public static RouteTemplate Parse(string routeTemplate)
|
||||||
{
|
{
|
||||||
if (routeTemplate == null)
|
if (routeTemplate == null)
|
||||||
{
|
{
|
||||||
routeTemplate = String.Empty;
|
throw new ArgumentNullException(routeTemplate);
|
||||||
}
|
}
|
||||||
|
|
||||||
var trimmedRouteTemplate = TrimPrefix(routeTemplate);
|
try
|
||||||
|
|
||||||
var context = new TemplateParserContext(trimmedRouteTemplate);
|
|
||||||
var segments = new List<TemplateSegment>();
|
|
||||||
|
|
||||||
while (context.Next())
|
|
||||||
{
|
{
|
||||||
if (context.Current == Separator)
|
var inner = RoutePatternFactory.Parse(routeTemplate);
|
||||||
{
|
return new RouteTemplate(inner);
|
||||||
// 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,
|
|
||||||
nameof(routeTemplate));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (!ParseSegment(context, segments))
|
|
||||||
{
|
|
||||||
throw new ArgumentException(context.Error, nameof(routeTemplate));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
catch (RoutePatternException ex)
|
||||||
if (IsAllValid(context, segments))
|
|
||||||
{
|
{
|
||||||
return new RouteTemplate(routeTemplate, segments);
|
// Preserving the existing behavior of this API even though the logic moved.
|
||||||
}
|
throw new ArgumentException(ex.Message, nameof(routeTemplate), ex);
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new ArgumentException(context.Error, nameof(routeTemplate));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string TrimPrefix(string routeTemplate)
|
|
||||||
{
|
|
||||||
if (routeTemplate.StartsWith("~/", StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
return routeTemplate.Substring(2);
|
|
||||||
}
|
|
||||||
else if (routeTemplate.StartsWith("/", StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
return routeTemplate.Substring(1);
|
|
||||||
}
|
|
||||||
else if (routeTemplate.StartsWith("~", StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
throw new ArgumentException(Resources.TemplateRoute_InvalidRouteTemplate, nameof(routeTemplate));
|
|
||||||
}
|
|
||||||
return routeTemplate;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool ParseSegment(TemplateParserContext context, List<TemplateSegment> segments)
|
|
||||||
{
|
|
||||||
Debug.Assert(context != null);
|
|
||||||
Debug.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 == OpenBrace)
|
|
||||||
{
|
|
||||||
// This is an open brace inside of a parameter, it has to be escaped
|
|
||||||
if (context.Next())
|
|
||||||
{
|
|
||||||
if (context.Current != OpenBrace)
|
|
||||||
{
|
|
||||||
// If we see something like "{p1:regex(^\d{3", we will come here.
|
|
||||||
context.Error = Resources.TemplateRoute_UnescapedBrace;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// This is a dangling open-brace, which is not allowed
|
|
||||||
// Example: "{p1:regex(^\d{"
|
|
||||||
context.Error = Resources.TemplateRoute_MismatchedParameter;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (context.Current == CloseBrace)
|
|
||||||
{
|
|
||||||
// When we encounter Closed brace here, it either means end of the parameter or it is a closed
|
|
||||||
// brace in the parameter, in that case it needs to be escaped.
|
|
||||||
// Example: {p1:regex(([}}])\w+}. First pair is escaped one and last marks end of the parameter
|
|
||||||
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
|
|
||||||
}
|
|
||||||
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 rawParameter = context.Capture();
|
|
||||||
var decoded = rawParameter.Replace("}}", "}").Replace("{{", "{");
|
|
||||||
|
|
||||||
// At this point, we need to parse the raw name for inline constraint,
|
|
||||||
// default values and optional parameters.
|
|
||||||
var templatePart = InlineRouteParameterParser.ParseRouteParameter(decoded);
|
|
||||||
|
|
||||||
if (templatePart.IsCatchAll && templatePart.IsOptional)
|
|
||||||
{
|
|
||||||
context.Error = Resources.TemplateRoute_CatchAllCannotBeOptional;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (templatePart.IsOptional && templatePart.DefaultValue != null)
|
|
||||||
{
|
|
||||||
// Cannot be optional and have a default value.
|
|
||||||
// The only way to declare an optional parameter is to have a ? at the end,
|
|
||||||
// hence we cannot have both default value and optional parameter within the template.
|
|
||||||
// A workaround is to add it as a separate entry in the defaults argument.
|
|
||||||
context.Error = Resources.TemplateRoute_OptionalCannotHaveDefaultValue;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var parameterName = templatePart.Name;
|
|
||||||
if (IsValidParameterName(context, parameterName))
|
|
||||||
{
|
|
||||||
segment.Parts.Add(templatePart);
|
|
||||||
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("{{", "{");
|
|
||||||
if (IsValidLiteral(context, decoded))
|
|
||||||
{
|
|
||||||
segment.Parts.Add(TemplatePart.CreateLiteral(decoded));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsAllValid(TemplateParserContext context, List<TemplateSegment> segments)
|
|
||||||
{
|
|
||||||
// A catch-all parameter must be the last part of the last segment
|
|
||||||
for (var i = 0; i < segments.Count; i++)
|
|
||||||
{
|
|
||||||
var segment = segments[i];
|
|
||||||
for (var 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 (var 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if a segment has multiple parts, then only the last one parameter can be optional
|
|
||||||
// if it is following a optional seperator.
|
|
||||||
for (var i = 0; i < segment.Parts.Count; i++)
|
|
||||||
{
|
|
||||||
var part = segment.Parts[i];
|
|
||||||
|
|
||||||
if (part.IsParameter && part.IsOptional && segment.Parts.Count > 1)
|
|
||||||
{
|
|
||||||
// This optional parameter is the last part in the segment
|
|
||||||
if (i == segment.Parts.Count - 1)
|
|
||||||
{
|
|
||||||
if (!segment.Parts[i - 1].IsLiteral)
|
|
||||||
{
|
|
||||||
// The optional parameter is preceded by something that is not a literal.
|
|
||||||
// Example of error message:
|
|
||||||
// "In the segment '{RouteValue}{param?}', the optional parameter 'param' is preceded
|
|
||||||
// by an invalid segment '{RouteValue}'. Only a period (.) can precede an optional parameter.
|
|
||||||
context.Error = string.Format(
|
|
||||||
Resources.TemplateRoute_OptionalParameterCanbBePrecededByPeriod,
|
|
||||||
segment.DebuggerToString(),
|
|
||||||
part.Name,
|
|
||||||
segment.Parts[i - 1].DebuggerToString());
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
else if (segment.Parts[i - 1].Text != PeriodString)
|
|
||||||
{
|
|
||||||
// The optional parameter is preceded by a literal other than period.
|
|
||||||
// Example of error message:
|
|
||||||
// "In the segment '{RouteValue}-{param?}', the optional parameter 'param' is preceded
|
|
||||||
// by an invalid segment '-'. Only a period (.) can precede an optional parameter.
|
|
||||||
context.Error = string.Format(
|
|
||||||
Resources.TemplateRoute_OptionalParameterCanbBePrecededByPeriod,
|
|
||||||
segment.DebuggerToString(),
|
|
||||||
part.Name,
|
|
||||||
segment.Parts[i - 1].Text);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
segment.Parts[i - 1].IsOptionalSeperator = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// This optional parameter is not the last one in the segment
|
|
||||||
// Example:
|
|
||||||
// An optional parameter must be at the end of the segment.In the segment '{RouteValue?})',
|
|
||||||
// optional parameter 'RouteValue' is followed by ')'
|
|
||||||
var nextPart = segment.Parts[i + 1];
|
|
||||||
var invalidPartText = nextPart.IsParameter ? nextPart.Name : nextPart.Text;
|
|
||||||
|
|
||||||
context.Error = string.Format(
|
|
||||||
Resources.TemplateRoute_OptionalParameterHasTobeTheLast,
|
|
||||||
segment.DebuggerToString(),
|
|
||||||
segment.Parts[i].Name,
|
|
||||||
invalidPartText);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// A segment cannot contain two consecutive parameters
|
|
||||||
var isLastSegmentParameter = false;
|
|
||||||
for (var 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 (var i = 0; i < parameterName.Length; i++)
|
|
||||||
{
|
|
||||||
var c = parameterName[i];
|
|
||||||
if (c == Separator || c == OpenBrace || c == CloseBrace || c == QuestionMark || c == Asterisk)
|
|
||||||
{
|
|
||||||
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 IsValidLiteral(TemplateParserContext context, string literal)
|
|
||||||
{
|
|
||||||
Debug.Assert(context != null);
|
|
||||||
Debug.Assert(literal != null);
|
|
||||||
|
|
||||||
if (literal.IndexOf(QuestionMark) != -1)
|
|
||||||
{
|
|
||||||
context.Error = String.Format(CultureInfo.CurrentCulture,
|
|
||||||
Resources.TemplateRoute_InvalidLiteral, literal);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsInvalidRouteTemplate(string routeTemplate)
|
|
||||||
{
|
|
||||||
return routeTemplate.StartsWith("~", StringComparison.Ordinal) ||
|
|
||||||
routeTemplate.StartsWith("/", StringComparison.Ordinal);
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
Debug.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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,47 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Microsoft.AspNetCore.Routing.Patterns;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing.Template
|
namespace Microsoft.AspNetCore.Routing.Template
|
||||||
{
|
{
|
||||||
[DebuggerDisplay("{DebuggerToString()}")]
|
[DebuggerDisplay("{DebuggerToString()}")]
|
||||||
public class TemplatePart
|
public class TemplatePart
|
||||||
{
|
{
|
||||||
|
public TemplatePart()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public TemplatePart(RoutePatternPart other)
|
||||||
|
{
|
||||||
|
IsLiteral = other.IsLiteral || other.IsSeparator;
|
||||||
|
IsParameter = other.IsParameter;
|
||||||
|
|
||||||
|
if (other.IsLiteral && other is RoutePatternLiteralPart literal)
|
||||||
|
{
|
||||||
|
Text = literal.Content;
|
||||||
|
}
|
||||||
|
else if (other.IsParameter && other is RoutePatternParameterPart parameter)
|
||||||
|
{
|
||||||
|
// Text is unused by TemplatePart and assumed to be null when the part is a parameter.
|
||||||
|
Name = parameter.Name;
|
||||||
|
IsCatchAll = parameter.IsCatchAll;
|
||||||
|
IsOptional = parameter.IsOptional;
|
||||||
|
DefaultValue = parameter.Default;
|
||||||
|
InlineConstraints = parameter.Constraints?.Select(p => new InlineConstraint(p));
|
||||||
|
}
|
||||||
|
else if (other.IsSeparator && other is RoutePatternSeparatorPart separator)
|
||||||
|
{
|
||||||
|
Text = separator.Content;
|
||||||
|
IsOptionalSeperator = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Unreachable
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static TemplatePart CreateLiteral(string text)
|
public static TemplatePart CreateLiteral(string text)
|
||||||
{
|
{
|
||||||
return new TemplatePart()
|
return new TemplatePart()
|
||||||
|
|
@ -20,11 +55,12 @@ namespace Microsoft.AspNetCore.Routing.Template
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static TemplatePart CreateParameter(string name,
|
public static TemplatePart CreateParameter(
|
||||||
bool isCatchAll,
|
string name,
|
||||||
bool isOptional,
|
bool isCatchAll,
|
||||||
object defaultValue,
|
bool isOptional,
|
||||||
IEnumerable<InlineConstraint> inlineConstraints)
|
object defaultValue,
|
||||||
|
IEnumerable<InlineConstraint> inlineConstraints)
|
||||||
{
|
{
|
||||||
if (name == null)
|
if (name == null)
|
||||||
{
|
{
|
||||||
|
|
@ -63,5 +99,28 @@ namespace Microsoft.AspNetCore.Routing.Template
|
||||||
return Text;
|
return Text;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public RoutePatternPart ToRoutePatternPart()
|
||||||
|
{
|
||||||
|
if (IsLiteral && IsOptionalSeperator)
|
||||||
|
{
|
||||||
|
return RoutePatternFactory.SeparatorPart(Text);
|
||||||
|
}
|
||||||
|
else if (IsLiteral)
|
||||||
|
{
|
||||||
|
return RoutePatternFactory.LiteralPart(Text);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var kind = IsCatchAll ?
|
||||||
|
RoutePatternParameterKind.CatchAll :
|
||||||
|
IsOptional ?
|
||||||
|
RoutePatternParameterKind.Optional :
|
||||||
|
RoutePatternParameterKind.Standard;
|
||||||
|
|
||||||
|
var constraints = InlineConstraints.Select(c => new RoutePatternConstraintReference(Name, c.Constraint));
|
||||||
|
return RoutePatternFactory.ParameterPart(Name, DefaultValue, kind, constraints);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,19 +4,36 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Microsoft.AspNetCore.Routing.Patterns;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing.Template
|
namespace Microsoft.AspNetCore.Routing.Template
|
||||||
{
|
{
|
||||||
[DebuggerDisplay("{DebuggerToString()}")]
|
[DebuggerDisplay("{DebuggerToString()}")]
|
||||||
public class TemplateSegment
|
public class TemplateSegment
|
||||||
{
|
{
|
||||||
|
public TemplateSegment()
|
||||||
|
{
|
||||||
|
Parts = new List<TemplatePart>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public TemplateSegment(RoutePatternPathSegment other)
|
||||||
|
{
|
||||||
|
Parts = new List<TemplatePart>(other.Parts.Select(s => new TemplatePart(s)));
|
||||||
|
}
|
||||||
|
|
||||||
public bool IsSimple => Parts.Count == 1;
|
public bool IsSimple => Parts.Count == 1;
|
||||||
|
|
||||||
public List<TemplatePart> Parts { get; } = new List<TemplatePart>();
|
public List<TemplatePart> Parts { get; }
|
||||||
|
|
||||||
internal string DebuggerToString()
|
internal string DebuggerToString()
|
||||||
{
|
{
|
||||||
return string.Join(string.Empty, Parts.Select(p => p.DebuggerToString()));
|
return string.Join(string.Empty, Parts.Select(p => p.DebuggerToString()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public RoutePatternPathSegment ToRoutePatternPathSegment()
|
||||||
|
{
|
||||||
|
var parts = Parts.Select(p => p.ToRoutePatternPart());
|
||||||
|
return RoutePatternFactory.Segment(parts);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,946 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Routing.Patterns
|
||||||
|
{
|
||||||
|
public class InlineRouteParameterParserTest
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData("=")]
|
||||||
|
[InlineData(":")]
|
||||||
|
public void ParseRouteParameter_WithoutADefaultValue(string parameterName)
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(parameterName);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(parameterName, templatePart.Name);
|
||||||
|
Assert.Null(templatePart.Default);
|
||||||
|
Assert.Empty(templatePart.Constraints);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_WithEmptyDefaultValue()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter("param=");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("param", templatePart.Name);
|
||||||
|
Assert.Equal("", templatePart.Default);
|
||||||
|
Assert.Empty(templatePart.Constraints);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_WithoutAConstraintName()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter("param:");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("param", templatePart.Name);
|
||||||
|
Assert.Null(templatePart.Default);
|
||||||
|
Assert.Empty(templatePart.Constraints);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_WithoutAConstraintNameOrParameterName()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter("param:=");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("param", templatePart.Name);
|
||||||
|
Assert.Equal("", templatePart.Default);
|
||||||
|
Assert.Empty(templatePart.Constraints);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_WithADefaultValueContainingConstraintSeparator()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter("param=:");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("param", templatePart.Name);
|
||||||
|
Assert.Equal(":", templatePart.Default);
|
||||||
|
Assert.Empty(templatePart.Constraints);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_ConstraintAndDefault_ParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter("param:int=111111");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("param", templatePart.Name);
|
||||||
|
Assert.Equal("111111", templatePart.Default);
|
||||||
|
|
||||||
|
var constraint = Assert.Single(templatePart.Constraints);
|
||||||
|
Assert.Equal("int", constraint.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_ConstraintWithArgumentsAndDefault_ParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@"param:test(\d+)=111111");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("param", templatePart.Name);
|
||||||
|
Assert.Equal("111111", templatePart.Default);
|
||||||
|
|
||||||
|
var constraint = Assert.Single(templatePart.Constraints);
|
||||||
|
Assert.Equal(@"test(\d+)", constraint.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_ConstraintAndOptional_ParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@"param:int?");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("param", templatePart.Name);
|
||||||
|
Assert.True(templatePart.IsOptional);
|
||||||
|
|
||||||
|
var constraint = Assert.Single(templatePart.Constraints);
|
||||||
|
Assert.Equal("int", constraint.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_ConstraintAndOptional_WithDefaultValue_ParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@"param:int=12?");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("param", templatePart.Name);
|
||||||
|
Assert.Equal("12", templatePart.Default);
|
||||||
|
Assert.True(templatePart.IsOptional);
|
||||||
|
|
||||||
|
var constraint = Assert.Single(templatePart.Constraints);
|
||||||
|
Assert.Equal("int", constraint.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_ConstraintAndOptional_WithDefaultValueWithQuestionMark_ParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@"param:int=12??");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("param", templatePart.Name);
|
||||||
|
Assert.Equal("12?", templatePart.Default);
|
||||||
|
Assert.True(templatePart.IsOptional);
|
||||||
|
|
||||||
|
var constraint = Assert.Single(templatePart.Constraints);
|
||||||
|
Assert.Equal("int", constraint.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_ConstraintWithArgumentsAndOptional_ParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@"param:test(\d+)?");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("param", templatePart.Name);
|
||||||
|
Assert.True(templatePart.IsOptional);
|
||||||
|
|
||||||
|
var constraint = Assert.Single(templatePart.Constraints);
|
||||||
|
Assert.Equal(@"test(\d+)", constraint.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_ConstraintWithArgumentsAndOptional_WithDefaultValue_ParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@"param:test(\d+)=abc?");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("param", templatePart.Name);
|
||||||
|
Assert.True(templatePart.IsOptional);
|
||||||
|
|
||||||
|
Assert.Equal("abc", templatePart.Default);
|
||||||
|
|
||||||
|
var constraint = Assert.Single(templatePart.Constraints);
|
||||||
|
Assert.Equal(@"test(\d+)", constraint.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_ChainedConstraints_ParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@"param:test(d+):test(w+)");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("param", templatePart.Name);
|
||||||
|
|
||||||
|
Assert.Collection(templatePart.Constraints,
|
||||||
|
constraint => Assert.Equal(@"test(d+)", constraint.Content),
|
||||||
|
constraint => Assert.Equal(@"test(w+)", constraint.Content));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_ChainedConstraints_DoubleDelimiters_ParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@"param::test(d+)::test(w+)");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("param", templatePart.Name);
|
||||||
|
|
||||||
|
Assert.Collection(
|
||||||
|
templatePart.Constraints,
|
||||||
|
constraint => Assert.Equal(@"test(d+)", constraint.Content),
|
||||||
|
constraint => Assert.Equal(@"test(w+)", constraint.Content));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_ChainedConstraints_ColonInPattern_ParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@"param:test(\d+):test(\w:+)");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("param", templatePart.Name);
|
||||||
|
|
||||||
|
Assert.Collection(templatePart.Constraints,
|
||||||
|
constraint => Assert.Equal(@"test(\d+)", constraint.Content),
|
||||||
|
constraint => Assert.Equal(@"test(\w:+)", constraint.Content));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_ChainedConstraints_WithDefaultValue_ParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@"param:test(\d+):test(\w+)=qwer");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("param", templatePart.Name);
|
||||||
|
|
||||||
|
Assert.Equal("qwer", templatePart.Default);
|
||||||
|
|
||||||
|
Assert.Collection(templatePart.Constraints,
|
||||||
|
constraint => Assert.Equal(@"test(\d+)", constraint.Content),
|
||||||
|
constraint => Assert.Equal(@"test(\w+)", constraint.Content));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_ChainedConstraints_WithDefaultValue_DoubleDelimiters_ParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@"param:test(\d+)::test(\w+)==qwer");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("param", templatePart.Name);
|
||||||
|
|
||||||
|
Assert.Equal("=qwer", templatePart.Default);
|
||||||
|
|
||||||
|
Assert.Collection(
|
||||||
|
templatePart.Constraints,
|
||||||
|
constraint => Assert.Equal(@"test(\d+)", constraint.Content),
|
||||||
|
constraint => Assert.Equal(@"test(\w+)", constraint.Content));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("=")]
|
||||||
|
[InlineData("+=")]
|
||||||
|
[InlineData(">= || <= || ==")]
|
||||||
|
public void ParseRouteParameter_WithDefaultValue_ContainingDelimiter(string defaultValue)
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter($"comparison-operator:length(6)={defaultValue}");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("comparison-operator", templatePart.Name);
|
||||||
|
Assert.Equal(defaultValue, templatePart.Default);
|
||||||
|
|
||||||
|
var constraint = Assert.Single(templatePart.Constraints);
|
||||||
|
Assert.Equal("length(6)", constraint.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteTemplate_ConstraintsDefaultsAndOptionalsInMultipleSections_ParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var routePattern = RoutePatternFactory.Parse(@"some/url-{p1:int:test(3)=hello}/{p2=abc}/{p3?}");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var parameters = routePattern.Parameters.ToArray();
|
||||||
|
|
||||||
|
var param1 = parameters[0];
|
||||||
|
Assert.Equal("p1", param1.Name);
|
||||||
|
Assert.Equal("hello", param1.Default);
|
||||||
|
Assert.False(param1.IsOptional);
|
||||||
|
|
||||||
|
Assert.Collection(param1.Constraints,
|
||||||
|
constraint => Assert.Equal("int", constraint.Content),
|
||||||
|
constraint => Assert.Equal("test(3)", constraint.Content)
|
||||||
|
);
|
||||||
|
|
||||||
|
var param2 = parameters[1];
|
||||||
|
Assert.Equal("p2", param2.Name);
|
||||||
|
Assert.Equal("abc", param2.Default);
|
||||||
|
Assert.False(param2.IsOptional);
|
||||||
|
|
||||||
|
var param3 = parameters[2];
|
||||||
|
Assert.Equal("p3", param3.Name);
|
||||||
|
Assert.True(param3.IsOptional);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_NoTokens_ParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter("world");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("world", templatePart.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_ParamDefault_ParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter("param=world");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("param", templatePart.Name);
|
||||||
|
Assert.Equal("world", templatePart.Default);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_ConstraintWithClosingBraceInPattern_ClosingBraceIsParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@"param:test(\})");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("param", templatePart.Name);
|
||||||
|
|
||||||
|
var constraint = Assert.Single(templatePart.Constraints);
|
||||||
|
Assert.Equal(@"test(\})", constraint.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_ConstraintWithClosingBraceInPattern_WithDefaultValue_ParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@"param:test(\})=wer");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("param", templatePart.Name);
|
||||||
|
|
||||||
|
Assert.Equal("wer", templatePart.Default);
|
||||||
|
|
||||||
|
var constraint = Assert.Single(templatePart.Constraints);
|
||||||
|
Assert.Equal(@"test(\})", constraint.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_ConstraintWithClosingParenInPattern_ClosingParenIsParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@"param:test(\))");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("param", templatePart.Name);
|
||||||
|
|
||||||
|
var constraint = Assert.Single(templatePart.Constraints);
|
||||||
|
Assert.Equal(@"test(\))", constraint.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_ConstraintWithClosingParenInPattern_WithDefaultValue_ParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@"param:test(\))=fsd");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("param", templatePart.Name);
|
||||||
|
|
||||||
|
Assert.Equal("fsd", templatePart.Default);
|
||||||
|
|
||||||
|
var constraint = Assert.Single(templatePart.Constraints);
|
||||||
|
Assert.Equal(@"test(\))", constraint.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_ConstraintWithColonInPattern_ColonIsParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@"param:test(:)");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("param", templatePart.Name);
|
||||||
|
|
||||||
|
var constraint = Assert.Single(templatePart.Constraints);
|
||||||
|
Assert.Equal(@"test(:)", constraint.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_ConstraintWithColonInPattern_WithDefaultValue_ParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@"param:test(:)=mnf");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("param", templatePart.Name);
|
||||||
|
|
||||||
|
Assert.Equal("mnf", templatePart.Default);
|
||||||
|
|
||||||
|
var constraint = Assert.Single(templatePart.Constraints);
|
||||||
|
Assert.Equal(@"test(:)", constraint.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_ConstraintWithColonsInPattern_ParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@"param:test(a:b:c)");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("param", templatePart.Name);
|
||||||
|
|
||||||
|
var constraint = Assert.Single(templatePart.Constraints);
|
||||||
|
Assert.Equal(@"test(a:b:c)", constraint.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_ConstraintWithColonInParamName_ParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@":param:test=12");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(":param", templatePart.Name);
|
||||||
|
|
||||||
|
Assert.Equal("12", templatePart.Default);
|
||||||
|
|
||||||
|
var constraint = Assert.Single(templatePart.Constraints);
|
||||||
|
Assert.Equal("test", constraint.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_ConstraintWithTwoColonInParamName_ParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@":param::test=12");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(":param", templatePart.Name);
|
||||||
|
|
||||||
|
Assert.Equal("12", templatePart.Default);
|
||||||
|
|
||||||
|
Assert.Collection(
|
||||||
|
templatePart.Constraints,
|
||||||
|
constraint => Assert.Equal("test", constraint.Content));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_EmptyConstraint_ParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@":param:test:");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(":param", templatePart.Name);
|
||||||
|
|
||||||
|
Assert.Collection(
|
||||||
|
templatePart.Constraints,
|
||||||
|
constraint => Assert.Equal("test", constraint.Content));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_ConstraintWithCommaInPattern_PatternIsParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@"param:test(\w,\w)");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("param", templatePart.Name);
|
||||||
|
|
||||||
|
var constraint = Assert.Single(templatePart.Constraints);
|
||||||
|
Assert.Equal(@"test(\w,\w)", constraint.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_ConstraintWithCommaInName_ParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@"par,am:test(\w)");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("par,am", templatePart.Name);
|
||||||
|
|
||||||
|
var constraint = Assert.Single(templatePart.Constraints);
|
||||||
|
Assert.Equal(@"test(\w)", constraint.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_ConstraintWithCommaInPattern_WithDefaultValue_ParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@"param:test(\w,\w)=jsd");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("param", templatePart.Name);
|
||||||
|
|
||||||
|
Assert.Equal("jsd", templatePart.Default);
|
||||||
|
|
||||||
|
var constraint = Assert.Single(templatePart.Constraints);
|
||||||
|
Assert.Equal(@"test(\w,\w)", constraint.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_ConstraintWithEqualsFollowedByQuestionMark_PatternIsParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@"param:int=?");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("param", templatePart.Name);
|
||||||
|
Assert.Equal("", templatePart.Default);
|
||||||
|
|
||||||
|
Assert.True(templatePart.IsOptional);
|
||||||
|
|
||||||
|
var constraint = Assert.Single(templatePart.Constraints);
|
||||||
|
Assert.Equal("int", constraint.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_ConstraintWithEqualsSignInPattern_PatternIsParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@"param:test(=)");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("param", templatePart.Name);
|
||||||
|
Assert.Null(templatePart.Default);
|
||||||
|
|
||||||
|
var constraint = Assert.Single(templatePart.Constraints);
|
||||||
|
Assert.Equal("test(=)", constraint.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_EqualsSignInDefaultValue_ParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@"param=test=bar");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("param", templatePart.Name);
|
||||||
|
Assert.Equal("test=bar", templatePart.Default);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_ConstraintWithEqualEqualSignInPattern_ParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@"param:test(a==b)");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("param", templatePart.Name);
|
||||||
|
Assert.Null(templatePart.Default);
|
||||||
|
|
||||||
|
var constraint = Assert.Single(templatePart.Constraints);
|
||||||
|
Assert.Equal("test(a==b)", constraint.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_ConstraintWithEqualEqualSignInPattern_WithDefaultValue_ParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@"param:test(a==b)=dvds");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("param", templatePart.Name);
|
||||||
|
Assert.Equal("dvds", templatePart.Default);
|
||||||
|
|
||||||
|
var constraint = Assert.Single(templatePart.Constraints);
|
||||||
|
Assert.Equal("test(a==b)", constraint.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_EqualEqualSignInName_WithDefaultValue_ParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@"par==am:test=dvds");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("par", templatePart.Name);
|
||||||
|
Assert.Equal("=am:test=dvds", templatePart.Default);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_EqualEqualSignInDefaultValue_ParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@"param:test==dvds");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("param", templatePart.Name);
|
||||||
|
Assert.Equal("=dvds", templatePart.Default);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_DefaultValueWithColonAndParens_ParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@"par=am:test(asd)");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("par", templatePart.Name);
|
||||||
|
Assert.Equal("am:test(asd)", templatePart.Default);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_DefaultValueWithEqualsSignIn_ParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@"par=test(am):est=asd");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("par", templatePart.Name);
|
||||||
|
Assert.Equal("test(am):est=asd", templatePart.Default);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_ConstraintWithEqualsSignInPattern_WithDefaultValue_ParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@"param:test(=)=sds");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("param", templatePart.Name);
|
||||||
|
Assert.Equal("sds", templatePart.Default);
|
||||||
|
|
||||||
|
var constraint = Assert.Single(templatePart.Constraints);
|
||||||
|
Assert.Equal("test(=)", constraint.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_ConstraintWithOpenBraceInPattern_PatternIsParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@"param:test(\{)");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("param", templatePart.Name);
|
||||||
|
|
||||||
|
var constraint = Assert.Single(templatePart.Constraints);
|
||||||
|
Assert.Equal(@"test(\{)", constraint.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_ConstraintWithOpenBraceInName_ParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@"par{am:test(\sd)");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("par{am", templatePart.Name);
|
||||||
|
|
||||||
|
var constraint = Assert.Single(templatePart.Constraints);
|
||||||
|
Assert.Equal(@"test(\sd)", constraint.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_ConstraintWithOpenBraceInPattern_WithDefaultValue_ParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@"param:test(\{)=xvc");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("param", templatePart.Name);
|
||||||
|
|
||||||
|
Assert.Equal("xvc", templatePart.Default);
|
||||||
|
|
||||||
|
var constraint = Assert.Single(templatePart.Constraints);
|
||||||
|
Assert.Equal(@"test(\{)", constraint.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_ConstraintWithOpenParenInName_ParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@"par(am:test(\()");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("par(am", templatePart.Name);
|
||||||
|
|
||||||
|
var constraint = Assert.Single(templatePart.Constraints);
|
||||||
|
Assert.Equal(@"test(\()", constraint.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_ConstraintWithOpenParenInPattern_ParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@"param:test(\()");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("param", templatePart.Name);
|
||||||
|
|
||||||
|
var constraint = Assert.Single(templatePart.Constraints);
|
||||||
|
Assert.Equal(@"test(\()", constraint.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_ConstraintWithOpenParenNoCloseParen_ParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@"param:test(#$%");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("param", templatePart.Name);
|
||||||
|
|
||||||
|
var constraint = Assert.Single(templatePart.Constraints);
|
||||||
|
Assert.Equal("test(#$%", constraint.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_ConstraintWithOpenParenAndColon_ParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@"param:test(#:test1");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("param", templatePart.Name);
|
||||||
|
|
||||||
|
Assert.Collection(templatePart.Constraints,
|
||||||
|
constraint => Assert.Equal(@"test(#", constraint.Content),
|
||||||
|
constraint => Assert.Equal(@"test1", constraint.Content));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_ConstraintWithOpenParenAndColonWithDefaultValue_ParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@"param:test(abc:somevalue):name(test1:differentname=default-value");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("param", templatePart.Name);
|
||||||
|
Assert.Equal("default-value", templatePart.Default);
|
||||||
|
|
||||||
|
Assert.Collection(templatePart.Constraints,
|
||||||
|
constraint => Assert.Equal(@"test(abc:somevalue)", constraint.Content),
|
||||||
|
constraint => Assert.Equal(@"name(test1", constraint.Content),
|
||||||
|
constraint => Assert.Equal(@"differentname", constraint.Content));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_ConstraintWithOpenParenAndDefaultValue_ParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@"param:test(constraintvalue=test1");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("param", templatePart.Name);
|
||||||
|
Assert.Equal("test1", templatePart.Default);
|
||||||
|
|
||||||
|
var constraint = Assert.Single(templatePart.Constraints);
|
||||||
|
Assert.Equal(@"test(constraintvalue", constraint.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_ConstraintWithOpenParenInPattern_WithDefaultValue_ParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@"param:test(\()=djk");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("param", templatePart.Name);
|
||||||
|
|
||||||
|
Assert.Equal("djk", templatePart.Default);
|
||||||
|
|
||||||
|
var constraint = Assert.Single(templatePart.Constraints);
|
||||||
|
Assert.Equal(@"test(\()", constraint.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_ConstraintWithQuestionMarkInPattern_PatternIsParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@"param:test(\?)");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("param", templatePart.Name);
|
||||||
|
Assert.Null(templatePart.Default);
|
||||||
|
Assert.False(templatePart.IsOptional);
|
||||||
|
|
||||||
|
var constraint = Assert.Single(templatePart.Constraints);
|
||||||
|
Assert.Equal(@"test(\?)", constraint.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_ConstraintWithQuestionMarkInPattern_Optional_PatternIsParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@"param:test(\?)?");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("param", templatePart.Name);
|
||||||
|
Assert.Null(templatePart.Default);
|
||||||
|
Assert.True(templatePart.IsOptional);
|
||||||
|
|
||||||
|
var constraint = Assert.Single(templatePart.Constraints);
|
||||||
|
Assert.Equal(@"test(\?)", constraint.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_ConstraintWithQuestionMarkInPattern_WithDefaultValue_ParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@"param:test(\?)=sdf");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("param", templatePart.Name);
|
||||||
|
Assert.Equal("sdf", templatePart.Default);
|
||||||
|
Assert.False(templatePart.IsOptional);
|
||||||
|
|
||||||
|
var constraint = Assert.Single(templatePart.Constraints);
|
||||||
|
Assert.Equal(@"test(\?)", constraint.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_ConstraintWithQuestionMarkInPattern_Optional_WithDefaultValue_ParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@"param:test(\?)=sdf?");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("param", templatePart.Name);
|
||||||
|
Assert.Equal("sdf", templatePart.Default);
|
||||||
|
Assert.True(templatePart.IsOptional);
|
||||||
|
|
||||||
|
var constraint = Assert.Single(templatePart.Constraints);
|
||||||
|
Assert.Equal(@"test(\?)", constraint.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_ConstraintWithQuestionMarkInName_ParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@"par?am:test(\?)");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("par?am", templatePart.Name);
|
||||||
|
Assert.Null(templatePart.Default);
|
||||||
|
Assert.False(templatePart.IsOptional);
|
||||||
|
|
||||||
|
var constraint = Assert.Single(templatePart.Constraints);
|
||||||
|
Assert.Equal(@"test(\?)", constraint.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_ConstraintWithClosedParenAndColonInPattern_ParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@"param:test(#):$)");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("param", templatePart.Name);
|
||||||
|
Assert.Null(templatePart.Default);
|
||||||
|
Assert.False(templatePart.IsOptional);
|
||||||
|
|
||||||
|
Assert.Collection(templatePart.Constraints,
|
||||||
|
constraint => Assert.Equal(@"test(#)", constraint.Content),
|
||||||
|
constraint => Assert.Equal(@"$)", constraint.Content));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_ConstraintWithColonAndClosedParenInPattern_PatternIsParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@"param:test(#:)$)");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("param", templatePart.Name);
|
||||||
|
Assert.Null(templatePart.Default);
|
||||||
|
Assert.False(templatePart.IsOptional);
|
||||||
|
|
||||||
|
var constraint = Assert.Single(templatePart.Constraints);
|
||||||
|
Assert.Equal(@"test(#:)$)", constraint.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_ContainingMultipleUnclosedParenthesisInConstraint()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@"foo:regex(\\(\\(\\(\\()");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("foo", templatePart.Name);
|
||||||
|
Assert.Null(templatePart.Default);
|
||||||
|
Assert.False(templatePart.IsOptional);
|
||||||
|
|
||||||
|
var constraint = Assert.Single(templatePart.Constraints);
|
||||||
|
Assert.Equal(@"regex(\\(\\(\\(\\()", constraint.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_ConstraintWithBraces_PatternIsParsedCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@"p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)"); // ssn
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("p1", templatePart.Name);
|
||||||
|
Assert.Null(templatePart.Default);
|
||||||
|
Assert.False(templatePart.IsOptional);
|
||||||
|
|
||||||
|
var constraint = Assert.Single(templatePart.Constraints);
|
||||||
|
Assert.Equal(@"regex(^\d{{3}}-\d{{3}}-\d{{4}}$)", constraint.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRouteParameter_ConstraintWithBraces_WithDefaultValue()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(@"p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)=123-456-7890"); // ssn
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("p1", templatePart.Name);
|
||||||
|
Assert.Equal("123-456-7890", templatePart.Default);
|
||||||
|
Assert.False(templatePart.IsOptional);
|
||||||
|
|
||||||
|
var constraint = Assert.Single(templatePart.Constraints);
|
||||||
|
Assert.Equal(@"regex(^\d{{3}}-\d{{3}}-\d{{4}}$)", constraint.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("", "")]
|
||||||
|
[InlineData("?", "")]
|
||||||
|
[InlineData("*", "")]
|
||||||
|
[InlineData(" ", " ")]
|
||||||
|
[InlineData("\t", "\t")]
|
||||||
|
[InlineData("#!@#$%Q@#@%", "#!@#$%Q@#@%")]
|
||||||
|
[InlineData(",,,", ",,,")]
|
||||||
|
public void ParseRouteParameter_ParameterWithoutInlineConstraint_ReturnsTemplatePartWithEmptyInlineValues(
|
||||||
|
string parameter,
|
||||||
|
string expectedParameterName)
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var templatePart = ParseParameter(parameter);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expectedParameterName, templatePart.Name);
|
||||||
|
Assert.Empty(templatePart.Constraints);
|
||||||
|
Assert.Null(templatePart.Default);
|
||||||
|
}
|
||||||
|
|
||||||
|
private RoutePatternParameterPart ParseParameter(string routeParameter)
|
||||||
|
{
|
||||||
|
// See: #475 - these tests don't pass the 'whole' text.
|
||||||
|
var templatePart = RouteParameterParser.ParseRouteParameter(routeParameter);
|
||||||
|
return templatePart;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,247 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using Microsoft.AspNetCore.Routing.Constraints;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Routing.Patterns
|
||||||
|
{
|
||||||
|
public class RoutePatternFactoryTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Pattern_MergesDefaultValues()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var template = "{a}/{b}/{c=19}";
|
||||||
|
var defaults = new { a = "15", b = 17 };
|
||||||
|
var constraints = new { };
|
||||||
|
|
||||||
|
var original = RoutePatternFactory.Parse(template);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var actual = RoutePatternFactory.Pattern(
|
||||||
|
original.RawText,
|
||||||
|
defaults,
|
||||||
|
constraints,
|
||||||
|
original.PathSegments);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("15", actual.GetParameter("a").Default);
|
||||||
|
Assert.Equal(17, actual.GetParameter("b").Default);
|
||||||
|
Assert.Equal("19", actual.GetParameter("c").Default);
|
||||||
|
|
||||||
|
Assert.Collection(
|
||||||
|
actual.Defaults.OrderBy(kvp => kvp.Key),
|
||||||
|
kvp => { Assert.Equal("a", kvp.Key); Assert.Equal("15", kvp.Value); },
|
||||||
|
kvp => { Assert.Equal("b", kvp.Key); Assert.Equal(17, kvp.Value); },
|
||||||
|
kvp => { Assert.Equal("c", kvp.Key); Assert.Equal("19", kvp.Value); });
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Pattern_ExtraDefaultValues()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var template = "{a}/{b}/{c}";
|
||||||
|
var defaults = new { d = "15", e = 17 };
|
||||||
|
var constraints = new { };
|
||||||
|
|
||||||
|
var original = RoutePatternFactory.Parse(template);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var actual = RoutePatternFactory.Pattern(
|
||||||
|
original.RawText,
|
||||||
|
defaults,
|
||||||
|
constraints,
|
||||||
|
original.PathSegments);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Collection(
|
||||||
|
actual.Defaults.OrderBy(kvp => kvp.Key),
|
||||||
|
kvp => { Assert.Equal("d", kvp.Key); Assert.Equal("15", kvp.Value); },
|
||||||
|
kvp => { Assert.Equal("e", kvp.Key); Assert.Equal(17, kvp.Value); });
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Pattern_DuplicateDefaultValue_Throws()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var template = "{a=13}/{b}/{c}";
|
||||||
|
var defaults = new { a = "15", };
|
||||||
|
var constraints = new { };
|
||||||
|
|
||||||
|
var original = RoutePatternFactory.Parse(template);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var ex = Assert.Throws<InvalidOperationException>(() => RoutePatternFactory.Pattern(
|
||||||
|
original.RawText,
|
||||||
|
defaults,
|
||||||
|
constraints,
|
||||||
|
original.PathSegments));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(
|
||||||
|
"The route parameter 'a' has both an inline default value and an explicit default " +
|
||||||
|
"value specified. A route parameter cannot contain an inline default value when a " +
|
||||||
|
"default value is specified explicitly. Consider removing one of them.",
|
||||||
|
ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Pattern_OptionalParameterDefaultValue_Throws()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var template = "{a}/{b}/{c?}";
|
||||||
|
var defaults = new { c = "15", };
|
||||||
|
var constraints = new { };
|
||||||
|
|
||||||
|
var original = RoutePatternFactory.Parse(template);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var ex = Assert.Throws<InvalidOperationException>(() => RoutePatternFactory.Pattern(
|
||||||
|
original.RawText,
|
||||||
|
defaults,
|
||||||
|
constraints,
|
||||||
|
original.PathSegments));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(
|
||||||
|
"An optional parameter cannot have default value.",
|
||||||
|
ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Pattern_MergesConstraints()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var template = "{a:int}/{b}/{c}";
|
||||||
|
var defaults = new { };
|
||||||
|
var constraints = new { a = new RegexRouteConstraint("foo"), b = new RegexRouteConstraint("bar") };
|
||||||
|
|
||||||
|
var original = RoutePatternFactory.Parse(template);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var actual = RoutePatternFactory.Pattern(
|
||||||
|
original.RawText,
|
||||||
|
defaults,
|
||||||
|
constraints,
|
||||||
|
original.PathSegments);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Collection(
|
||||||
|
actual.GetParameter("a").Constraints,
|
||||||
|
c => Assert.IsType<RegexRouteConstraint>(c.Constraint),
|
||||||
|
c => Assert.Equal("int", c.Content));
|
||||||
|
Assert.Collection(
|
||||||
|
actual.GetParameter("b").Constraints,
|
||||||
|
c => Assert.IsType<RegexRouteConstraint>(c.Constraint));
|
||||||
|
|
||||||
|
Assert.Collection(
|
||||||
|
actual.Constraints.OrderBy(kvp => kvp.Key),
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("a", kvp.Key);
|
||||||
|
Assert.Collection(
|
||||||
|
kvp.Value,
|
||||||
|
c => Assert.IsType<RegexRouteConstraint>(c.Constraint),
|
||||||
|
c => Assert.Equal("int", c.Content));
|
||||||
|
},
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("b", kvp.Key);
|
||||||
|
Assert.Collection(
|
||||||
|
kvp.Value,
|
||||||
|
c => Assert.IsType<RegexRouteConstraint>(c.Constraint));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Pattern_ExtraConstraints()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var template = "{a}/{b}/{c}";
|
||||||
|
var defaults = new { };
|
||||||
|
var constraints = new { d = new RegexRouteConstraint("foo"), e = new RegexRouteConstraint("bar") };
|
||||||
|
|
||||||
|
var original = RoutePatternFactory.Parse(template);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var actual = RoutePatternFactory.Pattern(
|
||||||
|
original.RawText,
|
||||||
|
defaults,
|
||||||
|
constraints,
|
||||||
|
original.PathSegments);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Collection(
|
||||||
|
actual.Constraints.OrderBy(kvp => kvp.Key),
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("d", kvp.Key);
|
||||||
|
Assert.Collection(
|
||||||
|
kvp.Value,
|
||||||
|
c => Assert.IsType<RegexRouteConstraint>(c.Constraint));
|
||||||
|
},
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("e", kvp.Key);
|
||||||
|
Assert.Collection(
|
||||||
|
kvp.Value,
|
||||||
|
c => Assert.IsType<RegexRouteConstraint>(c.Constraint));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Pattern_CreatesConstraintFromString()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var template = "{a}/{b}/{c}";
|
||||||
|
var defaults = new { };
|
||||||
|
var constraints = new { d = "foo", };
|
||||||
|
|
||||||
|
var original = RoutePatternFactory.Parse(template);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var actual = RoutePatternFactory.Pattern(
|
||||||
|
original.RawText,
|
||||||
|
defaults,
|
||||||
|
constraints,
|
||||||
|
original.PathSegments);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Collection(
|
||||||
|
actual.Constraints.OrderBy(kvp => kvp.Key),
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("d", kvp.Key);
|
||||||
|
var regex = Assert.IsType<RegexRouteConstraint>(Assert.Single(kvp.Value).Constraint);
|
||||||
|
Assert.Equal("^(foo)$", regex.Constraint.ToString());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Pattern_InvalidConstraintTypeThrows()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var template = "{a}/{b}/{c}";
|
||||||
|
var defaults = new { };
|
||||||
|
var constraints = new { d = 17, };
|
||||||
|
|
||||||
|
var original = RoutePatternFactory.Parse(template);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var ex = Assert.Throws<InvalidOperationException>(() => RoutePatternFactory.Pattern(
|
||||||
|
original.RawText,
|
||||||
|
defaults,
|
||||||
|
constraints,
|
||||||
|
original.PathSegments));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(
|
||||||
|
"The constraint entry 'd' - '17' must have a string value or be of a type " +
|
||||||
|
"which implements 'Microsoft.AspNetCore.Routing.IRouteConstraint'.",
|
||||||
|
ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,765 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using Microsoft.AspNetCore.Testing;
|
||||||
|
using Xunit;
|
||||||
|
using static Microsoft.AspNetCore.Routing.Patterns.RoutePatternFactory;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Routing.Patterns
|
||||||
|
{
|
||||||
|
public class RoutePatternParameterParserTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Parse_SingleLiteral()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var template = "cool";
|
||||||
|
|
||||||
|
var expected = Pattern(
|
||||||
|
template,
|
||||||
|
Segment(LiteralPart("cool")));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var actual = RoutePatternParser.Parse(template);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal<RoutePattern>(expected, actual, new RoutePatternEqualityComparer());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Parse_SingleParameter()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var template = "{p}";
|
||||||
|
|
||||||
|
var expected = Pattern(template, Segment(ParameterPart("p")));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var actual = RoutePatternParser.Parse(template);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal<RoutePattern>(expected, actual, new RoutePatternEqualityComparer());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Parse_OptionalParameter()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var template = "{p?}";
|
||||||
|
|
||||||
|
var expected = Pattern(template, Segment(ParameterPart("p", null, RoutePatternParameterKind.Optional)));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var actual = RoutePatternParser.Parse(template);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal<RoutePattern>(expected, actual, new RoutePatternEqualityComparer());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Parse_MultipleLiterals()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var template = "cool/awesome/super";
|
||||||
|
|
||||||
|
var expected = Pattern(
|
||||||
|
template,
|
||||||
|
Segment(LiteralPart("cool")),
|
||||||
|
Segment(LiteralPart("awesome")),
|
||||||
|
Segment(LiteralPart("super")));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var actual = RoutePatternParser.Parse(template);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal<RoutePattern>(expected, actual, new RoutePatternEqualityComparer());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Parse_MultipleParameters()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var template = "{p1}/{p2}/{*p3}";
|
||||||
|
|
||||||
|
var expected = Pattern(
|
||||||
|
template,
|
||||||
|
Segment(ParameterPart("p1")),
|
||||||
|
Segment(ParameterPart("p2")),
|
||||||
|
Segment(ParameterPart("p3", null, RoutePatternParameterKind.CatchAll)));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var actual = RoutePatternParser.Parse(template);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal<RoutePattern>(expected, actual, new RoutePatternEqualityComparer());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Parse_ComplexSegment_LP()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var template = "cool-{p1}";
|
||||||
|
|
||||||
|
var expected = Pattern(
|
||||||
|
template,
|
||||||
|
Segment(
|
||||||
|
LiteralPart("cool-"),
|
||||||
|
ParameterPart("p1")));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var actual = RoutePatternParser.Parse(template);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal<RoutePattern>(expected, actual, new RoutePatternEqualityComparer());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Parse_ComplexSegment_PL()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var template = "{p1}-cool";
|
||||||
|
|
||||||
|
var expected = Pattern(
|
||||||
|
template,
|
||||||
|
Segment(
|
||||||
|
ParameterPart("p1"),
|
||||||
|
LiteralPart("-cool")));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var actual = RoutePatternParser.Parse(template);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal<RoutePattern>(expected, actual, new RoutePatternEqualityComparer());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Parse_ComplexSegment_PLP()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var template = "{p1}-cool-{p2}";
|
||||||
|
|
||||||
|
var expected = Pattern(
|
||||||
|
template,
|
||||||
|
Segment(
|
||||||
|
ParameterPart("p1"),
|
||||||
|
LiteralPart("-cool-"),
|
||||||
|
ParameterPart("p2")));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var actual = RoutePatternParser.Parse(template);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal<RoutePattern>(expected, actual, new RoutePatternEqualityComparer());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Parse_ComplexSegment_LPL()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var template = "cool-{p1}-awesome";
|
||||||
|
|
||||||
|
var expected = Pattern(
|
||||||
|
template,
|
||||||
|
Segment(
|
||||||
|
LiteralPart("cool-"),
|
||||||
|
ParameterPart("p1"),
|
||||||
|
LiteralPart("-awesome")));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var actual = RoutePatternParser.Parse(template);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal<RoutePattern>(expected, actual, new RoutePatternEqualityComparer());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Parse_ComplexSegment_OptionalParameterFollowingPeriod()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var template = "{p1}.{p2?}";
|
||||||
|
|
||||||
|
var expected = Pattern(
|
||||||
|
template,
|
||||||
|
Segment(
|
||||||
|
ParameterPart("p1"),
|
||||||
|
SeparatorPart("."),
|
||||||
|
ParameterPart("p2", null, RoutePatternParameterKind.Optional)));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var actual = RoutePatternParser.Parse(template);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal<RoutePattern>(expected, actual, new RoutePatternEqualityComparer());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Parse_ComplexSegment_ParametersFollowingPeriod()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var template = "{p1}.{p2}";
|
||||||
|
|
||||||
|
var expected = Pattern(
|
||||||
|
template,
|
||||||
|
Segment(
|
||||||
|
ParameterPart("p1"),
|
||||||
|
LiteralPart("."),
|
||||||
|
ParameterPart("p2")));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var actual = RoutePatternParser.Parse(template);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal<RoutePattern>(expected, actual, new RoutePatternEqualityComparer());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_ThreeParameters()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var template = "{p1}.{p2}.{p3?}";
|
||||||
|
|
||||||
|
var expected = Pattern(
|
||||||
|
template,
|
||||||
|
Segment(
|
||||||
|
ParameterPart("p1"),
|
||||||
|
LiteralPart("."),
|
||||||
|
ParameterPart("p2"),
|
||||||
|
SeparatorPart("."),
|
||||||
|
ParameterPart("p3", null, RoutePatternParameterKind.Optional)));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var actual = RoutePatternParser.Parse(template);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal<RoutePattern>(expected, actual, new RoutePatternEqualityComparer());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Parse_ComplexSegment_ThreeParametersSeperatedByPeriod()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var template = "{p1}.{p2}.{p3}";
|
||||||
|
|
||||||
|
var expected = Pattern(
|
||||||
|
template,
|
||||||
|
Segment(
|
||||||
|
ParameterPart("p1"),
|
||||||
|
LiteralPart("."),
|
||||||
|
ParameterPart("p2"),
|
||||||
|
LiteralPart("."),
|
||||||
|
ParameterPart("p3")));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var actual = RoutePatternParser.Parse(template);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal<RoutePattern>(expected, actual, new RoutePatternEqualityComparer());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_MiddleSegment()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var template = "{p1}.{p2?}/{p3}";
|
||||||
|
|
||||||
|
var expected = Pattern(
|
||||||
|
template,
|
||||||
|
Segment(
|
||||||
|
ParameterPart("p1"),
|
||||||
|
SeparatorPart("."),
|
||||||
|
ParameterPart("p2", null, RoutePatternParameterKind.Optional)),
|
||||||
|
Segment(
|
||||||
|
ParameterPart("p3")));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var actual = RoutePatternParser.Parse(template);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal<RoutePattern>(expected, actual, new RoutePatternEqualityComparer());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_LastSegment()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var template = "{p1}/{p2}.{p3?}";
|
||||||
|
|
||||||
|
var expected = Pattern(
|
||||||
|
template,
|
||||||
|
Segment(
|
||||||
|
ParameterPart("p1")),
|
||||||
|
Segment(
|
||||||
|
ParameterPart("p2"),
|
||||||
|
SeparatorPart("."),
|
||||||
|
ParameterPart("p3", null, RoutePatternParameterKind.Optional)));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var actual = RoutePatternParser.Parse(template);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal<RoutePattern>(expected, actual, new RoutePatternEqualityComparer());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_PeriodAfterSlash()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var template = "{p2}/.{p3?}";
|
||||||
|
|
||||||
|
var expected = Pattern(
|
||||||
|
template,
|
||||||
|
Segment(ParameterPart("p2")),
|
||||||
|
Segment(
|
||||||
|
SeparatorPart("."),
|
||||||
|
ParameterPart("p3", null, RoutePatternParameterKind.Optional)));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var actual = RoutePatternParser.Parse(template);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal<RoutePattern>(expected, actual, new RoutePatternEqualityComparer());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}", @"regex(^\d{3}-\d{3}-\d{4}$)")] // ssn
|
||||||
|
[InlineData(@"{p1:regex(^\d{{1,2}}\/\d{{1,2}}\/\d{{4}}$)}", @"regex(^\d{1,2}\/\d{1,2}\/\d{4}$)")] // date
|
||||||
|
[InlineData(@"{p1:regex(^\w+\@\w+\.\w+)}", @"regex(^\w+\@\w+\.\w+)")] // email
|
||||||
|
[InlineData(@"{p1:regex(([}}])\w+)}", @"regex(([}])\w+)")] // Not balanced }
|
||||||
|
[InlineData(@"{p1:regex(([{{(])\w+)}", @"regex(([{(])\w+)")] // Not balanced {
|
||||||
|
public void Parse_RegularExpressions(string template, string constraint)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expected = Pattern(
|
||||||
|
template,
|
||||||
|
Segment(
|
||||||
|
ParameterPart(
|
||||||
|
"p1",
|
||||||
|
null,
|
||||||
|
RoutePatternParameterKind.Standard,
|
||||||
|
Constraint("p1", constraint))));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var actual = RoutePatternParser.Parse(template);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal<RoutePattern>(expected, actual, new RoutePatternEqualityComparer());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}}$)}")] // extra }
|
||||||
|
[InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}}")] // extra } at the end
|
||||||
|
[InlineData(@"{{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}")] // extra { at the begining
|
||||||
|
[InlineData(@"{p1:regex(([}])\w+}")] // Not escaped }
|
||||||
|
[InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}$)}")] // Not escaped }
|
||||||
|
[InlineData(@"{p1:regex(abc)")]
|
||||||
|
public void Parse_RegularExpressions_Invalid(string template)
|
||||||
|
{
|
||||||
|
// Act and Assert
|
||||||
|
ExceptionAssert.Throws<RoutePatternException>(
|
||||||
|
() => RoutePatternParser.Parse(template),
|
||||||
|
"There is an incomplete parameter in the route template. Check that each '{' character has a matching " +
|
||||||
|
"'}' character.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{{4}}$)}")] // extra {
|
||||||
|
[InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{4}}$)}")] // Not escaped {
|
||||||
|
public void Parse_RegularExpressions_Unescaped(string template)
|
||||||
|
{
|
||||||
|
// Act and Assert
|
||||||
|
ExceptionAssert.Throws<RoutePatternException>(
|
||||||
|
() => RoutePatternParser.Parse(template),
|
||||||
|
"In a route parameter, '{' and '}' must be escaped with '{{' and '}}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("{p1}.{p2?}.{p3}", "p2", ".")]
|
||||||
|
[InlineData("{p1?}{p2}", "p1", "{p2}")]
|
||||||
|
[InlineData("{p1?}{p2?}", "p1", "{p2?}")]
|
||||||
|
[InlineData("{p1}.{p2?})", "p2", ")")]
|
||||||
|
[InlineData("{foorb?}-bar-{z}", "foorb", "-bar-")]
|
||||||
|
public void Parse_ComplexSegment_OptionalParameter_NotTheLastPart(
|
||||||
|
string template,
|
||||||
|
string parameter,
|
||||||
|
string invalid)
|
||||||
|
{
|
||||||
|
// Act and Assert
|
||||||
|
ExceptionAssert.Throws<RoutePatternException>(
|
||||||
|
() => RoutePatternParser.Parse(template),
|
||||||
|
"An optional parameter must be at the end of the segment. In the segment '" + template +
|
||||||
|
"', optional parameter '" + parameter + "' is followed by '" + invalid + "'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("{p1}-{p2?}", "-")]
|
||||||
|
[InlineData("{p1}..{p2?}", "..")]
|
||||||
|
[InlineData("..{p2?}", "..")]
|
||||||
|
[InlineData("{p1}.abc.{p2?}", ".abc.")]
|
||||||
|
[InlineData("{p1}{p2?}", "{p1}")]
|
||||||
|
public void Parse_ComplexSegment_OptionalParametersSeperatedByPeriod_Invalid(string template, string parameter)
|
||||||
|
{
|
||||||
|
// Act and Assert
|
||||||
|
ExceptionAssert.Throws<RoutePatternException>(
|
||||||
|
() => RoutePatternParser.Parse(template),
|
||||||
|
"In the segment '" + template + "', the optional parameter 'p2' is preceded by an invalid " +
|
||||||
|
"segment '" + parameter + "'. Only a period (.) can precede an optional parameter.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_WithRepeatedParameter()
|
||||||
|
{
|
||||||
|
var ex = ExceptionAssert.Throws<RoutePatternException>(
|
||||||
|
() => RoutePatternParser.Parse("{Controller}.mvc/{id}/{controller}"),
|
||||||
|
"The route parameter name 'controller' appears more than one time in the route template.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[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)
|
||||||
|
{
|
||||||
|
ExceptionAssert.Throws<RoutePatternException>(
|
||||||
|
() => RoutePatternParser.Parse(template),
|
||||||
|
@"There is an incomplete parameter in the route template. Check that each '{' character has a " +
|
||||||
|
"matching '}' character.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_CannotHaveCatchAllInMultiSegment()
|
||||||
|
{
|
||||||
|
ExceptionAssert.Throws<RoutePatternException>(
|
||||||
|
() => RoutePatternParser.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.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_CannotHaveMoreThanOneCatchAll()
|
||||||
|
{
|
||||||
|
ExceptionAssert.Throws<RoutePatternException>(
|
||||||
|
() => RoutePatternParser.Parse("{*p1}/{*p2}"),
|
||||||
|
"A catch-all parameter can only appear as the last segment of the route template.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_CannotHaveMoreThanOneCatchAllInMultiSegment()
|
||||||
|
{
|
||||||
|
ExceptionAssert.Throws<RoutePatternException>(
|
||||||
|
() => RoutePatternParser.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.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_CannotHaveCatchAllWithNoName()
|
||||||
|
{
|
||||||
|
ExceptionAssert.Throws<RoutePatternException>(
|
||||||
|
() => RoutePatternParser.Parse("foo/{*}"),
|
||||||
|
"The route parameter name '' is invalid. Route parameter names must be non-empty and cannot" +
|
||||||
|
" contain these characters: '{', '}', '/'. The '?' character marks a parameter as optional," +
|
||||||
|
" and can occur only at the end of the parameter. The '*' character marks a parameter as catch-all," +
|
||||||
|
" and can occur only at the start of the parameter.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("{**}", "*")]
|
||||||
|
[InlineData("{a*}", "a*")]
|
||||||
|
[InlineData("{*a*}", "a*")]
|
||||||
|
[InlineData("{*a*:int}", "a*")]
|
||||||
|
[InlineData("{*a*=5}", "a*")]
|
||||||
|
[InlineData("{*a*b=5}", "a*b")]
|
||||||
|
[InlineData("{p1?}.{p2/}/{p3}", "p2/")]
|
||||||
|
[InlineData("{p{{}", "p{")]
|
||||||
|
[InlineData("{p}}}", "p}")]
|
||||||
|
[InlineData("{p/}", "p/")]
|
||||||
|
public void ParseRouteParameter_ThrowsIf_ParameterContainsSpecialCharacters(
|
||||||
|
string template,
|
||||||
|
string parameterName)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expectedMessage = "The route parameter name '" + parameterName + "' is invalid. Route parameter " +
|
||||||
|
"names must be non-empty and cannot contain these characters: '{', '}', '/'. The '?' character " +
|
||||||
|
"marks a parameter as optional, and can occur only at the end of the parameter. The '*' character " +
|
||||||
|
"marks a parameter as catch-all, and can occur only at the start of the parameter.";
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
ExceptionAssert.Throws<RoutePatternException>(() => RoutePatternParser.Parse(template), expectedMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_CannotHaveConsecutiveOpenBrace()
|
||||||
|
{
|
||||||
|
ExceptionAssert.Throws<RoutePatternException>(
|
||||||
|
() => RoutePatternParser.Parse("foo/{{p1}"),
|
||||||
|
"There is an incomplete parameter in the route template. Check that each '{' character has a " +
|
||||||
|
"matching '}' character.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_CannotHaveConsecutiveCloseBrace()
|
||||||
|
{
|
||||||
|
ExceptionAssert.Throws<RoutePatternException>(
|
||||||
|
() => RoutePatternParser.Parse("foo/{p1}}"),
|
||||||
|
"There is an incomplete parameter in the route template. Check that each '{' character has a " +
|
||||||
|
"matching '}' character.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_SameParameterTwiceThrows()
|
||||||
|
{
|
||||||
|
ExceptionAssert.Throws<RoutePatternException>(
|
||||||
|
() => RoutePatternParser.Parse("{aaa}/{AAA}"),
|
||||||
|
"The route parameter name 'AAA' appears more than one time in the route template.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_SameParameterTwiceAndOneCatchAllThrows()
|
||||||
|
{
|
||||||
|
ExceptionAssert.Throws<RoutePatternException>(
|
||||||
|
() => RoutePatternParser.Parse("{aaa}/{*AAA}"),
|
||||||
|
"The route parameter name 'AAA' appears more than one time in the route template.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_InvalidParameterNameWithCloseBracketThrows()
|
||||||
|
{
|
||||||
|
ExceptionAssert.Throws<RoutePatternException>(
|
||||||
|
() => RoutePatternParser.Parse("{a}/{aa}a}/{z}"),
|
||||||
|
"There is an incomplete parameter in the route template. Check that each '{' character has a " +
|
||||||
|
"matching '}' character.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_InvalidParameterNameWithOpenBracketThrows()
|
||||||
|
{
|
||||||
|
ExceptionAssert.Throws<RoutePatternException>(
|
||||||
|
() => RoutePatternParser.Parse("{a}/{a{aa}/{z}"),
|
||||||
|
"In a route parameter, '{' and '}' must be escaped with '{{' and '}}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_InvalidParameterNameWithEmptyNameThrows()
|
||||||
|
{
|
||||||
|
ExceptionAssert.Throws<RoutePatternException>(
|
||||||
|
() => RoutePatternParser.Parse("{a}/{}/{z}"),
|
||||||
|
"The route parameter name '' is invalid. Route parameter names must be non-empty and cannot" +
|
||||||
|
" contain these characters: '{', '}', '/'. The '?' character marks a parameter as optional, and" +
|
||||||
|
" can occur only at the end of the parameter. The '*' character marks a parameter as catch-all," +
|
||||||
|
" and can occur only at the start of the parameter.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_InvalidParameterNameWithQuestionThrows()
|
||||||
|
{
|
||||||
|
ExceptionAssert.Throws<RoutePatternException>(
|
||||||
|
() => RoutePatternParser.Parse("{Controller}.mvc/{?}"),
|
||||||
|
"The route parameter name '' is invalid. Route parameter names must be non-empty and cannot" +
|
||||||
|
" contain these characters: '{', '}', '/'. The '?' character marks a parameter as optional, and" +
|
||||||
|
" can occur only at the end of the parameter. The '*' character marks a parameter as catch-all," +
|
||||||
|
" and can occur only at the start of the parameter.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_ConsecutiveSeparatorsSlashSlashThrows()
|
||||||
|
{
|
||||||
|
ExceptionAssert.Throws<RoutePatternException>(
|
||||||
|
() => RoutePatternParser.Parse("{a}//{z}"),
|
||||||
|
"The route template separator character '/' cannot appear consecutively. It must be separated by " +
|
||||||
|
"either a parameter or a literal value.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_WithCatchAllNotAtTheEndThrows()
|
||||||
|
{
|
||||||
|
ExceptionAssert.Throws<RoutePatternException>(
|
||||||
|
() => RoutePatternParser.Parse("foo/{p1}/{*p2}/{p3}"),
|
||||||
|
"A catch-all parameter can only appear as the last segment of the route template.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_RepeatedParametersThrows()
|
||||||
|
{
|
||||||
|
ExceptionAssert.Throws<RoutePatternException>(
|
||||||
|
() => RoutePatternParser.Parse("foo/aa{p1}{p2}"),
|
||||||
|
"A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by " +
|
||||||
|
"a literal string.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("/foo")]
|
||||||
|
[InlineData("~/foo")]
|
||||||
|
public void ValidTemplate_CanStartWithSlashOrTildeSlash(string routePattern)
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var pattern = RoutePatternParser.Parse(routePattern);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(routePattern, pattern.RawText);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_CannotStartWithTilde()
|
||||||
|
{
|
||||||
|
ExceptionAssert.Throws<RoutePatternException>(
|
||||||
|
() => RoutePatternParser.Parse("~foo"),
|
||||||
|
"The route template cannot start with a '~' character unless followed by a '/'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_CannotContainQuestionMark()
|
||||||
|
{
|
||||||
|
ExceptionAssert.Throws<RoutePatternException>(
|
||||||
|
() => RoutePatternParser.Parse("foor?bar"),
|
||||||
|
"The literal section 'foor?bar' is invalid. Literal sections cannot contain the '?' character.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_ParameterCannotContainQuestionMark_UnlessAtEnd()
|
||||||
|
{
|
||||||
|
ExceptionAssert.Throws<RoutePatternException>(
|
||||||
|
() => RoutePatternParser.Parse("{foor?b}"),
|
||||||
|
"The route parameter name 'foor?b' is invalid. Route parameter names must be non-empty and cannot" +
|
||||||
|
" contain these characters: '{', '}', '/'. The '?' character marks a parameter as optional, and" +
|
||||||
|
" can occur only at the end of the parameter. The '*' character marks a parameter as catch-all," +
|
||||||
|
" and can occur only at the start of the parameter.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidTemplate_CatchAllMarkedOptional()
|
||||||
|
{
|
||||||
|
ExceptionAssert.Throws<RoutePatternException>(
|
||||||
|
() => RoutePatternParser.Parse("{a}/{*b?}"),
|
||||||
|
"A catch-all parameter cannot be marked optional.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private class RoutePatternEqualityComparer :
|
||||||
|
IEqualityComparer<RoutePattern>,
|
||||||
|
IEqualityComparer<RoutePatternConstraintReference>
|
||||||
|
{
|
||||||
|
public bool Equals(RoutePattern x, RoutePattern y)
|
||||||
|
{
|
||||||
|
if (x == null && y == null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (x == null || y == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!string.Equals(x.RawText, y.RawText, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x.PathSegments.Count != y.PathSegments.Count)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < x.PathSegments.Count; i++)
|
||||||
|
{
|
||||||
|
if (x.PathSegments[i].Parts.Count != y.PathSegments[i].Parts.Count)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int j = 0; j < x.PathSegments[i].Parts.Count; j++)
|
||||||
|
{
|
||||||
|
if (!Equals(x.PathSegments[i].Parts[j], y.PathSegments[i].Parts[j]))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x.Parameters.Count != y.Parameters.Count)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < x.Parameters.Count; i++)
|
||||||
|
{
|
||||||
|
if (!Equals(x.Parameters[i], y.Parameters[i]))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool Equals(RoutePatternPart x, RoutePatternPart y)
|
||||||
|
{
|
||||||
|
if (x.GetType() != y.GetType())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x.IsLiteral && y.IsLiteral)
|
||||||
|
{
|
||||||
|
return Equals((RoutePatternLiteralPart)x, (RoutePatternLiteralPart)y);
|
||||||
|
}
|
||||||
|
else if (x.IsParameter && y.IsParameter)
|
||||||
|
{
|
||||||
|
return Equals((RoutePatternParameterPart)x, (RoutePatternParameterPart)y);
|
||||||
|
}
|
||||||
|
else if (x.IsSeparator && y.IsSeparator)
|
||||||
|
{
|
||||||
|
return Equals((RoutePatternSeparatorPart)x, (RoutePatternSeparatorPart)y);
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.Fail("This should not be reachable. Do you need to update the comparison logic?");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool Equals(RoutePatternLiteralPart x, RoutePatternLiteralPart y)
|
||||||
|
{
|
||||||
|
return x.Content == y.Content;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool Equals(RoutePatternParameterPart x, RoutePatternParameterPart y)
|
||||||
|
{
|
||||||
|
return
|
||||||
|
x.Name == y.Name &&
|
||||||
|
x.Default == y.Default &&
|
||||||
|
x.ParameterKind == y.ParameterKind &&
|
||||||
|
Enumerable.SequenceEqual(x.Constraints, y.Constraints, this);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Equals(RoutePatternConstraintReference x, RoutePatternConstraintReference y)
|
||||||
|
{
|
||||||
|
return
|
||||||
|
x.ParameterName == y.ParameterName &&
|
||||||
|
x.Content == y.Content &&
|
||||||
|
x.Constraint == y.Constraint;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool Equals(RoutePatternSeparatorPart x, RoutePatternSeparatorPart y)
|
||||||
|
{
|
||||||
|
return x.Content == y.Content;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetHashCode(RoutePattern obj)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetHashCode(RoutePatternConstraintReference obj)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,9 +5,6 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Microsoft.AspNetCore.Testing;
|
using Microsoft.AspNetCore.Testing;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using Moq;
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing.Template.Tests
|
namespace Microsoft.AspNetCore.Routing.Template.Tests
|
||||||
|
|
@ -528,8 +525,8 @@ namespace Microsoft.AspNetCore.Routing.Template.Tests
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("{p1}.{p2?}.{p3}", "p2", ".")]
|
[InlineData("{p1}.{p2?}.{p3}", "p2", ".")]
|
||||||
[InlineData("{p1?}{p2}", "p1", "p2")]
|
[InlineData("{p1?}{p2}", "p1", "{p2}")]
|
||||||
[InlineData("{p1?}{p2?}", "p1", "p2")]
|
[InlineData("{p1?}{p2?}", "p1", "{p2?}")]
|
||||||
[InlineData("{p1}.{p2?})", "p2", ")")]
|
[InlineData("{p1}.{p2?})", "p2", ")")]
|
||||||
[InlineData("{foorb?}-bar-{z}", "foorb", "-bar-")]
|
[InlineData("{foorb?}-bar-{z}", "foorb", "-bar-")]
|
||||||
public void Parse_ComplexSegment_OptionalParameter_NotTheLastPart(
|
public void Parse_ComplexSegment_OptionalParameter_NotTheLastPart(
|
||||||
|
|
@ -655,18 +652,6 @@ namespace Microsoft.AspNetCore.Routing.Template.Tests
|
||||||
"Parameter name: routeTemplate");
|
"Parameter name: routeTemplate");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData("/foo")]
|
|
||||||
[InlineData("~/foo")]
|
|
||||||
public void ValidTemplate_CanStartWithSlashOrTildeSlash(string routeTemplate)
|
|
||||||
{
|
|
||||||
// Arrange & Act
|
|
||||||
var template = TemplateParser.Parse(routeTemplate);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Equal(routeTemplate, template.TemplateText);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void InvalidTemplate_CannotHaveConsecutiveOpenBrace()
|
public void InvalidTemplate_CannotHaveConsecutiveOpenBrace()
|
||||||
{
|
{
|
||||||
|
|
@ -780,6 +765,18 @@ namespace Microsoft.AspNetCore.Routing.Template.Tests
|
||||||
"Parameter name: routeTemplate");
|
"Parameter name: routeTemplate");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("/foo")]
|
||||||
|
[InlineData("~/foo")]
|
||||||
|
public void ValidTemplate_CanStartWithSlashOrTildeSlash(string routeTemplate)
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var pattern = TemplateParser.Parse(routeTemplate);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(routeTemplate, pattern.TemplateText);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void InvalidTemplate_CannotStartWithTilde()
|
public void InvalidTemplate_CannotStartWithTilde()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue