Skip to content

Commit 634a9bc

Browse files
committed
Matching/templating: Improve flag parsing, multitemplate parsing, naming (allow->chars, lower/upper aliases), or_var -> or-var, map_default -> map-default, schema parsing, and better error messages. Yet to complete template expressions that map/allow/validate, proper two way flag sharing, and making the equals/allow/only stuff symmetrical.
1 parent 7f0ec45 commit 634a9bc

23 files changed

+711
-328
lines changed

simplify.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Analyze the existing matching tests, and add a test for failures so we know how
1919
* **Argument Parsing:**
2020
* Matching: Complex type detection (`GetArgType`), `CharacterClass` parsing, diverse argument types (`[...]`, `a|b|c`).
2121
* Templating: Simpler string arguments, specific escaping (`\(\)\,`), comma delimiter default, pipe delimiter (`|`) for `equals`.
22-
* **Optionality:** Different syntax and slightly different semantics (`:?` in matching vs. `:?`/`:optional`/`:default`/`:or_var` in templating).
22+
* **Optionality:** Different syntax and slightly different semantics (`:?` in matching vs. `:?`/`:optional`/`:default`/`:or-var` in templating).
2323
* **Query Keys:** Literals only in matching; can contain variables in templating.
2424
* **Segment Boundaries:** Core concept in matching (`SegmentBoundary` influencing capture start/end); irrelevant in templating.
2525

@@ -50,15 +50,15 @@ Analyze the existing matching tests, and add a test for failures so we know how
5050

5151
* **Proposal:** Standardize on `:?` (and `:optional` as an alias) in both syntaxes where the concept applies.
5252
* **Matching:** Maintain existing `:?` behavior.
53-
* **Templating:** Consolidate `:?` and `:optional` to one form. Note that `:default` and `:or_var` still imply optional *handling* (suppressing output on null/empty) which is distinct from the marker itself.
53+
* **Templating:** Consolidate `:?` and `:optional` to one form. Note that `:default` and `:or-var` still imply optional *handling* (suppressing output on null/empty) which is distinct from the marker itself.
5454
* **Impact:** Minor syntax cleanup and improved consistency.
5555

5656
### D. Naming Alignment (Conditions vs. Transforms)
5757

5858
* **Proposal:** Align names where functionality clearly overlaps, potentially using aliases.
5959
* Examine `allow`/`only` (matching) vs. `equals` (templating). Direct replacement is hard due to `CharacterClass` vs. string array arguments. Consider adding an `equals(a|b|c)` condition to matching for parity?
60-
* Keep distinct names for clearly different concepts (`or_var`, `map`, `default`).
61-
* Ensure `map_default` (formerly `other`) is consistently named.
60+
* Keep distinct names for clearly different concepts (`or-var`, `map`, `default`).
61+
* Ensure `map-default` (formerly `other`) is consistently named.
6262
* **Impact:** Improved clarity, primarily in documentation and code readability. Requires careful analysis of any subtle behavioral differences.
6363

6464
### E. Enhanced Error Reporting

src/Imageflow.Server/Internal/MiddlewareOptionsServerBuilder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ IEnumerable<IBlobProvider> blobProviders
212212
h => WrapUrlEventArgs(h.PathPrefix, h.Handler, true)).ToList()));
213213
}
214214

215-
//TODO: Add a layer that can be used to set the cache key basis
215+
//TODO: Add a layer that can be used to set the cache key basis ? for licensing ? -> mutating the query should already do that
216216
builder.AddMediaLayer(new LicensingLayer(licenseChecker));
217217

218218

src/Imazen.Routing/Matching/ExpressionFlags.cs

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,35 @@
11
using System.Collections.ObjectModel;
22
using System.Diagnostics.CodeAnalysis;
33
using System.Text.RegularExpressions;
4+
using Microsoft.Extensions.Options;
45

