|
| 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 | + |
0 commit comments