diff --git a/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternBuilder.cs b/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternBuilder.cs index 3a54fcc9dc..1875ce4667 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternBuilder.cs +++ b/src/Microsoft.AspNetCore.Dispatcher/Patterns/RoutePatternBuilder.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; namespace Microsoft.AspNetCore.Dispatcher.Patterns { @@ -16,30 +15,36 @@ namespace Microsoft.AspNetCore.Dispatcher.Patterns public IList PathSegments { get; } = new List(); - public string Text { get; set; } + public string RawText { get; set; } - public RoutePatternBuilder AddPathSegment(RoutePatternPart part) + public RoutePatternBuilder AddPathSegment(params RoutePatternPart[] parts) { - return AddPathSegment(null, part, Array.Empty()); + if (parts == null) + { + throw new ArgumentNullException(nameof(parts)); + } + + if (parts.Length == 0) + { + throw new ArgumentException(Resources.RoutePatternBuilder_CollectionCannotBeEmpty, nameof(parts)); + } + + return AddPathSegmentFromText(null, parts); } - public RoutePatternBuilder AddPathSegment(RoutePatternPart part, params RoutePatternPart[] parts) + public RoutePatternBuilder AddPathSegmentFromText(string text, params RoutePatternPart[] parts) { - return AddPathSegment(null, part, Array.Empty()); - } + if (parts == null) + { + throw new ArgumentNullException(nameof(parts)); + } - public RoutePatternBuilder AddPathSegment(string text, RoutePatternPart part) - { - return AddPathSegment(text, part, Array.Empty()); - } + if (parts.Length == 0) + { + throw new ArgumentException(Resources.RoutePatternBuilder_CollectionCannotBeEmpty, nameof(parts)); + } - 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); + var segment = new RoutePatternPathSegment(text, parts.ToArray()); PathSegments.Add(segment); return this; @@ -61,12 +66,12 @@ namespace Microsoft.AspNetCore.Dispatcher.Patterns } } - return new RoutePattern(Text, parameters.ToArray(), PathSegments.ToArray()); + return new RoutePattern(RawText, parameters.ToArray(), PathSegments.ToArray()); } public static RoutePatternBuilder Create(string text) { - return new RoutePatternBuilder() { Text = text, }; + return new RoutePatternBuilder() { RawText = text, }; } } } diff --git a/src/Microsoft.AspNetCore.Dispatcher/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Dispatcher/Properties/Resources.Designer.cs index 01221b8b26..7cfa53c69e 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Dispatcher/Properties/Resources.Designer.cs @@ -38,6 +38,20 @@ namespace Microsoft.AspNetCore.Dispatcher internal static string FormatArgument_NullOrEmpty() => GetString("Argument_NullOrEmpty"); + /// + /// The collection cannot be empty. + /// + internal static string RoutePatternBuilder_CollectionCannotBeEmpty + { + get => GetString("RoutePatternBuilder_CollectionCannotBeEmpty"); + } + + /// + /// The collection cannot be empty. + /// + internal static string FormatRoutePatternBuilder_CollectionCannotBeEmpty() + => GetString("RoutePatternBuilder_CollectionCannotBeEmpty"); + /// /// 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 abe35916f1..23f44055ad 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/Resources.resx +++ b/src/Microsoft.AspNetCore.Dispatcher/Resources.resx @@ -124,6 +124,9 @@ Value cannot be null or empty. + + The collection cannot be 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.Routing/Template/RouteTemplate.cs b/src/Microsoft.AspNetCore.Routing/Template/RouteTemplate.cs index 2253d85257..c79cfd4158 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 Microsoft.AspNetCore.Dispatcher.Patterns; using Other = Microsoft.AspNetCore.Dispatcher.Patterns.RoutePattern; namespace Microsoft.AspNetCore.Routing.Template @@ -98,5 +99,50 @@ namespace Microsoft.AspNetCore.Routing.Template return null; } + + /// + /// Converts the to the equivalent + /// + /// + /// A . + public Other ToRoutePattern() + { + var builder = RoutePatternBuilder.Create(TemplateText); + + for (var i = 0; i < Segments.Count; i++) + { + var segment = Segments[i]; + + var parts = new List(); + for (var j = 0; j < segment.Parts.Count; j++) + { + var part = segment.Parts[j]; + if (part.IsLiteral && part.IsOptionalSeperator) + { + parts.Add(RoutePatternPart.CreateSeparator(part.Text)); + } + else if (part.IsLiteral) + { + parts.Add(RoutePatternPart.CreateLiteral(part.Text)); + } + else + { + var kind = part.IsCatchAll ? + RoutePatternParameterKind.CatchAll : + part.IsOptional ? + RoutePatternParameterKind.Optional : + RoutePatternParameterKind.Standard; + + var constraints = part.InlineConstraints.Select(c => ConstraintReference.Create(c.Constraint)).ToArray(); + + parts.Add(RoutePatternPart.CreateParameter(part.Name, part.DefaultValue, kind, constraints)); + } + } + + builder.AddPathSegment(parts.ToArray()); + } + + return builder.Build(); + } } } diff --git a/test/Microsoft.AspNetCore.Dispatcher.Test/Patterns/RoutePatternParserTest.cs b/test/Microsoft.AspNetCore.Dispatcher.Test/Patterns/RoutePatternParserTest.cs index 6a1f7b820e..febd928adb 100644 --- a/test/Microsoft.AspNetCore.Dispatcher.Test/Patterns/RoutePatternParserTest.cs +++ b/test/Microsoft.AspNetCore.Dispatcher.Test/Patterns/RoutePatternParserTest.cs @@ -19,7 +19,7 @@ namespace Microsoft.AspNetCore.Dispatcher.Patterns var template = "cool"; var builder = RoutePatternBuilder.Create(template); - builder.AddPathSegment("cool", RoutePatternPart.CreateLiteralFromText("cool", "cool")); + builder.AddPathSegmentFromText("cool", RoutePatternPart.CreateLiteralFromText("cool", "cool")); var expected = builder.Build(); @@ -37,7 +37,7 @@ namespace Microsoft.AspNetCore.Dispatcher.Patterns var template = "{p}"; var builder = RoutePatternBuilder.Create(template); - builder.AddPathSegment("{p}", RoutePatternPart.CreateParameterFromText("{p}", "p")); + builder.AddPathSegmentFromText("{p}", RoutePatternPart.CreateParameterFromText("{p}", "p")); var expected = builder.Build(); @@ -55,7 +55,7 @@ namespace Microsoft.AspNetCore.Dispatcher.Patterns var template = "{p?}"; var builder = RoutePatternBuilder.Create(template); - builder.AddPathSegment("{p?}", RoutePatternPart.CreateParameterFromText("{p?}", "p", null, RoutePatternParameterKind.Optional)); + builder.AddPathSegmentFromText("{p?}", RoutePatternPart.CreateParameterFromText("{p?}", "p", null, RoutePatternParameterKind.Optional)); var expected = builder.Build(); @@ -73,9 +73,9 @@ namespace Microsoft.AspNetCore.Dispatcher.Patterns 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")); + builder.AddPathSegmentFromText("cool", RoutePatternPart.CreateLiteralFromText("cool", "cool")); + builder.AddPathSegmentFromText("awesome", RoutePatternPart.CreateLiteralFromText("awesome", "awesome")); + builder.AddPathSegmentFromText("super", RoutePatternPart.CreateLiteralFromText("super", "super")); var expected = builder.Build(); @@ -93,9 +93,9 @@ namespace Microsoft.AspNetCore.Dispatcher.Patterns 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)); + builder.AddPathSegmentFromText("{p1}", RoutePatternPart.CreateParameterFromText("{p1}", "p1")); + builder.AddPathSegmentFromText("{p2}", RoutePatternPart.CreateParameterFromText("{p2}", "p2")); + builder.AddPathSegmentFromText("{*p3}", RoutePatternPart.CreateParameterFromText("{*p3}", "p3", null, RoutePatternParameterKind.CatchAll)); var expected = builder.Build(); @@ -113,7 +113,7 @@ namespace Microsoft.AspNetCore.Dispatcher.Patterns var template = "cool-{p1}"; var builder = RoutePatternBuilder.Create(template); - builder.AddPathSegment( + builder.AddPathSegmentFromText( "cool-{p1}", RoutePatternPart.CreateLiteralFromText("cool-", "cool-"), RoutePatternPart.CreateParameterFromText("{p1}", "p1")); @@ -134,7 +134,7 @@ namespace Microsoft.AspNetCore.Dispatcher.Patterns var template = "{p1}-cool"; var builder = RoutePatternBuilder.Create(template); - builder.AddPathSegment( + builder.AddPathSegmentFromText( "{p1}-cool", RoutePatternPart.CreateParameterFromText("{p1}", "p1"), RoutePatternPart.CreateLiteralFromText("-cool", "-cool")); @@ -155,7 +155,7 @@ namespace Microsoft.AspNetCore.Dispatcher.Patterns var template = "{p1}-cool-{p2}"; var builder = RoutePatternBuilder.Create(template); - builder.AddPathSegment( + builder.AddPathSegmentFromText( "{p1}-cool", RoutePatternPart.CreateParameterFromText("{p1}", "p1"), RoutePatternPart.CreateLiteralFromText("-cool-", "-cool-"), @@ -177,7 +177,7 @@ namespace Microsoft.AspNetCore.Dispatcher.Patterns var template = "cool-{p1}-awesome"; var builder = RoutePatternBuilder.Create(template); - builder.AddPathSegment( + builder.AddPathSegmentFromText( template, RoutePatternPart.CreateLiteralFromText("cool-", "cool-"), RoutePatternPart.CreateParameterFromText("{p1}", "p1"), @@ -199,7 +199,7 @@ namespace Microsoft.AspNetCore.Dispatcher.Patterns var template = "{p1}.{p2?}"; var builder = RoutePatternBuilder.Create(template); - builder.AddPathSegment( + builder.AddPathSegmentFromText( "{p1}.{p2?}", RoutePatternPart.CreateParameterFromText("{p1}", "p1"), RoutePatternPart.CreateSeparatorFromText(".", "."), @@ -221,7 +221,7 @@ namespace Microsoft.AspNetCore.Dispatcher.Patterns var template = "{p1}.{p2}"; var builder = RoutePatternBuilder.Create(template); - builder.AddPathSegment( + builder.AddPathSegmentFromText( "{p1}.{p2}", RoutePatternPart.CreateParameterFromText("{p1}", "p1"), RoutePatternPart.CreateLiteralFromText(".", "."), @@ -243,7 +243,7 @@ namespace Microsoft.AspNetCore.Dispatcher.Patterns var template = "{p1}.{p2}.{p3?}"; var builder = RoutePatternBuilder.Create(template); - builder.AddPathSegment( + builder.AddPathSegmentFromText( "{p1}.{p2}.{p3?}", RoutePatternPart.CreateParameterFromText("{p1}", "p1"), RoutePatternPart.CreateLiteralFromText(".", "."), @@ -267,7 +267,7 @@ namespace Microsoft.AspNetCore.Dispatcher.Patterns var template = "{p1}.{p2}.{p3}"; var builder = RoutePatternBuilder.Create(template); - builder.AddPathSegment( + builder.AddPathSegmentFromText( "{p1}.{p2}.{p3}", RoutePatternPart.CreateParameterFromText("{p1}", "p1"), RoutePatternPart.CreateLiteralFromText(".", "."), @@ -291,12 +291,12 @@ namespace Microsoft.AspNetCore.Dispatcher.Patterns var template = "{p1}.{p2?}/{p3}"; var builder = RoutePatternBuilder.Create(template); - builder.AddPathSegment( + builder.AddPathSegmentFromText( "{p1}.{p2?}", RoutePatternPart.CreateParameterFromText("{p1}", "p1"), RoutePatternPart.CreateSeparatorFromText(".", "."), RoutePatternPart.CreateParameterFromText("{p2?}", "p2", null, RoutePatternParameterKind.Optional)); - builder.AddPathSegment("{p3}", RoutePatternPart.CreateParameterFromText("{p3}", "p3")); + builder.AddPathSegmentFromText("{p3}", RoutePatternPart.CreateParameterFromText("{p3}", "p3")); var expected = builder.Build(); @@ -314,8 +314,8 @@ namespace Microsoft.AspNetCore.Dispatcher.Patterns var template = "{p1}/{p2}.{p3?}"; var builder = RoutePatternBuilder.Create(template); - builder.AddPathSegment("{p1}", RoutePatternPart.CreateParameterFromText("{p1}", "p1")); - builder.AddPathSegment("{p2}.{p3?}", + builder.AddPathSegmentFromText("{p1}", RoutePatternPart.CreateParameterFromText("{p1}", "p1")); + builder.AddPathSegmentFromText("{p2}.{p3?}", RoutePatternPart.CreateParameterFromText("{p2}", "p2"), RoutePatternPart.CreateSeparatorFromText(".", "."), RoutePatternPart.CreateParameterFromText("{p3?}", "p3", null, RoutePatternParameterKind.Optional)); @@ -336,8 +336,8 @@ namespace Microsoft.AspNetCore.Dispatcher.Patterns var template = "{p2}/.{p3?}"; var builder = RoutePatternBuilder.Create(template); - builder.AddPathSegment("{p2}", RoutePatternPart.CreateParameterFromText("{p2}", "p2")); - builder.AddPathSegment(".{p3?}", + builder.AddPathSegmentFromText("{p2}", RoutePatternPart.CreateParameterFromText("{p2}", "p2")); + builder.AddPathSegmentFromText(".{p3?}", RoutePatternPart.CreateSeparatorFromText(".", "."), RoutePatternPart.CreateParameterFromText("{p3?}", "p3", null, RoutePatternParameterKind.Optional)); @@ -360,7 +360,7 @@ namespace Microsoft.AspNetCore.Dispatcher.Patterns { // Arrange var builder = RoutePatternBuilder.Create(template); - builder.AddPathSegment( + builder.AddPathSegmentFromText( template, RoutePatternPart.CreateParameterFromText( template, diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Template/RouteTemplateTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Template/RouteTemplateTest.cs new file mode 100644 index 0000000000..d78a0bc419 --- /dev/null +++ b/test/Microsoft.AspNetCore.Routing.Tests/Template/RouteTemplateTest.cs @@ -0,0 +1,217 @@ +// 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 Microsoft.AspNetCore.Dispatcher.Patterns; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Template +{ + public class RouteTemplateTest + { + [Fact] + public void ToRoutePattern_ConvertsToRoutePattern_MultipleSegments() + { + // Arrange + var routeTemplate = TemplateParser.Parse("api/{foo}"); + + // Act + var routePattern = routeTemplate.ToRoutePattern(); + + // Assert + Assert.Same(routeTemplate.TemplateText, routePattern.RawText); + Assert.Collection( + routePattern.PathSegments, + s => + { + Assert.Collection( + s.Parts, + p => + { + var literal = Assert.IsType(p); + Assert.Equal("api", literal.Content); + }); + }, + s => + { + Assert.Collection( + s.Parts, + p => + { + var parameter = Assert.IsType(p); + Assert.Equal("foo", parameter.Name); + Assert.Equal(RoutePatternParameterKind.Standard, parameter.ParameterKind); + Assert.Null(parameter.DefaultValue); + Assert.Empty(parameter.Constraints); + }); + }); + } + + [Fact] + public void ToRoutePattern_ConvertsToRoutePattern_ComplexSegment() + { + // Arrange + var routeTemplate = TemplateParser.Parse("api-{foo}"); + + // Act + var routePattern = routeTemplate.ToRoutePattern(); + + // Assert + Assert.Same(routeTemplate.TemplateText, routePattern.RawText); + Assert.Collection( + routePattern.PathSegments, + s => + { + Assert.Collection( + s.Parts, + p => + { + var literal = Assert.IsType(p); + Assert.Equal("api-", literal.Content); + }, + p => + { + var parameter = Assert.IsType(p); + Assert.Equal("foo", parameter.Name); + Assert.Equal(RoutePatternParameterKind.Standard, parameter.ParameterKind); + Assert.Null(parameter.DefaultValue); + Assert.Empty(parameter.Constraints); + }); + }); + } + + [Fact] + public void ToRoutePattern_ConvertsToRoutePattern_CatchAllParameter() + { + // Arrange + var routeTemplate = TemplateParser.Parse("{*foo}"); + + // Act + var routePattern = routeTemplate.ToRoutePattern(); + + // Assert + Assert.Same(routeTemplate.TemplateText, routePattern.RawText); + Assert.Collection( + routePattern.PathSegments, + s => + { + Assert.Collection( + s.Parts, + p => + { + var parameter = Assert.IsType(p); + Assert.Equal("foo", parameter.Name); + Assert.Equal(RoutePatternParameterKind.CatchAll, parameter.ParameterKind); + Assert.Null(parameter.DefaultValue); + Assert.Empty(parameter.Constraints); + }); + }); + } + + [Fact] + public void ToRoutePattern_ConvertsToRoutePattern_OptionalParameter() + { + // Arrange + var routeTemplate = TemplateParser.Parse("{foo?}"); + + // Act + var routePattern = routeTemplate.ToRoutePattern(); + + // Assert + Assert.Same(routeTemplate.TemplateText, routePattern.RawText); + Assert.Collection( + routePattern.PathSegments, + s => + { + Assert.Collection( + s.Parts, + p => + { + var parameter = Assert.IsType(p); + Assert.Equal("foo", parameter.Name); + Assert.Equal(RoutePatternParameterKind.Optional, parameter.ParameterKind); + Assert.Null(parameter.DefaultValue); + Assert.Empty(parameter.Constraints); + }); + }); + } + + [Fact] + public void ToRoutePattern_ConvertsToRoutePattern_Constraints() + { + // Arrange + var routeTemplate = TemplateParser.Parse("{foo:bar:baz}"); + + // Act + var routePattern = routeTemplate.ToRoutePattern(); + + // Assert + Assert.Same(routeTemplate.TemplateText, routePattern.RawText); + Assert.Collection( + routePattern.PathSegments, + s => + { + Assert.Collection( + s.Parts, + p => + { + var parameter = Assert.IsType(p); + Assert.Equal("foo", parameter.Name); + Assert.Equal(RoutePatternParameterKind.Standard, parameter.ParameterKind); + Assert.Null(parameter.DefaultValue); + Assert.Collection( + parameter.Constraints, + c => + { + Assert.Equal("bar", c.Content); + }, + c => + { + Assert.Equal("baz", c.Content); + }); + }); + }); + } + + [Fact] + public void ToRoutePattern_ConvertsToRoutePattern_OptionalSeparator() + { + // Arrange + var routeTemplate = TemplateParser.Parse("{bar}.{foo?}"); + + // Act + var routePattern = routeTemplate.ToRoutePattern(); + + // Assert + Assert.Same(routeTemplate.TemplateText, routePattern.RawText); + Assert.Collection( + routePattern.PathSegments, + s => + { + Assert.Collection( + s.Parts, + p => + { + var parameter = Assert.IsType(p); + Assert.Equal("bar", parameter.Name); + Assert.Equal(RoutePatternParameterKind.Standard, parameter.ParameterKind); + Assert.Null(parameter.DefaultValue); + Assert.Empty(parameter.Constraints); + }, + p => + { + var separator = Assert.IsType(p); + Assert.Equal(".", separator.Content); + }, + p => + { + var parameter = Assert.IsType(p); + Assert.Equal("foo", parameter.Name); + Assert.Equal(RoutePatternParameterKind.Optional, parameter.ParameterKind); + Assert.Null(parameter.DefaultValue); + Assert.Empty(parameter.Constraints); + }); + }); + } + } +}