public void SubstituteRequiredValues_AllowRequiredValueAnyForParameter() { // Arrange var template = "{controller=Home}/{action=Index}/{id?}"; var defaults = new { }; var policies = new { }; var original = RoutePatternFactory.Parse(template, defaults, policies); var requiredValues = new { controller = RoutePattern.RequiredValueAny, }; // Act var actual = Transformer.SubstituteRequiredValues(original, requiredValues); // Assert Assert.Collection( actual.Defaults.OrderBy(kvp => kvp.Key), kvp => Assert.Equal(new KeyValuePair <string, object>("action", "Index"), kvp), kvp => Assert.Equal(new KeyValuePair <string, object>("controller", "Home"), kvp)); // default is preserved Assert.Collection( actual.RequiredValues.OrderBy(kvp => kvp.Key), kvp => Assert.Equal(new KeyValuePair <string, object>("controller", RoutePattern.RequiredValueAny), kvp)); }
private static bool IsSegmentValid(Context context, List <RoutePatternPart> parts) { // If a segment has multiple parts, then it can't contain a catch all. for (var i = 0; i < parts.Count; i++) { var part = parts[i]; if (part is RoutePatternParameterPart parameter && parameter.IsCatchAll && parts.Count > 1) { context.Error = Resources.TemplateRoute_CannotHaveCatchAllInMultiSegment; return(false); } } // if a segment has multiple parts, then only the last one parameter can be optional // if it is following a optional seperator. for (var i = 0; i < parts.Count; i++) { var part = parts[i]; if (part is RoutePatternParameterPart parameter && parameter.IsOptional && parts.Count > 1) { // This optional parameter is the last part in the segment if (i == parts.Count - 1) { var previousPart = parts[i - 1]; if (!previousPart.IsLiteral && !previousPart.IsSeparator) { // The optional parameter is preceded by something that is not a literal or separator // Example of error message: // "In the segment '{RouteValue}{param?}', the optional parameter 'param' is preceded // by an invalid segment '{RouteValue}'. Only a period (.) can precede an optional parameter. context.Error = string.Format( Resources.TemplateRoute_OptionalParameterCanbBePrecededByPeriod, RoutePatternPathSegment.DebuggerToString(parts), parameter.Name, parts[i - 1].DebuggerToString()); return(false); } else if (previousPart is RoutePatternLiteralPart literal && literal.Content != PeriodString) { // The optional parameter is preceded by a literal other than period. // Example of error message: // "In the segment '{RouteValue}-{param?}', the optional parameter 'param' is preceded // by an invalid segment '-'. Only a period (.) can precede an optional parameter. context.Error = string.Format( Resources.TemplateRoute_OptionalParameterCanbBePrecededByPeriod, RoutePatternPathSegment.DebuggerToString(parts), parameter.Name, parts[i - 1].DebuggerToString()); return(false); } parts[i - 1] = RoutePatternFactory.SeparatorPart(((RoutePatternLiteralPart)previousPart).Content); } else { // This optional parameter is not the last one in the segment // Example: // An optional parameter must be at the end of the segment. In the segment '{RouteValue?})', // optional parameter 'RouteValue' is followed by ')' context.Error = string.Format( Resources.TemplateRoute_OptionalParameterHasTobeTheLast, RoutePatternPathSegment.DebuggerToString(parts), parameter.Name, parts[i + 1].DebuggerToString()); return(false); } } } // A segment cannot contain two consecutive parameters var isLastSegmentParameter = false; for (var i = 0; i < parts.Count; i++) { var part = parts[i]; if (part.IsParameter && isLastSegmentParameter) { context.Error = Resources.TemplateRoute_CannotHaveConsecutiveParameters; return(false); } isLastSegmentParameter = part.IsParameter; } return(true); }
private static ParameterPolicyParseResults ParseConstraints( string text, int currentIndex, int endIndex) { var constraints = new ArrayBuilder <RoutePatternParameterPolicyReference>(0); var state = ParseState.Start; var startIndex = currentIndex; do { var currentChar = currentIndex > endIndex ? null : (char?)text[currentIndex]; switch (state) { case ParseState.Start: switch (currentChar) { case null: state = ParseState.End; break; case ':': state = ParseState.ParsingName; startIndex = currentIndex + 1; break; case '(': state = ParseState.InsideParenthesis; break; case '=': state = ParseState.End; currentIndex--; break; } break; case ParseState.InsideParenthesis: switch (currentChar) { case null: state = ParseState.End; var constraintText = text.Substring(startIndex, currentIndex - startIndex); constraints.Add(RoutePatternFactory.ParameterPolicy(constraintText)); break; case ')': // Only consume a ')' token if // (a) it is the last token // (b) the next character is the start of the new constraint ':' // (c) the next character is the start of the default value. var nextChar = currentIndex + 1 > endIndex ? null : (char?)text[currentIndex + 1]; switch (nextChar) { case null: state = ParseState.End; constraintText = text.Substring(startIndex, currentIndex - startIndex + 1); constraints.Add(RoutePatternFactory.ParameterPolicy(constraintText)); break; case ':': state = ParseState.Start; constraintText = text.Substring(startIndex, currentIndex - startIndex + 1); constraints.Add(RoutePatternFactory.ParameterPolicy(constraintText)); startIndex = currentIndex + 1; break; case '=': state = ParseState.End; constraintText = text.Substring(startIndex, currentIndex - startIndex + 1); constraints.Add(RoutePatternFactory.ParameterPolicy(constraintText)); break; } break; case ':': case '=': // In the original implementation, the Regex would've backtracked if it encountered an // unbalanced opening bracket followed by (not necessarily immediatiely) a delimiter. // Simply verifying that the parantheses will eventually be closed should suffice to // determine if the terminator needs to be consumed as part of the current constraint // specification. var indexOfClosingParantheses = text.IndexOf(')', currentIndex + 1); if (indexOfClosingParantheses == -1) { constraintText = text.Substring(startIndex, currentIndex - startIndex); constraints.Add(RoutePatternFactory.ParameterPolicy(constraintText)); if (currentChar == ':') { state = ParseState.ParsingName; startIndex = currentIndex + 1; } else { state = ParseState.End; currentIndex--; } } else { currentIndex = indexOfClosingParantheses; } break; } break; case ParseState.ParsingName: switch (currentChar) { case null: state = ParseState.End; var constraintText = text.Substring(startIndex, currentIndex - startIndex); if (constraintText.Length > 0) { constraints.Add(RoutePatternFactory.ParameterPolicy(constraintText)); } break; case ':': constraintText = text.Substring(startIndex, currentIndex - startIndex); if (constraintText.Length > 0) { constraints.Add(RoutePatternFactory.ParameterPolicy(constraintText)); } startIndex = currentIndex + 1; break; case '(': state = ParseState.InsideParenthesis; break; case '=': state = ParseState.End; constraintText = text.Substring(startIndex, currentIndex - startIndex); if (constraintText.Length > 0) { constraints.Add(RoutePatternFactory.ParameterPolicy(constraintText)); } currentIndex--; break; } break; } currentIndex++; } while (state != ParseState.End); return(new ParameterPolicyParseResults(currentIndex, constraints.ToArray())); }
private static bool ParseLiteral(Context context, List <RoutePatternPart> parts) { context.Mark(); while (true) { if (context.Current == Separator) { // End of the segment break; } else if (context.Current == OpenBrace) { if (!context.MoveNext()) { // This is a dangling open-brace, which is not allowed context.Error = Resources.TemplateRoute_MismatchedParameter; return(false); } if (context.Current == OpenBrace) { // This is an 'escaped' brace in a literal, like "{{foo" - keep going. } else { // We've just seen the start of a parameter, so back up. context.Back(); break; } } else if (context.Current == CloseBrace) { if (!context.MoveNext()) { // This is a dangling close-brace, which is not allowed context.Error = Resources.TemplateRoute_MismatchedParameter; return(false); } if (context.Current == CloseBrace) { // This is an 'escaped' brace in a literal, like "{{foo" - keep going. } else { // This is an unbalanced close-brace, which is not allowed context.Error = Resources.TemplateRoute_MismatchedParameter; return(false); } } if (!context.MoveNext()) { break; } } var encoded = context.Capture(); var decoded = encoded.Replace("}}", "}").Replace("{{", "{"); if (IsValidLiteral(context, decoded)) { parts.Add(RoutePatternFactory.LiteralPart(decoded)); return(true); } else { return(false); } }