Skip to content

Commit

Permalink
fix: request response snippet resolver should work for endpoint pairs…
Browse files Browse the repository at this point in the history
… (stream/batch) (#1818)
  • Loading branch information
abvthecity authored Nov 15, 2024
1 parent 0bc99e5 commit 21848dd
Show file tree
Hide file tree
Showing 10 changed files with 82 additions and 13 deletions.
16 changes: 16 additions & 0 deletions packages/ui/app/src/api-reference/endpoints/EndpointUrl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,19 @@ export const EndpointUrl = React.forwardRef<HTMLDivElement, PropsWithChildren<En
return elements;
}, [path]);

// if the environment is hidden, but it contains a basepath, we need to show the basepath
const environmentBasepath = useMemo(() => {
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 (
<div ref={ref} className={cn("flex items-center gap-1 pr-2", className)}>
<HttpMethodTag method={method} />
Expand Down Expand Up @@ -112,6 +125,9 @@ export const EndpointUrl = React.forwardRef<HTMLDivElement, PropsWithChildren<En
/>
</span>
)}
{!showEnvironment && environmentBasepath && (
<span className="t-muted">{environmentBasepath}</span>
)}
{pathParts}
</span>
</button>
Expand Down
9 changes: 9 additions & 0 deletions packages/ui/app/src/atoms/apis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -82,3 +83,11 @@ export function useIsApiReferenceShallowLink(node: FernNavigation.WithApiDefinit
),
);
}

export const ENDPOINT_ID_TO_SLUG_ATOM = atom<Record<FernNavigation.EndpointId, FernNavigation.Slug>>((get) => {
const { content } = get(DOCS_ATOM);
if (content.type === "markdown-page") {
return content.endpointIdsToSlugs;
}
return {};
});
1 change: 1 addition & 0 deletions packages/ui/app/src/atoms/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export const EMPTY_DOCS_STATE: DocsProps = {
neighbors: { prev: null, next: null },
hasAside: false,
apis: {},
endpointIdsToSlugs: {},
},
featureFlags: DEFAULT_FEATURE_FLAGS,
apis: [],
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/app/src/components/ApiReferenceButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export const ApiReferenceButton: React.FC<{ slug: FernNavigation.Slug }> = ({ sl
const href = useHref(slug);
return (
<FernTooltipProvider>
<FernTooltip content="View API reference">
<FernTooltip content="Open in API reference">
<FernLinkButton
className="-m-1"
rounded
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import type * as ApiDefinition from "@fern-api/fdr-sdk/api-definition";
import { EMPTY_OBJECT } from "@fern-api/ui-core-utils";
import { useAtomValue } from "jotai";
import { ReactElement } from "react";
import { CodeExampleClientDropdown } from "../../../api-reference/endpoints/CodeExampleClientDropdown";
import { EndpointUrlWithOverflow } from "../../../api-reference/endpoints/EndpointUrlWithOverflow";
import { useExampleSelection } from "../../../api-reference/endpoints/useExampleSelection";
import { CodeSnippetExample } from "../../../api-reference/examples/CodeSnippetExample";
import { ENDPOINT_ID_TO_SLUG_ATOM } from "../../../atoms";
import { ApiReferenceButton } from "../../../components/ApiReferenceButton";
import { usePlaygroundBaseUrl } from "../../../playground/utils/select-environment";
import { RequestSnippet } from "./types";
import { useFindEndpoint } from "./useFindEndpoint";
Expand All @@ -28,7 +31,7 @@ const EndpointRequestSnippetRenderer: React.FC<React.PropsWithChildren<RequestSn
path,
example,
}) => {
const endpoint = useFindEndpoint(method, path);
const endpoint = useFindEndpoint(method, path, example);

if (endpoint == null) {
return null;
Expand All @@ -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,
Expand Down Expand Up @@ -76,8 +80,7 @@ export function EndpointRequestSnippetInternal({
value={selectedExampleKey.language}
/>
)}
{/* TODO: Restore this button */}
{/* <ApiReferenceButton slug={endpoint.slug} /> */}
{slug != null && <ApiReferenceButton slug={slug} />}
</>
}
code={selectedExample.code}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
() =>
Expand All @@ -15,14 +19,15 @@ export function useFindEndpoint(method: string, path: string): EndpointDefinitio
apiDefinition,
path,
method,
example,
});
if (endpoint) {
break;
}
}
return endpoint;
}),
[method, path],
[example, method, path],
),
);
}
5 changes: 5 additions & 0 deletions packages/ui/app/src/resolver/DocsContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ export declare namespace DocsContent {
hasAside: boolean;
// TODO: downselect apis to only the fields we need
apis: Record<FernNavigation.ApiDefinitionId, ApiDefinition>;
/**
* 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<FernNavigation.EndpointId, FernNavigation.Slug>;
}

interface ApiEndpointPage {
Expand Down
17 changes: 16 additions & 1 deletion packages/ui/app/src/resolver/resolveMarkdownPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,21 @@ export async function resolveMarkdownPage({
}

const apiDefinitionIds = new Set<FernNavigation.ApiDefinitionId>();
const endpointIdsToSlugs = new Map<FernNavigation.EndpointId, FernNavigation.Slug>();

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(
Expand Down Expand Up @@ -80,6 +92,7 @@ export async function resolveMarkdownPage({
...markdownPageWithoutApiRefs,
type: "markdown-page",
apis: resolvedApis,
endpointIdsToSlugs: Object.fromEntries(endpointIdsToSlugs.entries()),
};
}

Expand All @@ -95,7 +108,9 @@ export async function resolveMarkdownPageWithoutApiRefs({
breadcrumb,
neighbors,
markdownLoader,
}: ResolveMarkdownPageWithoutApiRefsOptions): Promise<Omit<DocsContent.MarkdownPage, "type" | "apis"> | undefined> {
}: ResolveMarkdownPageWithoutApiRefsOptions): Promise<
Omit<DocsContent.MarkdownPage, "type" | "apis" | "endpointIdsToSlugs"> | undefined
> {
const rawMarkdown = markdownLoader.getRawMarkdown(node);

if (!rawMarkdown) {
Expand Down
25 changes: 20 additions & 5 deletions packages/ui/app/src/util/processRequestSnippetComponents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down

0 comments on commit 21848dd

Please sign in to comment.