Skip to content

Commit

Permalink
fix: async snippets in api playground (#1233)
Browse files Browse the repository at this point in the history
  • Loading branch information
abvthecity authored Aug 2, 2024
1 parent 5b11a42 commit f61b9ca
Show file tree
Hide file tree
Showing 21 changed files with 660 additions and 426 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ export declare namespace useCopyToClipboard {
}
}

export function useCopyToClipboard(content: string | (() => string) | undefined): useCopyToClipboard.Return {
export function useCopyToClipboard(
content: string | (() => string | Promise<string>) | undefined,
): useCopyToClipboard.Return {
const [wasJustCopied, setWasJustCopied] = useState(false);

const copyToClipboard = useMemo(() => {
Expand All @@ -17,7 +19,7 @@ export function useCopyToClipboard(content: string | (() => string) | undefined)
}
return async () => {
setWasJustCopied(true);
await navigator.clipboard.writeText(typeof content === "function" ? content() : content);
await navigator.clipboard.writeText(typeof content === "function" ? await content() : content);
};
}, [content]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const MOCK_ENDPOINT: ResolvedEndpointDefinition = {
auth: undefined,
availability: undefined,
defaultEnvironment: MOCK_ENV,
apiSectionId: "",
apiDefinitionId: "",
environments: [MOCK_ENV],
method: "GET",
title: "",
Expand Down
33 changes: 7 additions & 26 deletions packages/ui/app/src/api-playground/PlaygroundEndpointContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ import { PlaygroundRequestPreview } from "./PlaygroundRequestPreview";
import { PlaygroundResponsePreview } from "./PlaygroundResponsePreview";
import { PlaygroundSendRequestButton } from "./PlaygroundSendRequestButton";
import { HorizontalSplitPane, VerticalSplitPane } from "./VerticalSplitPane";
import { PlaygroundCodeSnippetResolverBuilder } from "./code-snippets/resolver";
import { PlaygroundEndpointRequestFormState, ProxyResponse } from "./types";
import { PlaygroundResponse } from "./types/playgroundResponse";
import { stringifyCurl, stringifyFetch, stringifyPythonRequests } from "./utils";

interface PlaygroundEndpointContentProps {
endpoint: ResolvedEndpointDefinition;
Expand Down Expand Up @@ -133,31 +133,12 @@ export const PlaygroundEndpointContent: FC<PlaygroundEndpointContentProps> = ({
<CopyToClipboardButton
content={() => {
const authState = store.get(PLAYGROUND_AUTH_STATE_ATOM);
return requestType === "curl"
? stringifyCurl({
endpoint,
formState,
authState,
redacted: false,
domain,
})
: requestType === "typescript"
? stringifyFetch({
endpoint,
formState,
authState,
redacted: false,
isSnippetTemplatesEnabled,
})
: requestType === "python"
? stringifyPythonRequests({
endpoint,
formState,
authState,
redacted: false,
isSnippetTemplatesEnabled,
})
: "";
const resolver = new PlaygroundCodeSnippetResolverBuilder(
endpoint,
isSnippetTemplatesEnabled,
domain,
).create(authState, formState);
return resolver.resolve(requestType);
}}
className="-mr-2"
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export function PlaygroundEndpointFormButtons({

<FernLink
href={`/${endpoint.slug}`}
shallow={apiReferenceId === endpoint.apiSectionId}
shallow={apiReferenceId === endpoint.apiDefinitionId}
className="t-muted inline-flex items-center gap-1 text-sm font-semibold underline decoration-1 underline-offset-4 hover:t-accent hover:decoration-2"
onClick={closePlayground}
>
Expand Down
43 changes: 10 additions & 33 deletions packages/ui/app/src/api-playground/PlaygroundRequestPreview.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { useAtomValue } from "jotai";
import { FC, useMemo } from "react";
import { PLAYGROUND_AUTH_STATE_ATOM, useDomain, useFeatureFlags } from "../atoms";
import { useSelectedEnvironmentId } from "../atoms/environment";
import { ResolvedEndpointDefinition, resolveEnvironmentUrlInCodeSnippet } from "../resolver/types";
import { ResolvedEndpointDefinition } from "../resolver/types";
import { FernSyntaxHighlighter } from "../syntax-highlighting/FernSyntaxHighlighter";
import { PlaygroundCodeSnippetResolverBuilder } from "./code-snippets/resolver";
import { useSnippet } from "./code-snippets/useSnippet";
import { PlaygroundEndpointRequestFormState } from "./types";
import { stringifyCurl, stringifyFetch, stringifyPythonRequests } from "./utils";

interface PlaygroundRequestPreviewProps {
endpoint: ResolvedEndpointDefinition;
Expand All @@ -17,41 +17,18 @@ export const PlaygroundRequestPreview: FC<PlaygroundRequestPreviewProps> = ({ en
const { isSnippetTemplatesEnabled } = useFeatureFlags();
const authState = useAtomValue(PLAYGROUND_AUTH_STATE_ATOM);
const domain = useDomain();
const selectedEnvironmentId = useSelectedEnvironmentId();
const code = useMemo(
() =>
requestType === "curl"
? stringifyCurl({
endpoint,
formState,
authState,
redacted: true,
domain,
})
: requestType === "typescript"
? stringifyFetch({
endpoint,
formState,
authState,
redacted: true,
isSnippetTemplatesEnabled,
})
: requestType === "python"
? stringifyPythonRequests({
endpoint,
formState,
authState,
redacted: true,
isSnippetTemplatesEnabled,
})
: "",
[authState, domain, endpoint, formState, isSnippetTemplatesEnabled, requestType],
const builder = useMemo(
() => new PlaygroundCodeSnippetResolverBuilder(endpoint, isSnippetTemplatesEnabled, domain),
[domain, endpoint, isSnippetTemplatesEnabled],
);
const resolver = useMemo(() => builder.createRedacted(authState, formState), [authState, builder, formState]);
const code = useSnippet(resolver, requestType);

return (
<FernSyntaxHighlighter
className="relative min-h-0 flex-1 shrink"
language={requestType === "curl" ? "bash" : requestType}
code={resolveEnvironmentUrlInCodeSnippet(endpoint, code, selectedEnvironmentId)}
code={code}
fontSize="sm"
id={endpoint.id}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`PlaygroundCodeSnippetBuilder > should render curl 1`] = `
"curl -X POST https://example.com/test/hello%40example \\
-H "Accept: application/json" \\
-H "Test: test" \\
-d '{
"test": "hello",
"deeply": {
"nested": 1
}
}'"
`;

exports[`PlaygroundCodeSnippetBuilder > should render python 1`] = `
"import requests
# My endpoint (POST /test/:test)
response = requests.post(
"https://example.com/test/hello%40example",
headers={
"Accept": "application/json",
"Test": "test"
},
json={
"test": "hello",
"deeply": {
"nested": 1
}
},
)
print(response.json())"
`;

exports[`PlaygroundCodeSnippetBuilder > should render typescript 1`] = `
"// My endpoint (POST /test/:test)
const response = await fetch("https://example.com/test/hello%40example", {
method: "POST",
headers: {
"Accept": "application/json",
"Test": "test"
},
body: JSON.stringify({
"test": "hello",
"deeply": {
"nested": 1
}
}),
});
const body = await response.json();
console.log(body);"
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { FernNavigation } from "@fern-api/fdr-sdk";
import { ResolvedEndpointDefinition } from "../../../resolver/types";
import { PlaygroundEndpointRequestFormState } from "../../types";
import { CurlSnippetBuilder } from "../builders/curl";
import { PythonRequestSnippetBuilder } from "../builders/python";
import { TypescriptFetchSnippetBuilder } from "../builders/typescript";

describe("PlaygroundCodeSnippetBuilder", () => {
const endpoint: ResolvedEndpointDefinition = {
type: "endpoint",
nodeId: FernNavigation.NodeId(""),
id: "",
apiDefinitionId: "",
slug: FernNavigation.Slug(""),
auth: undefined,
availability: undefined,
defaultEnvironment: {
id: "Prod",
baseUrl: "https://example.com",
},
environments: [],
method: "POST",
title: "My endpoint",
path: [
{ type: "literal", value: "/test/" },
{
key: "test",
type: "pathParameter",
valueShape: {
type: "primitive",
value: { type: "string" },
description: undefined,
availability: undefined,
},
hidden: false,
description: undefined,
availability: undefined,
},
],
pathParameters: [
{
key: "test",
valueShape: {
type: "primitive",
value: { type: "string" },
description: undefined,
availability: undefined,
},
hidden: false,
description: undefined,
availability: undefined,
},
],
queryParameters: [],
headers: [],
requestBody: undefined,
responseBody: undefined,
errors: [],
examples: [],
snippetTemplates: undefined,
stream: undefined,
description: undefined,
};
const formState: PlaygroundEndpointRequestFormState = {
type: "endpoint",
headers: {
Accept: "application/json",
Test: "test",
},
pathParameters: {
test: "hello@example",
},
queryParameters: {},
body: {
type: "json",
value: {
test: "hello",
deeply: {
nested: 1,
},
},
},
};

it("should render curl", () => {
expect(new CurlSnippetBuilder(endpoint, formState).build()).toMatchSnapshot();
});

it("should render python", () => {
expect(new PythonRequestSnippetBuilder(endpoint, formState).build()).toMatchSnapshot();
});

it("should render typescript", () => {
expect(new TypescriptFetchSnippetBuilder(endpoint, formState).build()).toMatchSnapshot();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { ResolvedEndpointPathParts } from "../../../resolver/types";
import { unknownToString } from "../../utils";

export function buildPath(path: ResolvedEndpointPathParts[], pathParameters?: Record<string, unknown>): string {
return path
.map((part) => {
if (part.type === "pathParameter") {
const stateValue = unknownToString(pathParameters?.[part.key]);
return stateValue.length > 0 ? encodeURIComponent(stateValue) : ":" + part.key;
}
return part.value;
})
.join("");
}

export function indentAfter(str: string, indent: number, afterLine?: number): string {
return str
.split("\n")
.map((line, idx) => {
if (afterLine == null || idx > afterLine) {
return " ".repeat(indent) + line;
}
return line;
})
.join("\n");
}
59 changes: 59 additions & 0 deletions packages/ui/app/src/api-playground/code-snippets/builders/curl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { visitDiscriminatedUnion } from "@fern-ui/core-utils";
import { isEmpty } from "lodash-es";
import { stringifyHttpRequestExampleToCurl } from "../../../api-page/examples/stringifyHttpRequestExampleToCurl";
import { ResolvedExampleEndpointRequest, ResolvedFormValue } from "../../../resolver/types";
import { convertPlaygroundFormDataEntryValueToResolvedExampleEndpointRequest } from "../../types";
import { PlaygroundCodeSnippetBuilder } from "./types";

export class CurlSnippetBuilder extends PlaygroundCodeSnippetBuilder {
private isFileForgeHackEnabled: boolean = false;

public setFileForgeHackEnabled(isFileForgeHackEnabled: boolean): CurlSnippetBuilder {
this.isFileForgeHackEnabled = isFileForgeHackEnabled;
return this;
}

public override build(): string {
return stringifyHttpRequestExampleToCurl({
method: this.endpoint.method,
url: this.url,
urlQueries: this.formState.queryParameters,
headers: this.formState.headers,
body: this.#convertFormStateToBody(),
});
}

#convertFormStateToBody(): ResolvedExampleEndpointRequest | undefined {
if (this.formState.body == null) {
return undefined;
}
return visitDiscriminatedUnion(this.formState.body, "type")._visit<ResolvedExampleEndpointRequest | undefined>({
json: ({ value }) => ({ type: "json", value }),
"form-data": ({ value }): ResolvedExampleEndpointRequest.Form | undefined => {
const properties =
this.endpoint.requestBody?.shape.type === "formData"
? this.endpoint.requestBody.shape.properties
: [];
const newValue: Record<string, ResolvedFormValue> = {};
for (const [key, v] of Object.entries(value)) {
const property = properties.find((property) => property.key === key);
const convertedV = convertPlaygroundFormDataEntryValueToResolvedExampleEndpointRequest(
v,
property,
this.isFileForgeHackEnabled,
);
if (convertedV != null) {
newValue[key] = convertedV;
}
}
if (isEmpty(newValue)) {
return undefined;
}
return { type: "form", value: newValue };
},
"octet-stream": ({ value }): ResolvedExampleEndpointRequest.Bytes | undefined =>
value != null ? { type: "bytes", fileName: value.name, value: undefined } : undefined,
_other: () => undefined,
});
}
}
Loading

0 comments on commit f61b9ca

Please sign in to comment.