Skip to content

Commit

Permalink
improvement: create a snippet resolver that caches the api definition…
Browse files Browse the repository at this point in the history
… across
  • Loading branch information
armandobelardo committed Aug 1, 2024
1 parent c0c793f commit ab17c9f
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 16 deletions.
11 changes: 10 additions & 1 deletion packages/template-resolver/src/ResolutionUtilities.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import { APIV1Read } from "@fern-api/fdr-sdk";
import { APIV1Read, FdrClient } from "@fern-api/fdr-sdk";

export async function getApiDefinition(apiDefinitionId: string): Promise<APIV1Read.ApiDefinition | undefined> {
const fdr = new FdrClient();
const apiDefinitionResponse = await fdr.api.v1.read.getApi(apiDefinitionId);
if (apiDefinitionResponse.ok) {
return apiDefinitionResponse.body;
}
return;
}

export class ObjectFlattener {
private flattenedObjects: Map<APIV1Read.TypeId, APIV1Read.ObjectProperty[]> = new Map<
Expand Down
101 changes: 101 additions & 0 deletions packages/template-resolver/src/SnippetTemplateResolutionHolder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { APIV1Read } from "@fern-api/fdr-sdk";
import { ObjectFlattener, getApiDefinition } from "./ResolutionUtilities";
import { SnippetTemplateResolver } from "./SnippetTemplateResolver";
import { CustomSnippetPayload, EndpointSnippetTemplate, Snippet } from "./generated/api";

export class SnippetTemplateResolutionHolder {
private maybeApiDefinition: APIV1Read.ApiDefinition | undefined;
private maybeObjectFlattener: ObjectFlattener | undefined;
private maybeApiDefinitionId: string | undefined;

private apiDefinitionHasBeenRequested: boolean;

constructor({
maybeApiDefinition,
maybeApiDefinitionId,
}: {
maybeApiDefinition: APIV1Read.ApiDefinition | undefined;
maybeApiDefinitionId: string | undefined;
}) {
this.maybeApiDefinition = maybeApiDefinition;
this.maybeApiDefinitionId = maybeApiDefinitionId;

if (maybeApiDefinition == null && maybeApiDefinitionId == null) {
throw new Error(
"Either an API definition or an API definition ID must be provided, if you have neither use the SnippetTemplateResolver directly.",
);
}
this.apiDefinitionHasBeenRequested = false;
}

// Get or create and cache ApiDefinition
private async getApiDefinition(): Promise<APIV1Read.ApiDefinition | undefined> {
if (this.maybeApiDefinition != null) {
return this.maybeApiDefinition;
}

// If we were not provided an API definition, try to get it from FDR
if (this.maybeApiDefinitionId != null && !this.apiDefinitionHasBeenRequested) {
this.apiDefinitionHasBeenRequested = true;
this.maybeApiDefinition = await getApiDefinition(this.maybeApiDefinitionId);
return this.maybeApiDefinition;
}

return;
}

// Get or create and cache ObjectFlattener
private getObjectFlattener(apiDefinition: APIV1Read.ApiDefinition): ObjectFlattener {
if (this.maybeObjectFlattener != null) {
return this.maybeObjectFlattener;
}

this.maybeObjectFlattener = new ObjectFlattener(apiDefinition);
return this.maybeObjectFlattener;
}

public async getTemplateResolver({
payload,
endpointSnippetTemplate,
}: {
payload: CustomSnippetPayload;
endpointSnippetTemplate: EndpointSnippetTemplate;
}): Promise<SnippetTemplateResolver> {
const apiDefinition = await this.getApiDefinition();
if (apiDefinition == null) {
throw new Error("Failed to get API definition");
}
const objectFlattener = this.getObjectFlattener(apiDefinition);

return new SnippetTemplateResolver({
apiDefinition,
objectFlattener,
payload,
endpointSnippetTemplate,
});
}

public async resolveWithFormatting({
payload,
endpointSnippetTemplate,
}: {
payload: CustomSnippetPayload;
endpointSnippetTemplate: EndpointSnippetTemplate;
}): Promise<Snippet> {
const resolver = await this.getTemplateResolver({ payload, endpointSnippetTemplate });

return resolver.resolveWithFormatting();
}

public async resolve({
payload,
endpointSnippetTemplate,
}: {
payload: CustomSnippetPayload;
endpointSnippetTemplate: EndpointSnippetTemplate;
}): Promise<Snippet> {
const resolver = await this.getTemplateResolver({ payload, endpointSnippetTemplate });

return resolver.resolve();
}
}
21 changes: 12 additions & 9 deletions packages/template-resolver/src/SnippetTemplateResolver.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { APIV1Read, FdrClient } from "@fern-api/fdr-sdk";
import { ObjectFlattener } from "./ResolutionUtilities";
import { APIV1Read } from "@fern-api/fdr-sdk";
import { ObjectFlattener, getApiDefinition } from "./ResolutionUtilities";
import { UnionMatcher } from "./UnionResolver";
import { accessByPathNonNull } from "./accessByPath";
import {
Expand Down Expand Up @@ -78,12 +78,18 @@ export class SnippetTemplateResolver {
constructor({
payload,
endpointSnippetTemplate,
apiDefinition,
objectFlattener,
}: {
payload: CustomSnippetPayload;
endpointSnippetTemplate: EndpointSnippetTemplate;
apiDefinition?: APIV1Read.ApiDefinition;
objectFlattener?: ObjectFlattener;
}) {
this.payload = payload;
this.endpointSnippetTemplate = endpointSnippetTemplate;
this.maybeApiDefinition = apiDefinition;
this.maybeObjectFlattener = objectFlattener;

// maybeApiDefinitionId is the ID of the API definition, stored on the template itself, used as a fallback
this.maybeApiDefinitionId = this.endpointSnippetTemplate.apiDefinitionId;
Expand Down Expand Up @@ -142,6 +148,7 @@ export class SnippetTemplateResolver {
}
}

// Get or create and cache ApiDefinition
private async getApiDefinition(): Promise<APIV1Read.ApiDefinition | undefined> {
if (this.maybeApiDefinition != null) {
return this.maybeApiDefinition;
Expand All @@ -150,18 +157,14 @@ export class SnippetTemplateResolver {
// If we were not provided an API definition, try to get it from FDR
if (this.maybeApiDefinitionId != null && !this.apiDefinitionHasBeenRequested) {
this.apiDefinitionHasBeenRequested = true;
const fdr = new FdrClient();
const apiDefinitionResponse = await fdr.api.v1.read.getApi(this.maybeApiDefinitionId);
if (apiDefinitionResponse.ok) {
// Cache the result for the next request
this.maybeApiDefinition = apiDefinitionResponse.body;
return this.maybeApiDefinition;
}
this.maybeApiDefinition = await getApiDefinition(this.maybeApiDefinitionId);
return this.maybeApiDefinition;
}

return;
}

// Get or create and cache ObjectFlattener
private getObjectFlattener(apiDefinition: APIV1Read.ApiDefinition): ObjectFlattener {
if (this.maybeObjectFlattener != null) {
return this.maybeObjectFlattener;
Expand Down
38 changes: 32 additions & 6 deletions packages/ui/app/src/api-playground/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { APIV1Read, Snippets } from "@fern-api/fdr-sdk";
import { SnippetTemplateResolver } from "@fern-api/template-resolver";
import { isNonNullish, isPlainObject, visitDiscriminatedUnion } from "@fern-ui/core-utils";
import { isEmpty, mapValues } from "lodash-es";
import { useEffect, useState } from "react";
import { stringifyHttpRequestExampleToCurl } from "../api-page/examples/stringifyHttpRequestExampleToCurl";
import {
ResolvedEndpointDefinition,
Expand Down Expand Up @@ -127,6 +128,7 @@ export function stringifyFetch({
}

const snippetTemplate = endpoint.snippetTemplates?.typescript;
const [resolvedTemplateSnippet, setResolvedTemplateSnippet] = useState<Snippets.Snippet | null>(null);

Check failure on line 131 in packages/ui/app/src/api-playground/utils.ts

View workflow job for this annotation

GitHub Actions / lint

React Hook "useState" is called in function "stringifyFetch" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use"

if (snippetTemplate != null && isSnippetTemplatesEnabled) {
const resolver = new SnippetTemplateResolver({
Expand All @@ -144,10 +146,22 @@ export function stringifyFetch({
snippetTemplate,
},
});
const resolvedTemplate = resolver.resolve();

if (resolvedTemplate.type === "typescript") {
return resolvedTemplate.client;
// TODO: We should expose a .unresolve() method or similar on
// the resolved APIDefinition, so we can just pass that to
// .resolveWithFormatting() instead of having the resolver make a DB call
useEffect(() => {

Check failure on line 153 in packages/ui/app/src/api-playground/utils.ts

View workflow job for this annotation

GitHub Actions / lint

React Hook "useEffect" is called in function "stringifyFetch" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use"
const resolveTemplate = async () => {
const resolvedTemplate = await resolver.resolveWithFormatting();

setResolvedTemplateSnippet(resolvedTemplate);
};

resolveTemplate();

Check failure on line 160 in packages/ui/app/src/api-playground/utils.ts

View workflow job for this annotation

GitHub Actions / lint

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator
}, []);

Check warning on line 161 in packages/ui/app/src/api-playground/utils.ts

View workflow job for this annotation

GitHub Actions / lint

React Hook useEffect has a missing dependency: 'resolver'. Either include it or remove the dependency array

if (resolvedTemplateSnippet && resolvedTemplateSnippet.type === "typescript") {
return resolvedTemplateSnippet.client;
}
}

Expand Down Expand Up @@ -251,6 +265,7 @@ export function stringifyPythonRequests({
}

const snippetTemplate = endpoint.snippetTemplates?.python;
const [resolvedTemplateSnippet, setResolvedTemplateSnippet] = useState<Snippets.Snippet | null>(null);

Check failure on line 268 in packages/ui/app/src/api-playground/utils.ts

View workflow job for this annotation

GitHub Actions / lint

React Hook "useState" is called in function "stringifyPythonRequests" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use"

if (snippetTemplate != null && isSnippetTemplatesEnabled) {
const resolver = new SnippetTemplateResolver({
Expand All @@ -269,10 +284,21 @@ export function stringifyPythonRequests({
},
});

const resolvedTemplate = resolver.resolve();
// TODO: We should expose a .unresolve() method or similar on
// the resolved APIDefinition, so we can just pass that to
// .resolveWithFormatting() instead of having the resolver make a DB call
useEffect(() => {

Check failure on line 290 in packages/ui/app/src/api-playground/utils.ts

View workflow job for this annotation

GitHub Actions / lint

React Hook "useEffect" is called in function "stringifyPythonRequests" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use"
const resolveTemplate = async () => {
const resolvedTemplate = await resolver.resolveWithFormatting();

setResolvedTemplateSnippet(resolvedTemplate);
};

resolveTemplate();

Check failure on line 297 in packages/ui/app/src/api-playground/utils.ts

View workflow job for this annotation

GitHub Actions / lint

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator
}, []);

Check warning on line 298 in packages/ui/app/src/api-playground/utils.ts

View workflow job for this annotation

GitHub Actions / lint

React Hook useEffect has a missing dependency: 'resolver'. Either include it or remove the dependency array

if (resolvedTemplate.type === "python") {
return resolvedTemplate.sync_client;
if (resolvedTemplateSnippet && resolvedTemplateSnippet.type === "python") {
return resolvedTemplateSnippet.sync_client;
}
}

Expand Down

0 comments on commit ab17c9f

Please sign in to comment.