From 1a3e52b99ff9f945fe61122ce11e0c3832b91a7a Mon Sep 17 00:00:00 2001 From: Fabien BERNARD Date: Fri, 17 May 2019 12:20:41 +0200 Subject: [PATCH 1/5] Improve TBody type & add requestOptions to useMutate With this version, `mutate` is waiting for a body, it's not anymore optional :) --- src/Mutate.tsx | 7 ++- src/useMutate.test.tsx | 118 +++++++++++++++++++++++++++++++++++++---- src/useMutate.tsx | 9 ++-- 3 files changed, 118 insertions(+), 16 deletions(-) diff --git a/src/Mutate.tsx b/src/Mutate.tsx index 4ba68303..4f28f956 100644 --- a/src/Mutate.tsx +++ b/src/Mutate.tsx @@ -16,7 +16,10 @@ export interface States { error?: GetState["error"]; } -export type MutateMethod = (data?: TRequestBody) => Promise; +export type MutateMethod = ( + data: TRequestBody, + mutateRequestOptions?: RequestInit, +) => Promise; /** * Meta information returned to the fetchable @@ -115,7 +118,7 @@ class ContextlessMutate extends React this.abortController.abort(); } - public mutate = async (body?: string | TRequestBody, mutateRequestOptions?: RequestInit) => { + public mutate = async (body: TRequestBody, mutateRequestOptions?: RequestInit) => { const { __internal_hasExplicitBase, base, diff --git a/src/useMutate.test.tsx b/src/useMutate.test.tsx index c7c352a0..54e7bccc 100644 --- a/src/useMutate.test.tsx +++ b/src/useMutate.test.tsx @@ -3,6 +3,8 @@ import nock from "nock"; import React from "react"; import { renderHook } from "react-hooks-testing-library"; import { RestfulProvider, useMutate } from "."; +import { Omit } from "./useGet"; +import { UseMutateProps } from "./useMutate"; describe("useMutate", () => { // Mute console.error -> https://github.com/kentcdodds/react-testing-library/issues/281 @@ -140,8 +142,10 @@ describe("useMutate", () => { const wrapper = ({ children }) => ( {children} ); - const { result } = renderHook(() => useMutate("POST", "plop"), { wrapper }); - result.current.mutate(); + const { result } = renderHook(() => useMutate<{ id: number }, unknown, {}, {}>("POST", "plop"), { + wrapper, + }); + result.current.mutate({}); expect(result.current).toMatchObject({ error: null, @@ -157,8 +161,8 @@ describe("useMutate", () => { const wrapper = ({ children }) => ( {children} ); - const { result } = renderHook(() => useMutate("POST", ""), { wrapper }); - const res = await result.current.mutate(); + const { result } = renderHook(() => useMutate<{ id: number }, unknown, {}, {}>("POST", ""), { wrapper }); + const res = await result.current.mutate({}); expect(result.current).toMatchObject({ error: null, @@ -254,9 +258,11 @@ describe("useMutate", () => { {children} ); - const { result } = renderHook(() => useMutate("POST", ""), { wrapper }); + const { result } = renderHook(() => useMutate<{ id: number }, { message: string }, {}, {}>("POST", ""), { + wrapper, + }); - await result.current.mutate().catch(() => { + await result.current.mutate({}).catch(() => { /* noop */ }); @@ -301,10 +307,10 @@ describe("useMutate", () => { {children} ); const { result } = renderHook( - () => useMutate<{ id: number }>("POST", "", { resolve: data => ({ id: data.id * 2 }) }), + () => useMutate<{ id: number }, unknown, {}, {}>("POST", "", { resolve: data => ({ id: data.id * 2 }) }), { wrapper }, ); - const res = await result.current.mutate(); + const res = await result.current.mutate({}); expect(result.current).toMatchObject({ error: null, @@ -323,7 +329,7 @@ describe("useMutate", () => { ); const { result } = renderHook( () => - useMutate<{ id: number }>("POST", "", { + useMutate<{ id: number }, unknown, {}, {}>("POST", "", { resolve: () => { throw new Error("I don't like your data!"); }, @@ -332,7 +338,7 @@ describe("useMutate", () => { ); try { - await result.current.mutate(); + await result.current.mutate({}); expect("this statement").toBe("not executed"); } catch (e) { expect(result.current).toMatchObject({ @@ -346,4 +352,96 @@ describe("useMutate", () => { } }); }); + + describe("generation pattern", () => { + it("should call the correct endpoint (DELETE)", async () => { + nock("https://my-awesome-api.fake") + .delete("/plop") + .query({ force: true }) + .reply(200, { id: 1 }); + + interface MyCustomEnpointResponse { + id: number; + } + + interface MyCustomEnpointQueryParams { + force?: boolean; + } + + interface MyCustomEnpointError { + message: string; + code: number; + } + + type UseDeleteMyCustomEndpoint = Omit< + UseMutateProps, + "path" | "verb" + >; + const useDeleteMyCustomEndpoint = (props?: UseDeleteMyCustomEndpoint) => + useMutate( + "DELETE", + "", + props, + ); + + const wrapper = ({ children }) => ( + {children} + ); + const { result } = renderHook(() => useDeleteMyCustomEndpoint({ queryParams: { force: true } }), { wrapper }); + const res = await result.current.mutate("plop"); + + expect(result.current).toMatchObject({ + error: null, + loading: false, + }); + expect(res).toEqual({ id: 1 }); + }); + + it("should call the correct endpoint (POST)", async () => { + nock("https://my-awesome-api.fake") + .post("/plop", { id: 1 }) + .query({ force: true }) + .reply(200, { id: 1 }); + + interface MyCustomEnpointResponse { + id: number; + } + + interface MyCustomEnpointQueryParams { + force?: boolean; + } + + interface MyCustomEnpointError { + message: string; + code: number; + } + + interface MyCustomEndpointBody { + id: number; + } + + type UseDeleteMyCustomEndpoint = Omit< + UseMutateProps, + "path" | "verb" + >; + const useDeleteMyCustomEndpoint = (props?: UseDeleteMyCustomEndpoint) => + useMutate( + "POST", + "plop", + props, + ); + + const wrapper = ({ children }) => ( + {children} + ); + const { result } = renderHook(() => useDeleteMyCustomEndpoint({ queryParams: { force: true } }), { wrapper }); + const res = await result.current.mutate({ id: 1 }); + + expect(result.current).toMatchObject({ + error: null, + loading: false, + }); + expect(res).toEqual({ id: 1 }); + }); + }); }); diff --git a/src/useMutate.tsx b/src/useMutate.tsx index ee67504e..e5b49691 100644 --- a/src/useMutate.tsx +++ b/src/useMutate.tsx @@ -5,7 +5,8 @@ import { MutateMethod, MutateState } from "./Mutate"; import { Omit, resolvePath, UseGetProps } from "./useGet"; import { processResponse } from "./util/processResponse"; -export interface UseMutateProps extends Omit, "lazy"> { +export interface UseMutateProps + extends Omit, "lazy" | "debounce"> { /** * What HTTP verb are we using? */ @@ -57,7 +58,7 @@ export function useMutate< useEffect(() => () => abortController.current.abort(), []); const mutate = useCallback>( - async body => { + async (body: TRequestBody, mutateRequestOptions?: RequestInit) => { if (state.error || !state.loading) { setState(prevState => ({ ...prevState, loading: true, error: null })); } @@ -69,7 +70,7 @@ export function useMutate< } const signal = abortController.current.signal; - const requestOptions = + const propsRequestOptions = (typeof props.requestOptions === "function" ? props.requestOptions() : props.requestOptions) || {}; const contextRequestOptions = @@ -88,7 +89,7 @@ export function useMutate< const request = new Request( resolvePath(base, isDelete ? `${path}/${body}` : path, queryParams), - merge({}, contextRequestOptions, options, requestOptions, { signal }), + merge({}, contextRequestOptions, options, propsRequestOptions, mutateRequestOptions, { signal }), ); const response = await fetch(request); From f0b417179f4ab5db83ae17a00b0696fe1580a05b Mon Sep 17 00:00:00 2001 From: Fabien BERNARD Date: Fri, 17 May 2019 13:47:21 +0200 Subject: [PATCH 2/5] Generate useMutate from openAPI --- src/scripts/import-open-api.ts | 20 +- .../import-open-api.test.ts.snap | 14 +- src/scripts/tests/import-open-api.test.ts | 445 +++++++++--------- 3 files changed, 257 insertions(+), 222 deletions(-) diff --git a/src/scripts/import-open-api.ts b/src/scripts/import-open-api.ts index dfae6947..253e3b8d 100644 --- a/src/scripts/import-open-api.ts +++ b/src/scripts/import-open-api.ts @@ -318,16 +318,16 @@ export const generateRestfulComponent = ( }` : `${needAResponseComponent ? componentName + "Response" : responseTypes}, ${errorTypes}, ${ queryParamsType ? componentName + "QueryParams" : "void" - }, ${requestBodyTypes}`; + }, ${verb === "delete" ? "string" : requestBodyTypes}`; - const genericsTypesWithoutError = + const genericsTypesForHooksProps = verb === "get" ? `${needAResponseComponent ? componentName + "Response" : responseTypes}, ${ queryParamsType ? componentName + "QueryParams" : "void" }` : `${needAResponseComponent ? componentName + "Response" : responseTypes}, ${ queryParamsType ? componentName + "QueryParams" : "void" - }, ${requestBodyTypes}`; + }`; let output = `${ needAResponseComponent @@ -366,18 +366,18 @@ export const ${componentName} = (${ `; // Hooks version - if (verb === "get" /* TODO: Remove this condition after `useMutate` implementation */) { - output += `export type Use${componentName}Props = Omit, "path"${ - verb === "get" ? "" : ` | "verb"` - }>${paramsInPath.length ? ` & {${paramsTypes}}` : ""}; + output += `export type Use${componentName}Props = Omit, "path"${ + verb === "get" ? "" : ` | "verb"` + }>${paramsInPath.length ? ` & {${paramsTypes}}` : ""}; ${operation.summary ? "// " + operation.summary : ""} export const use${componentName} = (${ - paramsInPath.length ? `{${paramsInPath.join(", ")}, ...props}` : "props" - }: Use${componentName}Props) => use${Component}<${genericsTypes}>(\`${route}\`, props); + paramsInPath.length ? `{${paramsInPath.join(", ")}, ...props}` : "props" + }: Use${componentName}Props) => use${Component}<${genericsTypes}>(${ + verb === "get" ? "" : `"${verb.toUpperCase()}", ` + }\`${route}\`, props); `; - } if (headerParams.map(({ name }) => name.toLocaleLowerCase()).includes("prefer")) { output += `export type Poll${componentName}Props = Omit, "path">${ 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 2ffe2bea..d7ddc618 100644 --- a/src/scripts/tests/__snapshots__/import-open-api.test.ts.snap +++ b/src/scripts/tests/__snapshots__/import-open-api.test.ts.snap @@ -43,6 +43,11 @@ export const AddPet = (props: AddPetProps) => ( /> ); +export type UseAddPetProps = Omit, \\"path\\" | \\"verb\\">; + + +export const useAddPet = (props: UseAddPetProps) => useMutate(\\"POST\\", \`/pets\`, props); + export type FindPetByIdProps = Omit, \\"path\\"> & {id: number}; @@ -60,16 +65,21 @@ export type UseFindPetByIdProps = Omit, \\"path\\"> & {id export const useFindPetById = ({id, ...props}: UseFindPetByIdProps) => useGet(\`/pets/\${id}\`, props); -export type DeletePetProps = Omit, \\"path\\" | \\"verb\\">; +export type DeletePetProps = Omit, \\"path\\" | \\"verb\\">; export const DeletePet = (props: DeletePetProps) => ( - + verb=\\"DELETE\\" path={\`/pets\`} {...props} /> ); +export type UseDeletePetProps = Omit, \\"path\\" | \\"verb\\">; + + +export const useDeletePet = (props: UseDeletePetProps) => useMutate(\\"DELETE\\", \`/pets\`, props); + " `; diff --git a/src/scripts/tests/import-open-api.test.ts b/src/scripts/tests/import-open-api.test.ts index 28514db6..93e3cf6b 100644 --- a/src/scripts/tests/import-open-api.test.ts +++ b/src/scripts/tests/import-open-api.test.ts @@ -505,24 +505,24 @@ export interface JobRunResponse {} }; expect(generateRestfulComponent(operation, "get", "/fields", [])).toMatchInlineSnapshot(` -" -export type ListFieldsProps = Omit, \\"path\\">; + " + export type ListFieldsProps = Omit, \\"path\\">; -// List all fields for the use case schema -export const ListFields = (props: ListFieldsProps) => ( - - path={\`/fields\`} - {...props} - /> -); + // List all fields for the use case schema + export const ListFields = (props: ListFieldsProps) => ( + + path={\`/fields\`} + {...props} + /> + ); -export type UseListFieldsProps = Omit, \\"path\\">; + export type UseListFieldsProps = Omit, \\"path\\">; -// List all fields for the use case schema -export const useListFields = (props: UseListFieldsProps) => useGet(\`/fields\`, props); + // List all fields for the use case schema + export const useListFields = (props: UseListFieldsProps) => useGet(\`/fields\`, props); -" -`); + " + `); }); it("should add a fallback if the error is not defined", () => { @@ -539,24 +539,24 @@ export const useListFields = (props: UseListFieldsProps) => useGet, \\"path\\">; + " + export type ListFieldsProps = Omit, \\"path\\">; -// List all fields for the use case schema -export const ListFields = (props: ListFieldsProps) => ( - - path={\`/fields\`} - {...props} - /> -); + // List all fields for the use case schema + export const ListFields = (props: ListFieldsProps) => ( + + path={\`/fields\`} + {...props} + /> + ); -export type UseListFieldsProps = Omit, \\"path\\">; + export type UseListFieldsProps = Omit, \\"path\\">; -// List all fields for the use case schema -export const useListFields = (props: UseListFieldsProps) => useGet(\`/fields\`, props); + // List all fields for the use case schema + export const useListFields = (props: UseListFieldsProps) => useGet(\`/fields\`, props); -" -`); + " + `); }); it("should remove duplicate types", () => { @@ -586,24 +586,24 @@ export const useListFields = (props: UseListFieldsProps) => useGet, \\"path\\">; + " + export type ListFieldsProps = Omit, \\"path\\">; -// List all fields for the use case schema -export const ListFields = (props: ListFieldsProps) => ( - - path={\`/fields\`} - {...props} - /> -); + // List all fields for the use case schema + export const ListFields = (props: ListFieldsProps) => ( + + path={\`/fields\`} + {...props} + /> + ); -export type UseListFieldsProps = Omit, \\"path\\">; + export type UseListFieldsProps = Omit, \\"path\\">; -// List all fields for the use case schema -export const useListFields = (props: UseListFieldsProps) => useGet(\`/fields\`, props); + // List all fields for the use case schema + export const useListFields = (props: UseListFieldsProps) => useGet(\`/fields\`, props); -" -`); + " + `); }); it("should deal with parameters in query", () => { @@ -648,26 +648,26 @@ export const useListFields = (props: UseListFieldsProps) => useGet, \\"path\\">; + export type ListFieldsProps = Omit, \\"path\\">; -// List all fields for the use case schema -export const ListFields = (props: ListFieldsProps) => ( - - path={\`/fields\`} - {...props} - /> -); + // List all fields for the use case schema + export const ListFields = (props: ListFieldsProps) => ( + + path={\`/fields\`} + {...props} + /> + ); -export type UseListFieldsProps = Omit, \\"path\\">; + export type UseListFieldsProps = Omit, \\"path\\">; -// List all fields for the use case schema -export const useListFields = (props: UseListFieldsProps) => useGet(\`/fields\`, props); + // List all fields for the use case schema + export const useListFields = (props: UseListFieldsProps) => useGet(\`/fields\`, props); -" -`); + " + `); }); it("should deal with parameters in query (root level)", () => { const operation: OperationObject = { @@ -718,26 +718,26 @@ export const useListFields = (props: UseListFieldsProps) => useGet, \\"path\\">; + export type ListFieldsProps = Omit, \\"path\\">; -// List all fields for the use case schema -export const ListFields = (props: ListFieldsProps) => ( - - path={\`/fields\`} - {...props} - /> -); + // List all fields for the use case schema + export const ListFields = (props: ListFieldsProps) => ( + + path={\`/fields\`} + {...props} + /> + ); -export type UseListFieldsProps = Omit, \\"path\\">; + export type UseListFieldsProps = Omit, \\"path\\">; -// List all fields for the use case schema -export const useListFields = (props: UseListFieldsProps) => useGet(\`/fields\`, props); + // List all fields for the use case schema + export const useListFields = (props: UseListFieldsProps) => useGet(\`/fields\`, props); -" -`); + " + `); }); it("should deal with parameters in path", () => { @@ -783,24 +783,24 @@ export const useListFields = (props: UseListFieldsProps) => useGet, \\"path\\"> & {id: string}; + " + export type ListFieldsProps = Omit, \\"path\\"> & {id: string}; -// List all fields for the use case schema -export const ListFields = ({id, ...props}: ListFieldsProps) => ( - - path={\`/fields/\${id}\`} - {...props} - /> -); + // List all fields for the use case schema + export const ListFields = ({id, ...props}: ListFieldsProps) => ( + + path={\`/fields/\${id}\`} + {...props} + /> + ); -export type UseListFieldsProps = Omit, \\"path\\"> & {id: string}; + export type UseListFieldsProps = Omit, \\"path\\"> & {id: string}; -// List all fields for the use case schema -export const useListFields = ({id, ...props}: UseListFieldsProps) => useGet(\`/fields/\${id}\`, props); + // List all fields for the use case schema + export const useListFields = ({id, ...props}: UseListFieldsProps) => useGet(\`/fields/\${id}\`, props); -" -`); + " + `); }); it("should deal with parameters in path (root level)", () => { @@ -853,24 +853,24 @@ export const useListFields = ({id, ...props}: UseListFieldsProps) => useGet, \\"path\\"> & {id: string}; + " + export type ListFieldsProps = Omit, \\"path\\"> & {id: string}; -// List all fields for the use case schema -export const ListFields = ({id, ...props}: ListFieldsProps) => ( - - path={\`/fields/\${id}\`} - {...props} - /> -); + // List all fields for the use case schema + export const ListFields = ({id, ...props}: ListFieldsProps) => ( + + path={\`/fields/\${id}\`} + {...props} + /> + ); -export type UseListFieldsProps = Omit, \\"path\\"> & {id: string}; + export type UseListFieldsProps = Omit, \\"path\\"> & {id: string}; -// List all fields for the use case schema -export const useListFields = ({id, ...props}: UseListFieldsProps) => useGet(\`/fields/\${id}\`, props); + // List all fields for the use case schema + export const useListFields = ({id, ...props}: UseListFieldsProps) => useGet(\`/fields/\${id}\`, props); -" -`); + " + `); }); it("should generate a Mutate type component", () => { @@ -916,20 +916,25 @@ export const useListFields = ({id, ...props}: UseListFieldsProps) => useGet, \\"path\\" | \\"verb\\"> & {useCaseId: string}; - -// Update use case details -export const UpdateUseCase = ({useCaseId, ...props}: UpdateUseCaseProps) => ( - - verb=\\"PUT\\" - path={\`/use-cases/\${useCaseId}\`} - {...props} - /> -); - -" -`); + " + export type UpdateUseCaseProps = Omit, \\"path\\" | \\"verb\\"> & {useCaseId: string}; + + // Update use case details + export const UpdateUseCase = ({useCaseId, ...props}: UpdateUseCaseProps) => ( + + verb=\\"PUT\\" + path={\`/use-cases/\${useCaseId}\`} + {...props} + /> + ); + + export type UseUpdateUseCaseProps = Omit, \\"path\\" | \\"verb\\"> & {useCaseId: string}; + + // Update use case details + export const useUpdateUseCase = ({useCaseId, ...props}: UseUpdateUseCaseProps) => useMutate(\\"PUT\\", \`/use-cases/\${useCaseId}\`, props); + + " + `); }); it("should generate a proper ComponentResponse type if the type is custom", () => { const operation: OperationObject = { @@ -989,22 +994,27 @@ export const UpdateUseCase = ({useCaseId, ...props}: UpdateUseCaseProps) => ( }; expect(generateRestfulComponent(operation, "put", "/use-cases/{useCaseId}", [])).toMatchInlineSnapshot(` -" -export interface UpdateUseCaseResponse {id: string; name?: string} + " + export interface UpdateUseCaseResponse {id: string; name?: string} -export type UpdateUseCaseProps = Omit, \\"path\\" | \\"verb\\"> & {useCaseId: string}; + export type UpdateUseCaseProps = Omit, \\"path\\" | \\"verb\\"> & {useCaseId: string}; -// Update use case details -export const UpdateUseCase = ({useCaseId, ...props}: UpdateUseCaseProps) => ( - - verb=\\"PUT\\" - path={\`/use-cases/\${useCaseId}\`} - {...props} - /> -); + // Update use case details + export const UpdateUseCase = ({useCaseId, ...props}: UpdateUseCaseProps) => ( + + verb=\\"PUT\\" + path={\`/use-cases/\${useCaseId}\`} + {...props} + /> + ); -" -`); + export type UseUpdateUseCaseProps = Omit, \\"path\\" | \\"verb\\"> & {useCaseId: string}; + + // Update use case details + export const useUpdateUseCase = ({useCaseId, ...props}: UseUpdateUseCaseProps) => useMutate(\\"PUT\\", \`/use-cases/\${useCaseId}\`, props); + + " + `); }); it("should ignore 3xx responses", () => { @@ -1068,22 +1078,27 @@ export const UpdateUseCase = ({useCaseId, ...props}: UpdateUseCaseProps) => ( }; expect(generateRestfulComponent(operation, "put", "/use-cases/{useCaseId}", [])).toMatchInlineSnapshot(` -" -export interface UpdateUseCaseResponse {id: string; name?: string} + " + export interface UpdateUseCaseResponse {id: string; name?: string} -export type UpdateUseCaseProps = Omit, \\"path\\" | \\"verb\\"> & {useCaseId: string}; + export type UpdateUseCaseProps = Omit, \\"path\\" | \\"verb\\"> & {useCaseId: string}; -// Update use case details -export const UpdateUseCase = ({useCaseId, ...props}: UpdateUseCaseProps) => ( - - verb=\\"PUT\\" - path={\`/use-cases/\${useCaseId}\`} - {...props} - /> -); + // Update use case details + export const UpdateUseCase = ({useCaseId, ...props}: UpdateUseCaseProps) => ( + + verb=\\"PUT\\" + path={\`/use-cases/\${useCaseId}\`} + {...props} + /> + ); -" -`); + export type UseUpdateUseCaseProps = Omit, \\"path\\" | \\"verb\\"> & {useCaseId: string}; + + // Update use case details + export const useUpdateUseCase = ({useCaseId, ...props}: UseUpdateUseCaseProps) => useMutate(\\"PUT\\", \`/use-cases/\${useCaseId}\`, props); + + " + `); }); it("should ignore the last param of a delete call (the id is give after)", () => { @@ -1124,20 +1139,25 @@ export const UpdateUseCase = ({useCaseId, ...props}: UpdateUseCaseProps) => ( }; expect(generateRestfulComponent(operation, "delete", "/use-cases/{useCaseId}", [])).toMatchInlineSnapshot(` -" -export type DeleteUseCaseProps = Omit, \\"path\\" | \\"verb\\">; - -// Delete use case -export const DeleteUseCase = (props: DeleteUseCaseProps) => ( - - verb=\\"DELETE\\" - path={\`/use-cases\`} - {...props} - /> -); - -" -`); + " + export type DeleteUseCaseProps = Omit, \\"path\\" | \\"verb\\">; + + // Delete use case + export const DeleteUseCase = (props: DeleteUseCaseProps) => ( + + verb=\\"DELETE\\" + path={\`/use-cases\`} + {...props} + /> + ); + + export type UseDeleteUseCaseProps = Omit, \\"path\\" | \\"verb\\">; + + // Delete use case + export const useDeleteUseCase = (props: UseDeleteUseCaseProps) => useMutate(\\"DELETE\\", \`/use-cases\`, props); + + " + `); }); it("should only remove the last params in delete operation", () => { const operation: OperationObject = { @@ -1177,20 +1197,25 @@ export const DeleteUseCase = (props: DeleteUseCaseProps) => ( }; expect(generateRestfulComponent(operation, "delete", "/use-cases/{useCaseId}/secret", [])).toMatchInlineSnapshot(` -" -export type DeleteUseCaseProps = Omit, \\"path\\" | \\"verb\\"> & {useCaseId: string}; - -// Delete use case -export const DeleteUseCase = ({useCaseId, ...props}: DeleteUseCaseProps) => ( - - verb=\\"DELETE\\" - path={\`/use-cases/\${useCaseId}/secret\`} - {...props} - /> -); - -" -`); + " + export type DeleteUseCaseProps = Omit, \\"path\\" | \\"verb\\"> & {useCaseId: string}; + + // Delete use case + export const DeleteUseCase = ({useCaseId, ...props}: DeleteUseCaseProps) => ( + + verb=\\"DELETE\\" + path={\`/use-cases/\${useCaseId}/secret\`} + {...props} + /> + ); + + export type UseDeleteUseCaseProps = Omit, \\"path\\" | \\"verb\\"> & {useCaseId: string}; + + // Delete use case + export const useDeleteUseCase = ({useCaseId, ...props}: UseDeleteUseCaseProps) => useMutate(\\"DELETE\\", \`/use-cases/\${useCaseId}/secret\`, props); + + " + `); }); it("should generate a Poll compoment if the `prefer` token is present", () => { @@ -1225,34 +1250,34 @@ export const DeleteUseCase = ({useCaseId, ...props}: DeleteUseCaseProps) => ( }; expect(generateRestfulComponent(operation, "get", "/fields", [])).toMatchInlineSnapshot(` -" -export type ListFieldsProps = Omit, \\"path\\">; - -// List all fields for the use case schema -export const ListFields = (props: ListFieldsProps) => ( - - path={\`/fields\`} - {...props} - /> -); - -export type UseListFieldsProps = Omit, \\"path\\">; - -// List all fields for the use case schema -export const useListFields = (props: UseListFieldsProps) => useGet(\`/fields\`, props); - -export type PollListFieldsProps = Omit, \\"path\\">; - -// List all fields for the use case schema (long polling) -export const PollListFields = (props: PollListFieldsProps) => ( - - path={\`/fields\`} - {...props} - /> -); - -" -`); + " + export type ListFieldsProps = Omit, \\"path\\">; + + // List all fields for the use case schema + export const ListFields = (props: ListFieldsProps) => ( + + path={\`/fields\`} + {...props} + /> + ); + + export type UseListFieldsProps = Omit, \\"path\\">; + + // List all fields for the use case schema + export const useListFields = (props: UseListFieldsProps) => useGet(\`/fields\`, props); + + export type PollListFieldsProps = Omit, \\"path\\">; + + // List all fields for the use case schema (long polling) + export const PollListFields = (props: PollListFieldsProps) => ( + + path={\`/fields\`} + {...props} + /> + ); + + " + `); }); }); it("should deal with no 2xx response case", () => { @@ -1276,23 +1301,23 @@ export const PollListFields = (props: PollListFieldsProps) => ( }; expect(generateRestfulComponent(operation, "get", "/fields", [])).toMatchInlineSnapshot(` -" -export type ListFieldsProps = Omit, \\"path\\">; + " + export type ListFieldsProps = Omit, \\"path\\">; -// List all fields for the use case schema -export const ListFields = (props: ListFieldsProps) => ( - - path={\`/fields\`} - {...props} - /> -); + // List all fields for the use case schema + export const ListFields = (props: ListFieldsProps) => ( + + path={\`/fields\`} + {...props} + /> + ); -export type UseListFieldsProps = Omit, \\"path\\">; + export type UseListFieldsProps = Omit, \\"path\\">; -// List all fields for the use case schema -export const useListFields = (props: UseListFieldsProps) => useGet(\`/fields\`, props); + // List all fields for the use case schema + export const useListFields = (props: UseListFieldsProps) => useGet(\`/fields\`, props); -" -`); + " + `); }); }); From 17bd22e43b382ae4e6d9f3e216e3afe058b1dcf1 Mon Sep 17 00:00:00 2001 From: Fabien BERNARD Date: Fri, 17 May 2019 16:06:56 +0200 Subject: [PATCH 3/5] Add hardcore open-api validator :metal: --- README.md | 51 ++++++++++++++++++++------ package.json | 2 + src/bin/restful-react-import.ts | 15 ++++---- src/scripts/ibm-openapi-validator.d.ts | 21 +++++++++++ src/scripts/import-open-api.ts | 51 ++++++++++++++++++++++++++ yarn.lock | 25 ++++++++----- 6 files changed, 137 insertions(+), 28 deletions(-) create mode 100644 src/scripts/ibm-openapi-validator.d.ts diff --git a/README.md b/README.md index 0e14f24a..593f608e 100644 --- a/README.md +++ b/README.md @@ -209,7 +209,11 @@ const MyAnimalsList = props => ( Here are all my {props.animal} s! -
    {animals.map(animal =>
  • {animal}
  • )}
