fix(core): scope requiredness to expressions, and encode + / # per RFC 6570
Addresses Copilot's round-3 review on #2035. It again reported "no new comments" while carrying two suppressed ones; both reproduced. 1. Requiredness could not live on a variable at all. Deduplication kept the first occurrence's group, so in `x{?a}{?b}{a,b}` both names ended up required with singleton groups and the form demanded both -- while the SDK expands that template with only `a` to "x?a=11". Widening the stored group would not have been enough either: in `{a,b}{a,c}`, filling `b` and `c` satisfies both expressions, which no per-variable flag can express. So requiredness is now returned per expression by `requiredGroups`, and `hasRequiredValues` asks that each group be satisfied by any one of its names. `TemplateVariable.groupNames` is gone rather than left as a field that quietly means something narrower than it reads; `required` remains, documented as driving the "Optional" marker and nothing else. 2. `encodeURI` is not the allow-reserved encoder `+` and `#` call for. It escapes `[` and `]`, which are reserved and must survive, and it escapes `%`, so an already-encoded value is double-encoded. Measured: "[::1]" -> "%5B::1%5D" and "%2F" -> "%252F". Both corrupt the URI rather than merely over-escaping it -- an IPv6 literal or a pre-encoded path reaches the server altered, which is the same class of defect #1919 is about. Added an RFC 6570 3.2.1 encoder that preserves reserved characters and existing pct-triplets, splitting on `%XX` so a lone `%` is still encoded to `%25`, and matching with the `u` flag so an astral character is encoded whole. `+` and `#` expressions are now taken over even for a single name, so both expansion paths agree on what those operators mean. Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
25c3ad272e
commit
e7d1246c86
1 changed files with 204 additions and 60 deletions
|
|
@ -18,6 +18,7 @@ import {
|
|||
expandUriTemplate,
|
||||
hasRequiredValues,
|
||||
previewUriTemplate,
|
||||
requiredGroups,
|
||||
templateVariables,
|
||||
} from "../../../utils/uriTemplate";
|
||||
|
||||
|
|
@ -96,6 +97,10 @@ export function ResourceTemplatePanel({
|
|||
() => declaredVariables.map((v) => v.name),
|
||||
[declaredVariables],
|
||||
);
|
||||
// The names of each expression that cannot be omitted. Kept separate from
|
||||
// `declaredVariables` (which is deduplicated for rendering) because each
|
||||
// required expression has to be satisfied on its own — see `requiredGroups`.
|
||||
const groups = useMemo(() => requiredGroups(uriTemplate), [uriTemplate]);
|
||||
|
||||
const [variables, setVariables] = useState<Record<string, string>>(() =>
|
||||
Object.fromEntries(variableNames.map((n) => [n, ""])),
|
||||
|
|
@ -221,7 +226,7 @@ export function ResourceTemplatePanel({
|
|||
// resource, and RFC 6570 drops the whole expression for it. The rule is
|
||||
// per-expression rather than per-variable -- `{a,b}` with only `a` filled
|
||||
// expands to `a`'s value -- so it lives in core beside the expander.
|
||||
const canSubmit = hasRequiredValues(declaredVariables, variables);
|
||||
const canSubmit = hasRequiredValues(groups, variables);
|
||||
|
||||
function handleSubmit() {
|
||||
onReadResource(expandUriTemplate(uriTemplate, variables));
|
||||
|
|
@ -240,7 +245,7 @@ export function ResourceTemplatePanel({
|
|||
</HeaderRow>
|
||||
{description && <DescriptionText>{description}</DescriptionText>}
|
||||
<Stack gap="sm">
|
||||
{declaredVariables.map(({ name: varName, required, groupNames }) => {
|
||||
{declaredVariables.map(({ name: varName, required }) => {
|
||||
/* v8 ignore next -- `?? ""` fallback unreachable: `variables` is seeded with every declared variable, so the key is always present. */
|
||||
const fieldValue = variables[varName] ?? "";
|
||||
// RFC 6570 omits an undefined variable under a query/path-segment
|
||||
|
|
@ -248,10 +253,13 @@ export function ResourceTemplatePanel({
|
|||
// required multi-name expression no single field is mandatory either
|
||||
// -- any one of them satisfies it -- so say which, rather than
|
||||
// marking each one required and blocking valid input.
|
||||
const sharedGroup = groups.find(
|
||||
(names) => names.length > 1 && names.includes(varName),
|
||||
);
|
||||
const description = !required
|
||||
? "Optional"
|
||||
: groupNames.length > 1
|
||||
? `Any one of: ${groupNames.join(", ")}`
|
||||
: sharedGroup
|
||||
? `Any one of: ${sharedGroup.join(", ")}`
|
||||
: undefined;
|
||||
return useAutocomplete ? (
|
||||
<Autocomplete
|
||||
|
|
|
|||
Loading…
Reference in a new issue