diff --git a/docs/src/pages/reference/configuration/output.md b/docs/src/pages/reference/configuration/output.md index d9ee72569..f6225672e 100644 --- a/docs/src/pages/reference/configuration/output.md +++ b/docs/src/pages/reference/configuration/output.md @@ -23,7 +23,7 @@ module.exports = { Type: `String | Function`. -Valid values: `angular`, `axios`, `axios-functions`, `react-query`, `svelte-query`, `vue-query`, `swr`, `zod`. +Valid values: `angular`, `axios`, `axios-functions`, `react-query`, `svelte-query`, `vue-query`, `swr`, `zod`, `fetch`. Default Value: `axios-functions`. diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 6d972694b..6c9072b72 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -212,6 +212,7 @@ export const OutputClient = { SWR: 'swr', ZOD: 'zod', HONO: 'hono', + FETCH: 'fetch', } as const; export type OutputClient = (typeof OutputClient)[keyof typeof OutputClient]; diff --git a/packages/fetch/README.md b/packages/fetch/README.md new file mode 100644 index 000000000..4364bb223 --- /dev/null +++ b/packages/fetch/README.md @@ -0,0 +1,29 @@ +[![npm version](https://badge.fury.io/js/orval.svg)](https://badge.fury.io/js/orval) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![tests](https://github.com/anymaniax/orval/actions/workflows/tests.yaml/badge.svg)](https://github.com/anymaniax/orval/actions/workflows/tests.yaml) + +

+ orval - Restfull Client Generator +

+

+ Visit orval.dev for docs, guides, API and beer! +

+ +### Code Generation + +`orval` is able to generate client with appropriate type-signatures (TypeScript) from any valid OpenAPI v3 or Swagger v2 specification, either in `yaml` or `json` formats. + +`Generate`, `valid`, `cache` and `mock` in your React, Vue, Svelte and Angular applications all with your OpenAPI specification. + +### Samples + +You can find below some samples + +- [react app](https://github.com/anymaniax/orval/tree/master/samples/react-app) +- [react query](https://github.com/anymaniax/orval/tree/master/samples/react-query) +- [svelte query](https://github.com/anymaniax/orval/tree/master/samples/svelte-query) +- [vue query](https://github.com/anymaniax/orval/tree/master/samples/vue-query) +- [react app with swr](https://github.com/anymaniax/orval/tree/master/samples/react-app-with-swr) +- [nx fastify react](https://github.com/anymaniax/orval/tree/master/samples/nx-fastify-react) +- [angular app](https://github.com/anymaniax/orval/tree/master/samples/angular-app) +- [hono](https://github.com/anymaniax/orval/tree/master/samples/hono) diff --git a/packages/fetch/package.json b/packages/fetch/package.json new file mode 100644 index 000000000..247fbc0d9 --- /dev/null +++ b/packages/fetch/package.json @@ -0,0 +1,18 @@ +{ + "name": "@orval/fetch", + "version": "6.29.1", + "license": "MIT", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsup ./src/index.ts --target node12 --clean --sourcemap --dts", + "dev": "tsup ./src/index.ts --target node12 --clean --sourcemap --watch src", + "lint": "eslint src/**/*.ts" + }, + "dependencies": { + "@orval/core": "6.29.1" + } +} diff --git a/packages/fetch/src/index.ts b/packages/fetch/src/index.ts new file mode 100644 index 000000000..e8aefdb04 --- /dev/null +++ b/packages/fetch/src/index.ts @@ -0,0 +1,144 @@ +import { + camel, + ClientBuilder, + ClientDependenciesBuilder, + ClientGeneratorsBuilder, + generateFormDataAndUrlEncodedFunction, + generateVerbImports, + GeneratorDependency, + GeneratorOptions, + GeneratorVerbOptions, + GetterPropType, + stringify, + toObjectString, + generateBodyOptions, + isObject, +} from '@orval/core'; + +const generateRequestFunction = ( + { + queryParams, + operationName, + response, + body, + props, + verb, + formData, + formUrlEncoded, + override, + }: GeneratorVerbOptions, + { route }: GeneratorOptions, +) => { + const isRequestOptions = override?.requestOptions !== false; + const isFormData = override?.formData !== false; + const isFormUrlEncoded = override?.formUrlEncoded !== false; + + const getUrlFnName = camel(`get-${operationName}-url`); + const getUrlFnProps = toObjectString( + props.filter( + (prop) => + prop.type === GetterPropType.PARAM || + prop.type === GetterPropType.NAMED_PATH_PARAMS || + prop.type === GetterPropType.QUERY_PARAM, + ), + 'implementation', + ); + const getUrlFnImplementation = `export const ${getUrlFnName} = (${getUrlFnProps}) => { +${ + queryParams + ? ` + 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()); + } + });` + : '' +} + + return \`${route}${queryParams ? '?${normalizedParams.toString()}' : ''}\` +}\n`; + const getUrlFnProperties = props + .filter( + (prop) => + prop.type === GetterPropType.PARAM || + prop.type === GetterPropType.QUERY_PARAM || + prop.type === GetterPropType.NAMED_PATH_PARAMS, + ) + .map((param) => { + if (param.type === GetterPropType.NAMED_PATH_PARAMS) { + return param.destructured; + } else { + return param.name; + } + }) + .join(','); + + const args = `${toObjectString(props, 'implementation')} ${isRequestOptions ? `options?: RequestInit` : ''}`; + const retrunType = `Promise<${response.definition.success || 'unknown'}>`; + + const globalFetchOptions = isObject(override?.requestOptions) + ? `${stringify(override?.requestOptions)?.slice(1, -1)?.trim()}` + : ''; + const fetchMethodOption = `method: '${verb.toUpperCase()}'`; + + const requestBodyParams = generateBodyOptions( + body, + isFormData, + isFormUrlEncoded, + ); + const fetchBodyOption = requestBodyParams + ? `body: JSON.stringify(${requestBodyParams})` + : ''; + + const fetchResponseImplementation = `const res = await fetch( + ${getUrlFnName}(${getUrlFnProperties}), + {${globalFetchOptions ? '\n' : ''} ${globalFetchOptions} + ${isRequestOptions ? '...options,' : ''} + ${fetchMethodOption}${fetchBodyOption ? ',' : ''} + ${fetchBodyOption} + } + ) + + return res.json() +`; + + const bodyForm = generateFormDataAndUrlEncodedFunction({ + formData, + formUrlEncoded, + body, + isFormData, + isFormUrlEncoded, + }); + + const fetchImplementationBody = + `${bodyForm ? ` ${bodyForm}\n` : ''}` + ` ${fetchResponseImplementation}`; + const fetchImplementation = `export const ${operationName} = async (${args}): ${retrunType} => {\n${fetchImplementationBody}}`; + + const implementation = + `${getUrlFnImplementation}\n` + `${fetchImplementation}\n`; + + return implementation; +}; + +export const generateClient: ClientBuilder = (verbOptions, options) => { + const imports = generateVerbImports(verbOptions); + const functionImplementation = generateRequestFunction(verbOptions, options); + + return { + implementation: `${functionImplementation}\n`, + imports, + }; +}; + +const fetchClientBuilder: ClientGeneratorsBuilder = { + client: generateClient, + dependencies: () => [], +}; + +export const builder = () => () => fetchClientBuilder; + +export default builder; diff --git a/packages/fetch/tsconfig.json b/packages/fetch/tsconfig.json new file mode 100644 index 000000000..9e25e6ece --- /dev/null +++ b/packages/fetch/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src/**/*.ts"] +} diff --git a/packages/orval/package.json b/packages/orval/package.json index 13896429a..1cb0bb26e 100644 --- a/packages/orval/package.json +++ b/packages/orval/package.json @@ -58,6 +58,7 @@ "@orval/angular": "6.29.1", "@orval/axios": "6.29.1", "@orval/core": "6.29.1", + "@orval/fetch": "6.29.1", "@orval/hono": "6.29.1", "@orval/mock": "6.29.1", "@orval/query": "6.29.1", diff --git a/packages/orval/src/client.ts b/packages/orval/src/client.ts index 0c8ac506d..ce5ee7e1c 100644 --- a/packages/orval/src/client.ts +++ b/packages/orval/src/client.ts @@ -25,6 +25,7 @@ import query from '@orval/query'; import swr from '@orval/swr'; import zod from '@orval/zod'; import hono from '@orval/hono'; +import fetchClient from '@orval/fetch'; const DEFAULT_CLIENT = OutputClient.AXIOS; @@ -42,6 +43,7 @@ const getGeneratorClient = ( swr: swr()(), zod: zod()(), hono: hono()(), + fetch: fetchClient()(), }; const generator = isFunction(outputClient) diff --git a/tests/configs/fetch.config.ts b/tests/configs/fetch.config.ts new file mode 100644 index 000000000..cd18f75e0 --- /dev/null +++ b/tests/configs/fetch.config.ts @@ -0,0 +1,75 @@ +import { defineConfig } from 'orval'; + +export default defineConfig({ + petstore: { + output: { + target: '../generated/fetch/petstore/endpoints.ts', + schemas: '../generated/fetch/petstore/model', + mock: true, + client: 'axios', + }, + input: { + target: '../specifications/petstore.yaml', + }, + }, + multiArguments: { + output: { + target: '../generated/fetch/multi-arguments/endpoints.ts', + schemas: '../generated/fetch/multi-arguments/model', + mock: true, + client: 'fetch', + }, + input: { + target: '../specifications/petstore.yaml', + }, + }, + petstoreTagsSplit: { + output: { + target: '../generated/fetch/petstore-tags-split/endpoints.ts', + schemas: '../generated/fetch/petstore-tags-split/model', + mock: true, + mode: 'tags-split', + client: 'fetch', + }, + input: { + target: '../specifications/petstore.yaml', + }, + }, + petstoreSplit: { + output: { + target: '../generated/fetch/split/endpoints.ts', + schemas: '../generated/fetch/split/model', + mock: true, + mode: 'split', + client: 'fetch', + }, + input: { + target: '../specifications/petstore.yaml', + }, + }, + petstoreTags: { + output: { + target: '../generated/fetch/tags/endpoints.ts', + schemas: '../generated/fetch/tags/model', + mock: true, + mode: 'tags', + client: 'fetch', + }, + input: { + target: '../specifications/petstore.yaml', + }, + }, + namedParameters: { + output: { + target: '../generated/fetch/named-parameters/endpoints.ts', + schemas: '../generated/fetch/named-parameters/model', + client: 'fetch', + override: { + useNamedParameters: true, + }, + }, + input: { + target: '../specifications/petstore.yaml', + }, + }, +}); diff --git a/tests/package.json b/tests/package.json index 6b7eca718..07d0b1cd4 100644 --- a/tests/package.json +++ b/tests/package.json @@ -17,6 +17,7 @@ "generate:multi-file": "yarn orval --config ./configs/multi-file.config.ts", "generate:zod": "yarn orval --config ./configs/zod.config.ts", "generate:mock": "yarn orval --config ./configs/mock.config.ts", + "generate:fetch": "yarn orval --config ./configs/fetch.config.ts", "build": "tsc" }, "author": "Victor Bury", diff --git a/yarn.lock b/yarn.lock index b09cc296c..8b22e9e5c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -909,6 +909,14 @@ __metadata: languageName: unknown linkType: soft +"@orval/fetch@npm:6.29.1, @orval/fetch@workspace:packages/fetch": + version: 0.0.0-use.local + resolution: "@orval/fetch@workspace:packages/fetch" + dependencies: + "@orval/core": "npm:6.29.1" + languageName: unknown + linkType: soft + "@orval/hono@npm:6.29.1, @orval/hono@workspace:packages/hono": version: 0.0.0-use.local resolution: "@orval/hono@workspace:packages/hono" @@ -7136,6 +7144,7 @@ __metadata: "@orval/angular": "npm:6.29.1" "@orval/axios": "npm:6.29.1" "@orval/core": "npm:6.29.1" + "@orval/fetch": "npm:6.29.1" "@orval/hono": "npm:6.29.1" "@orval/mock": "npm:6.29.1" "@orval/query": "npm:6.29.1"