feat(oas3): make OpenAPI Links interactive via a "Follow Link" action - #11026
Open
manwar wants to merge 1 commit into
Open
feat(oas3): make OpenAPI Links interactive via a "Follow Link" action#11026manwar wants to merge 1 commit into
manwar wants to merge 1 commit into
Conversation
Closes swagger-api#7533 OpenAPI `links` on a response were rendered as inert text only (link name, description, target operationId, raw parameters JSON) with no way to actually act on them. This adds a "Follow Link" button that expands the linked operation, scrolls to it, and resolves + pre-fills its parameters using the response that was just received. New * `specSelectors.operationById(operationId)` Finds an operation's flattened entry (path, method, operation, id, specPath) by its declared operationId. Deliberately a plain `(state, operationId)` function, not a curried `createSelector`. The plugin system's selector binding (`getBoundSelectors` in core/system.js) invokes a bound selector as `fn(state, ...args)`; if the result of that call is itself a function, the system treats it as a getSystem-consuming selector and invokes it with the whole system object instead of the caller's arguments. A curried `createSelector(operations, (operations) => (operationId) => {...})` here returns exactly such a function as its result, so `specSelectors.operationById(operationId)` would silently receive the system object in place of `operationId`. Matching the plain function pattern already used by `responseFor`/`requestFor`/ `findDefinition` elsewhere in this file avoids the class of bug entirely. * `oas3Actions.executeLink({ operationId, parameters, responseContext })` The thunk `OperationLink`'s "Follow Link" button dispatches. - Resolves the target operation via `operationById`. - Expands it under `["operations", tag, operationId]`, reading the tag directly off the operation object and falling back to Swagger UI's own "default" tag grouping when none is declared. (Not `["operations", path, method]`, that key is never read by anything and silently no-ops.) - Resolves each parameter's runtime expression against the response body. Supports `$response.body#/{json-pointer}`, including multi-segment pointers with RFC 6901 `~0`/`~1` unescaping. Other runtime expression types (`$request.*`, `$url`, `$method`, `$statusCode`, `$response.header.*`) and constant values pass through unresolved. - Infers `path` vs `query` parameter location from the target's path template (`{paramName}` presence), since parameters declared once at the shared path-item level rather than per-operation aren't currently merged into `operations`. - Coerces resolved values to strings before dispatch; parameter state is always string-driven from text inputs, regardless of the source value's JSON type. - Calls `specActions.changeParam` using its real positional signature, `(pathMethod, paramName, paramIn, value, isXml)`, not an options object, and with no OAS3-specific flag (plain parameter editing is shared between OAS2 and OAS3 specs here). - Scrolls to the target using the same DOM id `operation.jsx` itself renders on its root element (`escapeDeepLinkPath(["operations", tag, operationId].join("-"))`), not the deep-linking plugin's `readyToScroll`/`scrollTo` mechanism. That mechanism is a one-shot check that only fires once, at mount time, against whatever scroll target the URL hash already named when the operation first mounted, it does not fire again for an operation that's already mounted on an already-loaded page, which is always our case here. - Logs a `console.warn` (never throws or fails silently) when the link has no `operationId`, or when `operationId` doesn't resolve to a real operation. Changed * `OperationLink`: `responseContext` prop typed as `ImPropTypes.iterable`, matching its real (Immutable) runtime shape, consistent with how `link` is already typed in this file. * `Response`: forwards `oas3Actions` and `responseContext` down to `OperationLink` (previously neither was passed through at all, so the "Follow Link" button's own click guard silently no-op'd). * `Responses`: computes `responseContext`, scoped to only the response row whose status code matches what "Try it out" actually returned (reusing the `tryItOutResponse.get("status") == code` check this file already computes for its own `response_current` styling), so a link's `$response.body#/...` parameters resolve against the real result rather than an unrelated documented response row. Known limitations (intentionally out of scope for this change) * `operationRef` targets are not supported; only `operationId`-based links render a "Follow Link" button. * Path parameters declared once at the path-item level rather than per-operation aren't merged in by `operations`; the path-template check covers the common case, but a declared `in` for such a parameter won't be found via `declaredParams`. Tests * test/unit/core/plugins/spec/selectors.js: new `operationById` suite, including an explicit arity assertion (`operationById.length === 2`) that guards against a regression back to the curried shape described above. * test/unit/core/plugins/oas3/actions.js: new file. Covers the tag-based expand key and "default" fallback, the positional `changeParam` call, JSON value-to-string coercion, JSON Pointer resolution (including a multi-segment case), path- vs query-parameter inference, the real scroll-target DOM id via a live jsdom element, and both `console.warn` branches.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Make OpenAPI Links interactive via a "Follow Link" action
Closes #7533
Summary
linkson an OpenAPI response are currently rendered as inert text, a name, a description, the targetoperationId, and a raw JSON dump ofparameters-- with no way to actually act on them. This PR adds a "Follow Link" button that expands the linked operation, scrolls to it, and resolves + pre-fills its parameters using the response that was just received, matching what the OpenAPI Link Object was designed to enable.What changed
src/core/plugins/spec/selectors.jsoperationById(state, operationId)selectorsrc/core/plugins/oas3/actions.jsexecuteLink(payload)thunk actionsrc/core/plugins/oas3/components/operation-link.jsxresponseContextprop typed asImPropTypes.iterablesrc/core/components/response.jsxoas3Actions/responseContexttoOperationLinksrc/core/components/responses.jsxresponseContext, scoped to the matching response rowtest/unit/core/plugins/spec/selectors.jsoperationByIdsuitetest/unit/core/plugins/oas3/actions.jsexecuteLinksuiteDesign notes
A few of these decisions aren't obvious from the diff alone, and each one corresponds to a real bug that only surfaced under live testing in a browser, documenting them here so a review doesn't have to rediscover the same ground.
operationByIdis a plain function, not a curried selector. The natural-looking implementation iscreateSelector(operations, (operations) => (operationId) => {...}). That's wrong in this codebase specifically: the plugin system's selector binding (getBoundSelectorsincore/system.js) calls a bound selector asfn(state, ...args), and if the result of that call is itself a function, the system assumes it's a getSystem-consuming selector and invokes that returned function with the whole system object, not the caller's original arguments. A curried selector's inner function is exactly such a "result that is a function," sospecSelectors.operationById(operationId)would silently receive the system object in place ofoperationId, with no error.operationByIdis written as a plain(state, operationId)function instead, matching the existingresponseFor/requestFor/findDefinitionpattern already in this file.The expand/scroll target key is
["operations", tag, operationId], not[path, method]. The tag-based key is whatOperationContainer's ownisShownKey/toggleShownandoperation.jsx's rendered elementidboth actually use. A[path, method]-based key looks equally plausible but writes to a piece of layout state nothing else reads, so the operation panel never opens and nothing indicates why.Scrolling uses the real rendered DOM id, not the deep-linking plugin's
scrollTo/readyToScroll. That mechanism is a one-shot check: each operation's wrappingrefcallback fires once, at mount, and checks whether the current scroll target already matches its own key -- designed for the "page loads with a URL hash already set" case. It does not re-fire for an operation that's already mounted on an already-loaded page, which is always the case for a click-driven navigation like this one. Instead,executeLinklooks up the operation's real DOM node directly viaescapeDeepLinkPath(["operations", tag, operationId].join("-")), the same idoperation.jsxsets on its own root element -- and callsscrollIntoViewon it after asetTimeout(0)to let the just-dispatchedshowaction's re-render complete first.changeParamis called positionally, not with an options object. Its real signature ischangeParam(pathMethod, paramName, paramIn, value, isXml), and there is noisOas3flag on it at all, plain parameter editing is shared between OAS2 and OAS3 specs in this codebase. Calling it with an options object instead threw deep inside the parameter-update reducer (TypeError: i is not iterable), since the reducer destructures its first argument as the[path, method]tuple it expects.Resolved parameter values are coerced to strings. Parameter state is always driven from text inputs, so it's string-typed regardless of what a resolved runtime expression naturally is (a
$response.body#/idpointer into{"id": 2}resolves to the JS number2). Numbers, booleans, and objects are coerced (String(...)/JSON.stringify(...)respectively) before being handed tochangeParam.What's intentionally out of scope
operationReftargets. OnlyoperationId-based links render a "Follow Link" button;operationReflinks still render as text-only, same as before this change.$response.body#/....$request.*,$url,$method,$statusCode, and$response.header.*are valid per spec but not resolved here; they pass through as their literal, unresolved expression string.operationsselector this is built on. The path-template check ({paramName}presence) still correctly identifies it as a path parameter in that case; only the fallback lookup into an operation's own declared parameter list would miss it.Any of these would be reasonable, separable follow-ups if there's interest.
Testing
test/unit/core/plugins/spec/selectors.js: newoperationByIdsuite runs against the realoperations->paths->specJsonWithResolvedSubtreesselector chain (no mocking of the selector's own dependencies), plus an explicit assertion on the exported function's arity (operationById.length === 2) as a standing regression guard against the curried-selector bug described above. Includes a case against the repo's own Petstore fixture.test/unit/core/plugins/oas3/actions.js: new file. Covers the tag-based expand key and its"default"fallback, the positionalchangeParamcall, value coercion, JSON Pointer resolution (including a multi-segment case), path- vs. query-parameter inference, the real scroll-target DOM id (via a livejsdomelement), and bothconsole.warnfailure branches.operationId-based links with both empty and non-emptyparametersmaps, including a link targeting a path parameter with a$response.body#/idruntime expression: clicking "Follow Link" correctly expands and scrolls tothe target operation and pre-fills its parameter field with the real value from the response.
Checklist
npm run test:unit)