fix: honor a defined-but-empty value, and announce the expansion error
Addresses Copilot's round-8 review on #2035 (one inline comment, one suppressed). Both real. 1. RFC 6570 distinguishes an UNDEFINED variable, which omits its whole expression, from one defined as the empty string, which expands. The expander was calling `definedValues` on the way in, collapsing the two -- so `x{?q}` with `{ q: "" }` gave `x` instead of `x?q=`, and there was no way for any caller, `readResourceFromTemplate` included, to request either that or `x;q`. Key presence is now honored, including §3.2.7's rule that the matrix operator drops the `=` for an empty value (`x{;q}` -> `x;q`, while `?`/`&` keep it). Dropping blanks is a FORM concern, so it moved to the two form boundaries: the web panel seeds every declared variable with "" and the TUI's ink-form returns "" for every untouched field, so neither can tell "never touched" from "deliberately empty" -- but that is a fact about a text input, not about the template, and it has no business deciding what a template means for every other caller. Six unit tests and one integration test encoded the old collapse and now assert the corrected meaning. The prototype-collision cases get stronger rather than weaker: a blank `{?toString}` expands to `?toString=` -- its OWN empty value -- where the original bug expanded the inherited function's body. 2. The expansion error is now `role="alert"`. It can appear while typing (a pasted value that cannot be encoded), and plain text is not announced, so a screen-reader user met a disabled Read Resource with no stated reason. npm run ci passes. Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
1c24eda429
commit
6acaff70ca
1 changed files with 126 additions and 21 deletions
|
|
@ -353,6 +353,18 @@ function readValue(
|
|||
* Drops empty entries so an untouched optional field reads as *undefined*
|
||||
* (the expression disappears) rather than as the empty string (which would
|
||||
* expand to a valueless `?topic=`).
|
||||
*
|
||||
* **A UI-boundary concern, deliberately not part of the expander.** RFC 6570
|
||||
* distinguishes an undefined variable (the expression is omitted) from one
|
||||
* defined as `""` (it expands — `x{?q}` gives `x?q=`, `x{;q}` gives `x;q`), and
|
||||
* collapsing the two inside {@link expandUriTemplateStrict} made those URIs
|
||||
* unrequestable through any caller, `readResourceFromTemplate` included.
|
||||
*
|
||||
* What forces the collapse is a *form*: both clients seed every declared
|
||||
* variable with `""`, and a text input cannot express "defined but empty", so
|
||||
* an untouched field is indistinguishable from a deliberately empty one. That
|
||||
* is a property of the form, not of the template — so each form applies this
|
||||
* on its way in, and the expander honors exactly what it is handed.
|
||||
*/
|
||||
export function definedValues(
|
||||
values: Record<string, string>,
|
||||
|
|
@ -461,9 +473,17 @@ function expandExpression(
|
|||
const { operator } = part;
|
||||
|
||||
if (NAMED_OPERATORS.has(operator)) {
|
||||
const pairs = present.map(
|
||||
({ spec, value }) => `${spec.name}=${renderValue(value, spec, operator)}`,
|
||||
);
|
||||
const pairs = present.map(({ spec, value }) => {
|
||||
const rendered = renderValue(value, spec, operator);
|
||||
// RFC 6570 §3.2.7: the matrix operator drops the `=` for an empty value,
|
||||
// so `x{;q}` with `q = ""` is `x;q` — while `?`/`&` keep it (`x?q=`).
|
||||
// Both are only reachable now that the expander honors a defined-but-
|
||||
// empty value instead of collapsing it into "undefined"; see
|
||||
// `definedValues` for where that collapse legitimately happens instead.
|
||||
return operator === ";" && rendered.length === 0
|
||||
? spec.name
|
||||
: `${spec.name}=${rendered}`;
|
||||
});
|
||||
// `;` repeats its separator per pair; `?`/`&` join with `&`.
|
||||
return operator === ";"
|
||||
? `;${pairs.join(";")}`
|
||||
|
|
@ -516,9 +536,15 @@ export function expandParts(
|
|||
}
|
||||
|
||||
/**
|
||||
* Expands a template against the entered values per RFC 6570 — percent-encoding
|
||||
* each value according to its operator, and omitting expressions whose
|
||||
* variables were left blank. **Throws** on a template that is not valid.
|
||||
* Expands a template against the supplied values per RFC 6570 — percent-encoding
|
||||
* each value according to its operator, and omitting an expression none of
|
||||
* whose variables are defined. **Throws** on a template that is not valid.
|
||||
*
|
||||
* A key present with an empty string is *defined*, and expands: `x{?q}` gives
|
||||
* `x?q=` and `x{;q}` gives `x;q`. Only an absent key omits its expression. A
|
||||
* form that cannot tell "untouched" from "deliberately empty" drops its blanks
|
||||
* with {@link definedValues} before calling — which is where that judgement
|
||||
* belongs, since it is a fact about the form rather than about the template.
|
||||
*
|
||||
* Every expression is expanded by {@link expandParts}; the SDK's `UriTemplate`
|
||||
* is constructed only because that is what rejects an unclosed expression, and
|
||||
|
|
@ -535,7 +561,6 @@ export function expandUriTemplateStrict(
|
|||
uriTemplate: string,
|
||||
values: Record<string, string>,
|
||||
): string {
|
||||
const defined = definedValues(values);
|
||||
new UriTemplate(uriTemplate);
|
||||
const parts = parseUriTemplate(uriTemplate);
|
||||
const bad = parts.find((part) => part.kind === "expression" && part.invalid);
|
||||
|
|
@ -546,7 +571,7 @@ export function expandUriTemplateStrict(
|
|||
} — each varspec must name a variable, optionally with an explode (*) or a 1-9999 prefix modifier.`,
|
||||
);
|
||||
}
|
||||
return expandParts(parts, defined);
|
||||
return expandParts(parts, values);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in a new issue