Introduce RoutePattern (#585)
* Introduce RoutePattern Introduces RoutePattern - a new parser and representation for routing templates, defaults, and constraints. This is a new representation for all of the 'inputs' to routing that is immutable and captures 'out of line' information for defaults and constraints. This will allow us to unify the handling of constraints and values from attribute style routes and conventional style routes.
This commit is contained in:
parent
bc79a47959
commit
9e114b547d
|
|
@ -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