Skip to content
This repository has been archived by the owner on Nov 11, 2023. It is now read-only.

Commit

Permalink
Generate Mutate component FTW \o/
Browse files Browse the repository at this point in the history
  • Loading branch information
fabien0102 committed Oct 9, 2018
1 parent bed4153 commit 019a147
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 24 deletions.
37 changes: 23 additions & 14 deletions src/scripts/import-open-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
ParameterObject,
PathItemObject,
ReferenceObject,
RequestBodyObject,
ResponseObject,
SchemaObject,
} from "openapi3-ts";
Expand Down Expand Up @@ -139,13 +140,19 @@ export const getObject = (item: SchemaObject): string => {
export const resolveValue = (schema: SchemaObject) => (isReference(schema) ? getRef(schema.$ref) : getScalar(schema));

/**
* Extract responses types from open-api specs
* Extract responses / request types from open-api specs
*
* @param responses reponses object from open-api specs
* @param responsesOrRequests reponses or requests object from open-api specs
*/
export const getResponseTypes = (responses: Array<[string, ResponseObject | ReferenceObject]>) =>
export const getResReqTypes = (
responsesOrRequests: Array<[string, ResponseObject | ReferenceObject | RequestBodyObject]>,
) =>
uniq(
responses.map(([_, res]) => {
responsesOrRequests.map(([_, res]) => {
if (!res) {
return "void";
}

if (isReference(res)) {
throw new Error("$ref are not implemented inside responses");
} else {
Expand Down Expand Up @@ -196,24 +203,26 @@ const importSpecs = (path: string): OpenAPIObject => {
};

/**
* Generate a restful-react Get compoment from openapi operation specs
* Generate a restful-react compoment from openapi operation specs
*
* @param operation
*/
export const generateGetComponent = (operation: OperationObject, verb: string, route: string, baseUrl: string) => {
export const generateRestfulComponent = (operation: OperationObject, verb: string, route: string, baseUrl: string) => {
if (!operation.operationId) {
throw new Error(`Every path must have a operationId - No operationId set for ${verb} ${route}`);
}

route = route.replace(/\{/g, "${"); // `/pet/{id}` => `/pet/${id}`
const componentName = pascal(operation.operationId!);
const Component = verb === "get" ? "Get" : "Mutate";

const isOk = ([statusCode]: [string, ResponseObject | ReferenceObject]) =>
statusCode.toString().startsWith("2") || statusCode.toString().startsWith("3");
const isError = (responses: [string, ResponseObject | ReferenceObject]) => !isOk(responses);

const responseTypes = getResponseTypes(Object.entries(operation.responses).filter(isOk));
const errorTypes = getResponseTypes(Object.entries(operation.responses).filter(isError));
const responseTypes = getResReqTypes(Object.entries(operation.responses).filter(isOk));
const errorTypes = getResReqTypes(Object.entries(operation.responses).filter(isError));
const requestBodyTypes = getResReqTypes([["body", operation.requestBody!]]);

const paramsInPath = getParamsInPath(route);
const { query: queryParams = [], path: pathParams = [] } = groupBy(
Expand All @@ -238,16 +247,19 @@ export const generateGetComponent = (operation: OperationObject, verb: string, r
...queryParams.map(p => `${p.name}${p.required ? "" : "?"}: ${resolveValue(p.schema!)}`),
].join("; ");

const genericsTypes =
verb === "get" ? `${responseTypes}, ${errorTypes}` : `${errorTypes}, ${responseTypes}, ${requestBodyTypes}`;

return `
export type ${componentName}Props = Omit<GetProps<${responseTypes}, ${errorTypes}>, "path">${
export type ${componentName}Props = Omit<${Component}Props<${genericsTypes}>, "path">${
params.length ? ` & {${paramsTypes}}` : ""
};
${operation.summary ? "// " + operation.summary : ""}
export const ${componentName} = (${
params.length ? `{${params.join(", ")}, ...props}` : "props"
}: ${componentName}Props) => (
<Get<${responseTypes}, ${errorTypes}>
<${Component}<${genericsTypes}>
path=${
queryParams.length
? `{\`${route}?\${qs.stringify({${queryParams.map(p => p.name).join(", ")}})}\`}`
Expand Down Expand Up @@ -299,10 +311,7 @@ export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
output += generateSchemaDefinition(schema.components && schema.components.schemas);
Object.entries(schema.paths).forEach(([route, verbs]: [string, PathItemObject]) => {
Object.entries(verbs).forEach(([verb, operation]: [string, OperationObject]) => {
if (verb === "get") {
output += generateGetComponent(operation, verb, route, baseUrl);
}
// @todo deal with `post`, `put`, `patch`, `delete` verbs
output += generateRestfulComponent(operation, verb, route, baseUrl);
});
});

Expand Down
24 changes: 24 additions & 0 deletions src/scripts/tests/__snapshots__/import-open-api.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,18 @@ export const FindPets = ({tags, limit, ...props}: FindPetsProps) => (
);
export type AddPetProps = Omit<MutateProps<Error, Pet, NewPet>, \\"path\\">;
export const AddPet = (props: AddPetProps) => (
<Mutate<Error, Pet, NewPet>
path={\`/pets\`}
base=\\"http://localhost\\"
{...props}
/>
);
export type FindPetByIdProps = Omit<GetProps<Pet, Error>, \\"path\\"> & {id: number};
Expand All @@ -38,5 +50,17 @@ export const FindPetById = ({id, ...props}: FindPetByIdProps) => (
/>
);
export type DeletePetProps = Omit<MutateProps<Error, void, void>, \\"path\\"> & {id: number};
export const DeletePet = ({id, ...props}: DeletePetProps) => (
<Mutate<Error, void, void>
path={\`/pets/\${id}\`}
base=\\"http://localhost\\"
{...props}
/>
);
"
`;
77 changes: 67 additions & 10 deletions src/scripts/tests/import-open-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import { join } from "path";
import { OperationObject, ResponseObject } from "openapi3-ts";

import importOpenApi, {
generateGetComponent,
generateRestfulComponent,
generateSchemaDefinition,
getArray,
getObject,
getRef,
getResponseTypes,
getResReqTypes,
getScalar,
isReference,
} from "../import-open-api";
Expand Down Expand Up @@ -268,7 +268,7 @@ describe("scripts/import-open-api", () => {
],
];

expect(getResponseTypes(responses)).toEqual("FieldListResponse");
expect(getResReqTypes(responses)).toEqual("FieldListResponse");
});

it("should return the type of application/octet-stream if we don't have application/json response", () => {
Expand All @@ -282,7 +282,7 @@ describe("scripts/import-open-api", () => {
],
];

expect(getResponseTypes(responses)).toEqual("FieldListResponse");
expect(getResReqTypes(responses)).toEqual("FieldListResponse");
});

it("should return a union if we have multi responses", () => {
Expand All @@ -307,7 +307,7 @@ describe("scripts/import-open-api", () => {
],
];

expect(getResponseTypes(responses)).toEqual("FieldListResponse | {id: string}");
expect(getResReqTypes(responses)).toEqual("FieldListResponse | {id: string}");
});

it("should not generate type duplication", () => {
Expand All @@ -328,7 +328,7 @@ describe("scripts/import-open-api", () => {
],
];

expect(getResponseTypes(responses)).toEqual("FieldListResponse");
expect(getResReqTypes(responses)).toEqual("FieldListResponse");
});
});

Expand All @@ -355,7 +355,7 @@ describe("scripts/import-open-api", () => {
},
};

expect(generateGetComponent(operation, "get", "/fields", "http://localhost")).toEqual(`
expect(generateRestfulComponent(operation, "get", "/fields", "http://localhost")).toEqual(`
export type ListFieldsProps = Omit<GetProps<FieldListResponse, APIError>, "path">;
// List all fields for the use case schema
Expand Down Expand Up @@ -396,7 +396,7 @@ export const ListFields = (props: ListFieldsProps) => (
},
};

expect(generateGetComponent(operation, "get", "/fields", "http://localhost")).toEqual(`
expect(generateRestfulComponent(operation, "get", "/fields", "http://localhost")).toEqual(`
export type ListFieldsProps = Omit<GetProps<FieldListResponse, APIError>, "path">;
// List all fields for the use case schema
Expand Down Expand Up @@ -452,7 +452,7 @@ export const ListFields = (props: ListFieldsProps) => (
},
};

expect(generateGetComponent(operation, "get", "/fields", "http://localhost")).toEqual(`
expect(generateRestfulComponent(operation, "get", "/fields", "http://localhost")).toEqual(`
export type ListFieldsProps = Omit<GetProps<FieldListResponse, APIError>, "path"> & {tenantId: string; projectId?: string};
// List all fields for the use case schema
Expand Down Expand Up @@ -509,7 +509,7 @@ export const ListFields = ({tenantId, projectId, ...props}: ListFieldsProps) =>
},
};

expect(generateGetComponent(operation, "get", "/fields/{id}", "http://localhost")).toEqual(`
expect(generateRestfulComponent(operation, "get", "/fields/{id}", "http://localhost")).toEqual(`
export type ListFieldsProps = Omit<GetProps<FieldListResponse, APIError>, "path"> & {id: string};
// List all fields for the use case schema
Expand All @@ -521,6 +521,63 @@ export const ListFields = ({id, ...props}: ListFieldsProps) => (
/>
);
`);
});

it("should generate a Mutate type component", () => {
const operation: OperationObject = {
summary: "Update use case details",
operationId: "updateUseCase",
tags: ["use-case"],
parameters: [
{
name: "tenantId",
in: "path",
required: true,
description: "The id of the Contiamo tenant",
schema: { type: "string" },
},
{
name: "useCaseId",
in: "path",
required: true,
description: "The id of the use case",
schema: { type: "string", format: "uuid" },
},
],
requestBody: {
required: true,
content: { "application/json": { schema: { $ref: "#/components/schemas/UseCaseInstance" } } },
},
responses: {
"204": {
description: "Use case updated",
content: { "application/json": { schema: { $ref: "#/components/schemas/UseCaseResponse" } } },
},
default: {
description: "unexpected error",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/APIError" },
example: { errors: ["msg1", "msg2"] },
},
},
},
},
};

expect(generateRestfulComponent(operation, "put", "/use-cases/{useCaseId}", "http://localhost")).toEqual(`
export type UpdateUseCaseProps = Omit<MutateProps<APIError, UseCaseResponse, UseCaseInstance>, "path"> & {useCaseId: string};
// Update use case details
export const UpdateUseCase = ({useCaseId, ...props}: UpdateUseCaseProps) => (
<Mutate<APIError, UseCaseResponse, UseCaseInstance>
path={\`/use-cases/\${useCaseId}\`}
base="http://localhost"
{...props}
/>
);
`);
});
});
Expand Down

0 comments on commit 019a147

Please sign in to comment.