56
namespace Imazen.Routing.Matching;
7+
public readonly record struct ExpressionFlagParsingOptions(Regex ValidationRegex, bool TrimTrailingExpressionWhitespace,
8+
bool AllowWhitespaceAfterBrackets,
9+
bool TrimWhitespaceAroundFlags){
10+
public static readonly ExpressionFlagParsingOptions Permissive = new(ExpressionFlags.LatestPermissiveFlagSyntax(), true, true, true);
611

12+
13+
public static readonly ExpressionFlagParsingOptions LowercaseDash = new(ExpressionFlags.LowercaseDash(), true, true, true);
14+
15+
// WithValidationRegex
16+
public ExpressionFlagParsingOptions WithValidationRegex(Regex validationRegex)
17+
{
18+
return new ExpressionFlagParsingOptions(validationRegex, TrimTrailingExpressionWhitespace, AllowWhitespaceAfterBrackets, TrimWhitespaceAroundFlags);
19+
}
20+
}
721
public partial record ExpressionFlags(ReadOnlyCollection<string> Flags)
822
{
23+
24+
private static ReadOnlyMemory<char> TrimEnd(ReadOnlyMemory<char> text)
25+
{
26+
#if NET6_0_OR_GREATER
27+
return text.TrimEnd();
28+
#else
29+
var span = text.Span.TrimEnd();
30+
return text.Slice(0, span.Length);
31+
#endif
32+
}
933
/// <summary>
1034
/// Parses the flags from the end of the expression. Syntax is [flag1,flag2,flag3]
1135
/// </summary>
@@ -14,13 +38,22 @@ public partial record ExpressionFlags(ReadOnlyCollection<string> Flags)
1438
/// <param name="result"></param>
1539
/// <param name="error"></param>
1640
/// <returns></returns>
17-
public static bool TryParseFromEnd(ReadOnlyMemory<char> expression, out ReadOnlyMemory<char> remainingExpression, out List<string> result,
41+
public static bool TryParseFromEnd(ReadOnlyMemory<char> expression, out ReadOnlyMemory<char> remainingExpression, out List<string> result,
1842
[NotNullWhen(false)]
19-
out string? error, Regex validationRegex)
43+
out string? error, ExpressionFlagParsingOptions options)
2044
{
2145
var flags = new List<string>();
46+
if (options.TrimTrailingExpressionWhitespace)
47+
{
48+
expression = TrimEnd(expression);
49+
}
50+
2251
var span = expression.Span;
23-
52+
if (options.AllowWhitespaceAfterBrackets)
53+
{
54+
span = span.TrimEnd();
55+
}
56+
2457
if (span.Length == 0 || span[^1] != ']')
2558
{
2659
//It's ok for there to be none
@@ -37,33 +70,33 @@ public static bool TryParseFromEnd(ReadOnlyMemory<char> expression, out ReadOnly
3770
remainingExpression = expression;
3871
return false;
3972
}
40-
remainingExpression = expression[..startAt];
73+
remainingExpression = options.TrimTrailingExpressionWhitespace ? TrimEnd(expression[..startAt]) : expression[..startAt];
4174
var inner = expression[(startAt + 1)..^1];
4275
var innerSpan = inner.Span;
4376
while (innerSpan.Length > 0)
4477
{
4578
var commaIndex = innerSpan.IndexOf(',');
4679
if (commaIndex == -1)
4780
{
48-
flags.Add(inner.Span.Trim().ToString());
81+
flags.Add(options.TrimWhitespaceAroundFlags ? inner.Span.Trim().ToString() : inner.Span.ToString());
4982
break;
5083
}
51-
flags.Add(inner.Span[..commaIndex].Trim().ToString());
84+
flags.Add(options.TrimWhitespaceAroundFlags ? inner.Span[..commaIndex].Trim().ToString() : inner.Span[..commaIndex].ToString());
5285
inner = inner[(commaIndex + 1)..];
5386
innerSpan = inner.Span;
5487
}
55-
// validate
88+
// validate flags
5689
foreach (var flag in flags)
5790
{
58-
if (!validationRegex.IsMatch(flag))
91+
if (!options.ValidationRegex.IsMatch(flag))
5992
{
6093
result = flags;
61-
error = $"Invalid flag '{flag}', it does not match the required format {validationRegex}.";
94+
error = $"Invalid flag '{flag}', it does not match the required format {options.ValidationRegex}.";
6295
return false;
6396
}
6497
}
6598
// Handle [flag][flag]etc, recursively
66-
if (!TryParseFromEnd(remainingExpression, out var remainingExpression2, out var flags2, out error, validationRegex))
99+
if (!TryParseFromEnd(remainingExpression, out var remainingExpression2, out var flags2, out error, options))
67100
{
68101
remainingExpression = remainingExpression2;
69102
flags.AddRange(flags2);
@@ -103,4 +136,17 @@ public static bool TryParseFromEnd(ReadOnlyMemory<char> expression, out ReadOnly
103136

104137
public static Regex LowercaseDash() => LowercaseDashVar;
105138
#endif
139+
140+
// Allow k/v pairs as well, [a-zA-Z-][a-zA-Z0-9-]*([=][a-zA-Z0-9-]+)?
141+
#if NET8_0_OR_GREATER
142+
[GeneratedRegex(@"^[a-zA-Z-_][a-zA-Z0-9-]*([=][a-zA-Z0-9-_]+)?$")]
143+
public static partial Regex LatestPermissiveFlagSyntax();
144+
#else
145+
146+
public static readonly Regex LatestPermissiveFlagSyntaxVar =
147+
new(@"^[a-zA-Z-_][a-zA-Z0-9-]*([=][a-zA-Z0-9-_]+)?$");
148+
149+
public static Regex LatestPermissiveFlagSyntax() => LatestPermissiveFlagSyntaxVar;
150+
#endif
151+
106152
}

src/Imazen.Routing/Matching/ExpressionParsingHelpers.cs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,38 @@ internal static int FindCharNotEscaped(ReadOnlySpan<char> str,char c, char escap
8282
}
8383
return -1;
8484
}
85+
internal static int FindCharNotEscapedAtDepth0(ReadOnlySpan<char> str,char c, char escapeChar, char openChar, char closeChar)
86+
{
87+
var consecutiveEscapeChars = 0;
88+
var depth = 0;
89+
for (var i = 0; i < str.Length; i++)
90+
{
91+
if (str[i] == escapeChar)
92+
{
93+
consecutiveEscapeChars++;
94+
continue;
95+
}
96+
97+
if (consecutiveEscapeChars % 2 == 0)
98+
{
99+
if (str[i] == c && depth == 0)
100+
{
101+
return i;
102+
}
103+
if (str[i] == openChar)
104+
{
105+
depth++;
106+
}
107+
if (str[i] == closeChar)
108+
{
109+
depth--;
110+
}
111+
}
112+
consecutiveEscapeChars = 0;
113+
114+
}
115+
return -1;
116+
}
85117

86118

87119
#if NET8_0_OR_GREATER
@@ -131,6 +163,27 @@ internal static bool ValidateSegmentName(string name, ReadOnlySpan<char> segment
131163
return true;
132164
}
133165

166+
internal static bool ValidateVariableName(string name, [NotNullWhen(false)]out string? error)
167+
{
168+
if (name.Length == 0)
169+
{
170+
error = "Variable names cannot be empty";
171+
return false;
172+
}
173+
if (!ValidSegmentName().IsMatch(name))
174+
{
175+
error = $"Invalid variable name '{name}'. Names must start with a letter or underscore, and contain only letters, numbers, or underscores";
176+
return false;
177+
}
178+
if (StringCondition.IsReservedName(name))
179+
{
180+
error = $"Invalid variable name '{name}' - that is a reserved word.";
181+
return false;
182+
}
183+
error = null;
184+
return true;
185+
}
186+
134187
internal static bool TryReadConditionName(ReadOnlySpan<char> text,
135188
[NotNullWhen(true)] out int? conditionNameEnds,
136189
[NotNullWhen(false)] out string? error)

src/Imazen.Routing/Matching/ExpressionParsingOptions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ internal static ExpressionParsingOptions SubtractFromFlags(List<string> flags, E
4040
public static ExpressionParsingOptions ParseComplete(ReadOnlyMemory<char> expressionWithFlags, out ReadOnlyMemory<char> remainingExpression)
4141
{
4242
if (!ExpressionFlags.TryParseFromEnd(expressionWithFlags, out var expression, out var flags, out var error,
43-
ExpressionFlags.LowercaseDash()))
43+
ExpressionFlagParsingOptions.Permissive.WithValidationRegex(ExpressionFlags.LowercaseDash())))
4444
{
4545
throw new ArgumentException(error, nameof(expressionWithFlags));
4646
}

src/Imazen.Routing/Matching/MatchSegment.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ internal static bool TryParseSegmentExpression(ExpressionParsingOptions options,
6666
var innerMem = exprMemory[1..^1];
6767
if (innerMem.Length == 0)
6868
{
69-
error = "Segment {} cannot be empty. Try {*}, {name}, {name:condition1:condition2}";
69+
error = "Segment {} cannot be empty. Try {name}, {name:condition1:condition2}";
7070
segment = null;
7171
return false;
7272
}

src/Imazen.Routing/Matching/MultiValueMatcher.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ public static bool TryParse(ReadOnlyMemory<char> expressionWithFlags,
7373
[NotNullWhen(true)] out MultiValueMatcher? result, [NotNullWhen(false)] out string? error)
7474
{
7575
if (!ExpressionFlags.TryParseFromEnd(expressionWithFlags, out var expression, out var flags, out error,
76-
ExpressionFlags.LowercaseDash()))
76+
ExpressionFlagParsingOptions.LowercaseDash))
7777
{
7878
result = null;
7979
return false;
@@ -285,4 +285,4 @@ internal MultiMatchResult Match(in MatchingContext context, in ReadOnlyMemory<ch
285285
return new MultiMatchResult { Success = true, Captures = captures, ExcessQueryKeys = excessKeyNames, OriginalQuery = query };
286286
}
287287

288-
}
288+
}

src/Imazen.Routing/Matching/RoutingExpressionParser.cs

Lines changed: 0 additions & 136 deletions
This file was deleted.

0 commit comments

Comments
 (0)