From 21848dd73dc4aba1ab205f4e8d643ed7e51688e9 Mon Sep 17 00:00:00 2001 From: Andrew Jiang Date: Fri, 15 Nov 2024 11:29:17 -0500 Subject: [PATCH] fix: request response snippet resolver should work for endpoint pairs (stream/batch) (#1818) --- .../api-reference/endpoints/EndpointUrl.tsx | 16 ++++++++++++ packages/ui/app/src/atoms/apis.ts | 9 +++++++ packages/ui/app/src/atoms/docs.ts | 1 + .../app/src/components/ApiReferenceButton.tsx | 2 +- .../snippets/EndpointRequestSnippet.tsx | 9 ++++--- .../snippets/EndpointResponseSnippet.tsx | 2 +- .../components/snippets/useFindEndpoint.tsx | 9 +++++-- packages/ui/app/src/resolver/DocsContent.ts | 5 ++++ .../app/src/resolver/resolveMarkdownPage.ts | 17 ++++++++++++- .../util/processRequestSnippetComponents.ts | 25 +++++++++++++++---- 10 files changed, 82 insertions(+), 13 deletions(-) diff --git a/packages/ui/app/src/api-reference/endpoints/EndpointUrl.tsx b/packages/ui/app/src/api-reference/endpoints/EndpointUrl.tsx index ed9460c2c1..0e78659d91 100644 --- a/packages/ui/app/src/api-reference/endpoints/EndpointUrl.tsx +++ b/packages/ui/app/src/api-reference/endpoints/EndpointUrl.tsx @@ -71,6 +71,19 @@ export const EndpointUrl = React.forwardRef { + const url = baseUrl ?? options?.find((option) => option.id === environmentId)?.baseUrl; + if (url == null) { + return undefined; + } + try { + return new URL(url, "http://n").pathname; + } catch (error) { + return undefined; + } + }, [options, environmentId, baseUrl]); + return (
@@ -112,6 +125,9 @@ export const EndpointUrl = React.forwardRef )} + {!showEnvironment && environmentBasepath && ( + {environmentBasepath} + )} {pathParts} diff --git a/packages/ui/app/src/atoms/apis.ts b/packages/ui/app/src/atoms/apis.ts index 758f7d6eb8..cfd3505695 100644 --- a/packages/ui/app/src/atoms/apis.ts +++ b/packages/ui/app/src/atoms/apis.ts @@ -6,6 +6,7 @@ import { atomFamily } from "jotai/utils"; import { useEffect } from "react"; import { useMemoOne } from "use-memo-one"; import { useIsLocalPreview } from "../contexts/local-preview"; +import { DOCS_ATOM } from "./docs"; import { FEATURE_FLAGS_ATOM } from "./flags"; import { RESOLVED_API_DEFINITION_ATOM, RESOLVED_PATH_ATOM } from "./navigation"; @@ -82,3 +83,11 @@ export function useIsApiReferenceShallowLink(node: FernNavigation.WithApiDefinit ), ); } + +export const ENDPOINT_ID_TO_SLUG_ATOM = atom>((get) => { + const { content } = get(DOCS_ATOM); + if (content.type === "markdown-page") { + return content.endpointIdsToSlugs; + } + return {}; +}); diff --git a/packages/ui/app/src/atoms/docs.ts b/packages/ui/app/src/atoms/docs.ts index 7af5c24e4d..63f8beafcd 100644 --- a/packages/ui/app/src/atoms/docs.ts +++ b/packages/ui/app/src/atoms/docs.ts @@ -61,6 +61,7 @@ export const EMPTY_DOCS_STATE: DocsProps = { neighbors: { prev: null, next: null }, hasAside: false, apis: {}, + endpointIdsToSlugs: {}, }, featureFlags: DEFAULT_FEATURE_FLAGS, apis: [], diff --git a/packages/ui/app/src/components/ApiReferenceButton.tsx b/packages/ui/app/src/components/ApiReferenceButton.tsx index 6296fd6ac1..be51ba793e 100644 --- a/packages/ui/app/src/components/ApiReferenceButton.tsx +++ b/packages/ui/app/src/components/ApiReferenceButton.tsx @@ -8,7 +8,7 @@ export const ApiReferenceButton: React.FC<{ slug: FernNavigation.Slug }> = ({ sl const href = useHref(slug); return ( - + { - const endpoint = useFindEndpoint(method, path); + const endpoint = useFindEndpoint(method, path, example); if (endpoint == null) { return null; @@ -44,6 +47,7 @@ export function EndpointRequestSnippetInternal({ endpoint: ApiDefinition.EndpointDefinition; example: string | undefined; }): ReactElement | null { + const slug = useAtomValue(ENDPOINT_ID_TO_SLUG_ATOM)[endpoint.id]; const { selectedExample, selectedExampleKey, availableLanguages, setSelectedExampleKey } = useExampleSelection( endpoint, example, @@ -76,8 +80,7 @@ export function EndpointRequestSnippetInternal({ value={selectedExampleKey.language} /> )} - {/* TODO: Restore this button */} - {/* */} + {slug != null && } } code={selectedExample.code} diff --git a/packages/ui/app/src/mdx/components/snippets/EndpointResponseSnippet.tsx b/packages/ui/app/src/mdx/components/snippets/EndpointResponseSnippet.tsx index 6e6d3e6a51..efced79146 100644 --- a/packages/ui/app/src/mdx/components/snippets/EndpointResponseSnippet.tsx +++ b/packages/ui/app/src/mdx/components/snippets/EndpointResponseSnippet.tsx @@ -27,7 +27,7 @@ function EndpointResponseSnippetInternal({ method: HttpMethod; example: string | undefined; }) { - const endpoint = useFindEndpoint(method, path); + const endpoint = useFindEndpoint(method, path, example); if (endpoint == null) { return null; diff --git a/packages/ui/app/src/mdx/components/snippets/useFindEndpoint.tsx b/packages/ui/app/src/mdx/components/snippets/useFindEndpoint.tsx index 14caa73334..69a18b5cc5 100644 --- a/packages/ui/app/src/mdx/components/snippets/useFindEndpoint.tsx +++ b/packages/ui/app/src/mdx/components/snippets/useFindEndpoint.tsx @@ -4,7 +4,11 @@ import { useMemoOne } from "use-memo-one"; import { READ_APIS_ATOM } from "../../../atoms"; import { findEndpoint } from "../../../util/processRequestSnippetComponents"; -export function useFindEndpoint(method: string, path: string): EndpointDefinition | undefined { +export function useFindEndpoint( + method: string, + path: string, + example: string | undefined, +): EndpointDefinition | undefined { return useAtomValue( useMemoOne( () => @@ -15,6 +19,7 @@ export function useFindEndpoint(method: string, path: string): EndpointDefinitio apiDefinition, path, method, + example, }); if (endpoint) { break; @@ -22,7 +27,7 @@ export function useFindEndpoint(method: string, path: string): EndpointDefinitio } return endpoint; }), - [method, path], + [example, method, path], ), ); } diff --git a/packages/ui/app/src/resolver/DocsContent.ts b/packages/ui/app/src/resolver/DocsContent.ts index 66e0f1773e..8eeabb3555 100644 --- a/packages/ui/app/src/resolver/DocsContent.ts +++ b/packages/ui/app/src/resolver/DocsContent.ts @@ -47,6 +47,11 @@ export declare namespace DocsContent { hasAside: boolean; // TODO: downselect apis to only the fields we need apis: Record; + /** + * This is a lookup table for the slugs of endpoints referenced in the markdown page. + * The Request / Response snippets will use this to link back to the endpoint reference page. + */ + endpointIdsToSlugs: Record; } interface ApiEndpointPage { diff --git a/packages/ui/app/src/resolver/resolveMarkdownPage.ts b/packages/ui/app/src/resolver/resolveMarkdownPage.ts index 491d199ea7..68371a2437 100644 --- a/packages/ui/app/src/resolver/resolveMarkdownPage.ts +++ b/packages/ui/app/src/resolver/resolveMarkdownPage.ts @@ -46,9 +46,21 @@ export async function resolveMarkdownPage({ } const apiDefinitionIds = new Set(); + const endpointIdsToSlugs = new Map(); + if (shouldFetchApiRef(markdownPageWithoutApiRefs.content)) { + // note: we start from the version node because endpoint Ids can be duplicated across versions + // if we introduce versioned sections, and versioned api references, this logic will need to change FernNavigation.utils.collectApiReferences(version).forEach((apiRef) => { apiDefinitionIds.add(apiRef.apiDefinitionId); + + FernNavigation.traverseDF(apiRef, (node) => { + if (node.type !== "endpoint") { + return; + } + // TODO: handle duplicate endpoint Ids + endpointIdsToSlugs.set(node.endpointId, node.canonicalSlug ?? node.slug); + }); }); } const resolvedApis = Object.fromEntries( @@ -80,6 +92,7 @@ export async function resolveMarkdownPage({ ...markdownPageWithoutApiRefs, type: "markdown-page", apis: resolvedApis, + endpointIdsToSlugs: Object.fromEntries(endpointIdsToSlugs.entries()), }; } @@ -95,7 +108,9 @@ export async function resolveMarkdownPageWithoutApiRefs({ breadcrumb, neighbors, markdownLoader, -}: ResolveMarkdownPageWithoutApiRefsOptions): Promise | undefined> { +}: ResolveMarkdownPageWithoutApiRefsOptions): Promise< + Omit | undefined +> { const rawMarkdown = markdownLoader.getRawMarkdown(node); if (!rawMarkdown) { diff --git a/packages/ui/app/src/util/processRequestSnippetComponents.ts b/packages/ui/app/src/util/processRequestSnippetComponents.ts index 2c6f41a614..73a34ef119 100644 --- a/packages/ui/app/src/util/processRequestSnippetComponents.ts +++ b/packages/ui/app/src/util/processRequestSnippetComponents.ts @@ -6,19 +6,34 @@ export function findEndpoint({ apiDefinition, method, path, + example: exampleName, }: { apiDefinition: ApiDefinition.ApiDefinition; method: string; path: string; + example: string | undefined; }): ApiDefinition.EndpointDefinition | undefined { path = path.startsWith("/") ? path : `/${path}`; - for (const endpoint of Object.values(apiDefinition.endpoints)) { - if (endpoint.method === method && getMatchablePermutationsForEndpoint(endpoint).has(path)) { - return endpoint; - } + const matchingEndpoints = Object.values(apiDefinition.endpoints).filter( + (e) => e.method === method && getMatchablePermutationsForEndpoint(e).has(path), + ); + + if (exampleName != null && matchingEndpoints.length > 1) { + return ( + matchingEndpoints.find((e) => e.examples?.some(createExampleNamePredicate(exampleName))) ?? + matchingEndpoints[0] + ); } - return undefined; + return matchingEndpoints[0]; +} + +function createExampleNamePredicate(exampleName: string): (example: ApiDefinition.ExampleEndpointCall) => boolean { + return (example) => + example.name === exampleName || + Object.values(example.snippets ?? {}) + .flat() + .some((snippet) => snippet.name === exampleName); } export function getMatchablePermutationsForEndpoint(