diff --git a/src/Microsoft.AspNet.Routing/Constraints/CompoundRouteConstraint.cs b/src/Microsoft.AspNet.Routing/Constraints/CompoundRouteConstraint.cs new file mode 100644 index 0000000000..c03a2854b3 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Constraints/CompoundRouteConstraint.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.AspNet.Http; + +namespace Microsoft.AspNet.Routing +{ + /// + /// Constrains a route by several child constraints. + /// + public class CompoundRouteConstraint : IRouteConstraint + { + /// + /// Initializes a new instance of the class. + /// + /// The child constraints that must match for this constraint to match. + public CompoundRouteConstraint([NotNull] IEnumerable constraints) + { + Constraints = constraints; + } + + /// + /// Gets the child constraints that must match for this constraint to match. + /// + public IEnumerable Constraints { get; private set; } + + /// + public bool Match([NotNull] HttpContext httpContext, + [NotNull] IRouter route, + [NotNull] string routeKey, + [NotNull] IDictionary values, + RouteDirection routeDirection) + { + foreach (var constraint in Constraints) + { + if (!constraint.Match(httpContext, route, routeKey, values, routeDirection)) + { + return false; + } + } + + return true; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Routing/InlineRouteTemplateParser.cs b/src/Microsoft.AspNet.Routing/InlineRouteTemplateParser.cs new file mode 100644 index 0000000000..6220202542 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/InlineRouteTemplateParser.cs @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.Contracts; +using System.Text.RegularExpressions; +using Microsoft.AspNet.Routing.Template; + +namespace Microsoft.AspNet.Routing +{ + internal static class InlineRouteParameterParser + { + // One or more characters, matches "id" + private const string ParameterNameRegex = @"(?.+?)"; + + // Zero or more inline constraints that start with a colon followed by zero or more characters + // Optionally the constraint can have arguments within parentheses + // - necessary to capture characters like ":" and "}" + // Matches ":int", ":length(2)", ":regex(\})", ":regex(:)" zero or more times + private const string ConstraintRegex = @"(:(?.*?(\(.*?\))?))*"; + + // A default value with an equal sign followed by zero or more characters + // Matches "=", "=abc" + private const string DefaultValueRegex = @"(?(=.*?))?"; + + private static readonly Regex _parameterRegex = new Regex( + "^" + ParameterNameRegex + ConstraintRegex + DefaultValueRegex + "$", + RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); + public static TemplatePart ParseRouteParameter([NotNull] string routeParameter, + [NotNull] IInlineConstraintResolver constraintResolver) + { + var isCatchAll = routeParameter.StartsWith("*", StringComparison.Ordinal); + var isOptional = routeParameter.EndsWith("?", StringComparison.Ordinal); + + routeParameter = isCatchAll ? routeParameter.Substring(1) : routeParameter; + routeParameter = isOptional ? routeParameter.Substring(0, routeParameter.Length - 1) : routeParameter; + + var parameterMatch = _parameterRegex.Match(routeParameter); + string parameterName = parameterMatch.Groups["parameterName"].Value; + + // Add the default value if present + var defaultValueGroup = parameterMatch.Groups["defaultValue"]; + var defaultValue = GetDefaultValue(defaultValueGroup); + + // Register inline constraints if present + var constraintGroup = parameterMatch.Groups["constraint"]; + var inlineConstraint = GetInlineConstraint(constraintGroup, constraintResolver); + + return TemplatePart.CreateParameter(parameterName, + isCatchAll, + isOptional, + defaultValue, + inlineConstraint); + } + + private static object GetDefaultValue(Group defaultValueGroup) + { + if (defaultValueGroup.Success) + { + string defaultValueMatch = defaultValueGroup.Value; + + // Strip out the equal sign at the beginning + Contract.Assert(defaultValueMatch.StartsWith("=", StringComparison.Ordinal)); + return defaultValueMatch.Substring(1); + } + + return null; + } + + private static IRouteConstraint GetInlineConstraint(Group constraintGroup, + IInlineConstraintResolver constraintResolver) + { + var parameterConstraints = new List(); + foreach (Capture constraintCapture in constraintGroup.Captures) + { + string inlineConstraint = constraintCapture.Value; + var constraint = constraintResolver.ResolveConstraint(inlineConstraint); + if (constraint == null) + { + throw new InvalidOperationException( + Resources.FormatInlineRouteParser_CouldNotResolveConstraint(constraintResolver.GetType().Name, + inlineConstraint)); + } + + parameterConstraints.Add(constraint); + } + + if (parameterConstraints.Count > 0) + { + var constraint = parameterConstraints.Count == 1 ? + parameterConstraints[0] : + new CompoundRouteConstraint(parameterConstraints); + return constraint; + } + + return null; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs b/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs index be6dc62012..ab5f36252e 100644 --- a/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs +++ b/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs @@ -173,7 +173,7 @@ namespace Microsoft.AspNet.Routing.Template } var rawParameter = context.Capture(); - + // At this point, we need to parse the raw name for inline constraint, // default values and optional parameters. var templatePart = InlineRouteParameterParser.ParseRouteParameter(rawParameter, @@ -196,6 +196,10 @@ namespace Microsoft.AspNet.Routing.Template context.Error = Resources.TemplateRoute_OptionalCannotHaveDefaultValue; return false; } + // A workaround is to add it as a separate entry in the defaults argument. + context.Error = Resources.TemplateRoute_OptionalCannotHaveDefaultValue; + return false; + } var parameterName = templatePart.Name; if (IsValidParameterName(context, parameterName))