From 08a64048dacff771c105e998a9ffd9c2a3de1333 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Tue, 17 Oct 2017 21:01:07 -0700 Subject: [PATCH] Redesign public API for templates -Renamed RouteTemplate -> RoutePattern -Made immutable -Added Builder -Lots of fixes to parser to support new design There are a few small issues logged for follow-up but this is mostly in the place I want it design-wise. --- .../InlineConstraint.cs | 32 - .../Patterns/ConstraintReference.cs | 64 ++ .../InlineRouteParameterParser.cs | 96 +- .../Patterns/RoutePattern.cs | 71 ++ .../Patterns/RoutePatternBuilder.cs | 72 ++ .../Patterns/RoutePatternException.cs | 28 + .../Patterns/RoutePatternLiteral.cs | 32 + .../Patterns/RoutePatternParameter.cs | 51 + .../Patterns/RoutePatternParameterKind.cs | 12 + .../RoutePatternParser.cs} | 263 ++--- .../Patterns/RoutePatternPart.cs | 235 +++++ .../Patterns/RoutePatternPartKind.cs | 12 + .../Patterns/RoutePatternPathSegment.cs | 35 + .../Patterns/RoutePatternSeparator.cs | 32 + .../Properties/AssemblyInfo.cs | 6 + .../Properties/Resources.Designer.cs | 14 + .../Resources.resx | 3 + .../RouteTemplate.cs | 82 -- .../TemplatePart.cs | 67 -- .../TemplateSegment.cs | 22 - .../InlineRouteParameterParser.cs | 6 +- .../Template/InlineConstraint.cs | 9 +- .../Template/RouteTemplate.cs | 7 +- .../Template/TemplateMatcher.cs | 2 +- .../Template/TemplateParser.cs | 12 +- .../Template/TemplatePart.cs | 38 +- .../Template/TemplateSegment.cs | 5 +- .../InlineRouteParameterParserTest.cs} | 272 +++--- .../Patterns/RoutePatternParserTest.cs | 781 +++++++++++++++ .../TemplateParserTests.cs | 916 ------------------ .../InlineRouteParameterParserTests.cs | 2 + .../Template/TemplateParserTests.cs | 4 +- 32 files changed, 1839 insertions(+), 1444 deletions(-) delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/InlineConstraint.cs create mode 100644 src/Microsoft.AspNetCore.Dispatcher/Patterns/ConstraintReference.cs rename src/Microsoft.AspNetCore.Dispatcher/{ => Patterns}/InlineRouteParameterParser.cs (65%) create mode 100644 src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePattern.cs create mode 100644 src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternBuilder.cs create mode 100644 src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternException.cs create mode 100644 src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternLiteral.cs create mode 100644 src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternParameter.cs create mode 100644 src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternParameterKind.cs rename src/Microsoft.AspNetCore.Dispatcher/{TemplateParser.cs => Patterns/RoutePatternParser.cs} (67%) create mode 100644 src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternPart.cs create mode 100644 src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternPartKind.cs create mode 100644 src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternPathSegment.cs create mode 100644 src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternSeparator.cs create mode 100644 src/Microsoft.AspNetCore.Dispatcher/Properties/AssemblyInfo.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/RouteTemplate.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/TemplatePart.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/TemplateSegment.cs rename test/Microsoft.AspNetCore.Dispatcher.Test/{InlineRouteParameterParserTests.cs => Patterns/InlineRouteParameterParserTest.cs} (77%) create mode 100644 test/Microsoft.AspNetCore.Dispatcher.Test/Patterns/RoutePatternParserTest.cs delete mode 100644 test/Microsoft.AspNetCore.Dispatcher.Test/TemplateParserTests.cs diff --git a/src/Microsoft.AspNetCore.Dispatcher/InlineConstraint.cs b/src/Microsoft.AspNetCore.Dispatcher/InlineConstraint.cs deleted file mode 100644 index e8c88d7f02..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/InlineConstraint.cs +++ /dev/null @@ -1,32 +0,0 @@ -// 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/Patterns/ConstraintReference.cs b/src/Microsoft.AspNetCore.Dispatcher/Patterns/ConstraintReference.cs new file mode 100644 index 0000000000..1324c6c4f6 --- /dev/null +++ b/src/Microsoft.AspNetCore.Dispatcher/Patterns/ConstraintReference.cs @@ -0,0 +1,64 @@ +// 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.Diagnostics; + +namespace Microsoft.AspNetCore.Dispatcher.Patterns +{ + /// + /// The parsed representation of a constraint in a parameter. + /// + [DebuggerDisplay("{DebuggerToString()}")] + public sealed class ConstraintReference + { + /// + /// Creates a new . + /// + /// The constraint identifier. + /// A new . + public static ConstraintReference Create(string content) + { + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + + return new ConstraintReference(null, content); + } + + /// + /// Creates a new . + /// + /// The raw text of the constraint identifier. + /// The constraint identifier. + /// A new . + public static ConstraintReference CreateFromText(string rawText, string content) + { + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + + return new ConstraintReference(rawText, content); + } + + private ConstraintReference(string rawText, string content) + { + RawText = rawText; + Content = content; + } + + /// + /// Gets the constraint text. + /// + public string Content { get; } + + public string RawText { get; } + + private string DebuggerToString() + { + return RawText ?? Content; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Dispatcher/InlineRouteParameterParser.cs b/src/Microsoft.AspNetCore.Dispatcher/Patterns/InlineRouteParameterParser.cs similarity index 65% rename from src/Microsoft.AspNetCore.Dispatcher/InlineRouteParameterParser.cs rename to src/Microsoft.AspNetCore.Dispatcher/Patterns/InlineRouteParameterParser.cs index 8fdddcb9c7..05c4eb4a0a 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/InlineRouteParameterParser.cs +++ b/src/Microsoft.AspNetCore.Dispatcher/Patterns/InlineRouteParameterParser.cs @@ -3,43 +3,37 @@ using System; using System.Collections.Generic; +using System.Linq; -namespace Microsoft.AspNetCore.Dispatcher +namespace Microsoft.AspNetCore.Dispatcher.Patterns { public static class InlineRouteParameterParser { - public static TemplatePart ParseRouteParameter(string routeParameter) + public static RoutePatternParameter ParseRouteParameter(string text, string parameter) { - if (routeParameter == null) + if (parameter == null) { - throw new ArgumentNullException(nameof(routeParameter)); + throw new ArgumentNullException(nameof(parameter)); } - if (routeParameter.Length == 0) + if (parameter.Length == 0) { - return TemplatePart.CreateParameter( - name: string.Empty, - isCatchAll: false, - isOptional: false, - defaultValue: null, - inlineConstraints: null); + return new RoutePatternParameter(null, string.Empty, null, RoutePatternParameterKind.Standard, Array.Empty()); } var startIndex = 0; - var endIndex = routeParameter.Length - 1; + var endIndex = parameter.Length - 1; - var isCatchAll = false; - var isOptional = false; - - if (routeParameter[0] == '*') + var parameterKind = RoutePatternParameterKind.Standard; + if (parameter[0] == '*') { - isCatchAll = true; + parameterKind = RoutePatternParameterKind.CatchAll; startIndex++; } - if (routeParameter[endIndex] == '?') + if (parameter[endIndex] == '?') { - isOptional = true; + parameterKind = RoutePatternParameterKind.Optional; endIndex--; } @@ -50,14 +44,14 @@ namespace Microsoft.AspNetCore.Dispatcher while (currentIndex <= endIndex) { - var currentChar = routeParameter[currentIndex]; + var currentChar = parameter[currentIndex]; if ((currentChar == ':' || currentChar == '=') && startIndex != currentIndex) { // Parameter names are allowed to start with delimiters used to denote constraints or default values. // i.e. "=foo" or ":bar" would be treated as parameter names rather than default value or constraint // specifications. - parameterName = routeParameter.Substring(startIndex, currentIndex - startIndex); + parameterName = parameter.Substring(startIndex, currentIndex - startIndex); // Roll the index back and move to the constraint parsing stage. currentIndex--; @@ -65,40 +59,36 @@ namespace Microsoft.AspNetCore.Dispatcher } else if (currentIndex == endIndex) { - parameterName = routeParameter.Substring(startIndex, currentIndex - startIndex + 1); + parameterName = parameter.Substring(startIndex, currentIndex - startIndex + 1); } currentIndex++; } - var parseResults = ParseConstraints(routeParameter, currentIndex, endIndex); + var parseResults = ParseConstraints(parameter, currentIndex, endIndex); currentIndex = parseResults.CurrentIndex; string defaultValue = null; if (currentIndex <= endIndex && - routeParameter[currentIndex] == '=') + parameter[currentIndex] == '=') { - defaultValue = routeParameter.Substring(currentIndex + 1, endIndex - currentIndex); + defaultValue = parameter.Substring(currentIndex + 1, endIndex - currentIndex); } - return TemplatePart.CreateParameter(parameterName, - isCatchAll, - isOptional, - defaultValue, - parseResults.Constraints); + return new RoutePatternParameter(text, parameterName, defaultValue, parameterKind, parseResults.Constraints.ToArray()); } private static ConstraintParseResults ParseConstraints( - string routeParameter, + string parameter, int currentIndex, int endIndex) { - var inlineConstraints = new List(); + var constraints = new List(); var state = ParseState.Start; var startIndex = currentIndex; do { - var currentChar = currentIndex > endIndex ? null : (char?)routeParameter[currentIndex]; + var currentChar = currentIndex > endIndex ? null : (char?)parameter[currentIndex]; switch (state) { case ParseState.Start: @@ -125,8 +115,8 @@ namespace Microsoft.AspNetCore.Dispatcher { case null: state = ParseState.End; - var constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex); - inlineConstraints.Add(new InlineConstraint(constraintText)); + var constraintText = parameter.Substring(startIndex, currentIndex - startIndex); + constraints.Add(ConstraintReference.CreateFromText(constraintText, constraintText)); break; case ')': // Only consume a ')' token if @@ -134,24 +124,24 @@ namespace Microsoft.AspNetCore.Dispatcher // (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]; + var nextChar = currentIndex + 1 > endIndex ? null : (char?)parameter[currentIndex + 1]; switch (nextChar) { case null: state = ParseState.End; - constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex + 1); - inlineConstraints.Add(new InlineConstraint(constraintText)); + constraintText = parameter.Substring(startIndex, currentIndex - startIndex + 1); + constraints.Add(ConstraintReference.CreateFromText(constraintText, constraintText)); break; case ':': state = ParseState.Start; - constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex + 1); - inlineConstraints.Add(new InlineConstraint(constraintText)); + constraintText = parameter.Substring(startIndex, currentIndex - startIndex + 1); + constraints.Add(ConstraintReference.CreateFromText(constraintText, constraintText)); startIndex = currentIndex + 1; break; case '=': state = ParseState.End; - constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex + 1); - inlineConstraints.Add(new InlineConstraint(constraintText)); + constraintText = parameter.Substring(startIndex, currentIndex - startIndex + 1); + constraints.Add(ConstraintReference.CreateFromText(constraintText, constraintText)); break; } break; @@ -162,11 +152,11 @@ namespace Microsoft.AspNetCore.Dispatcher // 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); + var indexOfClosingParantheses = parameter.IndexOf(')', currentIndex + 1); if (indexOfClosingParantheses == -1) { - constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex); - inlineConstraints.Add(new InlineConstraint(constraintText)); + constraintText = parameter.Substring(startIndex, currentIndex - startIndex); + constraints.Add(ConstraintReference.CreateFromText(constraintText, constraintText)); if (currentChar == ':') { @@ -192,12 +182,12 @@ namespace Microsoft.AspNetCore.Dispatcher { case null: state = ParseState.End; - var constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex); - inlineConstraints.Add(new InlineConstraint(constraintText)); + var constraintText = parameter.Substring(startIndex, currentIndex - startIndex); + constraints.Add(ConstraintReference.CreateFromText(constraintText, constraintText)); break; case ':': - constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex); - inlineConstraints.Add(new InlineConstraint(constraintText)); + constraintText = parameter.Substring(startIndex, currentIndex - startIndex); + constraints.Add(ConstraintReference.CreateFromText(constraintText, constraintText)); startIndex = currentIndex + 1; break; case '(': @@ -205,8 +195,8 @@ namespace Microsoft.AspNetCore.Dispatcher break; case '=': state = ParseState.End; - constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex); - inlineConstraints.Add(new InlineConstraint(constraintText)); + constraintText = parameter.Substring(startIndex, currentIndex - startIndex); + constraints.Add(ConstraintReference.CreateFromText(constraintText, constraintText)); currentIndex--; break; } @@ -220,7 +210,7 @@ namespace Microsoft.AspNetCore.Dispatcher return new ConstraintParseResults { CurrentIndex = currentIndex, - Constraints = inlineConstraints + Constraints = constraints }; } @@ -236,7 +226,7 @@ namespace Microsoft.AspNetCore.Dispatcher { public int CurrentIndex; - public IEnumerable Constraints; + public IEnumerable Constraints; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePattern.cs b/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePattern.cs new file mode 100644 index 0000000000..c2100fcaa0 --- /dev/null +++ b/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePattern.cs @@ -0,0 +1,71 @@ +// 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.Patterns +{ + [DebuggerDisplay("{DebuggerToString()}")] + public sealed class RoutePattern + { + private const string SeparatorString = "/"; + + internal RoutePattern( + string rawText, + RoutePatternParameter[] parameters, + RoutePatternPathSegment[] pathSegments) + { + Debug.Assert(parameters != null); + Debug.Assert(pathSegments != null); + + RawText = rawText; + Parameters = parameters; + PathSegments = pathSegments; + } + + public string RawText { get; } + + public IReadOnlyList Parameters { get; } + + public IReadOnlyList PathSegments { get; } + + public static RoutePattern Parse(string pattern) + { + try + { + return RoutePatternParser.Parse(pattern); + } + catch (RoutePatternException ex) + { + throw new ArgumentException(ex.Message, nameof(pattern), ex); + } + } + + /// + /// 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 RoutePatternParameter 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; + } + + private string DebuggerToString() + { + return RawText ?? string.Join(SeparatorString, PathSegments.Select(s => s.DebuggerToString())); + } + } +} diff --git a/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternBuilder.cs b/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternBuilder.cs new file mode 100644 index 0000000000..3a54fcc9dc --- /dev/null +++ b/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternBuilder.cs @@ -0,0 +1,72 @@ +// 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 System.Text; + +namespace Microsoft.AspNetCore.Dispatcher.Patterns +{ + public sealed class RoutePatternBuilder + { + private RoutePatternBuilder() + { + } + + public IList PathSegments { get; } = new List(); + + public string Text { get; set; } + + public RoutePatternBuilder AddPathSegment(RoutePatternPart part) + { + return AddPathSegment(null, part, Array.Empty()); + } + + public RoutePatternBuilder AddPathSegment(RoutePatternPart part, params RoutePatternPart[] parts) + { + return AddPathSegment(null, part, Array.Empty()); + } + + public RoutePatternBuilder AddPathSegment(string text, RoutePatternPart part) + { + return AddPathSegment(text, part, Array.Empty()); + } + + public RoutePatternBuilder AddPathSegment(string text, RoutePatternPart part, params RoutePatternPart[] parts) + { + var allParts = new RoutePatternPart[1 + parts.Length]; + allParts[0] = part; + parts.CopyTo(allParts, 1); + + var segment = new RoutePatternPathSegment(text, allParts); + PathSegments.Add(segment); + + return this; + } + + public RoutePattern Build() + { + var parameters = new List(); + for (var i = 0; i < PathSegments.Count; i++) + { + var segment = PathSegments[i]; + for (var j = 0; j < segment.Parts.Count; j++) + { + var parameter = segment.Parts[j] as RoutePatternParameter; + if (parameter != null) + { + parameters.Add(parameter); + } + } + } + + return new RoutePattern(Text, parameters.ToArray(), PathSegments.ToArray()); + } + + public static RoutePatternBuilder Create(string text) + { + return new RoutePatternBuilder() { Text = text, }; + } + } +} diff --git a/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternException.cs b/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternException.cs new file mode 100644 index 0000000000..3ff669f318 --- /dev/null +++ b/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternException.cs @@ -0,0 +1,28 @@ +// 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.Patterns +{ + public class RoutePatternException : Exception + { + public RoutePatternException(string pattern, string message) + : base(message) + { + if (pattern == null) + { + throw new ArgumentNullException(nameof(pattern)); + } + + if (message == null) + { + throw new ArgumentNullException(nameof(message)); + } + + Pattern = pattern; + } + + public string Pattern { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternLiteral.cs b/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternLiteral.cs new file mode 100644 index 0000000000..11868416f0 --- /dev/null +++ b/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternLiteral.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.Diagnostics; + +namespace Microsoft.AspNetCore.Dispatcher.Patterns +{ + [DebuggerDisplay("{DebuggerToString()}")] + public sealed class RoutePatternLiteral : RoutePatternPart + { + internal RoutePatternLiteral(string rawText, string content) + { + Debug.Assert(!string.IsNullOrEmpty(content)); + + RawText = rawText; + Content = content; + + PartKind = RoutePatternPartKind.Literal; + } + + public string Content { get; } + + public override RoutePatternPartKind PartKind { get; } + + public override string RawText { get; } + + internal override string DebuggerToString() + { + return RawText; + } + } +} diff --git a/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternParameter.cs b/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternParameter.cs new file mode 100644 index 0000000000..14395dd8ba --- /dev/null +++ b/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternParameter.cs @@ -0,0 +1,51 @@ +// 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; + +namespace Microsoft.AspNetCore.Dispatcher.Patterns +{ + [DebuggerDisplay("{DebuggerToString()}")] + public class RoutePatternParameter : RoutePatternPart + { + internal RoutePatternParameter( + string rawText, + string name, + object defaultValue, + RoutePatternParameterKind parameterKind, + ConstraintReference[] constraints) + { + // See #475 - this code should have some asserts, but it can't because of the design of InlineRouteParameterParser. + + RawText = rawText; + Name = name; + DefaultValue = defaultValue; + ParameterKind = parameterKind; + Constraints = constraints; + + PartKind = RoutePatternPartKind.Parameter; + } + + public IReadOnlyList Constraints { get; } + + public object DefaultValue { get; } + + public bool IsCatchAll => ParameterKind == RoutePatternParameterKind.CatchAll; + + public bool IsOptional => ParameterKind == RoutePatternParameterKind.Optional; + + public RoutePatternParameterKind ParameterKind { get; } + + public override RoutePatternPartKind PartKind { get; } + + public string Name { get; } + + public override string RawText { get; } + + internal override string DebuggerToString() + { + return RawText ?? "{" + (IsCatchAll ? "*" : string.Empty) + Name + (IsOptional ? "?" : string.Empty) + "}"; + } + } +} diff --git a/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternParameterKind.cs b/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternParameterKind.cs new file mode 100644 index 0000000000..8dcacafa70 --- /dev/null +++ b/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternParameterKind.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Dispatcher.Patterns +{ + public enum RoutePatternParameterKind + { + Standard, + Optional, + CatchAll, + } +} diff --git a/src/Microsoft.AspNetCore.Dispatcher/TemplateParser.cs b/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternParser.cs similarity index 67% rename from src/Microsoft.AspNetCore.Dispatcher/TemplateParser.cs rename to src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternParser.cs index a11ea090b9..3a83b7d8e7 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/TemplateParser.cs +++ b/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternParser.cs @@ -4,11 +4,10 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Globalization; -namespace Microsoft.AspNetCore.Dispatcher +namespace Microsoft.AspNetCore.Dispatcher.Patterns { - public static class TemplateParser + internal static class RoutePatternParser { private const char Separator = '/'; private const char OpenBrace = '{'; @@ -18,61 +17,85 @@ namespace Microsoft.AspNetCore.Dispatcher private const char Asterisk = '*'; private const string PeriodString = "."; - public static RouteTemplate Parse(string routeTemplate) + internal static readonly char[] InvalidParameterNameChars = new char[] { - if (routeTemplate == null) + Separator, + OpenBrace, + CloseBrace, + QuestionMark, + Asterisk + }; + + public static RoutePattern Parse(string pattern) + { + if (pattern == null) { - routeTemplate = String.Empty; + throw new ArgumentNullException(nameof(pattern)); } - if (IsInvalidRouteTemplate(routeTemplate)) + if (IsInvalidPattern(pattern)) { - throw new ArgumentException(Resources.TemplateRoute_InvalidRouteTemplate, nameof(routeTemplate)); + throw new RoutePatternException(pattern, Resources.TemplateRoute_InvalidRouteTemplate); } - var context = new TemplateParserContext(routeTemplate); - var segments = new List(); + var context = new TemplateParserContext(pattern); + var builder = RoutePatternBuilder.Create(pattern); + var segments = new List(); - while (context.Next()) + while (context.MoveNext()) { + var i = context.Index; + if (context.Current == Separator) { // If we get here is means that there's a consecutive '/' character. // Templates don't start with a '/' and parsing a segment consumes the separator. - throw new ArgumentException(Resources.TemplateRoute_CannotHaveConsecutiveSeparators, - nameof(routeTemplate)); + throw new RoutePatternException(pattern, Resources.TemplateRoute_CannotHaveConsecutiveSeparators); } - else + + if (!ParseSegment(context, segments)) { - if (!ParseSegment(context, segments)) - { - throw new ArgumentException(context.Error, nameof(routeTemplate)); - } + throw new RoutePatternException(pattern, context.Error); + } + + // A successful parse should always result in us being at the end or at a separator. + Debug.Assert(context.AtEnd() || context.Current == Separator); + + if (context.Index <= i) + { + throw new InvalidProgramException("Infinite loop in the parser. This is a bug."); } } if (IsAllValid(context, segments)) { - return new RouteTemplate(routeTemplate, segments); + for (var i = 0; i < segments.Count; i++) + { + builder.PathSegments.Add(segments[i]); + } + + return builder.Build(); } else { - throw new ArgumentException(context.Error, nameof(routeTemplate)); + throw new RoutePatternException(pattern, context.Error); } } - private static bool ParseSegment(TemplateParserContext context, List segments) + private static bool ParseSegment(TemplateParserContext context, List segments) { Debug.Assert(context != null); Debug.Assert(segments != null); - var segment = new TemplateSegment(); + var parts = new List(); while (true) { + var i = context.Index; + if (context.Current == OpenBrace) { - if (!context.Next()) + if (!context.MoveNext()) { // This is a dangling open-brace, which is not allowed context.Error = Resources.TemplateRoute_MismatchedParameter; @@ -83,43 +106,44 @@ namespace Microsoft.AspNetCore.Dispatcher { // This is an 'escaped' brace in a literal, like "{{foo" context.Back(); - if (!ParseLiteral(context, segment)) + if (!ParseLiteral(context, parts)) { return false; } } else { - // This is the inside of a parameter - if (!ParseParameter(context, segment)) + // This is a parameter + context.Back(); + if (!ParseParameter(context, parts)) { return false; } } } - else if (context.Current == Separator) - { - // We've reached the end of the segment - break; - } else { - if (!ParseLiteral(context, segment)) + if (!ParseLiteral(context, parts)) { return false; } } - if (!context.Next()) + if (context.Current == Separator || context.AtEnd()) { - // We've reached the end of the string + // We've reached the end of the segment break; } + + if (context.Index <= i) + { + throw new InvalidProgramException("Infinite loop in the parser. This is a bug."); + } } - if (IsSegmentValid(context, segment)) + if (IsSegmentValid(context, parts)) { - segments.Add(segment); + segments.Add(new RoutePatternPathSegment(null, parts.ToArray())); return true; } else @@ -128,16 +152,19 @@ namespace Microsoft.AspNetCore.Dispatcher } } - private static bool ParseParameter(TemplateParserContext context, TemplateSegment segment) + private static bool ParseParameter(TemplateParserContext context, List parts) { + Debug.Assert(context.Current == OpenBrace); context.Mark(); + context.MoveNext(); + while (true) { if (context.Current == OpenBrace) { // This is an open brace inside of a parameter, it has to be escaped - if (context.Next()) + if (context.MoveNext()) { if (context.Current != OpenBrace) { @@ -159,10 +186,9 @@ namespace Microsoft.AspNetCore.Dispatcher // 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()) + if (!context.MoveNext()) { // This is the end of the string -and we have a valid parameter - context.Back(); break; } @@ -173,12 +199,11 @@ namespace Microsoft.AspNetCore.Dispatcher else { // This is the end of the parameter - context.Back(); break; } } - if (!context.Next()) + if (!context.MoveNext()) { // This is a dangling open-brace, which is not allowed context.Error = Resources.TemplateRoute_MismatchedParameter; @@ -186,14 +211,22 @@ namespace Microsoft.AspNetCore.Dispatcher } } - var rawParameter = context.Capture(); - var decoded = rawParameter.Replace("}}", "}").Replace("{{", "{"); + var text = context.Capture(); + if (text == "{}") + { + context.Error = Resources.FormatTemplateRoute_InvalidParameterName(string.Empty); + return false; + } + + var inside = text.Substring(1, text.Length - 2); + var decoded = inside.Replace("}}", "}").Replace("{{", "{"); // At this point, we need to parse the raw name for inline constraint, // default values and optional parameters. - var templatePart = InlineRouteParameterParser.ParseRouteParameter(decoded); + var templatePart = InlineRouteParameterParser.ParseRouteParameter(text, decoded); - if (templatePart.IsCatchAll && templatePart.IsOptional) + // See #475 - this is here because InlineRouteParameterParser can't return errors + if (decoded.StartsWith("*") && decoded.EndsWith("?")) { context.Error = Resources.TemplateRoute_CatchAllCannotBeOptional; return false; @@ -212,7 +245,7 @@ namespace Microsoft.AspNetCore.Dispatcher var parameterName = templatePart.Name; if (IsValidParameterName(context, parameterName)) { - segment.Parts.Add(templatePart); + parts.Add(templatePart); return true; } else @@ -221,22 +254,20 @@ namespace Microsoft.AspNetCore.Dispatcher } } - private static bool ParseLiteral(TemplateParserContext context, TemplateSegment segment) + private static bool ParseLiteral(TemplateParserContext context, List parts) { context.Mark(); - - string encoded; + while (true) { if (context.Current == Separator) { - encoded = context.Capture(); - context.Back(); + // End of the segment break; } else if (context.Current == OpenBrace) { - if (!context.Next()) + if (!context.MoveNext()) { // This is a dangling open-brace, which is not allowed context.Error = Resources.TemplateRoute_MismatchedParameter; @@ -249,16 +280,14 @@ namespace Microsoft.AspNetCore.Dispatcher } else { - // We've just seen the start of a parameter, so back up and return - context.Back(); - encoded = context.Capture(); + // We've just seen the start of a parameter, so back up. context.Back(); break; } } else if (context.Current == CloseBrace) { - if (!context.Next()) + if (!context.MoveNext()) { // This is a dangling close-brace, which is not allowed context.Error = Resources.TemplateRoute_MismatchedParameter; @@ -277,17 +306,17 @@ namespace Microsoft.AspNetCore.Dispatcher } } - if (!context.Next()) + if (!context.MoveNext()) { - encoded = context.Capture(); break; } } + var encoded = context.Capture(); var decoded = encoded.Replace("}}", "}").Replace("{{", "{"); if (IsValidLiteral(context, decoded)) { - segment.Parts.Add(TemplatePart.CreateLiteral(decoded)); + parts.Add(RoutePatternPart.CreateLiteralFromText(encoded, decoded)); return true; } else @@ -296,7 +325,7 @@ namespace Microsoft.AspNetCore.Dispatcher } } - private static bool IsAllValid(TemplateParserContext context, List segments) + 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++) @@ -306,7 +335,7 @@ namespace Microsoft.AspNetCore.Dispatcher { var part = segment.Parts[j]; if (part.IsParameter && - part.IsCatchAll && + ((RoutePatternParameter)part).IsCatchAll && (i != segments.Count - 1 || j != segment.Parts.Count - 1)) { context.Error = Resources.TemplateRoute_CatchAllMustBeLast; @@ -318,13 +347,13 @@ namespace Microsoft.AspNetCore.Dispatcher return true; } - private static bool IsSegmentValid(TemplateParserContext context, TemplateSegment segment) + private static bool IsSegmentValid(TemplateParserContext context, List parts) { // If a segment has multiple parts, then it can't contain a catch all. - for (var i = 0; i < segment.Parts.Count; i++) + for (var i = 0; i < parts.Count; i++) { - var part = segment.Parts[i]; - if (part.IsParameter && part.IsCatchAll && segment.Parts.Count > 1) + var part = parts[i]; + if (part.IsParameter && ((RoutePatternParameter)part).IsCatchAll && parts.Count > 1) { context.Error = Resources.TemplateRoute_CannotHaveCatchAllInMultiSegment; return false; @@ -333,30 +362,32 @@ namespace Microsoft.AspNetCore.Dispatcher // 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++) + for (var i = 0; i < parts.Count; i++) { - var part = segment.Parts[i]; + var part = parts[i]; - if (part.IsParameter && part.IsOptional && segment.Parts.Count > 1) + if (part.IsParameter && ((RoutePatternParameter)part).IsOptional && parts.Count > 1) { // This optional parameter is the last part in the segment - if (i == segment.Parts.Count - 1) + if (i == parts.Count - 1) { - if (!segment.Parts[i - 1].IsLiteral) + var previousPart = parts[i - 1]; + + if (!previousPart.IsLiteral && !previousPart.IsSeparator) { - // The optional parameter is preceded by something that is not a literal. + // The optional parameter is preceded by something that is not a literal or separator // Example of error message: // "In the segment '{RouteValue}{param?}', the optional parameter 'param' is preceded // by an invalid segment '{RouteValue}'. Only a period (.) can precede an optional parameter. context.Error = string.Format( Resources.TemplateRoute_OptionalParameterCanbBePrecededByPeriod, - segment.DebuggerToString(), - part.Name, - segment.Parts[i - 1].DebuggerToString()); + RoutePatternPathSegment.DebuggerToString(parts), + ((RoutePatternParameter)part).Name, + parts[i - 1].DebuggerToString()); return false; } - else if (segment.Parts[i - 1].Text != PeriodString) + else if (previousPart is RoutePatternLiteral literal && literal.Content != PeriodString) { // The optional parameter is preceded by a literal other than period. // Example of error message: @@ -364,29 +395,26 @@ namespace Microsoft.AspNetCore.Dispatcher // 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); + RoutePatternPathSegment.DebuggerToString(parts), + ((RoutePatternParameter)part).Name, + parts[i - 1].DebuggerToString()); return false; } - segment.Parts[i - 1].IsOptionalSeperator = true; + parts[i - 1] = RoutePatternPart.CreateSeparatorFromText(previousPart.RawText, ((RoutePatternLiteral)previousPart).Content); } else { // This optional parameter is not the last one in the segment // Example: - // An optional parameter must be at the end of the segment.In the segment '{RouteValue?})', + // 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); + RoutePatternPathSegment.DebuggerToString(parts), + ((RoutePatternParameter)part).Name, + parts[i + 1].DebuggerToString()); return false; } @@ -395,9 +423,9 @@ namespace Microsoft.AspNetCore.Dispatcher // A segment cannot contain two consecutive parameters var isLastSegmentParameter = false; - for (var i = 0; i < segment.Parts.Count; i++) + for (var i = 0; i < parts.Count; i++) { - var part = segment.Parts[i]; + var part = parts[i]; if (part.IsParameter && isLastSegmentParameter) { context.Error = Resources.TemplateRoute_CannotHaveConsecutiveParameters; @@ -412,28 +440,15 @@ namespace Microsoft.AspNetCore.Dispatcher private static bool IsValidParameterName(TemplateParserContext context, string parameterName) { - if (parameterName.Length == 0) + if (parameterName.Length == 0 || parameterName.IndexOfAny(InvalidParameterNameChars) >= 0) { - context.Error = String.Format(CultureInfo.CurrentCulture, - Resources.TemplateRoute_InvalidParameterName, parameterName); + context.Error = Resources.FormatTemplateRoute_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); + context.Error = Resources.FormatTemplateRoute_RepeatedParameter(parameterName); return false; } @@ -447,20 +462,20 @@ namespace Microsoft.AspNetCore.Dispatcher if (literal.IndexOf(QuestionMark) != -1) { - context.Error = String.Format(CultureInfo.CurrentCulture, - Resources.TemplateRoute_InvalidLiteral, literal); + context.Error = Resources.FormatTemplateRoute_InvalidLiteral(literal); return false; } return true; } - private static bool IsInvalidRouteTemplate(string routeTemplate) + private static bool IsInvalidPattern(string routeTemplate) { return routeTemplate.StartsWith("~", StringComparison.Ordinal) || routeTemplate.StartsWith("/", StringComparison.Ordinal); } + [DebuggerDisplay("{DebuggerToString()}")] private class TemplateParserContext { private readonly string _template; @@ -482,6 +497,8 @@ namespace Microsoft.AspNetCore.Dispatcher get { return (_index < _template.Length && _index >= 0) ? _template[_index] : (char)0; } } + public int Index => _index; + public string Error { get; @@ -498,13 +515,21 @@ namespace Microsoft.AspNetCore.Dispatcher return --_index >= 0; } - public bool Next() + public bool AtEnd() + { + return _index >= _template.Length; + } + + public bool MoveNext() { return ++_index < _template.Length; } public void Mark() { + Debug.Assert(_index >= 0); + + // Index is always the index of the character *past* Current - we want to 'mark' Current. _mark = _index; } @@ -521,6 +546,26 @@ namespace Microsoft.AspNetCore.Dispatcher return null; } } + + private string DebuggerToString() + { + if (_index == -1) + { + return _template; + } + else if (_mark.HasValue) + { + return _template.Substring(0, _mark.Value) + + "|" + + _template.Substring(_mark.Value, _index - _mark.Value) + + "|" + + _template.Substring(_index); + } + else + { + return _template.Substring(0, _index) + "|" + _template.Substring(_index); + } + } } } } diff --git a/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternPart.cs b/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternPart.cs new file mode 100644 index 0000000000..8f8733ea88 --- /dev/null +++ b/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternPart.cs @@ -0,0 +1,235 @@ +// 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.Patterns +{ + public abstract class RoutePatternPart + { + // This class is not an extensibility point. It is abstract so we can extend it + // or add semantics later inside the library. + internal RoutePatternPart() + { + } + + public static RoutePatternLiteral CreateLiteral(string content) + { + if (string.IsNullOrEmpty(content)) + { + throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(content)); + } + + if (content.IndexOf('?') >= 0) + { + throw new ArgumentException(Resources.FormatTemplateRoute_InvalidLiteral(content)); + } + + return new RoutePatternLiteral(null, content); + } + + public static RoutePatternLiteral CreateLiteralFromText(string rawText, string content) + { + if (string.IsNullOrEmpty(content)) + { + throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(content)); + } + + if (content.IndexOf('?') >= 0) + { + throw new ArgumentException(Resources.FormatTemplateRoute_InvalidLiteral(content)); + } + + return new RoutePatternLiteral(rawText, content); + } + + public static RoutePatternSeparator CreateSeparator(string content) + { + if (string.IsNullOrEmpty(content)) + { + throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(content)); + } + + return new RoutePatternSeparator(null, content); + } + + public static RoutePatternSeparator CreateSeparatorFromText(string rawText, string content) + { + if (string.IsNullOrEmpty(content)) + { + throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(content)); + } + + return new RoutePatternSeparator(rawText, content); + } + + public static RoutePatternParameter CreateParameter(string name) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(name)); + } + + if (name.IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0) + { + throw new ArgumentException(Resources.FormatTemplateRoute_InvalidParameterName(name)); + } + + return CreateParameterFromText(null, name, null, RoutePatternParameterKind.Standard, Array.Empty()); + } + + public static RoutePatternParameter CreateParameterFromText(string rawText, string name) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(name)); + } + + if (name.IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0) + { + throw new ArgumentException(Resources.FormatTemplateRoute_InvalidParameterName(name)); + } + + return CreateParameterFromText(rawText, name, null, RoutePatternParameterKind.Standard, Array.Empty()); + } + + public static RoutePatternParameter CreateParameter(string name, object defaultValue) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(name)); + } + + if (name.IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0) + { + throw new ArgumentException(Resources.FormatTemplateRoute_InvalidParameterName(name)); + } + + return CreateParameterFromText(null, name, defaultValue, RoutePatternParameterKind.Standard, Array.Empty()); + } + + public static RoutePatternParameter CreateParameterFromText(string rawText, string name, object defaultValue) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(name)); + } + + if (name.IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0) + { + throw new ArgumentException(Resources.FormatTemplateRoute_InvalidParameterName(name)); + } + + return CreateParameterFromText(rawText, name, defaultValue, RoutePatternParameterKind.Standard, Array.Empty()); + } + + public static RoutePatternParameter CreateParameter( + string name, + object defaultValue, + RoutePatternParameterKind parameterKind) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(name)); + } + + if (name.IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0) + { + throw new ArgumentException(Resources.FormatTemplateRoute_InvalidParameterName(name)); + } + + if (defaultValue != null && parameterKind == RoutePatternParameterKind.Optional) + { + throw new ArgumentNullException(Resources.TemplateRoute_OptionalCannotHaveDefaultValue, nameof(parameterKind)); + } + + return CreateParameterFromText(null, name, defaultValue, parameterKind, Array.Empty()); + } + + public static RoutePatternParameter CreateParameterFromText( + string rawText, + string name, + object defaultValue, + RoutePatternParameterKind parameterKind) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(name)); + } + + if (name.IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0) + { + throw new ArgumentException(Resources.FormatTemplateRoute_InvalidParameterName(name)); + } + + if (defaultValue != null && parameterKind == RoutePatternParameterKind.Optional) + { + throw new ArgumentNullException(Resources.TemplateRoute_OptionalCannotHaveDefaultValue, nameof(parameterKind)); + } + + return CreateParameterFromText(rawText, name, defaultValue, parameterKind, Array.Empty()); + } + + public static RoutePatternParameter CreateParameter( + string name, + object defaultValue, + RoutePatternParameterKind parameterKind, + params ConstraintReference[] constraints) + { + + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(name)); + } + + if (name.IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0) + { + throw new ArgumentException(Resources.FormatTemplateRoute_InvalidParameterName(name)); + } + + if (defaultValue != null && parameterKind == RoutePatternParameterKind.Optional) + { + throw new ArgumentNullException(Resources.TemplateRoute_OptionalCannotHaveDefaultValue, nameof(parameterKind)); + } + + return new RoutePatternParameter(null, name, defaultValue, parameterKind, constraints); + } + + public static RoutePatternParameter CreateParameterFromText( + string rawText, + string name, + object defaultValue, + RoutePatternParameterKind parameterKind, + params ConstraintReference[] constraints) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(name)); + } + + if (name.IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0) + { + throw new ArgumentException(Resources.FormatTemplateRoute_InvalidParameterName(name)); + } + + if (defaultValue != null && parameterKind == RoutePatternParameterKind.Optional) + { + throw new ArgumentNullException(Resources.TemplateRoute_OptionalCannotHaveDefaultValue, nameof(parameterKind)); + } + + return new RoutePatternParameter(rawText, name, defaultValue, parameterKind, constraints); + } + + public abstract RoutePatternPartKind PartKind { get; } + + public abstract string RawText { get; } + + public bool IsLiteral => PartKind == RoutePatternPartKind.Literal; + + public bool IsParameter => PartKind == RoutePatternPartKind.Parameter; + + public bool IsSeparator => PartKind == RoutePatternPartKind.Separator; + + internal abstract string DebuggerToString(); + } +} diff --git a/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternPartKind.cs b/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternPartKind.cs new file mode 100644 index 0000000000..1deb20cc94 --- /dev/null +++ b/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternPartKind.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Dispatcher.Patterns +{ + public enum RoutePatternPartKind + { + Literal, + Parameter, + Separator, + } +} diff --git a/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternPathSegment.cs b/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternPathSegment.cs new file mode 100644 index 0000000000..8a691472f7 --- /dev/null +++ b/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternPathSegment.cs @@ -0,0 +1,35 @@ +// 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.Patterns +{ + [DebuggerDisplay("{DebuggerToString()}")] + public sealed class RoutePatternPathSegment + { + internal RoutePatternPathSegment(string rawText, RoutePatternPart[] parts) + { + RawText = rawText; + Parts = parts; + } + + public bool IsSimple => Parts.Count == 1; + + public IReadOnlyList Parts { get; } + + public string RawText { get; set; } + + internal string DebuggerToString() + { + return RawText ?? DebuggerToString(Parts); + } + + internal static string DebuggerToString(IReadOnlyList parts) + { + return string.Join(string.Empty, parts.Select(p => p.DebuggerToString())); + } + } +} diff --git a/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternSeparator.cs b/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternSeparator.cs new file mode 100644 index 0000000000..240baa3824 --- /dev/null +++ b/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternSeparator.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.Diagnostics; + +namespace Microsoft.AspNetCore.Dispatcher.Patterns +{ + [DebuggerDisplay("{DebuggerToString()}")] + public sealed class RoutePatternSeparator : RoutePatternPart + { + internal RoutePatternSeparator(string rawText, string content) + { + Debug.Assert(!string.IsNullOrEmpty(content)); + + RawText = rawText; + Content = content; + + PartKind = RoutePatternPartKind.Separator; + } + + public string Content { get; } + + public override RoutePatternPartKind PartKind { get; } + + public override string RawText { get; } + + internal override string DebuggerToString() + { + return RawText ?? Content; + } + } +} diff --git a/src/Microsoft.AspNetCore.Dispatcher/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.Dispatcher/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..ec6acdf95f --- /dev/null +++ b/src/Microsoft.AspNetCore.Dispatcher/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// 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.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Dispatcher.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Microsoft.AspNetCore.Dispatcher/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Dispatcher/Properties/Resources.Designer.cs index 4b4f518aad..01221b8b26 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Dispatcher/Properties/Resources.Designer.cs @@ -24,6 +24,20 @@ namespace Microsoft.AspNetCore.Dispatcher internal static string FormatAmbiguousEndpoints(object p0, object p1) => string.Format(CultureInfo.CurrentCulture, GetString("AmbiguousEndpoints"), p0, p1); + /// + /// Value cannot be null or empty. + /// + internal static string Argument_NullOrEmpty + { + get => GetString("Argument_NullOrEmpty"); + } + + /// + /// Value cannot be null or empty. + /// + internal static string FormatArgument_NullOrEmpty() + => GetString("Argument_NullOrEmpty"); + /// /// A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter. /// diff --git a/src/Microsoft.AspNetCore.Dispatcher/Resources.resx b/src/Microsoft.AspNetCore.Dispatcher/Resources.resx index d9d1898387..abe35916f1 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/Resources.resx +++ b/src/Microsoft.AspNetCore.Dispatcher/Resources.resx @@ -121,6 +121,9 @@ 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 + + Value cannot be null or empty. + A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter. diff --git a/src/Microsoft.AspNetCore.Dispatcher/RouteTemplate.cs b/src/Microsoft.AspNetCore.Dispatcher/RouteTemplate.cs deleted file mode 100644 index eeda78f8e5..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/RouteTemplate.cs +++ /dev/null @@ -1,82 +0,0 @@ -// 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/TemplatePart.cs b/src/Microsoft.AspNetCore.Dispatcher/TemplatePart.cs deleted file mode 100644 index 91a7d5673a..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/TemplatePart.cs +++ /dev/null @@ -1,67 +0,0 @@ -// 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 deleted file mode 100644 index 2db8e0bab8..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/TemplateSegment.cs +++ /dev/null @@ -1,22 +0,0 @@ -// 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/InlineRouteParameterParser.cs b/src/Microsoft.AspNetCore.Routing/InlineRouteParameterParser.cs index 4229aceac4..f542cf5717 100644 --- a/src/Microsoft.AspNetCore.Routing/InlineRouteParameterParser.cs +++ b/src/Microsoft.AspNetCore.Routing/InlineRouteParameterParser.cs @@ -8,6 +8,9 @@ namespace Microsoft.AspNetCore.Routing { public static class InlineRouteParameterParser { + [Obsolete( + "This API is obsolete and will be removed in a future release. It does not report errors correctly. " + + "Use 'TemplateParser.Parse()' and filter for the desired parameter as an alternative.")] public static TemplatePart ParseRouteParameter(string routeParameter) { if (routeParameter == null) @@ -15,7 +18,8 @@ namespace Microsoft.AspNetCore.Routing throw new ArgumentNullException(nameof(routeParameter)); } - var inner = AspNetCore.Dispatcher.InlineRouteParameterParser.ParseRouteParameter(routeParameter); + // See: #475 - this API has no way to pass the 'raw' text + var inner = AspNetCore.Dispatcher.Patterns.InlineRouteParameterParser.ParseRouteParameter(string.Empty, routeParameter); return new TemplatePart(inner); } } diff --git a/src/Microsoft.AspNetCore.Routing/Template/InlineConstraint.cs b/src/Microsoft.AspNetCore.Routing/Template/InlineConstraint.cs index 50e6db3e9c..9274223cad 100644 --- a/src/Microsoft.AspNetCore.Routing/Template/InlineConstraint.cs +++ b/src/Microsoft.AspNetCore.Routing/Template/InlineConstraint.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using Other = Microsoft.AspNetCore.Dispatcher.Patterns.ConstraintReference; namespace Microsoft.AspNetCore.Routing.Template { @@ -24,14 +25,14 @@ namespace Microsoft.AspNetCore.Routing.Template Constraint = constraint; } - public InlineConstraint(AspNetCore.Dispatcher.InlineConstraint constraint) + public InlineConstraint(Other other) { - if (constraint == null) + if (other == null) { - throw new ArgumentNullException(nameof(constraint)); + throw new ArgumentNullException(nameof(other)); } - Constraint = constraint.Constraint; + Constraint = other.Content; } /// diff --git a/src/Microsoft.AspNetCore.Routing/Template/RouteTemplate.cs b/src/Microsoft.AspNetCore.Routing/Template/RouteTemplate.cs index b9b621ca12..2253d85257 100644 --- a/src/Microsoft.AspNetCore.Routing/Template/RouteTemplate.cs +++ b/src/Microsoft.AspNetCore.Routing/Template/RouteTemplate.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using Other = Microsoft.AspNetCore.Dispatcher.Patterns.RoutePattern; namespace Microsoft.AspNetCore.Routing.Template { @@ -13,10 +14,10 @@ namespace Microsoft.AspNetCore.Routing.Template { private const string SeparatorString = "/"; - public RouteTemplate(AspNetCore.Dispatcher.RouteTemplate routeTemplate) + public RouteTemplate(Other other) { - TemplateText = routeTemplate.TemplateText; - Segments = new List(routeTemplate.Segments.Select(p => new TemplateSegment(p))); + TemplateText = other.RawText; + Segments = new List(other.PathSegments.Select(p => new TemplateSegment(p))); Parameters = new List(); for (var i = 0; i < Segments.Count; i++) { diff --git a/src/Microsoft.AspNetCore.Routing/Template/TemplateMatcher.cs b/src/Microsoft.AspNetCore.Routing/Template/TemplateMatcher.cs index e7cc2ab9f5..d2bda6e6f9 100644 --- a/src/Microsoft.AspNetCore.Routing/Template/TemplateMatcher.cs +++ b/src/Microsoft.AspNetCore.Routing/Template/TemplateMatcher.cs @@ -335,7 +335,7 @@ namespace Microsoft.AspNetCore.Routing.Template } else { - Debug.Assert(part.IsLiteral); + Debug.Assert(part.IsLiteral || part.IsOptionalSeperator); lastLiteral = part; var startIndex = lastIndex - 1; diff --git a/src/Microsoft.AspNetCore.Routing/Template/TemplateParser.cs b/src/Microsoft.AspNetCore.Routing/Template/TemplateParser.cs index a315d3d949..6a8d69d019 100644 --- a/src/Microsoft.AspNetCore.Routing/Template/TemplateParser.cs +++ b/src/Microsoft.AspNetCore.Routing/Template/TemplateParser.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using Microsoft.AspNetCore.Dispatcher.Patterns; namespace Microsoft.AspNetCore.Routing.Template { @@ -14,8 +15,15 @@ namespace Microsoft.AspNetCore.Routing.Template throw new ArgumentNullException(routeTemplate); } - var inner = AspNetCore.Dispatcher.TemplateParser.Parse(routeTemplate); - return new RouteTemplate(inner); + try + { + var inner = Microsoft.AspNetCore.Dispatcher.Patterns.RoutePattern.Parse(routeTemplate); + return new RouteTemplate(inner); + } + catch (ArgumentException ex) when (ex.InnerException is RoutePatternException) + { + throw new ArgumentException(ex.InnerException.Message, nameof(routeTemplate), ex.InnerException); + } } } } diff --git a/src/Microsoft.AspNetCore.Routing/Template/TemplatePart.cs b/src/Microsoft.AspNetCore.Routing/Template/TemplatePart.cs index 6b358a6e06..deed52a093 100644 --- a/src/Microsoft.AspNetCore.Routing/Template/TemplatePart.cs +++ b/src/Microsoft.AspNetCore.Routing/Template/TemplatePart.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using Other = Microsoft.AspNetCore.Dispatcher.Patterns.RoutePatternPart; namespace Microsoft.AspNetCore.Routing.Template { @@ -15,17 +16,34 @@ namespace Microsoft.AspNetCore.Routing.Template { } - public TemplatePart(AspNetCore.Dispatcher.TemplatePart templatePart) + public TemplatePart(Other other) { - 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)); + IsLiteral = other.IsLiteral || other.IsSeparator; + IsParameter = other.IsParameter; + + if (other.IsLiteral && other is Microsoft.AspNetCore.Dispatcher.Patterns.RoutePatternLiteral literal) + { + Text = literal.Content; + } + else if (other.IsParameter && other is Microsoft.AspNetCore.Dispatcher.Patterns.RoutePatternParameter parameter) + { + // Text is unused by TemplatePart and assumed to be null when the part is a parameter. + Name = parameter.Name; + IsCatchAll = parameter.IsCatchAll; + IsOptional = parameter.IsOptional; + DefaultValue = parameter.DefaultValue; + InlineConstraints = parameter.Constraints?.Select(p => new InlineConstraint(p)); + } + else if (other.IsSeparator && other is Microsoft.AspNetCore.Dispatcher.Patterns.RoutePatternSeparator separator) + { + Text = separator.Content; + IsOptionalSeperator = true; + } + else + { + // Unreachable + throw new NotSupportedException(); + } } public static TemplatePart CreateLiteral(string text) diff --git a/src/Microsoft.AspNetCore.Routing/Template/TemplateSegment.cs b/src/Microsoft.AspNetCore.Routing/Template/TemplateSegment.cs index 2bb5b2682f..f7fb6e9cab 100644 --- a/src/Microsoft.AspNetCore.Routing/Template/TemplateSegment.cs +++ b/src/Microsoft.AspNetCore.Routing/Template/TemplateSegment.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using Other = Microsoft.AspNetCore.Dispatcher.Patterns.RoutePatternPathSegment; namespace Microsoft.AspNetCore.Routing.Template { @@ -14,9 +15,9 @@ namespace Microsoft.AspNetCore.Routing.Template { } - public TemplateSegment(AspNetCore.Dispatcher.TemplateSegment templateSegment) + public TemplateSegment(Other other) { - Parts = new List(templateSegment.Parts.Select(s => new TemplatePart(s))); + Parts = new List(other.Parts.Select(s => new TemplatePart(s))); } public bool IsSimple => Parts.Count == 1; diff --git a/test/Microsoft.AspNetCore.Dispatcher.Test/InlineRouteParameterParserTests.cs b/test/Microsoft.AspNetCore.Dispatcher.Test/Patterns/InlineRouteParameterParserTest.cs similarity index 77% rename from test/Microsoft.AspNetCore.Dispatcher.Test/InlineRouteParameterParserTests.cs rename to test/Microsoft.AspNetCore.Dispatcher.Test/Patterns/InlineRouteParameterParserTest.cs index 7ee32d589e..8785de4baf 100644 --- a/test/Microsoft.AspNetCore.Dispatcher.Test/InlineRouteParameterParserTests.cs +++ b/test/Microsoft.AspNetCore.Dispatcher.Test/Patterns/InlineRouteParameterParserTest.cs @@ -4,9 +4,9 @@ using System.Linq; using Xunit; -namespace Microsoft.AspNetCore.Dispatcher +namespace Microsoft.AspNetCore.Dispatcher.Patterns { - public class InlineRouteParameterParserTests + public class InlineRouteParameterParserTest { [Theory] [InlineData("=")] @@ -19,7 +19,7 @@ namespace Microsoft.AspNetCore.Dispatcher // Assert Assert.Equal(parameterName, templatePart.Name); Assert.Null(templatePart.DefaultValue); - Assert.Empty(templatePart.InlineConstraints); + Assert.Empty(templatePart.Constraints); } [Fact] @@ -31,7 +31,7 @@ namespace Microsoft.AspNetCore.Dispatcher // Assert Assert.Equal("param", templatePart.Name); Assert.Equal("", templatePart.DefaultValue); - Assert.Empty(templatePart.InlineConstraints); + Assert.Empty(templatePart.Constraints); } [Fact] @@ -43,8 +43,8 @@ namespace Microsoft.AspNetCore.Dispatcher // Assert Assert.Equal("param", templatePart.Name); Assert.Null(templatePart.DefaultValue); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Empty(constraint.Constraint); + var constraint = Assert.Single(templatePart.Constraints); + Assert.Empty(constraint.Content); } [Fact] @@ -56,8 +56,8 @@ namespace Microsoft.AspNetCore.Dispatcher // Assert Assert.Equal("param", templatePart.Name); Assert.Equal("", templatePart.DefaultValue); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Empty(constraint.Constraint); + var constraint = Assert.Single(templatePart.Constraints); + Assert.Empty(constraint.Content); } [Fact] @@ -69,7 +69,7 @@ namespace Microsoft.AspNetCore.Dispatcher // Assert Assert.Equal("param", templatePart.Name); Assert.Equal(":", templatePart.DefaultValue); - Assert.Empty(templatePart.InlineConstraints); + Assert.Empty(templatePart.Constraints); } [Fact] @@ -82,8 +82,8 @@ namespace Microsoft.AspNetCore.Dispatcher Assert.Equal("param", templatePart.Name); Assert.Equal("111111", templatePart.DefaultValue); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal("int", constraint.Constraint); + var constraint = Assert.Single(templatePart.Constraints); + Assert.Equal("int", constraint.Content); } [Fact] @@ -96,8 +96,8 @@ namespace Microsoft.AspNetCore.Dispatcher Assert.Equal("param", templatePart.Name); Assert.Equal("111111", templatePart.DefaultValue); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(\d+)", constraint.Constraint); + var constraint = Assert.Single(templatePart.Constraints); + Assert.Equal(@"test(\d+)", constraint.Content); } [Fact] @@ -110,8 +110,8 @@ namespace Microsoft.AspNetCore.Dispatcher Assert.Equal("param", templatePart.Name); Assert.True(templatePart.IsOptional); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal("int", constraint.Constraint); + var constraint = Assert.Single(templatePart.Constraints); + Assert.Equal("int", constraint.Content); } [Fact] @@ -125,8 +125,8 @@ namespace Microsoft.AspNetCore.Dispatcher Assert.Equal("12", templatePart.DefaultValue); Assert.True(templatePart.IsOptional); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal("int", constraint.Constraint); + var constraint = Assert.Single(templatePart.Constraints); + Assert.Equal("int", constraint.Content); } [Fact] @@ -140,8 +140,8 @@ namespace Microsoft.AspNetCore.Dispatcher Assert.Equal("12?", templatePart.DefaultValue); Assert.True(templatePart.IsOptional); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal("int", constraint.Constraint); + var constraint = Assert.Single(templatePart.Constraints); + Assert.Equal("int", constraint.Content); } [Fact] @@ -154,8 +154,8 @@ namespace Microsoft.AspNetCore.Dispatcher Assert.Equal("param", templatePart.Name); Assert.True(templatePart.IsOptional); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(\d+)", constraint.Constraint); + var constraint = Assert.Single(templatePart.Constraints); + Assert.Equal(@"test(\d+)", constraint.Content); } [Fact] @@ -170,8 +170,8 @@ namespace Microsoft.AspNetCore.Dispatcher Assert.Equal("abc", templatePart.DefaultValue); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(\d+)", constraint.Constraint); + var constraint = Assert.Single(templatePart.Constraints); + Assert.Equal(@"test(\d+)", constraint.Content); } [Fact] @@ -183,9 +183,9 @@ namespace Microsoft.AspNetCore.Dispatcher // Assert Assert.Equal("param", templatePart.Name); - Assert.Collection(templatePart.InlineConstraints, - constraint => Assert.Equal(@"test(d+)", constraint.Constraint), - constraint => Assert.Equal(@"test(w+)", constraint.Constraint)); + Assert.Collection(templatePart.Constraints, + constraint => Assert.Equal(@"test(d+)", constraint.Content), + constraint => Assert.Equal(@"test(w+)", constraint.Content)); } [Fact] @@ -197,11 +197,11 @@ namespace Microsoft.AspNetCore.Dispatcher // 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)); + Assert.Collection(templatePart.Constraints, + constraint => Assert.Empty(constraint.Content), + constraint => Assert.Equal(@"test(d+)", constraint.Content), + constraint => Assert.Empty(constraint.Content), + constraint => Assert.Equal(@"test(w+)", constraint.Content)); } [Fact] @@ -213,9 +213,9 @@ namespace Microsoft.AspNetCore.Dispatcher // Assert Assert.Equal("param", templatePart.Name); - Assert.Collection(templatePart.InlineConstraints, - constraint => Assert.Equal(@"test(\d+)", constraint.Constraint), - constraint => Assert.Equal(@"test(\w:+)", constraint.Constraint)); + Assert.Collection(templatePart.Constraints, + constraint => Assert.Equal(@"test(\d+)", constraint.Content), + constraint => Assert.Equal(@"test(\w:+)", constraint.Content)); } [Fact] @@ -229,9 +229,9 @@ namespace Microsoft.AspNetCore.Dispatcher Assert.Equal("qwer", templatePart.DefaultValue); - Assert.Collection(templatePart.InlineConstraints, - constraint => Assert.Equal(@"test(\d+)", constraint.Constraint), - constraint => Assert.Equal(@"test(\w+)", constraint.Constraint)); + Assert.Collection(templatePart.Constraints, + constraint => Assert.Equal(@"test(\d+)", constraint.Content), + constraint => Assert.Equal(@"test(\w+)", constraint.Content)); } [Fact] @@ -245,10 +245,10 @@ namespace Microsoft.AspNetCore.Dispatcher 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)); + Assert.Collection(templatePart.Constraints, + constraint => Assert.Equal(@"test(\d+)", constraint.Content), + constraint => Assert.Empty(constraint.Content), + constraint => Assert.Equal(@"test(\w+)", constraint.Content)); } [Theory] @@ -264,27 +264,27 @@ namespace Microsoft.AspNetCore.Dispatcher Assert.Equal("comparison-operator", templatePart.Name); Assert.Equal(defaultValue, templatePart.DefaultValue); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal("length(6)", constraint.Constraint); + var constraint = Assert.Single(templatePart.Constraints); + Assert.Equal("length(6)", constraint.Content); } [Fact] public void ParseRouteTemplate_ConstraintsDefaultsAndOptionalsInMultipleSections_ParsedCorrectly() { // Arrange & Act - var template = ParseRouteTemplate(@"some/url-{p1:int:test(3)=hello}/{p2=abc}/{p3?}"); + var routePattern = RoutePattern.Parse(@"some/url-{p1:int:test(3)=hello}/{p2=abc}/{p3?}"); // Assert - var parameters = template.Parameters.ToArray(); + var parameters = routePattern.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) + Assert.Collection(param1.Constraints, + constraint => Assert.Equal("int", constraint.Content), + constraint => Assert.Equal("test(3)", constraint.Content) ); var param2 = parameters[1]; @@ -327,8 +327,8 @@ namespace Microsoft.AspNetCore.Dispatcher // Assert Assert.Equal("param", templatePart.Name); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(\})", constraint.Constraint); + var constraint = Assert.Single(templatePart.Constraints); + Assert.Equal(@"test(\})", constraint.Content); } [Fact] @@ -342,8 +342,8 @@ namespace Microsoft.AspNetCore.Dispatcher Assert.Equal("wer", templatePart.DefaultValue); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(\})", constraint.Constraint); + var constraint = Assert.Single(templatePart.Constraints); + Assert.Equal(@"test(\})", constraint.Content); } [Fact] @@ -355,8 +355,8 @@ namespace Microsoft.AspNetCore.Dispatcher // Assert Assert.Equal("param", templatePart.Name); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(\))", constraint.Constraint); + var constraint = Assert.Single(templatePart.Constraints); + Assert.Equal(@"test(\))", constraint.Content); } [Fact] @@ -370,8 +370,8 @@ namespace Microsoft.AspNetCore.Dispatcher Assert.Equal("fsd", templatePart.DefaultValue); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(\))", constraint.Constraint); + var constraint = Assert.Single(templatePart.Constraints); + Assert.Equal(@"test(\))", constraint.Content); } [Fact] @@ -383,8 +383,8 @@ namespace Microsoft.AspNetCore.Dispatcher // Assert Assert.Equal("param", templatePart.Name); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(:)", constraint.Constraint); + var constraint = Assert.Single(templatePart.Constraints); + Assert.Equal(@"test(:)", constraint.Content); } [Fact] @@ -398,8 +398,8 @@ namespace Microsoft.AspNetCore.Dispatcher Assert.Equal("mnf", templatePart.DefaultValue); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(:)", constraint.Constraint); + var constraint = Assert.Single(templatePart.Constraints); + Assert.Equal(@"test(:)", constraint.Content); } [Fact] @@ -411,8 +411,8 @@ namespace Microsoft.AspNetCore.Dispatcher // Assert Assert.Equal("param", templatePart.Name); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(a:b:c)", constraint.Constraint); + var constraint = Assert.Single(templatePart.Constraints); + Assert.Equal(@"test(a:b:c)", constraint.Content); } [Fact] @@ -426,8 +426,8 @@ namespace Microsoft.AspNetCore.Dispatcher Assert.Equal("12", templatePart.DefaultValue); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal("test", constraint.Constraint); + var constraint = Assert.Single(templatePart.Constraints); + Assert.Equal("test", constraint.Content); } [Fact] @@ -441,9 +441,9 @@ namespace Microsoft.AspNetCore.Dispatcher Assert.Equal("12", templatePart.DefaultValue); - Assert.Collection(templatePart.InlineConstraints, - constraint => Assert.Empty(constraint.Constraint), - constraint => Assert.Equal("test", constraint.Constraint)); + Assert.Collection(templatePart.Constraints, + constraint => Assert.Empty(constraint.Content), + constraint => Assert.Equal("test", constraint.Content)); } [Fact] @@ -455,9 +455,9 @@ namespace Microsoft.AspNetCore.Dispatcher // Assert Assert.Equal(":param", templatePart.Name); - Assert.Collection(templatePart.InlineConstraints, - constraint => Assert.Equal("test", constraint.Constraint), - constraint => Assert.Empty(constraint.Constraint)); + Assert.Collection(templatePart.Constraints, + constraint => Assert.Equal("test", constraint.Content), + constraint => Assert.Empty(constraint.Content)); } [Fact] @@ -469,8 +469,8 @@ namespace Microsoft.AspNetCore.Dispatcher // Assert Assert.Equal("param", templatePart.Name); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(\w,\w)", constraint.Constraint); + var constraint = Assert.Single(templatePart.Constraints); + Assert.Equal(@"test(\w,\w)", constraint.Content); } [Fact] @@ -482,8 +482,8 @@ namespace Microsoft.AspNetCore.Dispatcher // Assert Assert.Equal("par,am", templatePart.Name); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(\w)", constraint.Constraint); + var constraint = Assert.Single(templatePart.Constraints); + Assert.Equal(@"test(\w)", constraint.Content); } [Fact] @@ -497,8 +497,8 @@ namespace Microsoft.AspNetCore.Dispatcher Assert.Equal("jsd", templatePart.DefaultValue); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(\w,\w)", constraint.Constraint); + var constraint = Assert.Single(templatePart.Constraints); + Assert.Equal(@"test(\w,\w)", constraint.Content); } [Fact] @@ -513,8 +513,8 @@ namespace Microsoft.AspNetCore.Dispatcher Assert.True(templatePart.IsOptional); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal("int", constraint.Constraint); + var constraint = Assert.Single(templatePart.Constraints); + Assert.Equal("int", constraint.Content); } [Fact] @@ -527,8 +527,8 @@ namespace Microsoft.AspNetCore.Dispatcher Assert.Equal("param", templatePart.Name); Assert.Null(templatePart.DefaultValue); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal("test(=)", constraint.Constraint); + var constraint = Assert.Single(templatePart.Constraints); + Assert.Equal("test(=)", constraint.Content); } [Fact] @@ -552,8 +552,8 @@ namespace Microsoft.AspNetCore.Dispatcher Assert.Equal("param", templatePart.Name); Assert.Null(templatePart.DefaultValue); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal("test(a==b)", constraint.Constraint); + var constraint = Assert.Single(templatePart.Constraints); + Assert.Equal("test(a==b)", constraint.Content); } [Fact] @@ -566,8 +566,8 @@ namespace Microsoft.AspNetCore.Dispatcher Assert.Equal("param", templatePart.Name); Assert.Equal("dvds", templatePart.DefaultValue); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal("test(a==b)", constraint.Constraint); + var constraint = Assert.Single(templatePart.Constraints); + Assert.Equal("test(a==b)", constraint.Content); } [Fact] @@ -624,8 +624,8 @@ namespace Microsoft.AspNetCore.Dispatcher Assert.Equal("param", templatePart.Name); Assert.Equal("sds", templatePart.DefaultValue); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal("test(=)", constraint.Constraint); + var constraint = Assert.Single(templatePart.Constraints); + Assert.Equal("test(=)", constraint.Content); } [Fact] @@ -637,8 +637,8 @@ namespace Microsoft.AspNetCore.Dispatcher // Assert Assert.Equal("param", templatePart.Name); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(\{)", constraint.Constraint); + var constraint = Assert.Single(templatePart.Constraints); + Assert.Equal(@"test(\{)", constraint.Content); } [Fact] @@ -650,8 +650,8 @@ namespace Microsoft.AspNetCore.Dispatcher // Assert Assert.Equal("par{am", templatePart.Name); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(\sd)", constraint.Constraint); + var constraint = Assert.Single(templatePart.Constraints); + Assert.Equal(@"test(\sd)", constraint.Content); } [Fact] @@ -665,8 +665,8 @@ namespace Microsoft.AspNetCore.Dispatcher Assert.Equal("xvc", templatePart.DefaultValue); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(\{)", constraint.Constraint); + var constraint = Assert.Single(templatePart.Constraints); + Assert.Equal(@"test(\{)", constraint.Content); } [Fact] @@ -678,8 +678,8 @@ namespace Microsoft.AspNetCore.Dispatcher // Assert Assert.Equal("par(am", templatePart.Name); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(\()", constraint.Constraint); + var constraint = Assert.Single(templatePart.Constraints); + Assert.Equal(@"test(\()", constraint.Content); } [Fact] @@ -691,8 +691,8 @@ namespace Microsoft.AspNetCore.Dispatcher // Assert Assert.Equal("param", templatePart.Name); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(\()", constraint.Constraint); + var constraint = Assert.Single(templatePart.Constraints); + Assert.Equal(@"test(\()", constraint.Content); } [Fact] @@ -704,8 +704,8 @@ namespace Microsoft.AspNetCore.Dispatcher // Assert Assert.Equal("param", templatePart.Name); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal("test(#$%", constraint.Constraint); + var constraint = Assert.Single(templatePart.Constraints); + Assert.Equal("test(#$%", constraint.Content); } [Fact] @@ -717,9 +717,9 @@ namespace Microsoft.AspNetCore.Dispatcher // Assert Assert.Equal("param", templatePart.Name); - Assert.Collection(templatePart.InlineConstraints, - constraint => Assert.Equal(@"test(#", constraint.Constraint), - constraint => Assert.Equal(@"test1", constraint.Constraint)); + Assert.Collection(templatePart.Constraints, + constraint => Assert.Equal(@"test(#", constraint.Content), + constraint => Assert.Equal(@"test1", constraint.Content)); } [Fact] @@ -732,10 +732,10 @@ namespace Microsoft.AspNetCore.Dispatcher 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)); + Assert.Collection(templatePart.Constraints, + constraint => Assert.Equal(@"test(abc:somevalue)", constraint.Content), + constraint => Assert.Equal(@"name(test1", constraint.Content), + constraint => Assert.Equal(@"differentname", constraint.Content)); } [Fact] @@ -748,8 +748,8 @@ namespace Microsoft.AspNetCore.Dispatcher Assert.Equal("param", templatePart.Name); Assert.Equal("test1", templatePart.DefaultValue); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(constraintvalue", constraint.Constraint); + var constraint = Assert.Single(templatePart.Constraints); + Assert.Equal(@"test(constraintvalue", constraint.Content); } [Fact] @@ -763,8 +763,8 @@ namespace Microsoft.AspNetCore.Dispatcher Assert.Equal("djk", templatePart.DefaultValue); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(\()", constraint.Constraint); + var constraint = Assert.Single(templatePart.Constraints); + Assert.Equal(@"test(\()", constraint.Content); } [Fact] @@ -778,8 +778,8 @@ namespace Microsoft.AspNetCore.Dispatcher Assert.Null(templatePart.DefaultValue); Assert.False(templatePart.IsOptional); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(\?)", constraint.Constraint); + var constraint = Assert.Single(templatePart.Constraints); + Assert.Equal(@"test(\?)", constraint.Content); } [Fact] @@ -793,8 +793,8 @@ namespace Microsoft.AspNetCore.Dispatcher Assert.Null(templatePart.DefaultValue); Assert.True(templatePart.IsOptional); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(\?)", constraint.Constraint); + var constraint = Assert.Single(templatePart.Constraints); + Assert.Equal(@"test(\?)", constraint.Content); } [Fact] @@ -808,8 +808,8 @@ namespace Microsoft.AspNetCore.Dispatcher Assert.Equal("sdf", templatePart.DefaultValue); Assert.False(templatePart.IsOptional); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(\?)", constraint.Constraint); + var constraint = Assert.Single(templatePart.Constraints); + Assert.Equal(@"test(\?)", constraint.Content); } [Fact] @@ -823,8 +823,8 @@ namespace Microsoft.AspNetCore.Dispatcher Assert.Equal("sdf", templatePart.DefaultValue); Assert.True(templatePart.IsOptional); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(\?)", constraint.Constraint); + var constraint = Assert.Single(templatePart.Constraints); + Assert.Equal(@"test(\?)", constraint.Content); } [Fact] @@ -838,8 +838,8 @@ namespace Microsoft.AspNetCore.Dispatcher Assert.Null(templatePart.DefaultValue); Assert.False(templatePart.IsOptional); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(\?)", constraint.Constraint); + var constraint = Assert.Single(templatePart.Constraints); + Assert.Equal(@"test(\?)", constraint.Content); } [Fact] @@ -853,9 +853,9 @@ namespace Microsoft.AspNetCore.Dispatcher Assert.Null(templatePart.DefaultValue); Assert.False(templatePart.IsOptional); - Assert.Collection(templatePart.InlineConstraints, - constraint => Assert.Equal(@"test(#)", constraint.Constraint), - constraint => Assert.Equal(@"$)", constraint.Constraint)); + Assert.Collection(templatePart.Constraints, + constraint => Assert.Equal(@"test(#)", constraint.Content), + constraint => Assert.Equal(@"$)", constraint.Content)); } [Fact] @@ -869,8 +869,8 @@ namespace Microsoft.AspNetCore.Dispatcher Assert.Null(templatePart.DefaultValue); Assert.False(templatePart.IsOptional); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(#:)$)", constraint.Constraint); + var constraint = Assert.Single(templatePart.Constraints); + Assert.Equal(@"test(#:)$)", constraint.Content); } [Fact] @@ -884,8 +884,8 @@ namespace Microsoft.AspNetCore.Dispatcher Assert.Null(templatePart.DefaultValue); Assert.False(templatePart.IsOptional); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"regex(\\(\\(\\(\\()", constraint.Constraint); + var constraint = Assert.Single(templatePart.Constraints); + Assert.Equal(@"regex(\\(\\(\\(\\()", constraint.Content); } [Fact] @@ -899,8 +899,8 @@ namespace Microsoft.AspNetCore.Dispatcher 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); + var constraint = Assert.Single(templatePart.Constraints); + Assert.Equal(@"regex(^\d{{3}}-\d{{3}}-\d{{4}}$)", constraint.Content); } [Fact] @@ -914,8 +914,8 @@ namespace Microsoft.AspNetCore.Dispatcher 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); + var constraint = Assert.Single(templatePart.Constraints); + Assert.Equal(@"regex(^\d{{3}}-\d{{3}}-\d{{4}}$)", constraint.Content); } [Theory] @@ -935,20 +935,16 @@ namespace Microsoft.AspNetCore.Dispatcher // Assert Assert.Equal(expectedParameterName, templatePart.Name); - Assert.Empty(templatePart.InlineConstraints); + Assert.Empty(templatePart.Constraints); Assert.Null(templatePart.DefaultValue); } - private TemplatePart ParseParameter(string routeParameter) + private RoutePatternParameter ParseParameter( string routeParameter) { - var templatePart = InlineRouteParameterParser.ParseRouteParameter(routeParameter); + // See: #475 - these tests don't pass the 'whole' text. + var templatePart = InlineRouteParameterParser.ParseRouteParameter(string.Empty, routeParameter); return templatePart; } - - private static RouteTemplate ParseRouteTemplate(string template) - { - return TemplateParser.Parse(template); - } } } diff --git a/test/Microsoft.AspNetCore.Dispatcher.Test/Patterns/RoutePatternParserTest.cs b/test/Microsoft.AspNetCore.Dispatcher.Test/Patterns/RoutePatternParserTest.cs new file mode 100644 index 0000000000..6a1f7b820e --- /dev/null +++ b/test/Microsoft.AspNetCore.Dispatcher.Test/Patterns/RoutePatternParserTest.cs @@ -0,0 +1,781 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Microsoft.AspNetCore.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Dispatcher.Patterns +{ + public class RoutePatternParameterParserTest + { + [Fact] + public void Parse_SingleLiteral() + { + // Arrange + var template = "cool"; + + var builder = RoutePatternBuilder.Create(template); + builder.AddPathSegment("cool", RoutePatternPart.CreateLiteralFromText("cool", "cool")); + + var expected = builder.Build(); + + // Act + var actual = RoutePatternParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new RoutePatternEqualityComparer()); + } + + [Fact] + public void Parse_SingleParameter() + { + // Arrange + var template = "{p}"; + + var builder = RoutePatternBuilder.Create(template); + builder.AddPathSegment("{p}", RoutePatternPart.CreateParameterFromText("{p}", "p")); + + var expected = builder.Build(); + + // Act + var actual = RoutePatternParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new RoutePatternEqualityComparer()); + } + + [Fact] + public void Parse_OptionalParameter() + { + // Arrange + var template = "{p?}"; + + var builder = RoutePatternBuilder.Create(template); + builder.AddPathSegment("{p?}", RoutePatternPart.CreateParameterFromText("{p?}", "p", null, RoutePatternParameterKind.Optional)); + + var expected = builder.Build(); + + // Act + var actual = RoutePatternParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new RoutePatternEqualityComparer()); + } + + [Fact] + public void Parse_MultipleLiterals() + { + // Arrange + var template = "cool/awesome/super"; + + var builder = RoutePatternBuilder.Create(template); + builder.AddPathSegment("cool", RoutePatternPart.CreateLiteralFromText("cool", "cool")); + builder.AddPathSegment("awesome", RoutePatternPart.CreateLiteralFromText("awesome", "awesome")); + builder.AddPathSegment("super", RoutePatternPart.CreateLiteralFromText("super", "super")); + + var expected = builder.Build(); + + // Act + var actual = RoutePatternParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new RoutePatternEqualityComparer()); + } + + [Fact] + public void Parse_MultipleParameters() + { + // Arrange + var template = "{p1}/{p2}/{*p3}"; + + var builder = RoutePatternBuilder.Create(template); + builder.AddPathSegment("{p1}", RoutePatternPart.CreateParameterFromText("{p1}", "p1")); + builder.AddPathSegment("{p2}", RoutePatternPart.CreateParameterFromText("{p2}", "p2")); + builder.AddPathSegment("{*p3}", RoutePatternPart.CreateParameterFromText("{*p3}", "p3", null, RoutePatternParameterKind.CatchAll)); + + var expected = builder.Build(); + + // Act + var actual = RoutePatternParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new RoutePatternEqualityComparer()); + } + + [Fact] + public void Parse_ComplexSegment_LP() + { + // Arrange + var template = "cool-{p1}"; + + var builder = RoutePatternBuilder.Create(template); + builder.AddPathSegment( + "cool-{p1}", + RoutePatternPart.CreateLiteralFromText("cool-", "cool-"), + RoutePatternPart.CreateParameterFromText("{p1}", "p1")); + + var expected = builder.Build(); + + // Act + var actual = RoutePatternParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new RoutePatternEqualityComparer()); + } + + [Fact] + public void Parse_ComplexSegment_PL() + { + // Arrange + var template = "{p1}-cool"; + + var builder = RoutePatternBuilder.Create(template); + builder.AddPathSegment( + "{p1}-cool", + RoutePatternPart.CreateParameterFromText("{p1}", "p1"), + RoutePatternPart.CreateLiteralFromText("-cool", "-cool")); + + var expected = builder.Build(); + + // Act + var actual = RoutePatternParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new RoutePatternEqualityComparer()); + } + + [Fact] + public void Parse_ComplexSegment_PLP() + { + // Arrange + var template = "{p1}-cool-{p2}"; + + var builder = RoutePatternBuilder.Create(template); + builder.AddPathSegment( + "{p1}-cool", + RoutePatternPart.CreateParameterFromText("{p1}", "p1"), + RoutePatternPart.CreateLiteralFromText("-cool-", "-cool-"), + RoutePatternPart.CreateParameterFromText("{p2}", "p2")); + + var expected = builder.Build(); + + // Act + var actual = RoutePatternParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new RoutePatternEqualityComparer()); + } + + [Fact] + public void Parse_ComplexSegment_LPL() + { + // Arrange + var template = "cool-{p1}-awesome"; + + var builder = RoutePatternBuilder.Create(template); + builder.AddPathSegment( + template, + RoutePatternPart.CreateLiteralFromText("cool-", "cool-"), + RoutePatternPart.CreateParameterFromText("{p1}", "p1"), + RoutePatternPart.CreateLiteralFromText("-awesome", "-awesome")); + + var expected = builder.Build(); + + // Act + var actual = RoutePatternParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new RoutePatternEqualityComparer()); + } + + [Fact] + public void Parse_ComplexSegment_OptionalParameterFollowingPeriod() + { + // Arrange + var template = "{p1}.{p2?}"; + + var builder = RoutePatternBuilder.Create(template); + builder.AddPathSegment( + "{p1}.{p2?}", + RoutePatternPart.CreateParameterFromText("{p1}", "p1"), + RoutePatternPart.CreateSeparatorFromText(".", "."), + RoutePatternPart.CreateParameterFromText("{p2?}", "p2", null, RoutePatternParameterKind.Optional)); + + var expected = builder.Build(); + + // Act + var actual = RoutePatternParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new RoutePatternEqualityComparer()); + } + + [Fact] + public void Parse_ComplexSegment_ParametersFollowingPeriod() + { + // Arrange + var template = "{p1}.{p2}"; + + var builder = RoutePatternBuilder.Create(template); + builder.AddPathSegment( + "{p1}.{p2}", + RoutePatternPart.CreateParameterFromText("{p1}", "p1"), + RoutePatternPart.CreateLiteralFromText(".", "."), + RoutePatternPart.CreateParameterFromText("{p2}", "p2")); + + var expected = builder.Build(); + + // Act + var actual = RoutePatternParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new RoutePatternEqualityComparer()); + } + + [Fact] + public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_ThreeParameters() + { + // Arrange + var template = "{p1}.{p2}.{p3?}"; + + var builder = RoutePatternBuilder.Create(template); + builder.AddPathSegment( + "{p1}.{p2}.{p3?}", + RoutePatternPart.CreateParameterFromText("{p1}", "p1"), + RoutePatternPart.CreateLiteralFromText(".", "."), + RoutePatternPart.CreateParameterFromText("{p2}", "p2"), + RoutePatternPart.CreateSeparatorFromText(".", "."), + RoutePatternPart.CreateParameterFromText("{p3?}", "p3", null, RoutePatternParameterKind.Optional)); + + var expected = builder.Build(); + + // Act + var actual = RoutePatternParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new RoutePatternEqualityComparer()); + } + + [Fact] + public void Parse_ComplexSegment_ThreeParametersSeperatedByPeriod() + { + // Arrange + var template = "{p1}.{p2}.{p3}"; + + var builder = RoutePatternBuilder.Create(template); + builder.AddPathSegment( + "{p1}.{p2}.{p3}", + RoutePatternPart.CreateParameterFromText("{p1}", "p1"), + RoutePatternPart.CreateLiteralFromText(".", "."), + RoutePatternPart.CreateParameterFromText("{p2}", "p2"), + RoutePatternPart.CreateLiteralFromText(".", "."), + RoutePatternPart.CreateParameterFromText("{p3}", "p3")); + + var expected = builder.Build(); + + // Act + var actual = RoutePatternParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new RoutePatternEqualityComparer()); + } + + [Fact] + public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_MiddleSegment() + { + // Arrange + var template = "{p1}.{p2?}/{p3}"; + + var builder = RoutePatternBuilder.Create(template); + builder.AddPathSegment( + "{p1}.{p2?}", + RoutePatternPart.CreateParameterFromText("{p1}", "p1"), + RoutePatternPart.CreateSeparatorFromText(".", "."), + RoutePatternPart.CreateParameterFromText("{p2?}", "p2", null, RoutePatternParameterKind.Optional)); + builder.AddPathSegment("{p3}", RoutePatternPart.CreateParameterFromText("{p3}", "p3")); + + var expected = builder.Build(); + + // Act + var actual = RoutePatternParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new RoutePatternEqualityComparer()); + } + + [Fact] + public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_LastSegment() + { + // Arrange + var template = "{p1}/{p2}.{p3?}"; + + var builder = RoutePatternBuilder.Create(template); + builder.AddPathSegment("{p1}", RoutePatternPart.CreateParameterFromText("{p1}", "p1")); + builder.AddPathSegment("{p2}.{p3?}", + RoutePatternPart.CreateParameterFromText("{p2}", "p2"), + RoutePatternPart.CreateSeparatorFromText(".", "."), + RoutePatternPart.CreateParameterFromText("{p3?}", "p3", null, RoutePatternParameterKind.Optional)); + + var expected = builder.Build(); + + // Act + var actual = RoutePatternParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new RoutePatternEqualityComparer()); + } + + [Fact] + public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_PeriodAfterSlash() + { + // Arrange + var template = "{p2}/.{p3?}"; + + var builder = RoutePatternBuilder.Create(template); + builder.AddPathSegment("{p2}", RoutePatternPart.CreateParameterFromText("{p2}", "p2")); + builder.AddPathSegment(".{p3?}", + RoutePatternPart.CreateSeparatorFromText(".", "."), + RoutePatternPart.CreateParameterFromText("{p3?}", "p3", null, RoutePatternParameterKind.Optional)); + + var expected = builder.Build(); + + // Act + var actual = RoutePatternParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new RoutePatternEqualityComparer()); + } + + [Theory] + [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}", @"regex(^\d{3}-\d{3}-\d{4}$)")] // ssn + [InlineData(@"{p1:regex(^\d{{1,2}}\/\d{{1,2}}\/\d{{4}}$)}", @"regex(^\d{1,2}\/\d{1,2}\/\d{4}$)")] // date + [InlineData(@"{p1:regex(^\w+\@\w+\.\w+)}", @"regex(^\w+\@\w+\.\w+)")] // email + [InlineData(@"{p1:regex(([}}])\w+)}", @"regex(([}])\w+)")] // Not balanced } + [InlineData(@"{p1:regex(([{{(])\w+)}", @"regex(([{(])\w+)")] // Not balanced { + public void Parse_RegularExpressions(string template, string constraint) + { + // Arrange + var builder = RoutePatternBuilder.Create(template); + builder.AddPathSegment( + template, + RoutePatternPart.CreateParameterFromText( + template, + "p1", + null, + RoutePatternParameterKind.Standard, + ConstraintReference.CreateFromText(constraint, constraint))); + + var expected = builder.Build(); + + // Act + var actual = RoutePatternParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new RoutePatternEqualityComparer()); + } + + [Theory] + [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}}$)}")] // extra } + [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}}")] // extra } at the end + [InlineData(@"{{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}")] // extra { at the begining + [InlineData(@"{p1:regex(([}])\w+}")] // Not escaped } + [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}$)}")] // Not escaped } + [InlineData(@"{p1:regex(abc)")] + public void Parse_RegularExpressions_Invalid(string template) + { + // Act and Assert + ExceptionAssert.Throws( + () => RoutePatternParser.Parse(template), + "There is an incomplete parameter in the route template. Check that each '{' character has a matching " + + "'}' character."); + } + + [Theory] + [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{{4}}$)}")] // extra { + [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{4}}$)}")] // Not escaped { + public void Parse_RegularExpressions_Unescaped(string template) + { + // Act and Assert + ExceptionAssert.Throws( + () => RoutePatternParser.Parse(template), + "In a route parameter, '{' and '}' must be escaped with '{{' and '}}'."); + } + + [Theory] + [InlineData("{p1}.{p2?}.{p3}", "p2", ".")] + [InlineData("{p1?}{p2}", "p1", "{p2}")] + [InlineData("{p1?}{p2?}", "p1", "{p2?}")] + [InlineData("{p1}.{p2?})", "p2", ")")] + [InlineData("{foorb?}-bar-{z}", "foorb", "-bar-")] + public void Parse_ComplexSegment_OptionalParameter_NotTheLastPart( + string template, + string parameter, + string invalid) + { + // Act and Assert + ExceptionAssert.Throws( + () => RoutePatternParser.Parse(template), + "An optional parameter must be at the end of the segment. In the segment '" + template + + "', optional parameter '" + parameter + "' is followed by '" + invalid + "'."); + } + + [Theory] + [InlineData("{p1}-{p2?}", "-")] + [InlineData("{p1}..{p2?}", "..")] + [InlineData("..{p2?}", "..")] + [InlineData("{p1}.abc.{p2?}", ".abc.")] + [InlineData("{p1}{p2?}", "{p1}")] + public void Parse_ComplexSegment_OptionalParametersSeperatedByPeriod_Invalid(string template, string parameter) + { + // Act and Assert + ExceptionAssert.Throws( + () => RoutePatternParser.Parse(template), + "In the segment '"+ template +"', the optional parameter 'p2' is preceded by an invalid " + + "segment '" + parameter +"'. Only a period (.) can precede an optional parameter."); + } + + [Fact] + public void InvalidTemplate_WithRepeatedParameter() + { + var ex = ExceptionAssert.Throws( + () => RoutePatternParser.Parse("{Controller}.mvc/{id}/{controller}"), + "The route parameter name 'controller' appears more than one time in the route template."); + } + + [Theory] + [InlineData("123{a}abc{")] + [InlineData("123{a}abc}")] + [InlineData("xyz}123{a}abc}")] + [InlineData("{{p1}")] + [InlineData("{p1}}")] + [InlineData("p1}}p2{")] + public void InvalidTemplate_WithMismatchedBraces(string template) + { + ExceptionAssert.Throws( + () => RoutePatternParser.Parse(template), + @"There is an incomplete parameter in the route template. Check that each '{' character has a " + + "matching '}' character."); + } + + [Fact] + public void InvalidTemplate_CannotHaveCatchAllInMultiSegment() + { + ExceptionAssert.Throws( + () => RoutePatternParser.Parse("123{a}abc{*moo}"), + "A path segment that contains more than one section, such as a literal section or a parameter, " + + "cannot contain a catch-all parameter."); + } + + [Fact] + public void InvalidTemplate_CannotHaveMoreThanOneCatchAll() + { + ExceptionAssert.Throws( + () => RoutePatternParser.Parse("{*p1}/{*p2}"), + "A catch-all parameter can only appear as the last segment of the route template."); + } + + [Fact] + public void InvalidTemplate_CannotHaveMoreThanOneCatchAllInMultiSegment() + { + ExceptionAssert.Throws( + () => RoutePatternParser.Parse("{*p1}abc{*p2}"), + "A path segment that contains more than one section, such as a literal section or a parameter, " + + "cannot contain a catch-all parameter."); + } + + [Fact] + public void InvalidTemplate_CannotHaveCatchAllWithNoName() + { + ExceptionAssert.Throws( + () => RoutePatternParser.Parse("foo/{*}"), + "The route parameter name '' is invalid. Route parameter names must be non-empty and cannot" + + " contain these characters: '{', '}', '/'. The '?' character marks a parameter as optional," + + " and can occur only at the end of the parameter. The '*' character marks a parameter as catch-all," + + " and can occur only at the start of the parameter."); + } + + [Theory] + [InlineData("{**}", "*")] + [InlineData("{a*}", "a*")] + [InlineData("{*a*}", "a*")] + [InlineData("{*a*:int}", "a*")] + [InlineData("{*a*=5}", "a*")] + [InlineData("{*a*b=5}", "a*b")] + [InlineData("{p1?}.{p2/}/{p3}", "p2/")] + [InlineData("{p{{}", "p{")] + [InlineData("{p}}}", "p}")] + [InlineData("{p/}", "p/")] + public void ParseRouteParameter_ThrowsIf_ParameterContainsSpecialCharacters( + string template, + string parameterName) + { + // Arrange + var expectedMessage = "The route parameter name '" + parameterName + "' is invalid. Route parameter " + + "names must be non-empty and cannot contain these characters: '{', '}', '/'. The '?' character " + + "marks a parameter as optional, and can occur only at the end of the parameter. The '*' character " + + "marks a parameter as catch-all, and can occur only at the start of the parameter."; + + // Act & Assert + ExceptionAssert.Throws(() => RoutePatternParser.Parse(template), expectedMessage); + } + + [Fact] + public void InvalidTemplate_CannotHaveConsecutiveOpenBrace() + { + ExceptionAssert.Throws( + () => RoutePatternParser.Parse("foo/{{p1}"), + "There is an incomplete parameter in the route template. Check that each '{' character has a " + + "matching '}' character."); + } + + [Fact] + public void InvalidTemplate_CannotHaveConsecutiveCloseBrace() + { + ExceptionAssert.Throws( + () => RoutePatternParser.Parse("foo/{p1}}"), + "There is an incomplete parameter in the route template. Check that each '{' character has a " + + "matching '}' character."); + } + + [Fact] + public void InvalidTemplate_SameParameterTwiceThrows() + { + ExceptionAssert.Throws( + () => RoutePatternParser.Parse("{aaa}/{AAA}"), + "The route parameter name 'AAA' appears more than one time in the route template."); + } + + [Fact] + public void InvalidTemplate_SameParameterTwiceAndOneCatchAllThrows() + { + ExceptionAssert.Throws( + () => RoutePatternParser.Parse("{aaa}/{*AAA}"), + "The route parameter name 'AAA' appears more than one time in the route template."); + } + + [Fact] + public void InvalidTemplate_InvalidParameterNameWithCloseBracketThrows() + { + ExceptionAssert.Throws( + () => RoutePatternParser.Parse("{a}/{aa}a}/{z}"), + "There is an incomplete parameter in the route template. Check that each '{' character has a " + + "matching '}' character."); + } + + [Fact] + public void InvalidTemplate_InvalidParameterNameWithOpenBracketThrows() + { + ExceptionAssert.Throws( + () => RoutePatternParser.Parse("{a}/{a{aa}/{z}"), + "In a route parameter, '{' and '}' must be escaped with '{{' and '}}'."); + } + + [Fact] + public void InvalidTemplate_InvalidParameterNameWithEmptyNameThrows() + { + ExceptionAssert.Throws( + () => RoutePatternParser.Parse("{a}/{}/{z}"), + "The route parameter name '' is invalid. Route parameter names must be non-empty and cannot" + + " contain these characters: '{', '}', '/'. The '?' character marks a parameter as optional, and" + + " can occur only at the end of the parameter. The '*' character marks a parameter as catch-all," + + " and can occur only at the start of the parameter."); + } + + [Fact] + public void InvalidTemplate_InvalidParameterNameWithQuestionThrows() + { + ExceptionAssert.Throws( + () => RoutePatternParser.Parse("{Controller}.mvc/{?}"), + "The route parameter name '' is invalid. Route parameter names must be non-empty and cannot" + + " contain these characters: '{', '}', '/'. The '?' character marks a parameter as optional, and" + + " can occur only at the end of the parameter. The '*' character marks a parameter as catch-all," + + " and can occur only at the start of the parameter."); + } + + [Fact] + public void InvalidTemplate_ConsecutiveSeparatorsSlashSlashThrows() + { + ExceptionAssert.Throws( + () => RoutePatternParser.Parse("{a}//{z}"), + "The route template separator character '/' cannot appear consecutively. It must be separated by " + + "either a parameter or a literal value."); + } + + [Fact] + public void InvalidTemplate_WithCatchAllNotAtTheEndThrows() + { + ExceptionAssert.Throws( + () => RoutePatternParser.Parse("foo/{p1}/{*p2}/{p3}"), + "A catch-all parameter can only appear as the last segment of the route template."); + } + + [Fact] + public void InvalidTemplate_RepeatedParametersThrows() + { + ExceptionAssert.Throws( + () => RoutePatternParser.Parse("foo/aa{p1}{p2}"), + "A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by " + + "a literal string."); + } + + [Fact] + public void InvalidTemplate_CannotStartWithSlash() + { + ExceptionAssert.Throws( + () => RoutePatternParser.Parse("/foo"), + "The route template cannot start with a '/' or '~' character."); + } + + [Fact] + public void InvalidTemplate_CannotStartWithTilde() + { + ExceptionAssert.Throws( + () => RoutePatternParser.Parse("~foo"), + "The route template cannot start with a '/' or '~' character."); + } + + [Fact] + public void InvalidTemplate_CannotContainQuestionMark() + { + ExceptionAssert.Throws( + () => RoutePatternParser.Parse("foor?bar"), + "The literal section 'foor?bar' is invalid. Literal sections cannot contain the '?' character."); + } + + [Fact] + public void InvalidTemplate_ParameterCannotContainQuestionMark_UnlessAtEnd() + { + ExceptionAssert.Throws( + () => RoutePatternParser.Parse("{foor?b}"), + "The route parameter name 'foor?b' is invalid. Route parameter names must be non-empty and cannot" + + " contain these characters: '{', '}', '/'. The '?' character marks a parameter as optional, and" + + " can occur only at the end of the parameter. The '*' character marks a parameter as catch-all," + + " and can occur only at the start of the parameter."); + } + + [Fact] + public void InvalidTemplate_CatchAllMarkedOptional() + { + ExceptionAssert.Throws( + () => RoutePatternParser.Parse("{a}/{*b?}"), + "A catch-all parameter cannot be marked optional."); + } + + private class RoutePatternEqualityComparer : IEqualityComparer + { + public bool Equals(RoutePattern x, RoutePattern y) + { + if (x == null && y == null) + { + return true; + } + else if (x == null || y == null) + { + return false; + } + else + { + if (!string.Equals(x.RawText, y.RawText, StringComparison.Ordinal)) + { + return false; + } + + if (x.PathSegments.Count != y.PathSegments.Count) + { + return false; + } + + for (int i = 0; i < x.PathSegments.Count; i++) + { + if (x.PathSegments[i].RawText == y.PathSegments[i].RawText) + { + return false; + } + + if (x.PathSegments[i].Parts.Count != y.PathSegments[i].Parts.Count) + { + return false; + } + + for (int j = 0; j < x.PathSegments[i].Parts.Count; j++) + { + if (!Equals(x.PathSegments[i].Parts[j], y.PathSegments[i].Parts[j])) + { + return false; + } + } + } + + if (x.Parameters.Count != y.Parameters.Count) + { + return false; + } + + for (int i = 0; i < x.Parameters.Count; i++) + { + if (!Equals(x.Parameters[i], y.Parameters[i])) + { + return false; + } + } + + return true; + } + } + + private bool Equals(RoutePatternPart x, RoutePatternPart y) + { + if (x.GetType() != y.GetType()) + { + return false; + } + + if (x.IsLiteral && y.IsLiteral) + { + return Equals((RoutePatternLiteral)x, (RoutePatternLiteral)y); + } + else if (x.IsParameter && y.IsParameter) + { + return Equals((RoutePatternParameter)x, (RoutePatternParameter)y); + } + else if (x.IsSeparator && y.IsSeparator) + { + return Equals((RoutePatternSeparator)x, (RoutePatternSeparator)y); + } + + Debug.Fail("This should not be reachable. Do you need to update the comparison logic?"); + return false; + } + + private bool Equals(RoutePatternLiteral x, RoutePatternLiteral y) + { + return x.RawText == y.RawText && x.Content == y.Content; + } + + private bool Equals(RoutePatternParameter x, RoutePatternParameter y) + { + return + x.RawText == y.RawText && + x.Name == y.Name && + x.DefaultValue == y.DefaultValue && + x.ParameterKind == y.ParameterKind && + Enumerable.SequenceEqual(x.Constraints.Select(c => (c.Content, c.RawText)), y.Constraints.Select(c => (c.Content, c.RawText))); + + } + + private bool Equals(RoutePatternSeparator x, RoutePatternSeparator y) + { + return x.RawText == y.RawText && x.Content == y.Content; + } + + public int GetHashCode(RoutePattern obj) + { + throw new NotImplementedException(); + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Dispatcher.Test/TemplateParserTests.cs b/test/Microsoft.AspNetCore.Dispatcher.Test/TemplateParserTests.cs deleted file mode 100644 index e02347a3da..0000000000 --- a/test/Microsoft.AspNetCore.Dispatcher.Test/TemplateParserTests.cs +++ /dev/null @@ -1,916 +0,0 @@ -// 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 b428cf1dfc..8849815e48 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/InlineRouteParameterParserTests.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/InlineRouteParameterParserTests.cs @@ -948,7 +948,9 @@ namespace Microsoft.AspNetCore.Routing.Tests private TemplatePart ParseParameter(string routeParameter) { var _constraintResolver = GetConstraintResolver(); +#pragma warning disable CS0618 // Type or member is obsolete var templatePart = InlineRouteParameterParser.ParseRouteParameter(routeParameter); +#pragma warning restore CS0618 // Type or member is obsolete return templatePart; } diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateParserTests.cs b/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateParserTests.cs index b376eb0ec7..ce54e9485e 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateParserTests.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateParserTests.cs @@ -525,8 +525,8 @@ namespace Microsoft.AspNetCore.Routing.Template.Tests [Theory] [InlineData("{p1}.{p2?}.{p3}", "p2", ".")] - [InlineData("{p1?}{p2}", "p1", "p2")] - [InlineData("{p1?}{p2?}", "p1", "p2")] + [InlineData("{p1?}{p2}", "p1", "{p2}")] + [InlineData("{p1?}{p2?}", "p1", "{p2?}")] [InlineData("{p1}.{p2?})", "p2", ")")] [InlineData("{foorb?}-bar-{z}", "foorb", "-bar-")] public void Parse_ComplexSegment_OptionalParameter_NotTheLastPart(