+
    + {animals.map(animal => ( +
  • {animal}
  • + ))} +
)} @@ -234,7 +238,11 @@ const MyAnimalsList = props => ( Here are all my {props.animal} s! -
    {animals.map(animal =>
  • {animal}
  • )}
+
    + {animals.map(animal => ( +
  • {animal}
  • + ))} +
) } @@ -256,7 +264,13 @@ It is possible to render a `Get` component and defer the fetch to a later stage.

Are you ready to unleash all the magic? If yes, click this button!

- {unicorns &&
    {unicorns.map((unicorn, index) =>
  • {unicorn}
  • )}
} + {unicorns && ( +
    + {unicorns.map((unicorn, index) => ( +
  • {unicorn}
  • + ))} +
+ )} )} @@ -281,7 +295,11 @@ const myNestedData = props => ( {data => (

Here's all the things I want

-
    {data.map(thing =>
  • {thing}
  • )}
+
    + {data.map(thing => ( +
  • {thing}
  • + ))} +
)} @@ -302,7 +320,11 @@ const SearchThis = props => ( {data => (

Here's all the things I search

-
    {data.map(thing =>
  • {thing}
  • )}
+
    + {data.map(thing => ( +
  • {thing}
  • + ))} +
)} @@ -373,13 +395,12 @@ const Movies = ({ dispatch }) => (
  • {movie.name} - {(deleteMovie, {loading: isDeleting}) => () - } + {(deleteMovie, { loading: isDeleting }) => ( + + )} +
  • )) } @@ -516,6 +537,12 @@ Your components can then be generated by running `npm run generate-fetcher`. Opt } ``` +#### Validation of the specification + +To enforce the best quality as possible of specification, we have integrate the amazing open-api linter from ibm ([OpenAPI Validator](https://github.com/IBM/openapi-validator)). We strongly encourage you to setup your custom rules with a `.validaterc` file, you can find all useful information about this configuration [here](https://github.com/IBM/openapi-validator/#configuration). + +If it's too noisy, you don't have the time or can't control the open-api specification: just add `--no-validation` flag to the command and this validation step will be skipped :wink: + #### Import from GitHub Adding the `--github` flag to `restful-react import` instead of a `--file` allows us to **create React components from an OpenAPI spec _remotely hosted on GitHub._** _(how is this real life_ 🔥 _)_ diff --git a/package.json b/package.json index 11a419f0..da36e289 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ }, "devDependencies": { "@operational/scripts": "1.4.0-1c795b9", + "@types/chalk": "^2.2.0", "@types/commander": "^2.12.2", "@types/inquirer": "0.0.44", "@types/lodash": "^4.14.123", @@ -85,6 +86,7 @@ }, "dependencies": { "case": "^1.6.1", + "chalk": "^2.4.2", "commander": "^2.19.0", "ibm-openapi-validator": "^0.3.1", "inquirer": "^6.2.2", diff --git a/src/bin/restful-react-import.ts b/src/bin/restful-react-import.ts index 9341d450..35a9580c 100644 --- a/src/bin/restful-react-import.ts +++ b/src/bin/restful-react-import.ts @@ -1,3 +1,4 @@ +import chalk from "chalk"; import program from "commander"; import { existsSync, readFileSync, writeFileSync } from "fs"; import inquirer from "inquirer"; @@ -6,10 +7,13 @@ import request from "request"; import importOpenApi from "../scripts/import-open-api"; +const log = console.log; // tslint:disable-line:no-console + program.option("-o, --output [value]", "output file destination"); program.option("-f, --file [value]", "input file (yaml or json openapi specs)"); program.option("-g, --github [value]", "github path (format: `owner:repo:branch:path`)"); program.option("-t, --transformer [value]", "transformer function path"); +program.option("--no-validation", "skip the validation step (provided by ibm-openapi-validator)"); program.parse(process.argv); (async () => { @@ -27,7 +31,7 @@ program.parse(process.argv); const { ext } = parse(program.file); const format = [".yaml", ".yml"].includes(ext.toLowerCase()) ? "yaml" : "json"; - return importOpenApi(data, format, transformer); + return importOpenApi(data, format, transformer, program.validation); } else if (program.github) { let accessToken: string; const githubTokenPath = join(__dirname, ".githubToken"); @@ -85,7 +89,7 @@ program.parse(process.argv); program.github.toLowerCase().includes(".yaml") || program.github.toLowerCase().includes(".yml") ? "yaml" : "json"; - resolve(importOpenApi(JSON.parse(body).data.repository.object.text, format, transformer)); + resolve(importOpenApi(JSON.parse(body).data.repository.object.text, format, transformer, program.validation)); }); }); } else { @@ -94,11 +98,8 @@ program.parse(process.argv); })() .then(data => { writeFileSync(join(process.cwd(), program.output), data); - - // tslint:disable-next-line:no-console - console.log(`🎉 Your OpenAPI spec has been converted into ready to use restful-react components!`); + log(chalk.green(`🎉 Your OpenAPI spec has been converted into ready to use restful-react components!`)); }) .catch(err => { - // tslint:disable-next-line:no-console - console.error(err); + log(chalk.red(err)); }); diff --git a/src/scripts/ibm-openapi-validator.d.ts b/src/scripts/ibm-openapi-validator.d.ts new file mode 100644 index 00000000..0a2de527 --- /dev/null +++ b/src/scripts/ibm-openapi-validator.d.ts @@ -0,0 +1,21 @@ +declare module "ibm-openapi-validator" { + interface OpenAPIError { + path: string; + message: string; + } + + interface ValidatorResults { + errors: OpenAPIError[]; + warnings: OpenAPIError[]; + } + + /** + * Returns a Promise with the validation results. + * + * @param openApiDoc An object that represents an OpenAPI document. + * @param defaultMode If set to true, the validator will ignore the .validaterc file and will use the configuration defaults. + */ + function validator(openApiDoc: any, defaultMode = false): Promise; + + export default validator; +} diff --git a/src/scripts/import-open-api.ts b/src/scripts/import-open-api.ts index 253e3b8d..55742bc1 100644 --- a/src/scripts/import-open-api.ts +++ b/src/scripts/import-open-api.ts @@ -1,4 +1,6 @@ import { pascal } from "case"; +import chalk from "chalk"; +import openApiValidator from "ibm-openapi-validator"; import get from "lodash/get"; import groupBy from "lodash/groupBy"; import isEmpty from "lodash/isEmpty"; @@ -468,17 +470,62 @@ export interface ${pascal(name)}Response ${type}`; ); }; +/** + * Validate the spec with ibm-openapi-validator (with a custom pretty logger). + * + * @param schema openAPI spec + */ +const validate = async (schema: OpenAPIObject) => { + // tslint:disable:no-console + const log = console.log; + + // Catch the internal console.log to add some information if needed + let haveOpenAPIValidatorOutput = false; + console.log = (...props: any) => { + haveOpenAPIValidatorOutput = true; + log(...props); + }; + const { errors, warnings } = await openApiValidator(schema); + console.log = log; + if (haveOpenAPIValidatorOutput) { + log("More information: https://github.com/IBM/openapi-validator/#configuration"); + } + if (warnings.length) { + log(chalk.yellow("(!) Warnings")); + warnings.forEach(i => + log( + chalk.yellow(` +Message : ${i.message} +Path : ${i.path}`), + ), + ); + } + if (errors.length) { + log(chalk.red("(!) Errors")); + errors.forEach(i => + log( + chalk.red(` +Message : ${i.message} +Path : ${i.path}`), + ), + ); + } + // tslint:enable:no-console +}; + /** * Main entry of the generator. Generate restful-react component from openAPI. * * @param data raw data of the spec * @param format format of the spec * @param transformer custom function to transform your spec + * @param validation validate the spec with ibm-openapi-validator tool */ const importOpenApi = async ( data: string, format: "yaml" | "json", transformer?: (schema: OpenAPIObject) => OpenAPIObject, + validation = false, ) => { const operationIds: string[] = []; let schema = await importSpecs(data, format); @@ -486,6 +533,10 @@ const importOpenApi = async ( schema = transformer(schema); } + if (validation) { + await validate(schema); + } + let output = ""; output += generateSchemasDefinition(schema.components && schema.components.schemas); diff --git a/yarn.lock b/yarn.lock index 48e52024..6172e30e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -616,6 +616,13 @@ resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.2.tgz#f65d3d6389e01eeb458bd54dc8f52b95a9463bc8" integrity sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w== +"@types/chalk@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@types/chalk/-/chalk-2.2.0.tgz#b7f6e446f4511029ee8e3f43075fb5b73fbaa0ba" + integrity sha512-1zzPV9FDe1I/WHhRkf9SNgqtRJWZqrBWgu7JGveuHmmyR9CnAPCie2N/x+iHrgnpYBIcCJWHBoMRv2TRWktsvw== + dependencies: + chalk "*" + "@types/commander@^2.12.2": version "2.12.2" resolved "https://registry.yarnpkg.com/@types/commander/-/commander-2.12.2.tgz#183041a23842d4281478fa5d23c5ca78e6fd08ae" @@ -1903,6 +1910,15 @@ chai@^4.1.2: pathval "^1.1.0" type-detect "^4.0.5" +chalk@*, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.3.0, chalk@^2.3.1, chalk@^2.4.0, chalk@^2.4.1, chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + chalk@^1.0.0, chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" @@ -1914,15 +1930,6 @@ chalk@^1.0.0, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" -chalk@^2.0.0, chalk@^2.0.1, chalk@^2.3.0, chalk@^2.3.1, chalk@^2.4.0, chalk@^2.4.1, chalk@^2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - character-entities-legacy@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-1.1.2.tgz#7c6defb81648498222c9855309953d05f4d63a9c" From ed78ff2f7b8586991e14497fdc5779a59c96e9f1 Mon Sep 17 00:00:00 2001 From: Fabien BERNARD Date: Fri, 17 May 2019 16:11:52 +0200 Subject: [PATCH 4/5] Import useMutate on generated components --- src/scripts/import-open-api.ts | 2 +- src/scripts/tests/__snapshots__/import-open-api.test.ts.snap | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/scripts/import-open-api.ts b/src/scripts/import-open-api.ts index 55742bc1..c28abcc7 100644 --- a/src/scripts/import-open-api.ts +++ b/src/scripts/import-open-api.ts @@ -558,7 +558,7 @@ const importOpenApi = async ( imports.push("Get", "GetProps", "useGet", "UseGetProps"); } if (haveMutate) { - imports.push("Mutate", "MutateProps"); + imports.push("Mutate", "MutateProps", "useMutate", "UseMutateProps"); } if (havePoll) { imports.push("Poll", "PollProps"); 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 d7ddc618..6e3d4f95 100644 --- a/src/scripts/tests/__snapshots__/import-open-api.test.ts.snap +++ b/src/scripts/tests/__snapshots__/import-open-api.test.ts.snap @@ -4,7 +4,7 @@ exports[`scripts/import-open-api should parse correctly petstore-expanded.yaml 1 "/* Generated by restful-react */ import React from \\"react\\"; -import { Get, GetProps, useGet, UseGetProps, Mutate, MutateProps } from \\"restful-react\\"; +import { Get, GetProps, useGet, UseGetProps, Mutate, MutateProps, useMutate, UseMutateProps } from \\"restful-react\\"; export type Omit = Pick>; From bc4df4b28191f91584660b100bde06143f495dcb Mon Sep 17 00:00:00 2001 From: Fabien BERNARD Date: Fri, 17 May 2019 17:57:03 +0200 Subject: [PATCH 5/5] Add a bit more explainations about the console.log override --- src/scripts/import-open-api.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/scripts/import-open-api.ts b/src/scripts/import-open-api.ts index c28abcc7..5871bfc9 100644 --- a/src/scripts/import-open-api.ts +++ b/src/scripts/import-open-api.ts @@ -480,14 +480,17 @@ const validate = async (schema: OpenAPIObject) => { const log = console.log; // Catch the internal console.log to add some information if needed - let haveOpenAPIValidatorOutput = false; + // because openApiValidator() calls console.log internally and + // we want to add more context if it's used + let wasConsoleLogCalledFromBlackBox = false; console.log = (...props: any) => { - haveOpenAPIValidatorOutput = true; + wasConsoleLogCalledFromBlackBox = true; log(...props); }; const { errors, warnings } = await openApiValidator(schema); - console.log = log; - if (haveOpenAPIValidatorOutput) { + console.log = log; // reset console.log because we're done with the black box + + if (wasConsoleLogCalledFromBlackBox) { log("More information: https://github.com/IBM/openapi-validator/#configuration"); } if (warnings.length) {