Skip to content

Commit 8473ae8

Browse files
committed
Draft of templating engine
1 parent a9cbd52 commit 8473ae8

20 files changed

+1899
-93
lines changed

.cursor/dotnet.mdc

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
---
2+
description:
3+
globs:
4+
alwaysApply: true
5+
---
6+
7+
# .NET Development Rules
8+
9+
You are a senior .NET backend developer and an expert in C#, ASP.NET Core, and Entity Framework Core.
10+
11+
## Code Style and Structure
12+
- Write concise, idiomatic C# code with accurate examples.
13+
- Follow .NET and ASP.NET Core conventions and best practices.
14+
- Use object-oriented and functional programming patterns as appropriate.
15+
- Prefer LINQ and lambda expressions for collection operations.
16+
- Use descriptive variable and method names (e.g., 'IsUserSignedIn', 'CalculateTotal').
17+
- Structure files according to .NET conventions (Controllers, Models, Services, etc.).
18+
19+
## Naming Conventions
20+
- Use PascalCase for class names, method names, and public members.
21+
- Use camelCase for local variables and private fields.
22+
- Use UPPERCASE for constants.
23+
- Prefix interface names with "I" (e.g., 'IUserService').
24+
25+
## C# and .NET Usage
26+
- Use C# 10+ features when appropriate (e.g., record types, pattern matching, null-coalescing assignment).
27+
- Leverage built-in ASP.NET Core features and middleware.
28+
- Use Entity Framework Core effectively for database operations.
29+
30+
## Syntax and Formatting
31+
- Follow the C# Coding Conventions (https://docs.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions)
32+
- Use C#'s expressive syntax (e.g., null-conditional operators, string interpolation)
33+
- Use 'var' for implicit typing when the type is obvious.
34+
35+
## Error Handling and Validation
36+
- Use exceptions for exceptional cases, not for control flow.
37+
- Implement proper error logging using built-in .NET logging or a third-party logger.
38+
- Use Data Annotations or Fluent Validation for model validation.
39+
- Implement global exception handling middleware.
40+
- Return appropriate HTTP status codes and consistent error responses.
41+
42+
## API Design
43+
- Follow RESTful API design principles.
44+
- Use attribute routing in controllers.
45+
- Implement versioning for your API.
46+
- Use action filters for cross-cutting concerns.
47+
48+
## Performance Optimization
49+
- Use asynchronous programming with async/await for I/O-bound operations.
50+
- Implement caching strategies using IMemoryCache or distributed caching.
51+
- Use efficient LINQ queries and avoid N+1 query problems.
52+
- Implement pagination for large data sets.
53+
54+
## Key Conventions
55+
- Use Dependency Injection for loose coupling and testability.
56+
- Implement repository pattern or use Entity Framework Core directly, depending on the complexity.
57+
- Use AutoMapper for object-to-object mapping if needed.
58+
- Implement background tasks using IHostedService or BackgroundService.
59+
60+
## Testing
61+
- Write unit tests using xUnit, NUnit, or MSTest.
62+
- Use Moq or NSubstitute for mocking dependencies.
63+
- Implement integration tests for API endpoints.
64+
65+
## Security
66+
- Use Authentication and Authorization middleware.
67+
- Implement JWT authentication for stateless API authentication.
68+
- Use HTTPS and enforce SSL.
69+
- Implement proper CORS policies.
70+
71+
## API Documentation
72+
- Use Swagger/OpenAPI for API documentation (as per installed Swashbuckle.AspNetCore package).
73+
- Provide XML comments for controllers and models to enhance Swagger documentation.
74+
75+
Follow the official Microsoft documentation and ASP.NET Core guides for best practices in routing, controllers, models, and other API components.
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
---
2+
description:
3+
globs: *.cs
4+
alwaysApply: false
5+
---
6+
7+
Okay, here is a consolidated document outlining the design principles, rules, implementation steps, and testing strategy for the URL templating system, ensuring alignment with the existing matching system.
8+
9+
## I. Matching System Design Principles (Reference)
10+
11+
To ensure consistency, the templating system should adhere to the core design principles of the existing matching system:
12+
13+
1. **Performance Focus:** The primary goal is extremely fast execution *after* parsing. Matching is O(n) in the length of the path and non-backtracking. Parsed templates should allow for similarly efficient evaluation.
14+
2. **Explicit Syntax:** The syntax uses clear delimiters (`{`, `}`), separators (`:`, `?`), and keywords (`int`, `guid`, `starts`, `ends`, `map`, `default`, etc.) for conditions and variables. Templating should follow this explicit style.
15+
3. **Segment-Based Processing:** Matching breaks the input path into segments based on boundary conditions (`equals`, `prefix`, `suffix`, `len`, `/`, `.`). While templating builds strings rather than segmenting input, the concept of distinct literal and variable parts remains relevant.
16+
4. **Separation of Concerns:**
17+
* **Boundary Matching vs. Validation:** Matching distinguishes between conditions that define segment boundaries (affecting capture) and those that validate content *after* segmentation. Templating doesn't segment input, but transformations operate on captured values.
18+
* **Path vs. Query:** Matching can handle paths and query strings distinctly (unless `[raw]` is used). Templating should also distinctly handle path, query key, and query value construction.
19+
5. **Flags for Behavior Modification:** Matching uses flags (`[ignore-case]`, `[raw]`) to modify overall behavior. Templating might adopt flags if global behaviors (like default encoding) are needed, but transformations handle most variations currently.
20+
6. **Clear Error Handling:** While not explicitly detailed in `matching.md`, robust systems require good error messages during parsing and matching failures. Templating must prioritize this.
21+
7. **Extensibility:** The use of conditions and interfaces suggests an extensible design. The templating system should also be designed with potential future transformations in mind.
22+
23+
## II. Templating System Rules
24+
25+
These rules define the syntax, behavior, and validation for the URL templating system.
26+
27+
### A. Syntax Rules
28+
29+
1. **Delimiters:** Variable substitutions and transformations are enclosed in curly braces: `{variableName:transform1:...}`.
30+
2. **Variable Names:** Must match the variable names captured by the corresponding match expression. Follow C# identifier rules potentially (letters, numbers, underscore, starting with letter or underscore).
31+
3. **Transformations:** Separated by colons (`:`). Applied sequentially from left to right.
32+
4. **Escaping:** Backslash (`\`) is used to escape special characters within template literals and transformation arguments:
33+
* `\{` -> Literal `{`
34+
* `\}` -> Literal `}`
35+
* `\\` -> Literal `\`
36+
* `\:` -> Literal `:` (mainly if needed within variable names, unlikely)
37+
* `\,` -> Literal `,` (within `map`/`default`/`or` arguments)
38+
* `\(` -> Literal `(` (within `map`/`default`/`or` arguments)
39+
* `\)` -> Literal `)` (within `map`/`default`/`or` arguments)
40+
5. **Transformation Arguments:** Arguments for transformations like `map`, `default`, `or` are enclosed in parentheses `()`. Commas `,` separate arguments within the parentheses. Arguments themselves use backslash escaping.
41+
42+
### B. Transformation Definitions
43+
44+
Applied sequentially to the variable's current value.
45+
46+
1. `lower`: Converts the string to lowercase (invariant culture).
47+
2. `upper`: Converts the string to uppercase (invariant culture).
48+
3. `encode`: Applies URL component encoding (RFC 3986, UTF-8). Encodes characters unsafe for path segments or query values (e.g., space -> `%20`, `/` -> `%2F`).
49+
4. `map(from1,to1)`:
50+
* Maps input string `from1` to output string `to1`.
51+
* Compares the *current* value being transformed against `from1` (case-sensitive, ordinal comparison).
52+
* If a match occurs, the value is replaced with `to1`, and *no subsequent `map` transforms* in the chain are evaluated for this variable.
53+
* `from` and `to` values within the parentheses use backslash escaping.
54+
5. `or(fallbackVariable)`:
55+
* If the current value is null or empty, attempts to retrieve the value associated with `fallbackVariable` from the input variable dictionary.
56+
* If `fallbackVariable` exists and has a non-empty value, that value becomes the new current value.
57+
* If `fallbackVariable` does not exist or is null/empty, the current value remains null/empty.
58+
* `fallbackVariable` name must adhere to variable name rules.
59+
6. `default(defaultValue)`:
60+
* If the current value is null or empty, it is replaced by `defaultValue`.
61+
* `defaultValue` within the parentheses uses backslash escaping.
62+
7. `optional` (or `?`):
63+
* A marker transformation; it does not change the value.
64+
* Signals that if the *final* resulting value for this specific `{...}` segment (after all other transformations) is null or empty, it may trigger special handling (like query pair omission).
65+
66+
### C. Query String Generation Rules
67+
68+
1. **Pair Omission:** When constructing a query string (e.g., `keyTemplate=valueTemplate`), the *entire* pair (`&key=value` or `?key=value`) is **omitted** if the final evaluated result of *any* `{variable:optional}` or `{variable:?}` segment within *either* the `keyTemplate` or the `valueTemplate` is null or empty.
69+
2. **Empty Values (Not Optional):** If a variable evaluation results in a null/empty string but was *not* marked `:optional` or `:?` (and not replaced by `:default`), the key-value pair is included with the empty value (e.g., `?key=`, `&key=`, `?emptykey=value`).
70+
3. **Structure:** The template defines the literal structure (e.g., `?`, `&`, `=`). The evaluation inserts values and omits pairs according to the rules.
71+
72+
### D. Parser Validation Rules (Optional Mode)
73+
74+
When the parser is provided with metadata about the associated matcher's variables (names, optionality):
75+
76+
1. **Undefined Variable:** Reject if a template uses `{var}` and `var` is not present in the matcher's variable list.
77+
* *Error Example:* `"Template error at position X: Uses variable '{var}' which is not defined by the match expression."*
78+
2. **Unhandled Optional Variable:** Reject if a template uses `{optVar}` where `optVar` is optional in the matcher, *unless* the transformation chain includes `:or(...)`, `:default(...)`, or `:optional` (or `:?`).
79+
* *Error Example:* `"Template error at position X: Optional variable '{optVar}' is used without providing a fallback (:or, :default) or marking as ignorable (:optional, :?)."`
80+
3. **Undefined `or` Fallback:** Reject if `or(fallbackVar)` is used and `fallbackVar` is not present in the matcher's variable list.
81+
* *Error Example:* `"Template error at position X: ':or' transform uses fallback variable '{fallbackVar}' which is not defined by the match expression."*
82+
83+
## III. Implementation Steps
84+
85+
### A. Data Structures (in `Imazen.Routing.Matching.Templating` namespace)
86+
87+
1. **`MultiTemplate` Record:**
88+
* `StringTemplate? PathTemplate { get; }`
89+
* `IReadOnlyList<(StringTemplate KeyTemplate, StringTemplate ValueTemplate)>? QueryTemplates { get; }` (Using a list preserves order from template)
90+
* `MultiTemplateOptions? Options { get; }` (For future use, e.g., global flags)
91+
* `// Potentially store MatcherVariableInfo used for validation`
92+
* Static `TryParse` and `Parse` methods.
93+
* `Evaluate(IDictionary<string, string> variables)` method.
94+
2. **`StringTemplate` Record:**
95+
* `IReadOnlyList<ITemplateSegment> Segments { get; }`
96+
* Static `TryParse` and `Parse` methods.
97+
* Internal `Evaluate(IDictionary<string, string> variables, out bool containsOptionalEmptyResult)` method.
98+
3. **`ITemplateSegment` Interface:**
99+
* `record LiteralSegment(string Value) : ITemplateSegment;`
100+
* `record VariableSegment(string VariableName, IReadOnlyList<ITransformation> Transformations) : ITemplateSegment;`
101+
4. **`ITransformation` Interface:**
102+
* `string? Apply(string? currentValue, IDictionary<string, string> variables);`
103+
* Implementations:
104+
* `ToLowerTransform : ITransformation`
105+
* `ToUpperTransform : ITransformation`
106+
* `UrlEncodeTransform : ITransformation`
107+
* `MapTransform(IReadOnlyList<(string From, string To)> Mappings) : ITransformation`
108+
* `OrTransform(string FallbackVariableName) : ITransformation`
109+
* `DefaultTransform(string DefaultValue) : ITransformation`
110+
* `OptionalMarkerTransform : ITransformation` (Apply returns `currentValue` unchanged, used for flagging)
111+
112+
### B. Parsing Logic
113+
114+
1. **`MultiTemplate.TryParse`:**
115+
* Separate path and query string parts (split on `?`).
116+
* Parse the path using `StringTemplate.TryParse`.
117+
* Parse the query string:
118+
* Split into pairs by `&`.
119+
* For each pair, split by `=` (handle cases with no `=`).
120+
* Parse the key part using `StringTemplate.TryParse`.
121+
* Parse the value part using `StringTemplate.TryParse`.
122+
* Store as `(KeyTemplate, ValueTemplate)` tuples.
123+
* Handle flags `[...]` at the end if needed later.
124+
* If `matcherVariableInfo` is provided, perform validation checks during/after parsing segments.
125+
2. **`StringTemplate.TryParse`:**
126+
* Iterate through the input string.
127+
* Identify literal segments vs. variable segments (`{...}`).
128+
* Handle backslash escapes (`\\`, `\{`, `\}` etc.) in literals.
129+
* For `{...}` segments:
130+
* Parse `variableName`.
131+
* Parse transformation chain (`:transform1:transform2...`).
132+
* For each transform:
133+
* Identify transform name (e.g., `map`, `lower`, `?`).
134+
* If arguments `(...)` are present, parse them, handling escaping (`\,`, `\(`, `\)` etc.) and commas.
135+
* Create corresponding `ITransformation` objects.
136+
* Validate variable name and transforms against known syntax.
137+
* Create `VariableSegment`.
138+
* Perform validation rules if in validation mode.
139+
* Return parsed `Segments` list or error message.
140+
3. **Error Handling:** Generate specific error messages with context (e.g., position, problematic text).
141+
142+
### C. Evaluation Logic
143+
144+
1. **`MultiTemplate.Evaluate`:**
145+
* Input: `IDictionary<string, string> variables`.
146+
* Evaluate `PathTemplate` using `StringTemplate.Evaluate`, get `pathResult`.
147+
* Initialize a `StringBuilder` for the query string.
148+
* Iterate through `QueryTemplates` list:
149+
* Evaluate `KeyTemplate` -> `keyResult`, `keyOptionalEmpty`.
150+
* Evaluate `ValueTemplate` -> `valueResult`, `valueOptionalEmpty`.
151+
* If `keyOptionalEmpty` or `valueOptionalEmpty` is `true`, **skip** this pair entirely.
152+
* Otherwise, append the correct separator (`?` or `&`) and the `keyResult=valueResult` to the query `StringBuilder`. Handle cases where `valueResult` might be empty.
153+
* Combine `pathResult` and the query `StringBuilder` result.
154+
* Return the final URL string.
155+
2. **`StringTemplate.Evaluate`:**
156+
* Input: `IDictionary<string, string> variables`. Output: `string? result`, `out bool containsOptionalEmptyResult`.
157+
* Initialize `StringBuilder` for the result.
158+
* Initialize `containsOptionalEmptyResult = false`.
159+
* Iterate through `Segments`:
160+
* If `LiteralSegment`, append `Value`.
161+
* If `VariableSegment`:
162+
* Get `variableName`. Check if it exists in `variables`. If not, and it's required (based on validation info or runtime check), throw error.
163+
* Initialize `currentValue` from `variables`.
164+
* Initialize `isOptional = false`.
165+
* For each `ITransformation` in `Transformations`:
166+
* If `OptionalMarkerTransform`, set `isOptional = true`.
167+
* Call `transformation.Apply(currentValue, variables)` to get the new `currentValue`.
168+
* If `isOptional` is `true` and `currentValue` is null or empty, set `containsOptionalEmptyResult = true`.
169+
* Append `currentValue` to the result `StringBuilder`.
170+
* Return `StringBuilder.ToString()` and `containsOptionalEmptyResult`.
171+
3. **`ITransformation.Apply` Implementations:** Implement the logic defined in section II.B for each transformation type, handling null/empty inputs appropriately.
172+
173+
## IV. Testing Strategy (`TemplatingExpressionTests.cs`)
174+
175+
Create comprehensive unit tests covering:
176+
177+
1. **Parsing (`StringTemplate.TryParse`, `MultiTemplate.TryParse`):**
178+
* Valid syntax: path only, query only, path + query, various transformations.
179+
* Correct parsing of literals and variables.
180+
* Correct parsing of transformation chains and arguments.
181+
* Correct handling of all defined backslash escape sequences.
182+
* Invalid syntax detection and specific error messages (malformed braces, unknown transforms, bad arguments).
183+
2. **Validation Mode Parsing:**
184+
* Test with `matcherVariableInfo` provided.
185+
* Correct detection of undefined variables.
186+
* Correct detection of unhandled optional variables.
187+
* Correct detection of undefined `:or` fallback variables.
188+
* Cases where validation should pass.
189+
3. **Transformation Logic (`ITransformation.Apply`):**
190+
* Unit test each transformation individually with various inputs (null, empty, different cases, values to map/not map).
191+
* Test `map` with escaped characters in `from`/`to` values.
192+
* Test `or` and `default` behavior with null/empty inputs and missing fallback variables.
193+
4. **Transformation Chaining (`StringTemplate.Evaluate`):**
194+
* Test combinations: `lower:map`, `map:default`, `or:upper`, `map:map:default`, `encode:lower`.
195+
* Ensure left-to-right application order is respected.
196+
* Verify `map` short-circuiting (only first match applies).
197+
5. **Optionality and Query Pair Omission (`MultiTemplate.Evaluate`):**
198+
* Variable marked `:optional` results in null/empty -> pair omitted.
199+
* Variable *not* marked `:optional` results in null/empty -> pair included (`key=`).
200+
* Optional marker in key template -> pair omitted.
201+
* Optional marker in value template -> pair omitted.
202+
* Optional marker in both -> pair omitted.
203+
* No optional markers -> pair included even if values are empty.
204+
* Interaction with `:default` (if default provides value, pair is included even if original was optional and missing).
205+
6. **Full Evaluation (`MultiTemplate.Evaluate`):**
206+
* End-to-end tests with various templates and input variable dictionaries.
207+
* Paths, simple queries, complex queries.
208+
* Mix of required and optional variables.
209+
7. **Edge Cases:**
210+
* Empty template string.
211+
* Empty input variable dictionary.
212+
* Variables containing special characters (test interaction with `encode` and `map`).
213+
* Templates containing only literals or only variables.
214+
8. **Runtime Errors:**
215+
* Test evaluation fails if a required variable is missing from the input dictionary (when not handled by `:or` or `:default`).
216+

global.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"sdk": {
3-
"version": "8.0.0",
3+
"version": "10.0.0",
44
"rollForward": "latestMajor",
55
"allowPrerelease": true
66
}

justfile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
set shell := ["pwsh", "-c"]
2+
3+
# Run tests for the ImazenShared.Tests project using dotnet run
4+
test-shared *ARGS:
5+
dotnet run --project tests/ImazenShared.Tests/ImazenShared.Tests.csproj -- {{ARGS}}

src/Imazen.Routing/Matching/ExpressionParsingOptions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ namespace Imazen.Routing.Matching;
22

33
public record ExpressionParsingOptions
44
{
5+
6+
public static ExpressionParsingOptions Default { get; } = new ExpressionParsingOptions();
57
/// <summary>
68
/// Does not affect character classes.
79
/// </summary>

0 commit comments

Comments
 (0)