From 8e9f8ac4de3028558dcdfd706000d5c0bee0fcf6 Mon Sep 17 00:00:00 2001 From: Shodai Suzuki Date: Sun, 1 Sep 2024 17:17:14 +0900 Subject: [PATCH] feat(fetch): support `explode` property (#1604) * fix: properly append array values to `normalizedParams` object * chore: compress nest of if statement * feat: reference `explode` property * fix: remove unnecessary `else if` statement * chore: refactoring for extract `else if` statements * chore: remove leading newline * fix: avoid null error in array * chore: refactoring to decompose `if` statements * chore: add using `explode` test case * fix: avoid error in `parameters` not exsit case * chore: update sample apps * chore: ignore `.next` directory * fix: add `return` to explode array proccess * fix: normal append is not necessary when only explode * fix: supports cases where explode is both true/false --------- Co-authored-by: Paul ter Laak --- .gitignore | 3 +- packages/fetch/src/index.ts | 55 ++++++++++++++++--- .../next-app/app/gen/pets/pets.msw.ts | 3 + .../next-app/app/gen/pets/pets.ts | 6 +- .../next-app/next-env.d.ts | 5 ++ .../next-app-with-fetch/app/gen/pets/pets.ts | 6 +- .../src/api/endpoints/swaggerPetstore.ts | 6 +- .../custom-fetch/src/gen/pets/pets.ts | 6 +- .../custom-fetch/src/gen/pets/pets.ts | 6 +- .../src/gen/endpoints/pets/pets.ts | 6 +- .../custom-fetch/src/gen/pets/pets.ts | 6 +- tests/configs/fetch.config.ts | 10 ++++ tests/specifications/parameters.yaml | 9 +++ 13 files changed, 90 insertions(+), 37 deletions(-) create mode 100644 samples/hono/hono-with-fetch-client/next-app/next-env.d.ts diff --git a/.gitignore b/.gitignore index a6c68cb03..322351819 100644 --- a/.gitignore +++ b/.gitignore @@ -102,4 +102,5 @@ typings/ package-lock.json tests/generated/** -.turbo \ No newline at end of file +.turbo +.next diff --git a/packages/fetch/src/index.ts b/packages/fetch/src/index.ts index 33bf089d5..cbc165775 100644 --- a/packages/fetch/src/index.ts +++ b/packages/fetch/src/index.ts @@ -11,7 +11,13 @@ import { toObjectString, generateBodyOptions, isObject, + resolveRef, } from '@orval/core'; +import { + PathItemObject, + ParameterObject, + ReferenceObject, +} from 'openapi3-ts/oas30'; export const generateRequestFunction = ( { @@ -26,7 +32,7 @@ export const generateRequestFunction = ( formUrlEncoded, override, }: GeneratorVerbOptions, - { route }: GeneratorOptions, + { route, context, pathRoute }: GeneratorOptions, ) => { const isRequestOptions = override?.requestOptions !== false; const isFormData = override?.formData !== false; @@ -42,18 +48,51 @@ export const generateRequestFunction = ( ), 'implementation', ); + + const spec = context.specs[context.specKey].paths[pathRoute] as + | PathItemObject + | undefined; + const parameters = + spec?.[verb]?.parameters || ([] as (ParameterObject | ReferenceObject)[]); + + const explodeParameters = parameters.filter((parameter) => { + const { schema } = resolveRef(parameter, context); + + return schema.in === 'query' && schema.explode; + }); + + const explodeParametersNames = explodeParameters.map((parameter) => { + const { schema } = resolveRef(parameter, context); + + return schema.name; + }); + + const explodeArrayImplementation = + explodeParameters.length > 0 + ? `const explodeParameters = ${JSON.stringify(explodeParametersNames)}; + + if (value instanceof Array && explodeParameters.includes(key)) { + value.forEach((v) => normalizedParams.append(key, v === null ? 'null' : v.toString())); + return; + } + ` + : ''; + + const isExplodeParametersOnly = + explodeParameters.length === parameters.length; + + const nomalParamsImplementation = `if (value !== undefined) { + normalizedParams.append(key, value === null ? 'null' : value.toString()) + }`; + const getUrlFnImplementation = `export const ${getUrlFnName} = (${getUrlFnProps}) => { ${ queryParams - ? ` - const normalizedParams = new URLSearchParams(); + ? ` const normalizedParams = new URLSearchParams(); Object.entries(params || {}).forEach(([key, value]) => { - if (value === null) { - normalizedParams.append(key, 'null'); - } else if (value !== undefined) { - normalizedParams.append(key, value.toString()); - } + ${explodeArrayImplementation} + ${!isExplodeParametersOnly ? nomalParamsImplementation : ''} });` : '' } diff --git a/samples/hono/hono-with-fetch-client/next-app/app/gen/pets/pets.msw.ts b/samples/hono/hono-with-fetch-client/next-app/app/gen/pets/pets.msw.ts index 27ae3c3fa..6d152dbe1 100644 --- a/samples/hono/hono-with-fetch-client/next-app/app/gen/pets/pets.msw.ts +++ b/samples/hono/hono-with-fetch-client/next-app/app/gen/pets/pets.msw.ts @@ -23,6 +23,7 @@ export const getCreatePetsResponseMock = ( ): Pet => ({ id: faker.number.int({ min: undefined, max: undefined }), name: faker.word.sample(), + tag: faker.word.sample(), ...overrideResponse, }); @@ -31,6 +32,7 @@ export const getUpdatePetsResponseMock = ( ): Pet => ({ id: faker.number.int({ min: undefined, max: undefined }), name: faker.word.sample(), + tag: faker.word.sample(), ...overrideResponse, }); @@ -39,6 +41,7 @@ export const getShowPetByIdResponseMock = ( ): Pet => ({ id: faker.number.int({ min: undefined, max: undefined }), name: faker.word.sample(), + tag: faker.word.sample(), ...overrideResponse, }); diff --git a/samples/hono/hono-with-fetch-client/next-app/app/gen/pets/pets.ts b/samples/hono/hono-with-fetch-client/next-app/app/gen/pets/pets.ts index 04ba03b68..191dda872 100644 --- a/samples/hono/hono-with-fetch-client/next-app/app/gen/pets/pets.ts +++ b/samples/hono/hono-with-fetch-client/next-app/app/gen/pets/pets.ts @@ -23,10 +23,8 @@ export const getListPetsUrl = (params?: ListPetsParams) => { const normalizedParams = new URLSearchParams(); Object.entries(params || {}).forEach(([key, value]) => { - if (value === null) { - normalizedParams.append(key, 'null'); - } else if (value !== undefined) { - normalizedParams.append(key, value.toString()); + if (value !== undefined) { + normalizedParams.append(key, value === null ? 'null' : value.toString()); } }); diff --git a/samples/hono/hono-with-fetch-client/next-app/next-env.d.ts b/samples/hono/hono-with-fetch-client/next-app/next-env.d.ts new file mode 100644 index 000000000..4f11a03dc --- /dev/null +++ b/samples/hono/hono-with-fetch-client/next-app/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/samples/next-app-with-fetch/app/gen/pets/pets.ts b/samples/next-app-with-fetch/app/gen/pets/pets.ts index 46acfd228..4e197ed9a 100644 --- a/samples/next-app-with-fetch/app/gen/pets/pets.ts +++ b/samples/next-app-with-fetch/app/gen/pets/pets.ts @@ -52,10 +52,8 @@ export const getListPetsUrl = (params?: ListPetsParams) => { const normalizedParams = new URLSearchParams(); Object.entries(params || {}).forEach(([key, value]) => { - if (value === null) { - normalizedParams.append(key, 'null'); - } else if (value !== undefined) { - normalizedParams.append(key, value.toString()); + if (value !== undefined) { + normalizedParams.append(key, value === null ? 'null' : value.toString()); } }); diff --git a/samples/react-app-with-swr/fetch-client/src/api/endpoints/swaggerPetstore.ts b/samples/react-app-with-swr/fetch-client/src/api/endpoints/swaggerPetstore.ts index 9a69a903c..06afdcd5f 100644 --- a/samples/react-app-with-swr/fetch-client/src/api/endpoints/swaggerPetstore.ts +++ b/samples/react-app-with-swr/fetch-client/src/api/endpoints/swaggerPetstore.ts @@ -23,10 +23,8 @@ export const getListPetsUrl = (params?: ListPetsParams) => { const normalizedParams = new URLSearchParams(); Object.entries(params || {}).forEach(([key, value]) => { - if (value === null) { - normalizedParams.append(key, 'null'); - } else if (value !== undefined) { - normalizedParams.append(key, value.toString()); + if (value !== undefined) { + normalizedParams.append(key, value === null ? 'null' : value.toString()); } }); diff --git a/samples/react-query/custom-fetch/src/gen/pets/pets.ts b/samples/react-query/custom-fetch/src/gen/pets/pets.ts index 5fe866b9f..6a6b6635e 100644 --- a/samples/react-query/custom-fetch/src/gen/pets/pets.ts +++ b/samples/react-query/custom-fetch/src/gen/pets/pets.ts @@ -68,10 +68,8 @@ export const getListPetsUrl = (params?: ListPetsParams) => { const normalizedParams = new URLSearchParams(); Object.entries(params || {}).forEach(([key, value]) => { - if (value === null) { - normalizedParams.append(key, 'null'); - } else if (value !== undefined) { - normalizedParams.append(key, value.toString()); + if (value !== undefined) { + normalizedParams.append(key, value === null ? 'null' : value.toString()); } }); diff --git a/samples/svelte-query/custom-fetch/src/gen/pets/pets.ts b/samples/svelte-query/custom-fetch/src/gen/pets/pets.ts index 796f85137..05180ca29 100644 --- a/samples/svelte-query/custom-fetch/src/gen/pets/pets.ts +++ b/samples/svelte-query/custom-fetch/src/gen/pets/pets.ts @@ -65,10 +65,8 @@ export const getListPetsUrl = (params?: ListPetsParams) => { const normalizedParams = new URLSearchParams(); Object.entries(params || {}).forEach(([key, value]) => { - if (value === null) { - normalizedParams.append(key, 'null'); - } else if (value !== undefined) { - normalizedParams.append(key, value.toString()); + if (value !== undefined) { + normalizedParams.append(key, value === null ? 'null' : value.toString()); } }); diff --git a/samples/swr-with-zod/src/gen/endpoints/pets/pets.ts b/samples/swr-with-zod/src/gen/endpoints/pets/pets.ts index 2f40d1b45..3367a76b0 100644 --- a/samples/swr-with-zod/src/gen/endpoints/pets/pets.ts +++ b/samples/swr-with-zod/src/gen/endpoints/pets/pets.ts @@ -56,10 +56,8 @@ export const getListPetsUrl = (params?: ListPetsParams) => { const normalizedParams = new URLSearchParams(); Object.entries(params || {}).forEach(([key, value]) => { - if (value === null) { - normalizedParams.append(key, 'null'); - } else if (value !== undefined) { - normalizedParams.append(key, value.toString()); + if (value !== undefined) { + normalizedParams.append(key, value === null ? 'null' : value.toString()); } }); diff --git a/samples/vue-query/custom-fetch/src/gen/pets/pets.ts b/samples/vue-query/custom-fetch/src/gen/pets/pets.ts index 065503534..35e6ef48f 100644 --- a/samples/vue-query/custom-fetch/src/gen/pets/pets.ts +++ b/samples/vue-query/custom-fetch/src/gen/pets/pets.ts @@ -67,10 +67,8 @@ export const getListPetsUrl = (params?: ListPetsParams) => { const normalizedParams = new URLSearchParams(); Object.entries(params || {}).forEach(([key, value]) => { - if (value === null) { - normalizedParams.append(key, 'null'); - } else if (value !== undefined) { - normalizedParams.append(key, value.toString()); + if (value !== undefined) { + normalizedParams.append(key, value === null ? 'null' : value.toString()); } }); diff --git a/tests/configs/fetch.config.ts b/tests/configs/fetch.config.ts index 810bccc97..ec8e88631 100644 --- a/tests/configs/fetch.config.ts +++ b/tests/configs/fetch.config.ts @@ -168,4 +168,14 @@ export default defineConfig({ target: '../specifications/form-url-encoded.yaml', }, }, + parameters: { + output: { + target: '../generated/fetch/parameters/endpoints.ts', + schemas: '../generated/fetch/parameters/model', + client: 'fetch', + }, + input: { + target: '../specifications/parameters.yaml', + }, + }, }); diff --git a/tests/specifications/parameters.yaml b/tests/specifications/parameters.yaml index 0f82dff00..ba70bc2ee 100644 --- a/tests/specifications/parameters.yaml +++ b/tests/specifications/parameters.yaml @@ -71,6 +71,15 @@ paths: required: false schema: type: boolean + - name: tag + in: query + description: include pets tag + required: false + explode: true + schema: + type: array + items: + type: string responses: '200': description: A paged array of pets