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)
|
||||
=> 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)
|
||||
{
|
||||
var value = _resourceManager.GetString(name);
|
||||
|
|
|
|||
|
|
@ -204,4 +204,13 @@
|
|||
<data name="AmbiguousEndpoints" xml:space="preserve">
|
||||
<value>The request matched multiple endpoints. Matches: {0}{0}{1}</value>
|
||||
</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>
|
||||
|
|
@ -161,7 +161,7 @@ namespace Microsoft.AspNetCore.Routing
|
|||
queryString = queryString.ToLowerInvariant();
|
||||
}
|
||||
|
||||
if (_options.AppendTrailingSlash && !urlWithoutQueryString.EndsWith("/"))
|
||||
if (_options.AppendTrailingSlash && !urlWithoutQueryString.EndsWith("/", StringComparison.Ordinal))
|
||||
{
|
||||
urlWithoutQueryString += "/";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Routing.Patterns;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Template
|
||||
{
|
||||
|
|
@ -24,6 +25,16 @@ namespace Microsoft.AspNetCore.Routing.Template
|
|||
Constraint = constraint;
|
||||
}
|
||||
|
||||
public InlineConstraint(RoutePatternConstraintReference other)
|
||||
{
|
||||
if (other == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(other));
|
||||
}
|
||||
|
||||
Constraint = other.Content;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the constraint text.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Routing.Patterns;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Template
|
||||
{
|
||||
|
|
@ -13,6 +14,25 @@ namespace Microsoft.AspNetCore.Routing.Template
|
|||
{
|
||||
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)
|
||||
{
|
||||
if (segments == null)
|
||||
|
|
@ -78,5 +98,16 @@ namespace Microsoft.AspNetCore.Routing.Template
|
|||
|
||||
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.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing.Internal;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Template
|
||||
{
|
||||
|
|
@ -19,6 +17,7 @@ namespace Microsoft.AspNetCore.Routing.Template
|
|||
private readonly object[] _defaultValues;
|
||||
|
||||
private static readonly char[] Delimiters = new char[] { SeparatorChar };
|
||||
private RoutePatternMatcher _routePatternMatcher;
|
||||
|
||||
public TemplateMatcher(
|
||||
RouteTemplate template,
|
||||
|
|
@ -57,6 +56,9 @@ namespace Microsoft.AspNetCore.Routing.Template
|
|||
_defaultValues[i] = value;
|
||||
}
|
||||
}
|
||||
|
||||
var routePattern = Template.ToRoutePattern();
|
||||
_routePatternMatcher = new RoutePatternMatcher(routePattern, Defaults);
|
||||
}
|
||||
|
||||
public RouteValueDictionary Defaults { get; }
|
||||
|
|
@ -70,387 +72,7 @@ namespace Microsoft.AspNetCore.Routing.Template
|
|||
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 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;
|
||||
return _routePatternMatcher.TryMatch(path, values);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using Microsoft.AspNetCore.Routing.Patterns;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Template
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (routeTemplate == null)
|
||||
{
|
||||
routeTemplate = String.Empty;
|
||||
throw new ArgumentNullException(routeTemplate);
|
||||
}
|
||||
|
||||
var trimmedRouteTemplate = TrimPrefix(routeTemplate);
|
||||
|
||||
var context = new TemplateParserContext(trimmedRouteTemplate);
|
||||
var segments = new List<TemplateSegment>();
|
||||
|
||||
while (context.Next())
|
||||
try
|
||||
{
|
||||
if (context.Current == Separator)
|
||||
{
|
||||
// If we get here is means that there's a consecutive '/' character.
|
||||
// Templates don't start with a '/' and parsing a segment consumes the separator.
|
||||
throw new ArgumentException(Resources.TemplateRoute_CannotHaveConsecutiveSeparators,
|
||||
nameof(routeTemplate));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!ParseSegment(context, segments))
|
||||
{
|
||||
throw new ArgumentException(context.Error, nameof(routeTemplate));
|
||||
}
|
||||
}
|
||||
var inner = RoutePatternFactory.Parse(routeTemplate);
|
||||
return new RouteTemplate(inner);
|
||||
}
|
||||
|
||||
if (IsAllValid(context, segments))
|
||||
catch (RoutePatternException ex)
|
||||
{
|
||||
return new RouteTemplate(routeTemplate, segments);
|
||||
}
|
||||
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;
|
||||
}
|
||||
// Preserving the existing behavior of this API even though the logic moved.
|
||||
throw new ArgumentException(ex.Message, nameof(routeTemplate), ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,12 +5,47 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Routing.Patterns;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Template
|
||||
{
|
||||
[DebuggerDisplay("{DebuggerToString()}")]
|
||||
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)
|
||||
{
|
||||
return new TemplatePart()
|
||||
|
|
@ -20,11 +55,12 @@ namespace Microsoft.AspNetCore.Routing.Template
|
|||
};
|
||||
}
|
||||
|
||||
public static TemplatePart CreateParameter(string name,
|
||||
bool isCatchAll,
|
||||
bool isOptional,
|
||||
object defaultValue,
|
||||
IEnumerable<InlineConstraint> inlineConstraints)
|
||||
public static TemplatePart CreateParameter(
|
||||
string name,
|
||||
bool isCatchAll,
|
||||
bool isOptional,
|
||||
object defaultValue,
|
||||
IEnumerable<InlineConstraint> inlineConstraints)
|
||||
{
|
||||
if (name == null)
|
||||
{
|
||||
|
|
@ -63,5 +99,28 @@ namespace Microsoft.AspNetCore.Routing.Template
|
|||
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.Diagnostics;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Routing.Patterns;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Template
|
||||
{
|
||||
[DebuggerDisplay("{DebuggerToString()}")]
|
||||
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 List<TemplatePart> Parts { get; } = new List<TemplatePart>();
|
||||
public List<TemplatePart> Parts { get; }
|
||||
|
||||
internal string 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.Linq;
|
||||
using Microsoft.AspNetCore.Testing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Template.Tests
|
||||
|
|
@ -528,8 +525,8 @@ namespace Microsoft.AspNetCore.Routing.Template.Tests
|
|||
|
||||
[Theory]
|
||||
[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("{foorb?}-bar-{z}", "foorb", "-bar-")]
|
||||
public void Parse_ComplexSegment_OptionalParameter_NotTheLastPart(
|
||||
|
|
@ -655,18 +652,6 @@ namespace Microsoft.AspNetCore.Routing.Template.Tests
|
|||
"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]
|
||||
public void InvalidTemplate_CannotHaveConsecutiveOpenBrace()
|
||||
{
|
||||
|
|
@ -780,6 +765,18 @@ namespace Microsoft.AspNetCore.Routing.Template.Tests
|
|||
"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]
|
||||
public void InvalidTemplate_CannotStartWithTilde()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue