From 3a5cd6dd25ccc122f4b421f1c7f4b60f972d4979 Mon Sep 17 00:00:00 2001 From: Jass Bagga Date: Tue, 17 Oct 2017 11:52:26 -0700 Subject: [PATCH] Port TemplateParser to Dispatcher project (#473) Addresses #466 --- .../DispatcherBenchmark.cs | 11 +- .../InlineConstraint.cs | 32 + .../InlineRouteParameterParser.cs | 242 +++++ .../Properties/Resources.Designer.cs | 196 ++++ .../Resources.resx | 42 + .../RouteTemplate.cs | 82 ++ .../TemplateParser.cs | 526 ++++++++++ .../TemplatePart.cs | 67 ++ .../TemplateSegment.cs | 22 + .../Dispatcher/RouteTemplateUrlGenerator.cs | 2 +- .../Dispatcher/TreeMatcher.cs | 2 +- .../InlineRouteParameterParser.cs | 229 +---- .../Properties/Resources.Designer.cs | 252 +---- .../Resources.resx | 54 +- .../Template/InlineConstraint.cs | 10 + .../Template/RouteTemplate.cs | 19 + .../Template/TemplateParser.cs | 513 +--------- .../Template/TemplatePart.cs | 17 + .../Template/TemplateSegment.cs | 9 + .../InlineRouteParameterParserTests.cs | 954 ++++++++++++++++++ .../TemplateParserTests.cs | 916 +++++++++++++++++ .../InlineRouteParameterParserTests.cs | 1 - .../Template/TemplateParserTests.cs | 3 - 23 files changed, 3183 insertions(+), 1018 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Dispatcher/InlineConstraint.cs create mode 100644 src/Microsoft.AspNetCore.Dispatcher/InlineRouteParameterParser.cs create mode 100644 src/Microsoft.AspNetCore.Dispatcher/RouteTemplate.cs create mode 100644 src/Microsoft.AspNetCore.Dispatcher/TemplateParser.cs create mode 100644 src/Microsoft.AspNetCore.Dispatcher/TemplatePart.cs create mode 100644 src/Microsoft.AspNetCore.Dispatcher/TemplateSegment.cs create mode 100644 test/Microsoft.AspNetCore.Dispatcher.Test/InlineRouteParameterParserTests.cs create mode 100644 test/Microsoft.AspNetCore.Dispatcher.Test/TemplateParserTests.cs diff --git a/benchmarks/Microsoft.AspNetCore.Dispatcher.Performance/DispatcherBenchmark.cs b/benchmarks/Microsoft.AspNetCore.Dispatcher.Performance/DispatcherBenchmark.cs index 3ce51fddc0..03b3019101 100644 --- a/benchmarks/Microsoft.AspNetCore.Dispatcher.Performance/DispatcherBenchmark.cs +++ b/benchmarks/Microsoft.AspNetCore.Dispatcher.Performance/DispatcherBenchmark.cs @@ -9,7 +9,6 @@ using BenchmarkDotNet.Attributes; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Internal; -using Microsoft.AspNetCore.Routing.Template; using Microsoft.AspNetCore.Routing.Tree; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.ObjectPool; @@ -35,11 +34,11 @@ namespace Microsoft.AspNetCore.Dispatcher.Performance new DefaultObjectPool(new UriBuilderContextPooledObjectPolicy(UrlEncoder.Default)), new DefaultInlineConstraintResolver(new OptionsManager(new OptionsFactory(Enumerable.Empty>(), Enumerable.Empty>())))); - treeBuilder.MapInbound(handler, TemplateParser.Parse("api/Widgets"), "default", 0); - treeBuilder.MapInbound(handler, TemplateParser.Parse("api/Widgets/{id}"), "default", 0); - treeBuilder.MapInbound(handler, TemplateParser.Parse("api/Widgets/search/{term}"), "default", 0); - treeBuilder.MapInbound(handler, TemplateParser.Parse("admin/users/{id}"), "default", 0); - treeBuilder.MapInbound(handler, TemplateParser.Parse("admin/users/{id}/manage"), "default", 0); + treeBuilder.MapInbound(handler, Routing.Template.TemplateParser.Parse("api/Widgets"), "default", 0); + treeBuilder.MapInbound(handler, Routing.Template.TemplateParser.Parse("api/Widgets/{id}"), "default", 0); + treeBuilder.MapInbound(handler, Routing.Template.TemplateParser.Parse("api/Widgets/search/{term}"), "default", 0); + treeBuilder.MapInbound(handler, Routing.Template.TemplateParser.Parse("admin/users/{id}"), "default", 0); + treeBuilder.MapInbound(handler, Routing.Template.TemplateParser.Parse("admin/users/{id}/manage"), "default", 0); _treeRouter = treeBuilder.Build(); diff --git a/src/Microsoft.AspNetCore.Dispatcher/InlineConstraint.cs b/src/Microsoft.AspNetCore.Dispatcher/InlineConstraint.cs new file mode 100644 index 0000000000..e8c88d7f02 --- /dev/null +++ b/src/Microsoft.AspNetCore.Dispatcher/InlineConstraint.cs @@ -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; + +namespace Microsoft.AspNetCore.Dispatcher +{ + /// + /// The parsed representation of an inline constraint in a route parameter. + /// + public class InlineConstraint + { + /// + /// Creates a new . + /// + /// The constraint text. + public InlineConstraint(string constraint) + { + if (constraint == null) + { + throw new ArgumentNullException(nameof(constraint)); + } + + Constraint = constraint; + } + + /// + /// Gets the constraint text. + /// + public string Constraint { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Dispatcher/InlineRouteParameterParser.cs b/src/Microsoft.AspNetCore.Dispatcher/InlineRouteParameterParser.cs new file mode 100644 index 0000000000..8fdddcb9c7 --- /dev/null +++ b/src/Microsoft.AspNetCore.Dispatcher/InlineRouteParameterParser.cs @@ -0,0 +1,242 @@ +// 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; + +namespace Microsoft.AspNetCore.Dispatcher +{ + public static class InlineRouteParameterParser + { + public static TemplatePart ParseRouteParameter(string routeParameter) + { + if (routeParameter == null) + { + throw new ArgumentNullException(nameof(routeParameter)); + } + + if (routeParameter.Length == 0) + { + return TemplatePart.CreateParameter( + name: string.Empty, + isCatchAll: false, + isOptional: false, + defaultValue: null, + inlineConstraints: null); + } + + var startIndex = 0; + var endIndex = routeParameter.Length - 1; + + var isCatchAll = false; + var isOptional = false; + + if (routeParameter[0] == '*') + { + isCatchAll = true; + startIndex++; + } + + if (routeParameter[endIndex] == '?') + { + isOptional = true; + endIndex--; + } + + var currentIndex = startIndex; + + // Parse parameter name + var parameterName = string.Empty; + + while (currentIndex <= endIndex) + { + var currentChar = routeParameter[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 = routeParameter.Substring(startIndex, currentIndex - startIndex); + + // Roll the index back and move to the constraint parsing stage. + currentIndex--; + break; + } + else if (currentIndex == endIndex) + { + parameterName = routeParameter.Substring(startIndex, currentIndex - startIndex + 1); + } + + currentIndex++; + } + + var parseResults = ParseConstraints(routeParameter, currentIndex, endIndex); + currentIndex = parseResults.CurrentIndex; + + string defaultValue = null; + if (currentIndex <= endIndex && + routeParameter[currentIndex] == '=') + { + defaultValue = routeParameter.Substring(currentIndex + 1, endIndex - currentIndex); + } + + return TemplatePart.CreateParameter(parameterName, + isCatchAll, + isOptional, + defaultValue, + parseResults.Constraints); + } + + private static ConstraintParseResults ParseConstraints( + string routeParameter, + int currentIndex, + int endIndex) + { + var inlineConstraints = new List(); + var state = ParseState.Start; + var startIndex = currentIndex; + do + { + var currentChar = currentIndex > endIndex ? null : (char?)routeParameter[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 = routeParameter.Substring(startIndex, currentIndex - startIndex); + inlineConstraints.Add(new InlineConstraint(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?)routeParameter[currentIndex + 1]; + switch (nextChar) + { + case null: + state = ParseState.End; + constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex + 1); + inlineConstraints.Add(new InlineConstraint(constraintText)); + break; + case ':': + state = ParseState.Start; + constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex + 1); + inlineConstraints.Add(new InlineConstraint(constraintText)); + startIndex = currentIndex + 1; + break; + case '=': + state = ParseState.End; + constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex + 1); + inlineConstraints.Add(new InlineConstraint(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 = routeParameter.IndexOf(')', currentIndex + 1); + if (indexOfClosingParantheses == -1) + { + constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex); + inlineConstraints.Add(new InlineConstraint(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 = routeParameter.Substring(startIndex, currentIndex - startIndex); + inlineConstraints.Add(new InlineConstraint(constraintText)); + break; + case ':': + constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex); + inlineConstraints.Add(new InlineConstraint(constraintText)); + startIndex = currentIndex + 1; + break; + case '(': + state = ParseState.InsideParenthesis; + break; + case '=': + state = ParseState.End; + constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex); + inlineConstraints.Add(new InlineConstraint(constraintText)); + currentIndex--; + break; + } + break; + } + + currentIndex++; + + } while (state != ParseState.End); + + return new ConstraintParseResults + { + CurrentIndex = currentIndex, + Constraints = inlineConstraints + }; + } + + private enum ParseState + { + Start, + ParsingName, + InsideParenthesis, + End + } + + private struct ConstraintParseResults + { + public int CurrentIndex; + + public IEnumerable Constraints; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Dispatcher/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Dispatcher/Properties/Resources.Designer.cs index 2a15f90c99..4b4f518aad 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Dispatcher/Properties/Resources.Designer.cs @@ -24,6 +24,202 @@ namespace Microsoft.AspNetCore.Dispatcher internal static string FormatAmbiguousEndpoints(object p0, object p1) => string.Format(CultureInfo.CurrentCulture, GetString("AmbiguousEndpoints"), p0, p1); + /// + /// A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter. + /// + internal static string TemplateRoute_CannotHaveCatchAllInMultiSegment + { + get => GetString("TemplateRoute_CannotHaveCatchAllInMultiSegment"); + } + + /// + /// A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter. + /// + internal static string FormatTemplateRoute_CannotHaveCatchAllInMultiSegment() + => GetString("TemplateRoute_CannotHaveCatchAllInMultiSegment"); + + /// + /// A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string. + /// + internal static string TemplateRoute_CannotHaveConsecutiveParameters + { + get => GetString("TemplateRoute_CannotHaveConsecutiveParameters"); + } + + /// + /// A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string. + /// + internal static string FormatTemplateRoute_CannotHaveConsecutiveParameters() + => GetString("TemplateRoute_CannotHaveConsecutiveParameters"); + + /// + /// The route template separator character '/' cannot appear consecutively. It must be separated by either a parameter or a literal value. + /// + internal static string TemplateRoute_CannotHaveConsecutiveSeparators + { + get => GetString("TemplateRoute_CannotHaveConsecutiveSeparators"); + } + + /// + /// The route template separator character '/' cannot appear consecutively. It must be separated by either a parameter or a literal value. + /// + internal static string FormatTemplateRoute_CannotHaveConsecutiveSeparators() + => GetString("TemplateRoute_CannotHaveConsecutiveSeparators"); + + /// + /// A catch-all parameter cannot be marked optional. + /// + internal static string TemplateRoute_CatchAllCannotBeOptional + { + get => GetString("TemplateRoute_CatchAllCannotBeOptional"); + } + + /// + /// A catch-all parameter cannot be marked optional. + /// + internal static string FormatTemplateRoute_CatchAllCannotBeOptional() + => GetString("TemplateRoute_CatchAllCannotBeOptional"); + + /// + /// A catch-all parameter can only appear as the last segment of the route template. + /// + internal static string TemplateRoute_CatchAllMustBeLast + { + get => GetString("TemplateRoute_CatchAllMustBeLast"); + } + + /// + /// A catch-all parameter can only appear as the last segment of the route template. + /// + internal static string FormatTemplateRoute_CatchAllMustBeLast() + => GetString("TemplateRoute_CatchAllMustBeLast"); + + /// + /// The literal section '{0}' is invalid. Literal sections cannot contain the '?' character. + /// + internal static string TemplateRoute_InvalidLiteral + { + get => GetString("TemplateRoute_InvalidLiteral"); + } + + /// + /// The literal section '{0}' is invalid. Literal sections cannot contain the '?' character. + /// + internal static string FormatTemplateRoute_InvalidLiteral(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_InvalidLiteral"), p0); + + /// + /// The route parameter name '{0}' 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. + /// + internal static string TemplateRoute_InvalidParameterName + { + get => GetString("TemplateRoute_InvalidParameterName"); + } + + /// + /// The route parameter name '{0}' 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. + /// + internal static string FormatTemplateRoute_InvalidParameterName(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_InvalidParameterName"), p0); + + /// + /// The route template cannot start with a '/' or '~' character. + /// + internal static string TemplateRoute_InvalidRouteTemplate + { + get => GetString("TemplateRoute_InvalidRouteTemplate"); + } + + /// + /// The route template cannot start with a '/' or '~' character. + /// + internal static string FormatTemplateRoute_InvalidRouteTemplate() + => GetString("TemplateRoute_InvalidRouteTemplate"); + + /// + /// There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character. + /// + internal static string TemplateRoute_MismatchedParameter + { + get => GetString("TemplateRoute_MismatchedParameter"); + } + + /// + /// There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character. + /// + internal static string FormatTemplateRoute_MismatchedParameter() + => GetString("TemplateRoute_MismatchedParameter"); + + /// + /// An optional parameter cannot have default value. + /// + internal static string TemplateRoute_OptionalCannotHaveDefaultValue + { + get => GetString("TemplateRoute_OptionalCannotHaveDefaultValue"); + } + + /// + /// An optional parameter cannot have default value. + /// + internal static string FormatTemplateRoute_OptionalCannotHaveDefaultValue() + => GetString("TemplateRoute_OptionalCannotHaveDefaultValue"); + + /// + /// In the segment '{0}', the optional parameter '{1}' is preceded by an invalid segment '{2}'. Only a period (.) can precede an optional parameter. + /// + internal static string TemplateRoute_OptionalParameterCanbBePrecededByPeriod + { + get => GetString("TemplateRoute_OptionalParameterCanbBePrecededByPeriod"); + } + + /// + /// In the segment '{0}', the optional parameter '{1}' is preceded by an invalid segment '{2}'. Only a period (.) can precede an optional parameter. + /// + internal static string FormatTemplateRoute_OptionalParameterCanbBePrecededByPeriod(object p0, object p1, object p2) + => string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_OptionalParameterCanbBePrecededByPeriod"), p0, p1, p2); + + /// + /// An optional parameter must be at the end of the segment. In the segment '{0}', optional parameter '{1}' is followed by '{2}'. + /// + internal static string TemplateRoute_OptionalParameterHasTobeTheLast + { + get => GetString("TemplateRoute_OptionalParameterHasTobeTheLast"); + } + + /// + /// An optional parameter must be at the end of the segment. In the segment '{0}', optional parameter '{1}' is followed by '{2}'. + /// + internal static string FormatTemplateRoute_OptionalParameterHasTobeTheLast(object p0, object p1, object p2) + => string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_OptionalParameterHasTobeTheLast"), p0, p1, p2); + + /// + /// The route parameter name '{0}' appears more than one time in the route template. + /// + internal static string TemplateRoute_RepeatedParameter + { + get => GetString("TemplateRoute_RepeatedParameter"); + } + + /// + /// The route parameter name '{0}' appears more than one time in the route template. + /// + internal static string FormatTemplateRoute_RepeatedParameter(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_RepeatedParameter"), p0); + + /// + /// In a route parameter, '{' and '}' must be escaped with '{{' and '}}'. + /// + internal static string TemplateRoute_UnescapedBrace + { + get => GetString("TemplateRoute_UnescapedBrace"); + } + + /// + /// In a route parameter, '{' and '}' must be escaped with '{{' and '}}'. + /// + internal static string FormatTemplateRoute_UnescapedBrace() + => GetString("TemplateRoute_UnescapedBrace"); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNetCore.Dispatcher/Resources.resx b/src/Microsoft.AspNetCore.Dispatcher/Resources.resx index f7aa8ef240..d9d1898387 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/Resources.resx +++ b/src/Microsoft.AspNetCore.Dispatcher/Resources.resx @@ -121,4 +121,46 @@ Multiple endpoints matched. The following endpoints matched the request:{0}{0}{1} 0 is the newline - 1 is a newline separate list of action display names + + A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter. + + + A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string. + + + The route template separator character '/' cannot appear consecutively. It must be separated by either a parameter or a literal value. + + + A catch-all parameter cannot be marked optional. + + + A catch-all parameter can only appear as the last segment of the route template. + + + The literal section '{0}' is invalid. Literal sections cannot contain the '?' character. + + + The route parameter name '{0}' 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. + + + The route template cannot start with a '/' or '~' character. + + + There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character. + + + An optional parameter cannot have default value. + + + In the segment '{0}', the optional parameter '{1}' is preceded by an invalid segment '{2}'. Only a period (.) can precede an optional parameter. + + + An optional parameter must be at the end of the segment. In the segment '{0}', optional parameter '{1}' is followed by '{2}'. + + + The route parameter name '{0}' appears more than one time in the route template. + + + In a route parameter, '{' and '}' must be escaped with '{{' and '}}'. + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Dispatcher/RouteTemplate.cs b/src/Microsoft.AspNetCore.Dispatcher/RouteTemplate.cs new file mode 100644 index 0000000000..eeda78f8e5 --- /dev/null +++ b/src/Microsoft.AspNetCore.Dispatcher/RouteTemplate.cs @@ -0,0 +1,82 @@ +// 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.Dispatcher +{ + [DebuggerDisplay("{DebuggerToString()}")] + public class RouteTemplate + { + private const string SeparatorString = "/"; + + public RouteTemplate(string template, List segments) + { + if (segments == null) + { + throw new ArgumentNullException(nameof(segments)); + } + + TemplateText = template; + + Segments = segments; + + Parameters = new List(); + 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 string TemplateText { get; } + + public IList Parameters { get; } + + public IList Segments { get; } + + public TemplateSegment GetSegment(int index) + { + if (index < 0) + { + throw new IndexOutOfRangeException(); + } + + return index >= Segments.Count ? null : Segments[index]; + } + + private string DebuggerToString() + { + return string.Join(SeparatorString, Segments.Select(s => s.DebuggerToString())); + } + + /// + /// Gets the parameter matching the given name. + /// + /// The name of the parameter to match. + /// The matching parameter or null if no parameter matches the given name. + public TemplatePart GetParameter(string 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; + } + } +} diff --git a/src/Microsoft.AspNetCore.Dispatcher/TemplateParser.cs b/src/Microsoft.AspNetCore.Dispatcher/TemplateParser.cs new file mode 100644 index 0000000000..a11ea090b9 --- /dev/null +++ b/src/Microsoft.AspNetCore.Dispatcher/TemplateParser.cs @@ -0,0 +1,526 @@ +// 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; + +namespace Microsoft.AspNetCore.Dispatcher +{ + 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; + } + + if (IsInvalidRouteTemplate(routeTemplate)) + { + throw new ArgumentException(Resources.TemplateRoute_InvalidRouteTemplate, nameof(routeTemplate)); + } + + var context = new TemplateParserContext(routeTemplate); + var segments = new List(); + + while (context.Next()) + { + if (context.Current == Separator) + { + // If we get here is means that there's a consecutive '/' character. + // Templates don't start with a '/' and parsing a segment consumes the separator. + throw new ArgumentException(Resources.TemplateRoute_CannotHaveConsecutiveSeparators, + nameof(routeTemplate)); + } + else + { + if (!ParseSegment(context, segments)) + { + throw new ArgumentException(context.Error, nameof(routeTemplate)); + } + } + } + + if (IsAllValid(context, segments)) + { + return new RouteTemplate(routeTemplate, segments); + } + else + { + throw new ArgumentException(context.Error, nameof(routeTemplate)); + } + } + + private static bool ParseSegment(TemplateParserContext context, List 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 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 _parameterNames = new HashSet(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 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; + } + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Dispatcher/TemplatePart.cs b/src/Microsoft.AspNetCore.Dispatcher/TemplatePart.cs new file mode 100644 index 0000000000..91a7d5673a --- /dev/null +++ b/src/Microsoft.AspNetCore.Dispatcher/TemplatePart.cs @@ -0,0 +1,67 @@ +// 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.Dispatcher +{ + [DebuggerDisplay("{DebuggerToString()}")] + public class TemplatePart + { + public static TemplatePart CreateLiteral(string text) + { + return new TemplatePart() + { + IsLiteral = true, + Text = text, + }; + } + + public static TemplatePart CreateParameter(string name, + bool isCatchAll, + bool isOptional, + object defaultValue, + IEnumerable inlineConstraints) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + return new TemplatePart() + { + IsParameter = true, + Name = name, + IsCatchAll = isCatchAll, + IsOptional = isOptional, + DefaultValue = defaultValue, + InlineConstraints = inlineConstraints ?? Enumerable.Empty(), + }; + } + + public bool IsCatchAll { get; private set; } + public bool IsLiteral { get; private set; } + public bool IsParameter { get; private set; } + public bool IsOptional { get; private set; } + public bool IsOptionalSeperator { get; set; } + public string Name { get; private set; } + public string Text { get; private set; } + public object DefaultValue { get; private set; } + public IEnumerable InlineConstraints { get; private set; } + + internal string DebuggerToString() + { + if (IsParameter) + { + return "{" + (IsCatchAll ? "*" : string.Empty) + Name + (IsOptional ? "?" : string.Empty) + "}"; + } + else + { + return Text; + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Dispatcher/TemplateSegment.cs b/src/Microsoft.AspNetCore.Dispatcher/TemplateSegment.cs new file mode 100644 index 0000000000..2db8e0bab8 --- /dev/null +++ b/src/Microsoft.AspNetCore.Dispatcher/TemplateSegment.cs @@ -0,0 +1,22 @@ +// 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.Dispatcher +{ + [DebuggerDisplay("{DebuggerToString()}")] + public class TemplateSegment + { + public bool IsSimple => Parts.Count == 1; + + public List Parts { get; } = new List(); + + internal string DebuggerToString() + { + return string.Join(string.Empty, Parts.Select(p => p.DebuggerToString())); + } + } +} diff --git a/src/Microsoft.AspNetCore.Routing/Dispatcher/RouteTemplateUrlGenerator.cs b/src/Microsoft.AspNetCore.Routing/Dispatcher/RouteTemplateUrlGenerator.cs index ac7bf97a56..7f4e631e5f 100644 --- a/src/Microsoft.AspNetCore.Routing/Dispatcher/RouteTemplateUrlGenerator.cs +++ b/src/Microsoft.AspNetCore.Routing/Dispatcher/RouteTemplateUrlGenerator.cs @@ -43,7 +43,7 @@ namespace Microsoft.AspNetCore.Routing.Dispatcher throw new InvalidOperationException("Can't find address"); } - var binder = new TemplateBinder(_urlEncoder, _pool, TemplateParser.Parse(address.Template), new RouteValueDictionary()); + var binder = new TemplateBinder(_urlEncoder, _pool, Template.TemplateParser.Parse(address.Template), new RouteValueDictionary()); var feature = httpContext.Features.Get(); var result = binder.GetValues(feature.Values.AsRouteValueDictionary(), new RouteValueDictionary(values)); diff --git a/src/Microsoft.AspNetCore.Routing/Dispatcher/TreeMatcher.cs b/src/Microsoft.AspNetCore.Routing/Dispatcher/TreeMatcher.cs index 3b87455ac9..af8bfd0dfe 100644 --- a/src/Microsoft.AspNetCore.Routing/Dispatcher/TreeMatcher.cs +++ b/src/Microsoft.AspNetCore.Routing/Dispatcher/TreeMatcher.cs @@ -146,7 +146,7 @@ namespace Microsoft.AspNetCore.Routing.Dispatcher var entries = new List(); foreach (var group in groups) { - var template = TemplateParser.Parse(group.Key.RouteTemplate); + var template = Template.TemplateParser.Parse(group.Key.RouteTemplate); var defaults = new RouteValueDictionary(); for (var i = 0; i < template.Parameters.Count; i++) diff --git a/src/Microsoft.AspNetCore.Routing/InlineRouteParameterParser.cs b/src/Microsoft.AspNetCore.Routing/InlineRouteParameterParser.cs index 78131eba97..4229aceac4 100644 --- a/src/Microsoft.AspNetCore.Routing/InlineRouteParameterParser.cs +++ b/src/Microsoft.AspNetCore.Routing/InlineRouteParameterParser.cs @@ -1,8 +1,7 @@ -// 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 Microsoft.AspNetCore.Routing.Template; namespace Microsoft.AspNetCore.Routing @@ -16,228 +15,8 @@ namespace Microsoft.AspNetCore.Routing throw new ArgumentNullException(nameof(routeParameter)); } - if (routeParameter.Length == 0) - { - return TemplatePart.CreateParameter( - name: string.Empty, - isCatchAll: false, - isOptional: false, - defaultValue: null, - inlineConstraints: null); - } - - var startIndex = 0; - var endIndex = routeParameter.Length - 1; - - var isCatchAll = false; - var isOptional = false; - - if (routeParameter[0] == '*') - { - isCatchAll = true; - startIndex++; - } - - if (routeParameter[endIndex] == '?') - { - isOptional = true; - endIndex--; - } - - var currentIndex = startIndex; - - // Parse parameter name - var parameterName = string.Empty; - - while (currentIndex <= endIndex) - { - var currentChar = routeParameter[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 = routeParameter.Substring(startIndex, currentIndex - startIndex); - - // Roll the index back and move to the constraint parsing stage. - currentIndex--; - break; - } - else if (currentIndex == endIndex) - { - parameterName = routeParameter.Substring(startIndex, currentIndex - startIndex + 1); - } - - currentIndex++; - } - - var parseResults = ParseConstraints(routeParameter, currentIndex, endIndex); - currentIndex = parseResults.CurrentIndex; - - string defaultValue = null; - if (currentIndex <= endIndex && - routeParameter[currentIndex] == '=') - { - defaultValue = routeParameter.Substring(currentIndex + 1, endIndex - currentIndex); - } - - return TemplatePart.CreateParameter(parameterName, - isCatchAll, - isOptional, - defaultValue, - parseResults.Constraints); - } - - private static ConstraintParseResults ParseConstraints( - string routeParameter, - int currentIndex, - int endIndex) - { - var inlineConstraints = new List(); - var state = ParseState.Start; - var startIndex = currentIndex; - do - { - var currentChar = currentIndex > endIndex ? null : (char?)routeParameter[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 = routeParameter.Substring(startIndex, currentIndex - startIndex); - inlineConstraints.Add(new InlineConstraint(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?)routeParameter[currentIndex + 1]; - switch (nextChar) - { - case null: - state = ParseState.End; - constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex + 1); - inlineConstraints.Add(new InlineConstraint(constraintText)); - break; - case ':': - state = ParseState.Start; - constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex + 1); - inlineConstraints.Add(new InlineConstraint(constraintText)); - startIndex = currentIndex + 1; - break; - case '=': - state = ParseState.End; - constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex + 1); - inlineConstraints.Add(new InlineConstraint(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 = routeParameter.IndexOf(')', currentIndex + 1); - if (indexOfClosingParantheses == -1) - { - constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex); - inlineConstraints.Add(new InlineConstraint(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 = routeParameter.Substring(startIndex, currentIndex - startIndex); - inlineConstraints.Add(new InlineConstraint(constraintText)); - break; - case ':': - constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex); - inlineConstraints.Add(new InlineConstraint(constraintText)); - startIndex = currentIndex + 1; - break; - case '(': - state = ParseState.InsideParenthesis; - break; - case '=': - state = ParseState.End; - constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex); - inlineConstraints.Add(new InlineConstraint(constraintText)); - currentIndex--; - break; - } - break; - } - - currentIndex++; - - } while (state != ParseState.End); - - return new ConstraintParseResults - { - CurrentIndex = currentIndex, - Constraints = inlineConstraints - }; - } - - private enum ParseState - { - Start, - ParsingName, - InsideParenthesis, - End - } - - private struct ConstraintParseResults - { - public int CurrentIndex; - - public IEnumerable Constraints; + var inner = AspNetCore.Dispatcher.InlineRouteParameterParser.ParseRouteParameter(routeParameter); + return new TemplatePart(inner); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNetCore.Routing/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Routing/Properties/Resources.Designer.cs index cf107358c3..fc8f6159b3 100644 --- a/src/Microsoft.AspNetCore.Routing/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Routing/Properties/Resources.Designer.cs @@ -122,174 +122,6 @@ namespace Microsoft.AspNetCore.Routing internal static string FormatDefaultInlineConstraintResolver_TypeNotConstraint(object p0, object p1, object p2) => string.Format(CultureInfo.CurrentCulture, GetString("DefaultInlineConstraintResolver_TypeNotConstraint"), p0, p1, p2); - /// - /// A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter. - /// - internal static string TemplateRoute_CannotHaveCatchAllInMultiSegment - { - get => GetString("TemplateRoute_CannotHaveCatchAllInMultiSegment"); - } - - /// - /// A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter. - /// - internal static string FormatTemplateRoute_CannotHaveCatchAllInMultiSegment() - => GetString("TemplateRoute_CannotHaveCatchAllInMultiSegment"); - - /// - /// The route parameter '{0}' 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. - /// - internal static string TemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly - { - get => GetString("TemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly"); - } - - /// - /// The route parameter '{0}' 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. - /// - internal static string FormatTemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly(object p0) - => string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly"), p0); - - /// - /// A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string. - /// - internal static string TemplateRoute_CannotHaveConsecutiveParameters - { - get => GetString("TemplateRoute_CannotHaveConsecutiveParameters"); - } - - /// - /// A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string. - /// - internal static string FormatTemplateRoute_CannotHaveConsecutiveParameters() - => GetString("TemplateRoute_CannotHaveConsecutiveParameters"); - - /// - /// The route template separator character '/' cannot appear consecutively. It must be separated by either a parameter or a literal value. - /// - internal static string TemplateRoute_CannotHaveConsecutiveSeparators - { - get => GetString("TemplateRoute_CannotHaveConsecutiveSeparators"); - } - - /// - /// The route template separator character '/' cannot appear consecutively. It must be separated by either a parameter or a literal value. - /// - internal static string FormatTemplateRoute_CannotHaveConsecutiveSeparators() - => GetString("TemplateRoute_CannotHaveConsecutiveSeparators"); - - /// - /// A catch-all parameter cannot be marked optional. - /// - internal static string TemplateRoute_CatchAllCannotBeOptional - { - get => GetString("TemplateRoute_CatchAllCannotBeOptional"); - } - - /// - /// A catch-all parameter cannot be marked optional. - /// - internal static string FormatTemplateRoute_CatchAllCannotBeOptional() - => GetString("TemplateRoute_CatchAllCannotBeOptional"); - - /// - /// An optional parameter cannot have default value. - /// - internal static string TemplateRoute_OptionalCannotHaveDefaultValue - { - get => GetString("TemplateRoute_OptionalCannotHaveDefaultValue"); - } - - /// - /// An optional parameter cannot have default value. - /// - internal static string FormatTemplateRoute_OptionalCannotHaveDefaultValue() - => GetString("TemplateRoute_OptionalCannotHaveDefaultValue"); - - /// - /// A catch-all parameter can only appear as the last segment of the route template. - /// - internal static string TemplateRoute_CatchAllMustBeLast - { - get => GetString("TemplateRoute_CatchAllMustBeLast"); - } - - /// - /// A catch-all parameter can only appear as the last segment of the route template. - /// - internal static string FormatTemplateRoute_CatchAllMustBeLast() - => GetString("TemplateRoute_CatchAllMustBeLast"); - - /// - /// The literal section '{0}' is invalid. Literal sections cannot contain the '?' character. - /// - internal static string TemplateRoute_InvalidLiteral - { - get => GetString("TemplateRoute_InvalidLiteral"); - } - - /// - /// The literal section '{0}' is invalid. Literal sections cannot contain the '?' character. - /// - internal static string FormatTemplateRoute_InvalidLiteral(object p0) - => string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_InvalidLiteral"), p0); - - /// - /// The route parameter name '{0}' 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. - /// - internal static string TemplateRoute_InvalidParameterName - { - get => GetString("TemplateRoute_InvalidParameterName"); - } - - /// - /// The route parameter name '{0}' 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. - /// - internal static string FormatTemplateRoute_InvalidParameterName(object p0) - => string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_InvalidParameterName"), p0); - - /// - /// The route template cannot start with a '/' or '~' character. - /// - internal static string TemplateRoute_InvalidRouteTemplate - { - get => GetString("TemplateRoute_InvalidRouteTemplate"); - } - - /// - /// The route template cannot start with a '/' or '~' character. - /// - internal static string FormatTemplateRoute_InvalidRouteTemplate() - => GetString("TemplateRoute_InvalidRouteTemplate"); - - /// - /// There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character. - /// - internal static string TemplateRoute_MismatchedParameter - { - get => GetString("TemplateRoute_MismatchedParameter"); - } - - /// - /// There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character. - /// - internal static string FormatTemplateRoute_MismatchedParameter() - => GetString("TemplateRoute_MismatchedParameter"); - - /// - /// The route parameter name '{0}' appears more than one time in the route template. - /// - internal static string TemplateRoute_RepeatedParameter - { - get => GetString("TemplateRoute_RepeatedParameter"); - } - - /// - /// The route parameter name '{0}' appears more than one time in the route template. - /// - internal static string FormatTemplateRoute_RepeatedParameter(object p0) - => string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_RepeatedParameter"), p0); - /// /// The constraint entry '{0}' - '{1}' on the route '{2}' must have a string value or be of a type which implements '{3}'. /// @@ -318,48 +150,6 @@ namespace Microsoft.AspNetCore.Routing internal static string FormatRouteConstraintBuilder_CouldNotResolveConstraint(object p0, object p1, object p2, object p3) => string.Format(CultureInfo.CurrentCulture, GetString("RouteConstraintBuilder_CouldNotResolveConstraint"), p0, p1, p2, p3); - /// - /// In a route parameter, '{' and '}' must be escaped with '{{' and '}}'. - /// - internal static string TemplateRoute_UnescapedBrace - { - get => GetString("TemplateRoute_UnescapedBrace"); - } - - /// - /// In a route parameter, '{' and '}' must be escaped with '{{' and '}}'. - /// - internal static string FormatTemplateRoute_UnescapedBrace() - => GetString("TemplateRoute_UnescapedBrace"); - - /// - /// In the segment '{0}', the optional parameter '{1}' is preceded by an invalid segment '{2}'. Only a period (.) can precede an optional parameter. - /// - internal static string TemplateRoute_OptionalParameterCanbBePrecededByPeriod - { - get => GetString("TemplateRoute_OptionalParameterCanbBePrecededByPeriod"); - } - - /// - /// In the segment '{0}', the optional parameter '{1}' is preceded by an invalid segment '{2}'. Only a period (.) can precede an optional parameter. - /// - internal static string FormatTemplateRoute_OptionalParameterCanbBePrecededByPeriod(object p0, object p1, object p2) - => string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_OptionalParameterCanbBePrecededByPeriod"), p0, p1, p2); - - /// - /// An optional parameter must be at the end of the segment. In the segment '{0}', optional parameter '{1}' is followed by '{2}'. - /// - internal static string TemplateRoute_OptionalParameterHasTobeTheLast - { - get => GetString("TemplateRoute_OptionalParameterHasTobeTheLast"); - } - - /// - /// An optional parameter must be at the end of the segment. In the segment '{0}', optional parameter '{1}' is followed by '{2}'. - /// - internal static string FormatTemplateRoute_OptionalParameterHasTobeTheLast(object p0, object p1, object p2) - => string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_OptionalParameterHasTobeTheLast"), p0, p1, p2); - /// /// Two or more routes named '{0}' have different templates. /// @@ -388,20 +178,6 @@ namespace Microsoft.AspNetCore.Routing internal static string FormatUnableToFindServices(object p0, object p1, object p2) => string.Format(CultureInfo.CurrentCulture, GetString("UnableToFindServices"), p0, p1, p2); - /// - /// An error occurred while creating the route with name '{0}' and template '{1}'. - /// - internal static string TemplateRoute_Exception - { - get => GetString("TemplateRoute_Exception"); - } - - /// - /// An error occurred while creating the route with name '{0}' and template '{1}'. - /// - internal static string FormatTemplateRoute_Exception(object p0, object p1) - => string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_Exception"), p0, p1); - /// /// The '{0}' has no '{1}'. '{2}' requires a dispatcher. /// @@ -416,6 +192,34 @@ namespace Microsoft.AspNetCore.Routing internal static string FormatDispatcherFeatureIsRequired(object p0, object p1, object p2) => string.Format(CultureInfo.CurrentCulture, GetString("DispatcherFeatureIsRequired"), p0, p1, p2); + /// + /// The route parameter '{0}' 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. + /// + internal static string TemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly + { + get => GetString("TemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly"); + } + + /// + /// The route parameter '{0}' 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. + /// + internal static string FormatTemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly"), p0); + + /// + /// An error occurred while creating the route with name '{0}' and template '{1}'. + /// + internal static string TemplateRoute_Exception + { + get => GetString("TemplateRoute_Exception"); + } + + /// + /// An error occurred while creating the route with name '{0}' and template '{1}'. + /// + internal static string FormatTemplateRoute_Exception(object p0, object p1) + => string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_Exception"), p0, p1); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNetCore.Routing/Resources.resx b/src/Microsoft.AspNetCore.Routing/Resources.resx index ceb82ab3b6..35c7533bfd 100644 --- a/src/Microsoft.AspNetCore.Routing/Resources.resx +++ b/src/Microsoft.AspNetCore.Routing/Resources.resx @@ -141,67 +141,25 @@ The constraint type '{0}' which is mapped to constraint key '{1}' must implement the '{2}' interface. - - A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter. - - - The route parameter '{0}' 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. - - - A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string. - - - The route template separator character '/' cannot appear consecutively. It must be separated by either a parameter or a literal value. - - - A catch-all parameter cannot be marked optional. - - - An optional parameter cannot have default value. - - - A catch-all parameter can only appear as the last segment of the route template. - - - The literal section '{0}' is invalid. Literal sections cannot contain the '?' character. - - - The route parameter name '{0}' 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. - - - The route template cannot start with a '/' or '~' character. - - - There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character. - - - The route parameter name '{0}' appears more than one time in the route template. - The constraint entry '{0}' - '{1}' on the route '{2}' must have a string value or be of a type which implements '{3}'. The constraint entry '{0}' - '{1}' on the route '{2}' could not be resolved by the constraint resolver of type '{3}'. - - In a route parameter, '{' and '}' must be escaped with '{{' and '}}'. - - - In the segment '{0}', the optional parameter '{1}' is preceded by an invalid segment '{2}'. Only a period (.) can precede an optional parameter. - - - An optional parameter must be at the end of the segment. In the segment '{0}', optional parameter '{1}' is followed by '{2}'. - Two or more routes named '{0}' have different templates. Unable to find the required services. Please add all the required services by calling '{0}.{1}' inside the call to '{2}' in the application startup code. - - An error occurred while creating the route with name '{0}' and template '{1}'. - The '{0}' has no '{1}'. '{2}' requires a dispatcher. + + The route parameter '{0}' 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. + + + An error occurred while creating the route with name '{0}' and template '{1}'. + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Routing/Template/InlineConstraint.cs b/src/Microsoft.AspNetCore.Routing/Template/InlineConstraint.cs index 5c50eefb0e..50e6db3e9c 100644 --- a/src/Microsoft.AspNetCore.Routing/Template/InlineConstraint.cs +++ b/src/Microsoft.AspNetCore.Routing/Template/InlineConstraint.cs @@ -24,6 +24,16 @@ namespace Microsoft.AspNetCore.Routing.Template Constraint = constraint; } + public InlineConstraint(AspNetCore.Dispatcher.InlineConstraint constraint) + { + if (constraint == null) + { + throw new ArgumentNullException(nameof(constraint)); + } + + Constraint = constraint.Constraint; + } + /// /// Gets the constraint text. /// diff --git a/src/Microsoft.AspNetCore.Routing/Template/RouteTemplate.cs b/src/Microsoft.AspNetCore.Routing/Template/RouteTemplate.cs index d482f3a1b4..b9b621ca12 100644 --- a/src/Microsoft.AspNetCore.Routing/Template/RouteTemplate.cs +++ b/src/Microsoft.AspNetCore.Routing/Template/RouteTemplate.cs @@ -13,6 +13,25 @@ namespace Microsoft.AspNetCore.Routing.Template { private const string SeparatorString = "/"; + public RouteTemplate(AspNetCore.Dispatcher.RouteTemplate routeTemplate) + { + TemplateText = routeTemplate.TemplateText; + Segments = new List(routeTemplate.Segments.Select(p => new TemplateSegment(p))); + Parameters = new List(); + 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 segments) { if (segments == null) diff --git a/src/Microsoft.AspNetCore.Routing/Template/TemplateParser.cs b/src/Microsoft.AspNetCore.Routing/Template/TemplateParser.cs index 51f598a486..a315d3d949 100644 --- a/src/Microsoft.AspNetCore.Routing/Template/TemplateParser.cs +++ b/src/Microsoft.AspNetCore.Routing/Template/TemplateParser.cs @@ -1,526 +1,21 @@ -// 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; 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); } - if (IsInvalidRouteTemplate(routeTemplate)) - { - throw new ArgumentException(Resources.TemplateRoute_InvalidRouteTemplate, nameof(routeTemplate)); - } - - var context = new TemplateParserContext(routeTemplate); - var segments = new List(); - - while (context.Next()) - { - if (context.Current == Separator) - { - // If we get here is means that there's a consecutive '/' character. - // Templates don't start with a '/' and parsing a segment consumes the separator. - throw new ArgumentException(Resources.TemplateRoute_CannotHaveConsecutiveSeparators, - nameof(routeTemplate)); - } - else - { - if (!ParseSegment(context, segments)) - { - throw new ArgumentException(context.Error, nameof(routeTemplate)); - } - } - } - - if (IsAllValid(context, segments)) - { - return new RouteTemplate(routeTemplate, segments); - } - else - { - throw new ArgumentException(context.Error, nameof(routeTemplate)); - } - } - - private static bool ParseSegment(TemplateParserContext context, List 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 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 _parameterNames = new HashSet(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 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; - } - } + var inner = AspNetCore.Dispatcher.TemplateParser.Parse(routeTemplate); + return new RouteTemplate(inner); } } } diff --git a/src/Microsoft.AspNetCore.Routing/Template/TemplatePart.cs b/src/Microsoft.AspNetCore.Routing/Template/TemplatePart.cs index 70f588a41a..6b358a6e06 100644 --- a/src/Microsoft.AspNetCore.Routing/Template/TemplatePart.cs +++ b/src/Microsoft.AspNetCore.Routing/Template/TemplatePart.cs @@ -11,6 +11,23 @@ namespace Microsoft.AspNetCore.Routing.Template [DebuggerDisplay("{DebuggerToString()}")] public class TemplatePart { + public TemplatePart() + { + } + + public TemplatePart(AspNetCore.Dispatcher.TemplatePart templatePart) + { + IsCatchAll = templatePart.IsCatchAll; + IsLiteral = templatePart.IsLiteral; + IsOptional = templatePart.IsOptional; + IsOptionalSeperator = templatePart.IsOptionalSeperator; + IsParameter = templatePart.IsParameter; + Name = templatePart.Name; + Text = templatePart.Text; + DefaultValue = templatePart.DefaultValue; + InlineConstraints = templatePart.InlineConstraints?.Select(p => new InlineConstraint(p)); + } + public static TemplatePart CreateLiteral(string text) { return new TemplatePart() diff --git a/src/Microsoft.AspNetCore.Routing/Template/TemplateSegment.cs b/src/Microsoft.AspNetCore.Routing/Template/TemplateSegment.cs index 4a86526509..2bb5b2682f 100644 --- a/src/Microsoft.AspNetCore.Routing/Template/TemplateSegment.cs +++ b/src/Microsoft.AspNetCore.Routing/Template/TemplateSegment.cs @@ -10,6 +10,15 @@ namespace Microsoft.AspNetCore.Routing.Template [DebuggerDisplay("{DebuggerToString()}")] public class TemplateSegment { + public TemplateSegment() + { + } + + public TemplateSegment(AspNetCore.Dispatcher.TemplateSegment templateSegment) + { + Parts = new List(templateSegment.Parts.Select(s => new TemplatePart(s))); + } + public bool IsSimple => Parts.Count == 1; public List Parts { get; } = new List(); diff --git a/test/Microsoft.AspNetCore.Dispatcher.Test/InlineRouteParameterParserTests.cs b/test/Microsoft.AspNetCore.Dispatcher.Test/InlineRouteParameterParserTests.cs new file mode 100644 index 0000000000..7ee32d589e --- /dev/null +++ b/test/Microsoft.AspNetCore.Dispatcher.Test/InlineRouteParameterParserTests.cs @@ -0,0 +1,954 @@ +// 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.Dispatcher +{ + public class InlineRouteParameterParserTests + { + [Theory] + [InlineData("=")] + [InlineData(":")] + public void ParseRouteParameter_WithoutADefaultValue(string parameterName) + { + // Arrange & Act + var templatePart = ParseParameter(parameterName); + + // Assert + Assert.Equal(parameterName, templatePart.Name); + Assert.Null(templatePart.DefaultValue); + Assert.Empty(templatePart.InlineConstraints); + } + + [Fact] + public void ParseRouteParameter_WithEmptyDefaultValue() + { + // Arrange & Act + var templatePart = ParseParameter("param="); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("", templatePart.DefaultValue); + Assert.Empty(templatePart.InlineConstraints); + } + + [Fact] + public void ParseRouteParameter_WithoutAConstraintName() + { + // Arrange & Act + var templatePart = ParseParameter("param:"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Null(templatePart.DefaultValue); + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Empty(constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_WithoutAConstraintNameOrParameterName() + { + // Arrange & Act + var templatePart = ParseParameter("param:="); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("", templatePart.DefaultValue); + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Empty(constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_WithADefaultValueContainingConstraintSeparator() + { + // Arrange & Act + var templatePart = ParseParameter("param=:"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal(":", templatePart.DefaultValue); + Assert.Empty(templatePart.InlineConstraints); + } + + [Fact] + public void ParseRouteParameter_ConstraintAndDefault_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter("param:int=111111"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("111111", templatePart.DefaultValue); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal("int", constraint.Constraint); + } + + [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.DefaultValue); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\d+)", constraint.Constraint); + } + + [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.InlineConstraints); + Assert.Equal("int", constraint.Constraint); + } + + [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.DefaultValue); + Assert.True(templatePart.IsOptional); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal("int", constraint.Constraint); + } + + [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.DefaultValue); + Assert.True(templatePart.IsOptional); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal("int", constraint.Constraint); + } + + [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.InlineConstraints); + Assert.Equal(@"test(\d+)", constraint.Constraint); + } + + [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.DefaultValue); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\d+)", constraint.Constraint); + } + + [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.InlineConstraints, + constraint => Assert.Equal(@"test(d+)", constraint.Constraint), + constraint => Assert.Equal(@"test(w+)", constraint.Constraint)); + } + + [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.InlineConstraints, + constraint => Assert.Empty(constraint.Constraint), + constraint => Assert.Equal(@"test(d+)", constraint.Constraint), + constraint => Assert.Empty(constraint.Constraint), + constraint => Assert.Equal(@"test(w+)", constraint.Constraint)); + } + + [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.InlineConstraints, + constraint => Assert.Equal(@"test(\d+)", constraint.Constraint), + constraint => Assert.Equal(@"test(\w:+)", constraint.Constraint)); + } + + [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.DefaultValue); + + Assert.Collection(templatePart.InlineConstraints, + constraint => Assert.Equal(@"test(\d+)", constraint.Constraint), + constraint => Assert.Equal(@"test(\w+)", constraint.Constraint)); + } + + [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.DefaultValue); + + Assert.Collection(templatePart.InlineConstraints, + constraint => Assert.Equal(@"test(\d+)", constraint.Constraint), + constraint => Assert.Empty(constraint.Constraint), + constraint => Assert.Equal(@"test(\w+)", constraint.Constraint)); + } + + [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.DefaultValue); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal("length(6)", constraint.Constraint); + } + + [Fact] + public void ParseRouteTemplate_ConstraintsDefaultsAndOptionalsInMultipleSections_ParsedCorrectly() + { + // Arrange & Act + var template = ParseRouteTemplate(@"some/url-{p1:int:test(3)=hello}/{p2=abc}/{p3?}"); + + // Assert + var parameters = template.Parameters.ToArray(); + + var param1 = parameters[0]; + Assert.Equal("p1", param1.Name); + Assert.Equal("hello", param1.DefaultValue); + Assert.False(param1.IsOptional); + + Assert.Collection(param1.InlineConstraints, + constraint => Assert.Equal("int", constraint.Constraint), + constraint => Assert.Equal("test(3)", constraint.Constraint) + ); + + var param2 = parameters[1]; + Assert.Equal("p2", param2.Name); + Assert.Equal("abc", param2.DefaultValue); + 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.DefaultValue); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithClosingBraceInPattern_ClosingBraceIsParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\})"); + + // Assert + Assert.Equal("param", templatePart.Name); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\})", constraint.Constraint); + } + + [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.DefaultValue); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\})", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithClosingParenInPattern_ClosingParenIsParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\))"); + + // Assert + Assert.Equal("param", templatePart.Name); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\))", constraint.Constraint); + } + + [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.DefaultValue); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\))", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithColonInPattern_ColonIsParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(:)"); + + // Assert + Assert.Equal("param", templatePart.Name); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(:)", constraint.Constraint); + } + + [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.DefaultValue); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(:)", constraint.Constraint); + } + + [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.InlineConstraints); + Assert.Equal(@"test(a:b:c)", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithColonInParamName_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@":param:test=12"); + + // Assert + Assert.Equal(":param", templatePart.Name); + + Assert.Equal("12", templatePart.DefaultValue); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal("test", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithTwoColonInParamName_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@":param::test=12"); + + // Assert + Assert.Equal(":param", templatePart.Name); + + Assert.Equal("12", templatePart.DefaultValue); + + Assert.Collection(templatePart.InlineConstraints, + constraint => Assert.Empty(constraint.Constraint), + constraint => Assert.Equal("test", constraint.Constraint)); + } + + [Fact] + public void ParseRouteParameter_EmptyConstraint_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@":param:test:"); + + // Assert + Assert.Equal(":param", templatePart.Name); + + Assert.Collection(templatePart.InlineConstraints, + constraint => Assert.Equal("test", constraint.Constraint), + constraint => Assert.Empty(constraint.Constraint)); + } + + [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.InlineConstraints); + Assert.Equal(@"test(\w,\w)", constraint.Constraint); + } + + [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.InlineConstraints); + Assert.Equal(@"test(\w)", constraint.Constraint); + } + + [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.DefaultValue); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\w,\w)", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithEqualsFollowedByQuestionMark_PatternIsParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:int=?"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("", templatePart.DefaultValue); + + Assert.True(templatePart.IsOptional); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal("int", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithEqualsSignInPattern_PatternIsParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(=)"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Null(templatePart.DefaultValue); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal("test(=)", constraint.Constraint); + } + + [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.DefaultValue); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithEqualEqualSignInPattern_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(a==b)"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Null(templatePart.DefaultValue); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal("test(a==b)", constraint.Constraint); + } + + [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.DefaultValue); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal("test(a==b)", constraint.Constraint); + } + + [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.DefaultValue); + } + + [Fact] + public void ParseRouteParameter_EqualEqualSignInDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test==dvds"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("=dvds", templatePart.DefaultValue); + } + + [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.DefaultValue); + } + + [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.DefaultValue); + } + + [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.DefaultValue); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal("test(=)", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithOpenBraceInPattern_PatternIsParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\{)"); + + // Assert + Assert.Equal("param", templatePart.Name); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\{)", constraint.Constraint); + } + + [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.InlineConstraints); + Assert.Equal(@"test(\sd)", constraint.Constraint); + } + + [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.DefaultValue); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\{)", constraint.Constraint); + } + + [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.InlineConstraints); + Assert.Equal(@"test(\()", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithOpenParenInPattern_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\()"); + + // Assert + Assert.Equal("param", templatePart.Name); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\()", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithOpenParenNoCloseParen_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(#$%"); + + // Assert + Assert.Equal("param", templatePart.Name); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal("test(#$%", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithOpenParenAndColon_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(#:test1"); + + // Assert + Assert.Equal("param", templatePart.Name); + + Assert.Collection(templatePart.InlineConstraints, + constraint => Assert.Equal(@"test(#", constraint.Constraint), + constraint => Assert.Equal(@"test1", constraint.Constraint)); + } + + [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.DefaultValue); + + Assert.Collection(templatePart.InlineConstraints, + constraint => Assert.Equal(@"test(abc:somevalue)", constraint.Constraint), + constraint => Assert.Equal(@"name(test1", constraint.Constraint), + constraint => Assert.Equal(@"differentname", constraint.Constraint)); + } + + [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.DefaultValue); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(constraintvalue", constraint.Constraint); + } + + [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.DefaultValue); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\()", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithQuestionMarkInPattern_PatternIsParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\?)"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Null(templatePart.DefaultValue); + Assert.False(templatePart.IsOptional); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\?)", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithQuestionMarkInPattern_Optional_PatternIsParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\?)?"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Null(templatePart.DefaultValue); + Assert.True(templatePart.IsOptional); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\?)", constraint.Constraint); + } + + [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.DefaultValue); + Assert.False(templatePart.IsOptional); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\?)", constraint.Constraint); + } + + [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.DefaultValue); + Assert.True(templatePart.IsOptional); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\?)", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithQuestionMarkInName_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"par?am:test(\?)"); + + // Assert + Assert.Equal("par?am", templatePart.Name); + Assert.Null(templatePart.DefaultValue); + Assert.False(templatePart.IsOptional); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\?)", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithClosedParenAndColonInPattern_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(#):$)"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Null(templatePart.DefaultValue); + Assert.False(templatePart.IsOptional); + + Assert.Collection(templatePart.InlineConstraints, + constraint => Assert.Equal(@"test(#)", constraint.Constraint), + constraint => Assert.Equal(@"$)", constraint.Constraint)); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithColonAndClosedParenInPattern_PatternIsParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(#:)$)"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Null(templatePart.DefaultValue); + Assert.False(templatePart.IsOptional); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(#:)$)", constraint.Constraint); + } + + [Fact] + public void ParseRouteParameter_ContainingMultipleUnclosedParenthesisInConstraint() + { + // Arrange & Act + var templatePart = ParseParameter(@"foo:regex(\\(\\(\\(\\()"); + + // Assert + Assert.Equal("foo", templatePart.Name); + Assert.Null(templatePart.DefaultValue); + Assert.False(templatePart.IsOptional); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"regex(\\(\\(\\(\\()", constraint.Constraint); + } + + [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.DefaultValue); + Assert.False(templatePart.IsOptional); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"regex(^\d{{3}}-\d{{3}}-\d{{4}}$)", constraint.Constraint); + } + + [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.DefaultValue); + Assert.False(templatePart.IsOptional); + + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"regex(^\d{{3}}-\d{{3}}-\d{{4}}$)", constraint.Constraint); + } + + [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.InlineConstraints); + Assert.Null(templatePart.DefaultValue); + } + + + private TemplatePart ParseParameter(string routeParameter) + { + var templatePart = InlineRouteParameterParser.ParseRouteParameter(routeParameter); + return templatePart; + } + + private static RouteTemplate ParseRouteTemplate(string template) + { + return TemplateParser.Parse(template); + } + } +} diff --git a/test/Microsoft.AspNetCore.Dispatcher.Test/TemplateParserTests.cs b/test/Microsoft.AspNetCore.Dispatcher.Test/TemplateParserTests.cs new file mode 100644 index 0000000000..e02347a3da --- /dev/null +++ b/test/Microsoft.AspNetCore.Dispatcher.Test/TemplateParserTests.cs @@ -0,0 +1,916 @@ +// 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.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Dispatcher +{ + public class TemplateRouteParserTests + { + [Fact] + public void Parse_SingleLiteral() + { + // Arrange + var template = "cool"; + + var expected = new RouteTemplate(template, new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool")); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateEqualityComparer()); + } + + [Fact] + public void Parse_SingleParameter() + { + // Arrange + var template = "{p}"; + + var expected = new RouteTemplate(template, new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add( + TemplatePart.CreateParameter("p", false, false, defaultValue: null, inlineConstraints: null)); + expected.Parameters.Add(expected.Segments[0].Parts[0]); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateEqualityComparer()); + } + + [Fact] + public void Parse_OptionalParameter() + { + // Arrange + var template = "{p?}"; + + var expected = new RouteTemplate(template, new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add( + TemplatePart.CreateParameter("p", false, true, defaultValue: null, inlineConstraints: null)); + expected.Parameters.Add(expected.Segments[0].Parts[0]); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateEqualityComparer()); + } + + [Fact] + public void Parse_MultipleLiterals() + { + // Arrange + var template = "cool/awesome/super"; + + var expected = new RouteTemplate(template, new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool")); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[1].Parts.Add(TemplatePart.CreateLiteral("awesome")); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[2].Parts.Add(TemplatePart.CreateLiteral("super")); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateEqualityComparer()); + } + + [Fact] + public void Parse_MultipleParameters() + { + // Arrange + var template = "{p1}/{p2}/{*p3}"; + + var expected = new RouteTemplate(template, new List()); + + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", + false, + false, + defaultValue: null, + inlineConstraints: null)); + expected.Parameters.Add(expected.Segments[0].Parts[0]); + + expected.Segments.Add(new TemplateSegment()); + expected.Segments[1].Parts.Add(TemplatePart.CreateParameter("p2", + false, + false, + defaultValue: null, + inlineConstraints: null)); + expected.Parameters.Add(expected.Segments[1].Parts[0]); + + expected.Segments.Add(new TemplateSegment()); + expected.Segments[2].Parts.Add(TemplatePart.CreateParameter("p3", + true, + false, + defaultValue: null, + inlineConstraints: null)); + expected.Parameters.Add(expected.Segments[2].Parts[0]); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateEqualityComparer()); + } + + [Fact] + public void Parse_ComplexSegment_LP() + { + // Arrange + var template = "cool-{p1}"; + + var expected = new RouteTemplate(template, new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool-")); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", + false, + false, + defaultValue: null, + inlineConstraints: null)); + expected.Parameters.Add(expected.Segments[0].Parts[1]); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateEqualityComparer()); + } + + [Fact] + public void Parse_ComplexSegment_PL() + { + // Arrange + var template = "{p1}-cool"; + + var expected = new RouteTemplate(template, new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", + false, + false, + defaultValue: null, + inlineConstraints: null)); + expected.Parameters.Add(expected.Segments[0].Parts[0]); + expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool-")); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateEqualityComparer()); + } + + [Fact] + public void Parse_ComplexSegment_PLP() + { + // Arrange + var template = "{p1}-cool-{p2}"; + + var expected = new RouteTemplate(template, new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", + false, + false, + defaultValue: null, + inlineConstraints: null)); + expected.Parameters.Add(expected.Segments[0].Parts[0]); + expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool-")); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2", + false, + false, + defaultValue: null, + inlineConstraints: null)); + expected.Parameters.Add(expected.Segments[0].Parts[2]); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateEqualityComparer()); + } + + [Fact] + public void Parse_ComplexSegment_LPL() + { + // Arrange + var template = "cool-{p1}-awesome"; + + var expected = new RouteTemplate(template, new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool-")); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", + false, + false, + defaultValue: null, + inlineConstraints: null)); + expected.Parameters.Add(expected.Segments[0].Parts[1]); + expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("-awesome")); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateEqualityComparer()); + } + + [Fact] + public void Parse_ComplexSegment_OptionalParameterFollowingPeriod() + { + // Arrange + var template = "{p1}.{p2?}"; + + var expected = new RouteTemplate(template, new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", + false, + false, + defaultValue: null, + inlineConstraints: null)); + expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral(".")); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2", + false, + true, + defaultValue: null, + inlineConstraints: null)); + + expected.Parameters.Add(expected.Segments[0].Parts[0]); + expected.Parameters.Add(expected.Segments[0].Parts[2]); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateEqualityComparer()); + } + + [Fact] + public void Parse_ComplexSegment_ParametersFollowingPeriod() + { + // Arrange + var template = "{p1}.{p2}"; + + var expected = new RouteTemplate(template, new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", + false, + false, + defaultValue: null, + inlineConstraints: null)); + expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral(".")); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2", + false, + false, + defaultValue: null, + inlineConstraints: null)); + + expected.Parameters.Add(expected.Segments[0].Parts[0]); + expected.Parameters.Add(expected.Segments[0].Parts[2]); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateEqualityComparer()); + } + + [Fact] + public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_ThreeParameters() + { + // Arrange + var template = "{p1}.{p2}.{p3?}"; + + var expected = new RouteTemplate(template, new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", + false, + false, + defaultValue: null, + inlineConstraints: null)); + expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral(".")); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2", + false, + false, + defaultValue: null, + inlineConstraints: null)); + + expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral(".")); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p3", + false, + true, + defaultValue: null, + inlineConstraints: null)); + + + expected.Parameters.Add(expected.Segments[0].Parts[0]); + expected.Parameters.Add(expected.Segments[0].Parts[2]); + expected.Parameters.Add(expected.Segments[0].Parts[4]); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateEqualityComparer()); + } + + [Fact] + public void Parse_ComplexSegment_ThreeParametersSeperatedByPeriod() + { + // Arrange + var template = "{p1}.{p2}.{p3}"; + + var expected = new RouteTemplate(template, new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", + false, + false, + defaultValue: null, + inlineConstraints: null)); + expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral(".")); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2", + false, + false, + defaultValue: null, + inlineConstraints: null)); + + expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral(".")); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p3", + false, + false, + defaultValue: null, + inlineConstraints: null)); + + + expected.Parameters.Add(expected.Segments[0].Parts[0]); + expected.Parameters.Add(expected.Segments[0].Parts[2]); + expected.Parameters.Add(expected.Segments[0].Parts[4]); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateEqualityComparer()); + } + + [Fact] + public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_MiddleSegment() + { + // Arrange + var template = "{p1}.{p2?}/{p3}"; + + var expected = new RouteTemplate(template, new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", + false, + false, + defaultValue: null, + inlineConstraints: null)); + expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral(".")); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2", + false, + true, + defaultValue: null, + inlineConstraints: null)); + + expected.Parameters.Add(expected.Segments[0].Parts[0]); + expected.Parameters.Add(expected.Segments[0].Parts[2]); + + expected.Segments.Add(new TemplateSegment()); + expected.Segments[1].Parts.Add(TemplatePart.CreateParameter("p3", + false, + false, + null, + null)); + expected.Parameters.Add(expected.Segments[1].Parts[0]); + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateEqualityComparer()); + } + + [Fact] + public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_LastSegment() + { + // Arrange + var template = "{p1}/{p2}.{p3?}"; + + var expected = new RouteTemplate(template, new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", + false, + false, + defaultValue: null, + inlineConstraints: null)); + + expected.Segments.Add(new TemplateSegment()); + expected.Segments[1].Parts.Add(TemplatePart.CreateParameter("p2", + false, + false, + defaultValue: null, + inlineConstraints: null)); + expected.Segments[1].Parts.Add(TemplatePart.CreateLiteral(".")); + expected.Segments[1].Parts.Add(TemplatePart.CreateParameter("p3", + false, + true, + null, + null)); + expected.Parameters.Add(expected.Segments[0].Parts[0]); + expected.Parameters.Add(expected.Segments[1].Parts[0]); + expected.Parameters.Add(expected.Segments[1].Parts[2]); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateEqualityComparer()); + } + + [Fact] + public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_PeriodAfterSlash() + { + // Arrange + var template = "{p2}/.{p3?}"; + + var expected = new RouteTemplate(template, new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2", + false, + false, + defaultValue: null, + inlineConstraints: null)); + + expected.Segments.Add(new TemplateSegment()); + expected.Segments[1].Parts.Add(TemplatePart.CreateLiteral(".")); + expected.Segments[1].Parts.Add(TemplatePart.CreateParameter("p3", + false, + true, + null, + null)); + expected.Parameters.Add(expected.Segments[0].Parts[0]); + expected.Parameters.Add(expected.Segments[1].Parts[1]); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateEqualityComparer()); + } + + [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 = new RouteTemplate(template, new List()); + expected.Segments.Add(new TemplateSegment()); + var c = new InlineConstraint(constraint); + expected.Segments[0].Parts.Add( + TemplatePart.CreateParameter("p1", + false, + false, + defaultValue: null, + inlineConstraints: new List { c })); + expected.Parameters.Add(expected.Segments[0].Parts[0]); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateEqualityComparer()); + } + + [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( + () => TemplateParser.Parse(template), + "There is an incomplete parameter in the route template. Check that each '{' character has a matching " + + "'}' character." + Environment.NewLine + "Parameter name: routeTemplate"); + } + + [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( + () => TemplateParser.Parse(template), + "In a route parameter, '{' and '}' must be escaped with '{{' and '}}'." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [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( + () => TemplateParser.Parse(template), + "An optional parameter must be at the end of the segment. In the segment '" + template + + "', optional parameter '" + parameter + "' is followed by '" + invalid + "'." + + Environment.NewLine + "Parameter name: routeTemplate"); + } + + [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( + () => TemplateParser.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." + + Environment.NewLine + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_WithRepeatedParameter() + { + var ex = ExceptionAssert.Throws( + () => TemplateParser.Parse("{Controller}.mvc/{id}/{controller}"), + "The route parameter name 'controller' appears more than one time in the route template." + + Environment.NewLine + "Parameter name: routeTemplate"); + } + + [Theory] + [InlineData("123{a}abc{")] + [InlineData("123{a}abc}")] + [InlineData("xyz}123{a}abc}")] + [InlineData("{{p1}")] + [InlineData("{p1}}")] + [InlineData("p1}}p2{")] + public void InvalidTemplate_WithMismatchedBraces(string template) + { + ExceptionAssert.Throws( + () => TemplateParser.Parse(template), + @"There is an incomplete parameter in the route template. Check that each '{' character has a " + + "matching '}' character." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_CannotHaveCatchAllInMultiSegment() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("123{a}abc{*moo}"), + "A path segment that contains more than one section, such as a literal section or a parameter, " + + "cannot contain a catch-all parameter." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_CannotHaveMoreThanOneCatchAll() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("{*p1}/{*p2}"), + "A catch-all parameter can only appear as the last segment of the route template." + + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_CannotHaveMoreThanOneCatchAllInMultiSegment() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("{*p1}abc{*p2}"), + "A path segment that contains more than one section, such as a literal section or a parameter, " + + "cannot contain a catch-all parameter." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_CannotHaveCatchAllWithNoName() + { + ExceptionAssert.Throws( + () => TemplateParser.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." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [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( + () => TemplateParser.Parse(template), expectedMessage + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_CannotHaveConsecutiveOpenBrace() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("foo/{{p1}"), + "There is an incomplete parameter in the route template. Check that each '{' character has a " + + "matching '}' character." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_CannotHaveConsecutiveCloseBrace() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("foo/{p1}}"), + "There is an incomplete parameter in the route template. Check that each '{' character has a " + + "matching '}' character." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_SameParameterTwiceThrows() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("{aaa}/{AAA}"), + "The route parameter name 'AAA' appears more than one time in the route template." + + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_SameParameterTwiceAndOneCatchAllThrows() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("{aaa}/{*AAA}"), + "The route parameter name 'AAA' appears more than one time in the route template." + + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_InvalidParameterNameWithCloseBracketThrows() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("{a}/{aa}a}/{z}"), + "There is an incomplete parameter in the route template. Check that each '{' character has a " + + "matching '}' character." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_InvalidParameterNameWithOpenBracketThrows() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("{a}/{a{aa}/{z}"), + "In a route parameter, '{' and '}' must be escaped with '{{' and '}}'." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_InvalidParameterNameWithEmptyNameThrows() + { + ExceptionAssert.Throws( + () => TemplateParser.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." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_InvalidParameterNameWithQuestionThrows() + { + ExceptionAssert.Throws( + () => TemplateParser.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." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_ConsecutiveSeparatorsSlashSlashThrows() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("{a}//{z}"), + "The route template separator character '/' cannot appear consecutively. It must be separated by " + + "either a parameter or a literal value." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_WithCatchAllNotAtTheEndThrows() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("foo/{p1}/{*p2}/{p3}"), + "A catch-all parameter can only appear as the last segment of the route template." + + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_RepeatedParametersThrows() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("foo/aa{p1}{p2}"), + "A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by " + + "a literal string." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_CannotStartWithSlash() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("/foo"), + "The route template cannot start with a '/' or '~' character." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_CannotStartWithTilde() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("~foo"), + "The route template cannot start with a '/' or '~' character." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_CannotContainQuestionMark() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("foor?bar"), + "The literal section 'foor?bar' is invalid. Literal sections cannot contain the '?' character." + + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_ParameterCannotContainQuestionMark_UnlessAtEnd() + { + ExceptionAssert.Throws( + () => TemplateParser.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." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_CatchAllMarkedOptional() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("{a}/{*b?}"), + "A catch-all parameter cannot be marked optional." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + private class TemplateEqualityComparer : IEqualityComparer + { + public bool Equals(RouteTemplate x, RouteTemplate y) + { + if (x == null && y == null) + { + return true; + } + else if (x == null || y == null) + { + return false; + } + else + { + if (!string.Equals(x.TemplateText, y.TemplateText, StringComparison.Ordinal)) + { + return false; + } + + if (x.Segments.Count != y.Segments.Count) + { + return false; + } + + for (int i = 0; i < x.Segments.Count; i++) + { + if (x.Segments[i].Parts.Count != y.Segments[i].Parts.Count) + { + return false; + } + + for (int j = 0; j < x.Segments[i].Parts.Count; j++) + { + if (!Equals(x.Segments[i].Parts[j], y.Segments[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(TemplatePart x, TemplatePart y) + { + if (x.IsLiteral != y.IsLiteral || + x.IsParameter != y.IsParameter || + x.IsCatchAll != y.IsCatchAll || + x.IsOptional != y.IsOptional || + !String.Equals(x.Name, y.Name, StringComparison.Ordinal) || + !String.Equals(x.Name, y.Name, StringComparison.Ordinal) || + (x.InlineConstraints == null && y.InlineConstraints != null) || + (x.InlineConstraints != null && y.InlineConstraints == null)) + { + return false; + } + + if (x.InlineConstraints == null && y.InlineConstraints == null) + { + return true; + } + + if (x.InlineConstraints.Count() != y.InlineConstraints.Count()) + { + return false; + } + + foreach (var xconstraint in x.InlineConstraints) + { + if (!y.InlineConstraints.Any( + c => string.Equals(c.Constraint, xconstraint.Constraint))) + { + return false; + } + } + + return true; + } + + public int GetHashCode(RouteTemplate obj) + { + throw new NotImplementedException(); + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Routing.Tests/InlineRouteParameterParserTests.cs b/test/Microsoft.AspNetCore.Routing.Tests/InlineRouteParameterParserTests.cs index 0e570c49cc..b428cf1dfc 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/InlineRouteParameterParserTests.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/InlineRouteParameterParserTests.cs @@ -2,7 +2,6 @@ // 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.Http; using Microsoft.AspNetCore.Routing.Template; diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateParserTests.cs b/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateParserTests.cs index 7ffd8f1026..b376eb0ec7 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateParserTests.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateParserTests.cs @@ -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