Skip to content

Commit

Permalink
implement
Browse files Browse the repository at this point in the history
  • Loading branch information
dsinghvi committed Oct 11, 2024
1 parent 63e0035 commit f52a173
Show file tree
Hide file tree
Showing 45 changed files with 665 additions and 348 deletions.
21 changes: 8 additions & 13 deletions fern/apis/fdr/definition/api/latest/endpoint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,19 +38,14 @@ types:
snippetTemplates: optional<EndpointSnippetTemplates>

QueryParameter:
union:
commaSeparated:
type: type.ObjectProperty
docs: An array of query parameters that are serialized as a comma delimited list
exploded:
type: type.ObjectProperty
docs: An array of query parameters that are sent as individual params in the URL
deepObject:
type: type.ObjectProperty
docs: A query parameter that is an object with properties and serialized as `a[key1]=value1`
basic:
type: type.ObjectProperty
docs: A basic primitive query parameter
extends: type.ObjectProperty
properties:
arrayEncoding: optional<QueryParameterArrayEncoding>

QueryParameterArrayEncoding:
enum:
- comma
- exploded

EndpointSnippetTemplates:
properties:
Expand Down
20 changes: 12 additions & 8 deletions packages/fdr-sdk/src/__test__/fixtures.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,27 @@ import { readFixture } from "./readFixtures";

const fixturesDir = path.join(__dirname, "fixtures");

