From 019a14732120ad3828decc4759bc100715bf05d5 Mon Sep 17 00:00:00 2001 From: Fabien BERNARD Date: Wed, 10 Oct 2018 00:07:10 +0200 Subject: [PATCH] Generate Mutate component FTW \o/ --- src/scripts/import-open-api.ts | 37 +++++---- .../import-open-api.test.ts.snap | 24 ++++++ src/scripts/tests/import-open-api.test.ts | 77 ++++++++++++++++--- 3 files changed, 114 insertions(+), 24 deletions(-) diff --git a/src/scripts/import-open-api.ts b/src/scripts/import-open-api.ts index 42aa38a8..8706e9ec 100644 --- a/src/scripts/import-open-api.ts +++ b/src/scripts/import-open-api.ts @@ -10,6 +10,7 @@ import { ParameterObject, PathItemObject, ReferenceObject, + RequestBodyObject, ResponseObject, SchemaObject, } from "openapi3-ts"; @@ -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 { @@ -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( @@ -238,8 +247,11 @@ 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, "path">${ +export type ${componentName}Props = Omit<${Component}Props<${genericsTypes}>, "path">${ params.length ? ` & {${paramsTypes}}` : "" }; @@ -247,7 +259,7 @@ ${operation.summary ? "// " + operation.summary : ""} export const ${componentName} = (${ params.length ? `{${params.join(", ")}, ...props}` : "props" }: ${componentName}Props) => ( - + <${Component}<${genericsTypes}> path=${ queryParams.length ? `{\`${route}?\${qs.stringify({${queryParams.map(p => p.name).join(", ")}})}\`}` @@ -299,10 +311,7 @@ export type Omit = Pick>; 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); }); }); diff --git a/src/scripts/tests/__snapshots__/import-open-api.test.ts.snap b/src/scripts/tests/__snapshots__/import-open-api.test.ts.snap index a5d61d75..eaca2020 100644 --- a/src/scripts/tests/__snapshots__/import-open-api.test.ts.snap +++ b/src/scripts/tests/__snapshots__/import-open-api.test.ts.snap @@ -27,6 +27,18 @@ export const FindPets = ({tags, limit, ...props}: FindPetsProps) => ( ); +export type AddPetProps = Omit, \\"path\\">; + + +export const AddPet = (props: AddPetProps) => ( + + path={\`/pets\`} + base=\\"http://localhost\\" + {...props} + /> +); + + export type FindPetByIdProps = Omit, \\"path\\"> & {id: number}; @@ -38,5 +50,17 @@ export const FindPetById = ({id, ...props}: FindPetByIdProps) => ( /> ); + +export type DeletePetProps = Omit, \\"path\\"> & {id: number}; + + +export const DeletePet = ({id, ...props}: DeletePetProps) => ( + + path={\`/pets/\${id}\`} + base=\\"http://localhost\\" + {...props} + /> +); + " `; diff --git a/src/scripts/tests/import-open-api.test.ts b/src/scripts/tests/import-open-api.test.ts index 08906959..2e16aad0 100644 --- a/src/scripts/tests/import-open-api.test.ts +++ b/src/scripts/tests/import-open-api.test.ts @@ -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"; @@ -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", () => { @@ -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", () => { @@ -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", () => { @@ -328,7 +328,7 @@ describe("scripts/import-open-api", () => { ], ]; - expect(getResponseTypes(responses)).toEqual("FieldListResponse"); + expect(getResReqTypes(responses)).toEqual("FieldListResponse"); }); }); @@ -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, "path">; // List all fields for the use case schema @@ -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, "path">; // List all fields for the use case schema @@ -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, "path"> & {tenantId: string; projectId?: string}; // List all fields for the use case schema @@ -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, "path"> & {id: string}; // List all fields for the use case schema @@ -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, "path"> & {useCaseId: string}; + +// Update use case details +export const UpdateUseCase = ({useCaseId, ...props}: UpdateUseCaseProps) => ( + + path={\`/use-cases/\${useCaseId}\`} + base="http://localhost" + {...props} + /> +); + `); }); });