function testNavigationConfigConverter(fixtureName: string): void {
async function testNavigationConfigConverter(fixtureName: string): Promise<void> {
const fixture = readFixture(fixtureName);
const v1 = FernNavigation.V1.toRootNode(fixture);
const latest = FernNavigationV1ToLatest.create().root(v1);

const v2Apis = Object.values(fixture.definition.apis).map((api) =>
ApiDefinitionV1ToLatest.from(api, {
useJavaScriptAsTypeScript: false,
alwaysEnableJavaScriptFetch: false,
usesApplicationJsonInFormDataValue: false,
}).migrate(),
const v2Apis = await Promise.all(
Object.values(fixture.definition.apis).map(
async (api) =>
await ApiDefinitionV1ToLatest.from(api, {
useJavaScriptAsTypeScript: false,
alwaysEnableJavaScriptFetch: false,
usesApplicationJsonInFormDataValue: false,
}).migrate(),
),
);

// eslint-disable-next-line vitest/valid-title
describe(fixtureName, () => {
const collector = new NodeCollector(latest);

it("gets all urls from docs config", () => {
it("gets all urls from docs config", async () => {
expect(JSON.stringify(sortObject(latest), undefined, 2)).toMatchFileSnapshot(
`output/${fixtureName}/node.json`,
);
Expand Down Expand Up @@ -98,6 +101,7 @@ function testNavigationConfigConverter(fixtureName: string): void {
const node = collector.slugMap.get(slug)!;
expect(node).toBeDefined();
if (!FernNavigation.isPage(node)) {
// eslint-disable-next-line no-console
console.log(node);
}
expect(FernNavigation.isPage(node), `${slug} is a page`).toBe(true);
Expand Down
1 change: 1 addition & 0 deletions packages/fdr-sdk/src/api-definition/__test__/join.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const endpoint1: Latest.EndpointDefinition = {
default: undefined,
},
},
arrayEncoding: "comma",
description: undefined,
availability: undefined,
},
Expand Down
1 change: 1 addition & 0 deletions packages/fdr-sdk/src/api-definition/__test__/prune.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const endpoint1: Latest.EndpointDefinition = {
queryParameters: [
{
key: Latest.PropertyKey("query"),
arrayEncoding: "exploded",
valueShape: {
type: "alias",
value: {
Expand Down
196 changes: 109 additions & 87 deletions packages/fdr-sdk/src/api-definition/migrators/v1ToV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export class ApiDefinitionV1ToLatest {
private webhooks: Record<V2.WebhookId, V2.WebhookDefinition> = {};
private subpackages: Record<V2.SubpackageId, V2.SubpackageMetadata> = {};
private types: Record<string, V2.TypeDefinition> = {};
public migrate = (): V2.ApiDefinition => {
public migrate = async (): Promise<V2.ApiDefinition> => {
Object.entries(this.v1.types).forEach(([id, type]) => {
this.types[V2.TypeId(id)] = {
name: type.name,
Expand All @@ -82,21 +82,25 @@ export class ApiDefinitionV1ToLatest {
};
});

[this.v1.rootPackage, ...Object.values(this.v1.subpackages)].forEach((pkg) => {
const [subpackageId, namespace] = this.collectNamespace(pkg, this.v1.subpackages);
pkg.endpoints.forEach((endpoint) => {
const id = ApiDefinitionV1ToLatest.createEndpointId(endpoint, subpackageId);
this.endpoints[id] = this.migrateEndpoint(id, endpoint, namespace);
});
pkg.websockets.forEach((webSocket) => {
const id = ApiDefinitionV1ToLatest.createWebSocketId(webSocket, subpackageId);
this.websockets[id] = this.migrateWebSocket(id, webSocket, namespace);
});
pkg.webhooks.forEach((webhook) => {
const id = ApiDefinitionV1ToLatest.createWebhookId(webhook, subpackageId);
this.webhooks[id] = this.migrateWebhook(id, webhook, namespace);
});
});
await Promise.all(
[this.v1.rootPackage, ...Object.values(this.v1.subpackages)].map(async (pkg) => {
const [subpackageId, namespace] = this.collectNamespace(pkg, this.v1.subpackages);
await Promise.all(
pkg.endpoints.map(async (endpoint) => {
const id = ApiDefinitionV1ToLatest.createEndpointId(endpoint, subpackageId);
this.endpoints[id] = await this.migrateEndpoint(id, endpoint, namespace);
}),
);
pkg.websockets.forEach((webSocket) => {
const id = ApiDefinitionV1ToLatest.createWebSocketId(webSocket, subpackageId);
this.websockets[id] = this.migrateWebSocket(id, webSocket, namespace);
});
pkg.webhooks.forEach((webhook) => {
const id = ApiDefinitionV1ToLatest.createWebhookId(webhook, subpackageId);
this.webhooks[id] = this.migrateWebhook(id, webhook, namespace);
});
}),
);

Object.values(this.v1.subpackages).forEach((subpackage) => {
this.subpackages[subpackage.subpackageId] = this.migrateSubpackage(subpackage);
Expand Down Expand Up @@ -142,11 +146,11 @@ export class ApiDefinitionV1ToLatest {
return [pkg.subpackageId, namespace];
};

migrateEndpoint = (
migrateEndpoint = async (
id: V2.EndpointId,
v1: APIV1Read.EndpointDefinition,
namespace: V2.SubpackageId[],
): V2.EndpointDefinition => {
): Promise<V2.EndpointDefinition> => {
const toRet: V2.EndpointDefinition = {
id,
namespace,
Expand All @@ -158,7 +162,7 @@ export class ApiDefinitionV1ToLatest {
defaultEnvironment: v1.defaultEnvironment,
environments: v1.environments,
pathParameters: this.migrateParameters(v1.path.pathParameters),
queryParameters: this.migrateParameters(v1.queryParameters),
queryParameters: this.migrateQueryParameters(v1.queryParameters),
requestHeaders: this.migrateParameters(v1.headers),
responseHeaders: undefined,
request: this.migrateHttpRequest(v1.request),
Expand All @@ -168,7 +172,7 @@ export class ApiDefinitionV1ToLatest {
snippetTemplates: v1.snippetTemplates,
};

toRet.examples = this.migrateHttpExamples(v1.examples, toRet);
toRet.examples = await this.migrateHttpExamples(v1.examples, toRet);

return toRet;
};
Expand Down Expand Up @@ -227,7 +231,7 @@ export class ApiDefinitionV1ToLatest {
};

migrateParameters = (
v1: APIV1Read.PathParameter[] | APIV1Read.QueryParameter[] | APIV1Read.Header[] | undefined,
v1: APIV1Read.PathParameter[] | APIV1Read.Header[] | undefined,
): V2.ObjectProperty[] | undefined => {
if (v1 == null || v1.length === 0) {
return undefined;
Expand All @@ -243,6 +247,22 @@ export class ApiDefinitionV1ToLatest {
}));
};

migrateQueryParameters = (v1: APIV1Read.QueryParameter[] | undefined): V2.QueryParameter[] | undefined => {
if (v1 == null || v1.length === 0) {
return undefined;
}
return v1.map((parameter) => ({
key: V2.PropertyKey(parameter.key),
valueShape: {
type: "alias",
value: this.migrateTypeReference(parameter.type),
},
arrayEncoding: parameter.arrayEncoding,
description: parameter.description,
availability: parameter.availability,
}));
};

migrateTypeReference = (typeRef: APIV1Read.TypeReference): V2.TypeReference => {
return visitDiscriminatedUnion(typeRef)._visit<V2.TypeReference>({
map: (value) => ({
Expand Down Expand Up @@ -397,77 +417,79 @@ export class ApiDefinitionV1ToLatest {
}));
};

migrateHttpExamples = (
migrateHttpExamples = async (
examples: APIV1Read.ExampleEndpointCall[],
endpoint: V2.EndpointDefinition,
): V2.ExampleEndpointCall[] | undefined => {
): Promise<Promise<V2.ExampleEndpointCall[]> | undefined> => {
if (examples.length === 0) {
return undefined;
}
return examples.map((example): V2.ExampleEndpointCall => {
const toRet: V2.ExampleEndpointCall = {
path: example.path,
responseStatusCode: example.responseStatusCode,
name: example.name,
description: example.description,
pathParameters: example.pathParameters,
queryParameters: example.queryParameters,
headers: example.headers,
requestBody: example.requestBodyV3,
responseBody: example.responseBodyV3,
snippets: undefined,
};

if (example.requestBodyV3) {
toRet.requestBody = visitDiscriminatedUnion(
example.requestBodyV3,
)._visit<APIV1Read.ExampleEndpointRequest>({
bytes: (value) => value,
json: (value) => ({
type: "json",
value: sortKeysByShape(value.value, endpoint.request?.body, this.types),
}),
form: (value) => ({
type: "form",
value: mapValues(value.value, (formValue, key): APIV1Read.FormValue => {
if (formValue.type === "json") {
const shape =
endpoint.request?.body.type === "formData"
? endpoint.request.body.fields.find(
(field): field is V2.FormDataField.Property =>
field.key === key && field.type === "property",
)?.valueShape
: undefined;
return {
type: "json",
value: sortKeysByShape(formValue.value, shape, this.types),
};
} else {
return formValue;
}
return await Promise.all(
examples.map(async (example): Promise<V2.ExampleEndpointCall> => {
const toRet: V2.ExampleEndpointCall = {
path: example.path,
responseStatusCode: example.responseStatusCode,
name: example.name,
description: example.description,
pathParameters: example.pathParameters,
queryParameters: example.queryParameters,
headers: example.headers,
requestBody: example.requestBodyV3,
responseBody: example.responseBodyV3,
snippets: undefined,
};

if (example.requestBodyV3) {
toRet.requestBody = visitDiscriminatedUnion(
example.requestBodyV3,
)._visit<APIV1Read.ExampleEndpointRequest>({
bytes: (value) => value,
json: (value) => ({
type: "json",
value: sortKeysByShape(value.value, endpoint.request?.body, this.types),
}),
}),
});
}

if (toRet.responseBody) {
toRet.responseBody.value = sortKeysByShape(
toRet.responseBody.value,
endpoint.response?.body,
this.types,
form: (value) => ({
type: "form",
value: mapValues(value.value, (formValue, key): APIV1Read.FormValue => {
if (formValue.type === "json") {
const shape =
endpoint.request?.body.type === "formData"
? endpoint.request.body.fields.find(
(field): field is V2.FormDataField.Property =>
field.key === key && field.type === "property",
)?.valueShape
: undefined;
return {
type: "json",
value: sortKeysByShape(formValue.value, shape, this.types),
};
} else {
return formValue;
}
}),
}),
});
}

if (toRet.responseBody) {
toRet.responseBody.value = sortKeysByShape(
toRet.responseBody.value,
endpoint.response?.body,
this.types,
);
}

toRet.snippets = await this.migrateEndpointSnippets(
endpoint,
toRet,
example.codeSamples,
example.codeExamples,
this.flags,
);
}

toRet.snippets = this.migrateEndpointSnippets(
endpoint,
toRet,
example.codeSamples,
example.codeExamples,
this.flags,
);

return toRet;
});
return toRet;
}),
);
};

migrateHttpErrors = (errors: APIV1Read.ErrorDeclarationV2[] | undefined): V2.ErrorResponse[] | undefined => {
Expand Down Expand Up @@ -611,13 +633,13 @@ export class ApiDefinitionV1ToLatest {
);
};

migrateEndpointSnippets(
async migrateEndpointSnippets(
endpoint: V2.EndpointDefinition,
example: V2.ExampleEndpointCall,
codeSamples: APIV1Read.CustomCodeSample[],
codeExamples: APIV1Read.CodeExamples,
flags: Flags,
): Record<string, V2.CodeSnippet[]> {
): Promise<Record<string, V2.CodeSnippet[]>> {
const toRet: Record<string, V2.CodeSnippet[]> = {};
function push(language: string, snippet: V2.CodeSnippet) {
(toRet[language] ??= []).push(snippet);
Expand All @@ -641,7 +663,7 @@ export class ApiDefinitionV1ToLatest {
});

if (!userProvidedLanguages.has(SupportedLanguage.Curl)) {
const code = convertToCurl(toSnippetHttpRequest(endpoint, example, this.auth), flags);
const code = await convertToCurl(toSnippetHttpRequest(endpoint, example, this.auth), flags);
push(SupportedLanguage.Curl, {
language: SupportedLanguage.Curl,
code,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ export type SnippetHttpRequestBody =
export interface SnippetHttpRequest {
method: string;
url: string;
searchParams: Record<string, unknown>;
queryParameters: Record<string, unknown>;
queryParametersEncoding: Record<string, Latest.QueryParameterArrayEncoding>;
headers: Record<string, unknown>;
basicAuth?: {
username: string;
Expand Down Expand Up @@ -107,7 +108,10 @@ export function toSnippetHttpRequest(
return {
method: endpoint.method,
url,
searchParams: example.queryParameters ?? {},
queryParameters: example.queryParameters ?? {},
queryParametersEncoding: Object.fromEntries(
(endpoint.queryParameters ?? []).map((param) => [param.key, param.arrayEncoding ?? "exploded"]),
),
headers: JSON.parse(JSON.stringify(headers)),
basicAuth,
body:
Expand Down
Loading

0 comments on commit f52a173

Please sign in to comment.